Unerwartetes Verhalten bei Try/Catch

Übersicht BlitzMax, BlitzMax NG Allgemein

Neue Antwort erstellen

BladeRunner

Moderator

Betreff: Unerwartetes Verhalten bei Try/Catch

BeitragMi, Nov 06, 2013 11:15
Antworten mit Zitat
Benutzer-Profile anzeigen
Hallo, mir ist eben ein seltsamer Fehler aufgefallen bei BMax und ich wollte wissen ob ihr ihn a) verifizieren könnt und b) eventuell eine Erklärung für mich habt:

Folgender Code:
BlitzMax: [AUSKLAPPEN]
SuperStrict
Local test:TBla
Try
test:TBla = TBla.Create()
Print test.blub 'hier wird die Exception ausgelöst und damit der Catch-Zweig abgefangen
Print "ich werde nie ausgeführt"
Catch TNull:TBlitzException
Print "catch: " + Tnull.tostring() 'hier wird das gecatchte ausgegeben. Stattdessen könnte eine Fehlerbehandlung stattfinden.
EndTry
Print "Dank dem Catch kann man mich noch lesen!"

Print test.blub

Type TBla
Field blub:Int

Function Create:TBla()
Return Null
End Function
End Type


Führe ich den Code als DebugBuild aus läuft er wie erwartet:
Zitat:
Building untitled1
Compiling:untitled1.bmx
flat assembler version 1.69.50 (1701393 kilobytes memory)
4 passes, 5349 bytes.
Linking:untitled1.debug.exe
Executing:untitled1.debug.exe
catch: Attempt to access field or method of Null object
Dank dem Catch kann man mich noch lesen!

Process terminated

Der Tryblock fängt den ersten invaliden Aufruf ab, leitet das durch die Catchausgabe und das Programm läuft weiter. Beim zweiten Aufruf ausserhalb des Try-Blockes bricht Blitz mit 2null Exception den Programmablauf erwartungsgemäß ab. Soweit, so gut.
Nun lasse ich das Programm als ReleaseBuild laufen, und die Ausgabe da lässt mich aus allen Wolken fallen:
Zitat:
Building untitled1
Compiling:untitled1.bmx
flat assembler version 1.69.50 (1695225 kilobytes memory)
3 passes, 3869 bytes.
Linking:untitled1.exe
Executing:untitled1.exe
0
ich werde nie ausgeführt
Dank dem Catch kann man mich noch lesen!
0

Process complete

Der Catchzweig wird nicht aufgerufen, klar, die Debugsachen sind ja raus. Aber warum zum Geier wird das Objekt augenscheinlich erzeugt, obwohl es keinen validen Aufruf eines new() gibt? Das Programm wird mit einem leeren Objekt durchlaufen, wo nie eins hätte erzeugt werden dürfen. Habt ihr eine Erklärung dafür?

Um es noch weirder zu machen: Wenn ich blub im Typekopf mit einem Wert initialisiere (Field blub:int = 35) wird dennoch eine 0 ausgegeben. Das widerspricht mEn. absolut jedem sinnvollen Verhalten.

EDIT: Es kommt noch besser: Wenn ich vor dem zweiten Print eine Zuweisung an test.blub vornehme wird die klaglos übernommen und auch wieder ausgegeben. Ghostobjekte, wtf?
Zu Diensten, Bürger.
Intel T2300, 2.5GB DDR 533, Mobility Radeon X1600 Win XP Home SP3
Intel T8400, 4GB DDR3, Nvidia GF9700M GTS Win 7/64
B3D BMax MaxGUI

Stolzer Gewinner des BAC#48, #52 & #92

DAK

BeitragMi, Nov 06, 2013 12:40
Antworten mit Zitat
Benutzer-Profile anzeigen
Sowas Ähnliches ist mir in BB letztens aufgefallen, da wird bei Array-Aufrufen nicht überprüft, ob der angegebene Index auch wirklich möglich ist. (Habe gerade überprüft, ist in BMax auch so)

Code: [AUSKLAPPEN]
Local x:Int[] = New Int[10]
x[20] = 5
Print ("Ungültig:"+x[20])


Wie es scheint, gibt es in BMax nur dann Gültigkeitschecks irgendeiner Art, wenn Debug eingeschalten ist, ansonsten muss man alle Checks selber machen.

Was ich mir vorstelle, was passiert, ist folgendes:
Normalerweise hat dein TBla folgende Struktur:

test:TBla -> blub
test:TBla = Adresse wo blub und eventuelle andere Werte liegen

Also du hast den Pointer test:TBla, der auf das Stuct zeigt, was nur blub beinhaltet.

Ich nehme an, dass BMax Null als eine bestimmte Adresse nimmt, an der normalerweise keine Variablen liegen dürfen (wahrscheinlich 0).

Wenn test = Null, und du test.blub aufrufst, dann sucht er an Stelle 0 mit dem Offset wo .blub normalerweise liegt, nach dem Wert von .blub und gibt das dort aus.


Wenn man mich fragt ist das Ganze eine sehr schlampige Optimierung von Mark Sibly um noch die letzten Nanosekunden Ausführungszeit rauszukitzeln.

Ich würde inzwischen Blitzprogramme nur noch im Debug-Modus verteilen, selbst im Release.
Gewinner der 6. und der 68. BlitzCodeCompo

BladeRunner

Moderator

BeitragMi, Nov 06, 2013 13:58
Antworten mit Zitat
Benutzer-Profile anzeigen
Naja, eine mangelnde Überprüfung der Grenzen könnte ich ja aus Optimierungsgründen ja verstehen, hier ist es ja aber so dass ein komplett leeres Objekt erzeugt wird, was definitiv nicht erwünschtes Verhalten sein muss.
Allerdings muss ich dir recht geben: Wenn ich ohne Try-catch die Instanz als Null erzeuge und dann den integer abfrage geht das auch.
Jetzt hakt es komplett.
Zu Diensten, Bürger.
Intel T2300, 2.5GB DDR 533, Mobility Radeon X1600 Win XP Home SP3
Intel T8400, 4GB DDR3, Nvidia GF9700M GTS Win 7/64
B3D BMax MaxGUI

Stolzer Gewinner des BAC#48, #52 & #92

DAK

BeitragMi, Nov 06, 2013 16:57
Antworten mit Zitat
Benutzer-Profile anzeigen
Was ich mir hald denk, ist dass BMax eine komische Definition von Objekten hat.

Ich versuch das von oben noch mal besser zu erklären, da ich mich da echt nicht sinnvoll ausgedrückt habe.

Ich denke, BMax sieht Instanzen eines Objekts als so eine Art Bank oder Array im Speicher, wobei jede Variable nur Länge und Offset der Werte angibt. Methoden werden ja nicht für jedes Objekt einzeln gespeichert und müssen deswegen nicht zu dem Objekt dazu gespeichert werden. Das heißt, ein Objekt folgendes Types
BlitzMax: [AUSKLAPPEN]
Type TTest
Field x:Int
Field y:Byte
Field z:Int
End Type


würde dann im Speicher so ausschauen (ein Buchstabe für ein Byte Speicher):
Code: [AUSKLAPPEN]
xxxxyzzzz


Die Variable, die die Instanz des Objekts repräsentiert, würde dann nur die Speicherposition des gesamten Objekts beinhalten.

Wird eine Variable des Objekts aufgerufen, dann schaut das dann so aus:

(Adresse)test.y = (Adresse)test+(Adresse)y

Wenn test an der Speicherstelle 0x1000 steht, dann würde test.y also an Speicherstelle 0x1000+0x5 = 0x1005 stehen.

Da Null in BMax tatsächlich den Wert 0 hat, schaut es dann in deinem Beispiel so aus:

test = Null -> test = 0
test.y = 3 -> Statt dass Blitz checkt, ob test<>null und erst dann weiter geht, geht es davon aus, dass test ok ist, und beschreibt dann einfach den Speicherplatz (Adresse)test+(Adresse)y = 0+5 = 5 mit dem Wert 3 befüllt.

Auslesen rennt analog.

-> Blitz erstellt also kein neues Objekt, sondern interpretiert den vorhandenen Speicherplatz einfach als ein Objekt des Types Test.

Sehr interessant wird das dann, wenn man das mit einem zweiten Fehler von BMax kombiniert: BMax checkt den Type eines Objekts nur zur Kompilierzeit.

Gegeben ist folgendes Beispiel:
BlitzMax: [AUSKLAPPEN]
Framework brl.standardio

Type TTest1
Field x:Int
Field y:Int
Field z:Int
End Type

Type TTest2
Field a:Int
Field b:Int
Field c:Int
End Type


Local t1:TTest1 = Null
t1.x = 3
t1.y = 2
t1.z = 1

Local t2:TTest2 = Null
Print(t2.a)
Print(t2.b)
Print(t2.c)


Hier werden Werte in t1 (was Null ist) geschrieben, und die gleichen Werte aus t2 (was auch Null ist, aber von einem anderen Type!) ausgelesen. Und das klappt so!

Auf der anderen Seite schreit der Compiler, wenn man t1 = t2 schreiben würde, selbst wenn beide Null sind.
Gewinner der 6. und der 68. BlitzCodeCompo

Thunder

BeitragMi, Nov 06, 2013 19:04
Antworten mit Zitat
Benutzer-Profile anzeigen
Wieder Was interessantes! Habe mir natürlich sofort den Assemblercode reingezogen und habe jetzt ungefähr eine Ahnung, was da los ist. Einiges davon habt ihr beide schon gesagt.

DAK hat Folgendes geschrieben:
Ich denke, BMax sieht Instanzen eines Objekts als so eine Art Bank oder Array im Speicher, wobei jede Variable nur Länge und Offset der Werte angibt. Methoden werden ja nicht für jedes Objekt einzeln gespeichert und müssen deswegen nicht zu dem Objekt dazu gespeichert werden.


Richtig. Und das ist auch die einzige effiziente Möglichkeit, Instanzen zu speichern. Nur ist dein "ASCII-Diagramm" nicht ganz korrekt. Es wäre wahrscheinlich
Code: [AUSKLAPPEN]
xxxxy000zzzz

weil man normalerweise alles auf 4-Byte-Grenzen bringt.

Was du danach schreibst ist auch alles richtig, DAK, nur bei Null ist es etwas anders:
Null ist in BlitzMax ein Zeiger auf die Variable _bbNullObject. Die ist statisch im Speicher von jedem BlitzMax-Programm. Also es wird bei Null kein leeres Objekt generiert, sondern immer der Zeiger auf _bbNullObject verwendet.
Wenn man dann dort irgendwelche Werte reinschreibt (im Release-Mode geht das ja), dann schreibt man im schlimmsten Fall in Speicher, der dem Programm nicht gehört (weil _bbNullObject keine Elemente hat), was schlecht ist. Wenn man kurz darauf mit einem zweiten Type auf dieselben Werte zugreift und die bis dahin immer noch die gleichen sind, kann man sie natürlich genauso wieder auslesen (ist ja dieselbe Adresse) - davon ist natürlich auch abzuraten.

Zu deinem Beispiel: So gehts aber ohne Umweg über NullObject:
BlitzMax: [AUSKLAPPEN]

Local t1:TTest1 = Null
t1.x = 3
t1.y = 2
t1.z = 1

Local t2:TTest2 = TTest2(Object t1)
Print(t2.a)
Print(t2.b)
Print(t2.c)


BlitzMax: [AUSKLAPPEN]
Wenn man mich fragt ist das Ganze eine sehr schlampige Optimierung von Mark Sibly um noch die letzten Nanosekunden Ausführungszeit rauszukitzeln. 

Ich würde inzwischen Blitzprogramme nur noch im Debug-Modus verteilen, selbst im Release.


Schlussplädoyer für die kompilierten Programmiersprachen: Das ist meiner Meinung nach keine schlampige Optimierung, sondern ein Zeichen für einen - in diesem Bereich - guten Compiler. Der Compiler sollte nicht den Code schwerer und komplizierter machen, als der Programmierer ihn entworfen hat, und er soll nur das übersetzen, was wirklich im Quelltext steht. Man muss sich um Sicherheitsprüfungen selber kümmern.
Blitz-Programme als Debug-Build verteilen zerstört wohl einen der größten Vorteile gegenüber interpretierten Sprachen: nämlich Geschwindigkeit und Effizienz, denn jede vermeidbare Überprüfung, die auf einer CPU ausgeführt werden muss, ist eine unnötige Überprüfung.

UNZ

BeitragMi, Nov 06, 2013 21:12
Antworten mit Zitat
Benutzer-Profile anzeigen
Wow. Das alle Checks im Release aus sind wusste ich auch nicht. Shocked
Bei Null möchte ich aber noch etwas ergänzen:
Null ist nicht nur ein Zeiger auf ein Null-Object, sondern wird vom Compiler je nach Bedarf ersetzt.

Beispiel:
Code: [AUSKLAPPEN]

SuperStrict
Framework brl.standardio

Local NullInteger:Int = Null 'tatsächlich 0
Local NullFunction() = Null 'zeiger zur null-function
Local NullObject:Object = Null 'zeiger zum null-object
Local NullString:String = Null 'zeiger zu einem leeren string
Local NullBytePtr:Byte Ptr = Null 'tatsächlich 0

Print(NullInteger)
Print(Int(Byte Ptr(NullFunction) ) )
Print(Int(Byte Ptr(NullObject) ) )
Print(Int(Byte Ptr(Object(NullString) ) ) ) 'String lässt sich nicht zu Byte Ptr casten, aber Object schon...
Print(Int(NullBytePtr) )


Als ich mal mit Newton Game Dynamics rumgespielt habe bin ich dabei auf die Nase gefallen, obwohl mein Code eigentlich richtig war.
Ich hatte Wrapper-Types gebastelt und bei manchen Functions die Parameter mit Null voreingestellt. Aber als das an Newton weitergegeben wurde gabs 'nen crash. Der Witzt war dann, das es funktioniert hat, als ich statt dem Null-Object direkt Null an Newton übergab Confused .

Solche Sachen zu wissen kann einem manchmal sehr viel Zeit bei der Fehlersuche ersparen bzw. einem dabei helfen Fehler gar nicht erst zu machen.
Das muss besser als perfekt!

DAK

BeitragMi, Nov 06, 2013 22:07
Antworten mit Zitat
Benutzer-Profile anzeigen
@Thunder:
Das wirklich Nervige dabei ist, dass BMax ja nicht alle Checks rausnimmt. Einen Division-By-Zero-Fehler wirft er auch zur Laufzeit.
Und dadurch, dass es inkonsistent ist, welche Fehler überprüft werden, und welche nicht (ein Null-Objekt aufzurufen kann dabei getrost als Fehler gewertet werden), und dieses Verhalten vor allem nicht dokumentiert ist, führt es dazu, dass selbst erfahrene Programmierer (oder gerade erfahrene Programmierer) auf ein Basis-Feature vertrauen, dass so gar nicht existiert.

Was man vom Debugger erwartet, ist dass er nicht mehr, sondern genauere Fehlermeldungen ausgibt.

BB löst das, was das angeht, noch deutlich sauberer.

Vergleichscode:
BlitzMax: [AUSKLAPPEN]
Type TTest
Field a
End Type

Local t:TTest = New TTest
t.a = 5
Print (t.a)
Release t.a
Print (t.a)
Print("done")


(Ich weiß, dass man in BMax normalerweise kein Release verwendet, aber ich brauche es, um die gleiche Funktionalität zu bekommen, wie in BB)

BlitzBasic: [AUSKLAPPEN]
Type TTest
Field a%
End Type

t.TTest = New TTest
t\a = 5

Print (t\a)
Delete t.TTest
Print ("Ready?")
Delay 1000
Print (t\a)
Delay 1000


Ergebnis (beides im Release-Modus):
-BMax schluckt es, und rennt einfach weiter. Der Wert wird dann als 0 ausgegeben, weil es wieder das Null-Objekt ist.
-BB stürzt ab, mit einem "Reagiert nicht mehr".

Man könnte meinen, dass BMax da besser damit umgeht, weil das Programm weiter rennt, auf der anderen Seite wird der Fehler einfach geschluckt, und könnte sich eventuell propagieren, was einerseits viel schwerer zu finden ist, weil es weniger auffällt (vor allem, versuch sowas in einem größeren Projekt mal zu lokalisieren!), und andererseits noch deutlich schlimmere Folgen haben kann, wie z.B. korrupte Speicherstände, wo der ganze Speicherstand dann zum Schmeißen ist.

BB hat diesen Check noch gehabt, BMax dann nicht mehr...

Das ist dann noch eine zusätzliche Falle für Programmierer, die mit BB angefangen haben, und mit BMax weiter machen (was ja doch ein beträchtlicher Anteil der BMax-Programmierer ist).
Gewinner der 6. und der 68. BlitzCodeCompo

BtbN

BeitragDo, Nov 07, 2013 16:44
Antworten mit Zitat
Benutzer-Profile anzeigen
Meiner Meinung nach hat eine NullPointerException auch im Release modus geworfen zu werden, alles andere führt zu wahnwitzigen Fehlermöglichkeiten, nach denen man sich dann tot suchen kann, gerade wenn es ein schwer zu reproduzierender Fehler ist.
Ein simpler Check ob ein Object Null ist verbraucht nur sehr wenige CPU zyklen und spaart viel ärger.

DAK

BeitragDo, Nov 07, 2013 17:20
Antworten mit Zitat
Benutzer-Profile anzeigen
Da ist dann die Frage: Was kostet mehr, ein paar CPU-Zyklen oder mehrere Stunden zusätzliches Debugging?

1980 hat die Frage auf diese Antwort sicher noch anders ausgeschaut, als jetzt.
Gewinner der 6. und der 68. BlitzCodeCompo

BtbN

BeitragDo, Nov 07, 2013 20:42
Antworten mit Zitat
Benutzer-Profile anzeigen
Ja, das wurde mit Sicherheit in der Vor-Planungsphase von BlitzMax in den 70er Jahren so im Standard vorgesehen.
Und was zur hölle hat die Debugging-Dauer mit einer vernachlässigbar langsameren ausführung zu tun?

Neue Antwort erstellen


Übersicht BlitzMax, BlitzMax NG Allgemein

Gehe zu:

Powered by phpBB © 2001 - 2006, phpBB Group