BlitzMax Interna
Übersicht

![]() |
ThunderBetreff: BlitzMax Interna |
![]() Antworten mit Zitat ![]() |
---|---|---|
Hallo,
ich beschäftige mich gerne mit BlitzMax und mit Assembler und ich verbinde auch gerne beides miteinander. Dabei stößt man auf ein paar Dinge, die man aus anderen Sprachen so nicht kennt. Ich habe schon ein Tutorial geschrieben, das praxisorientiert war und sich damit beschäftigt hat, wie man C/C++ oder Assembler mit BlitzMax verbindet (Variablen/Funktionen importieren/exportieren etc). Das hier ist was anderes, ich möchte ein bisschen von dem aufschreiben, das ich über BlitzMax-interne Sachen gelernt habe. Teilweise wird das schon bekannt sein, aber ich finde es interessant, wie alles intern vonstatten geht, und vielleicht hilft es noch jemandem. Strings "als Primitive" Beginnen wir Mal mit etwas, das wahrscheinlich viele schon wissen... Strings sind nie primitive Datentypen, aber sie werden besonders in benutzerfreundlichen Hochsprachen immer öfter stark in die Sprache integriert. Was für einen mathematischen Sinn hat es zum Beispiel, zwei Strings zu addieren? Gar keinen, aber über eine Stringklasse und Operatorenüberladung kann man das Pluszeichen mit der Konkatenation von Strings verbinden. Nur... BlitzMax hat keine Operatorenüberladung. Und eine Stringklasse? Ja, aber die ist versteckt in brl.blitz in blitz_string.c und blitz_string.h. Dass wir so einfach mit Strings umgehen können (insbesondere Verketten und Slices), kommt daher, dass der BlitzMax-Compiler so nett zu uns ist und das in ihn implementiert ist. Intern wird eine Konkatenation über bbStringConcat abgewickelt, Slices über bbStringSlice. Es wird also der Gebrauch eines Operators (Pluszeichen bzw. eckige Klammern) in Funktionsaufrufe umgewandelt. Ihr könnt das übrigens ganz einfach selber ausprobieren: BlitzMax: [AUSKLAPPEN] s1 = "Hallo " Die Zeile s1 = s1 + s2[3..] lässt sich auch so wiedergeben: s1 = bbStringConcat(s1, bbStringSlice(s2, 3, s2.length)) Diese Funktionen sind auch in brl.blitz definiert, ihr müsst sie nur noch als Extern deklarieren: BlitzMax: [AUSKLAPPEN] Extern Und genau diese Umwandlung nimmt uns der Compiler ab, wobei der natürlich dann Assemblercode generiert. Code außerhalb von Funktionen In BASIC ist das generell verbreitet, aber viele andere Programmiersprachen bieten nicht die Möglichkeit Code außerhalb von Funktionen zu platzieren, weil er ja dann logischerweise nie ausgeführt würde. BASIC fasst den Code außerhalb meistens zu einer main-Funktion zusammen. Das geschieht intern und in BlitzMax nicht nur für das eine Programm selber, das man kompiliert, sondern für jedes Modul. Jedes Modul hat seine eigene "main-Funktion", die alle aufgerufen werden, bevor das eigentliche Programm startet, das man programmiert hat. bb_main ist die "Haupt-main-Funktion". Sie besteht immer aus dem Code außerhalb aller Funktionen in der Datei, die man als App (also nicht als Modul) kompiliert hat. Daneben gibt es beispielsweise __bb_blitz_blitz oder __bb_max2d_max2d. Bei diesen Modul-main-Funktionen funktioniert also die Namensgebung nach dem Namen des Moduls. Okay... also diese Modul-main-Funktionen werden alle vor bb_main aufgerufen. Aber bestimmte Module können mehrfach importiert worden sein. Zum Beispiel so: BlitzMax: [AUSKLAPPEN] Strict Das heißt, der Compiler müsste darauf achtgeben, dass jede dieser main-Funktionen nur einmal aufgerufen wird. Tut er aber nicht. Das ganze funktioniert in der Laufzeit des Programms (und das ist wichtig, wenn man den BlitzMax-Assemblercode verstehen will). Jede dieser main-Funktionen merkt sich, ob sie schon Mal ausgeführt worden ist. Wird sie zufällig ein zweites oder drittes oder viertes... Mal aufgerufen, bemerkt sie das selbst über die Variable, die sie auf 1 setzt, wenn sie das erste Mal ausgeführt wird. Mich hat dieses Codestück überrascht und ich dachte zuerst es sei unnötig: Code: [AUSKLAPPEN] ___bb_blitz_blitz:
push ebp mov ebp,esp cmp dword [_112],0 ; Variable wird auf 0 geprüft je _113 ; wenn sie 0 ist, wird die echte Funktion ausgeführt mov eax,0 mov esp,ebp pop ebp ret ; ansonsten kommt ein Return 0 _113: ; echte Funktion dieses Codestück (mit anderen Labels) steht vor jeder main-Funktion. Das heißt einerseits, es ist nicht ohne weiteres möglich, die main-Funktion nochmal aufzurufen (über eingelinkten Assemblercode wäre das ja sonst kein Problem), und dieser Codeteil darf keinesfalls (z.B. durch einen Optimierer) entfernt werden, denn dann erhält man komische Fehler (z.B. Stack-Overflows) und interessante Mehrfach-Ausgaben. Besondere "Funktionen" Wenn man euch fragen würde, ob Min eine Funktion (z.B. Min(4,12)) ist, würdet ihr mit Ja oder Nein antworten? Es ist nämlich in BlitzMax keine Funktion, aber auch irgendwie schon. BlitzMax sieht einige aus Buchstaben bestehende Literale*, die aber wie Funktionen aufgerufen werden, als "mathematische" Operatoren an. Dazu gehören: Min, Max, Not, Abs, Sgn, SizeOf, VarPtr, Asc, Chr und Len. Ich hoffe, ich hab keinen ausgelassen. * wusste nicht, wie ich das besser formulieren soll Wenn ich mir selber eine Min-Funktion schreibe: BlitzMax: [AUSKLAPPEN] Function MeinMin:Int(x:Int, y:Int) Dann kann ich sie genauso verwenden, wie das Min von BlitzMax... und noch mehr. Weil Min als mathematischer Operator angesehen wird, darf es nicht außerhalb von einer Expression stehen. Meine eigene Funktion könnte ich aufrufen und auf den Rückgabewert pfeifen: BlitzMax: [AUSKLAPPEN] MeinMin 20, 123 ' einwandfrei Mit Min aus BlitzMax funktioniert das nicht: Expression of type 'Int' cannot be invoked. Bis auf Min und Max kann man diese Operatoren sogar ohne Klammern verwenden: BlitzMax: [AUSKLAPPEN] y = Abs -5 * Sgn Not 0 - Abs SizeOf y Das erklärt auch die "Überladbarkeit". Man kann nämlich z.B. Abs mit den Datentypen Int, Long, Float und Double verwenden. Das könnte ich meinem "MeinMin" nicht beibringen, weil BlitzMax keine Überladung beherrscht. In welcher Weise sind diese Operatoren dann aber Funktionen? Nun, viele davon sind als Funktion implementiert. Es gibt beispielsweise bbFloatAbs (für Float und Double), bbLongAbs (für Long) und bbIntAbs (für Int). Das sind drei Funktionen, die Abs für verschiedene Datentypen implementieren. Der Compiler generiert einfach für den jeweiligen Datentyp den richtigen Funktionsaufruf und es handelt sich wirklich um einen Funktionsaufruf, obwohl BlitzMax das verschleiert und nicht zulässt, dass die Funktion außerhalb einer Expression aufgerufen wird. Das bezieht sich auf Min, Max, Abs und Sgn. Und eines fehlt noch: Wenn BlitzMax die Rechnung schon zur Compilezeit auflösen kann, verschwinden diese Aufrufe natürlich. Der Code von oben (y = Abs -5 * Sgn Not 0 - Abs SizeOf y) kompiliert zu: Code: [AUSKLAPPEN] mov eax, 1
Die Rechnung fällt weg und es wird direkt die Zahl 1 geladen. Unterstützung für Long und Double In BlitzMax gibt es die Möglichkeit, 64bit Ganzzahlen (Long in BlitzMax) und 64bit double-precision Gleitkommazahlen (Double in BlitzMax) zu verwenden. In Assembler übrigens beide QWORD (weil gleichgroß). Long Zahlen werden dabei, um nicht bestimmte CPU-Techniken (MMX, SSE ...) vorauszusetzen, von normalen Prozessorbefehlen mit 32bit Registern verarbeitet (weil BlitzMax 32bit Programme generiert). Double Zahlen werden mit der FPU verarbeitet, die damit zurechtkommt, weil sie intern sogar mit 80bit Registern rechnet. Was haben beide jetzt miteinander zu tun? Die Operationen werden über Funktionen durchgeführt, die in brl.mod/blitz.mod/blitz_cclib.c definiert sind. Double und Float werden von denselben Funktionen bedient, die alle mit doppelter Genauigkeit arbeiten (der Name der Gleitkomma-Funktionen beginnt trotzdem immer mit bbFloat...) Da diese Prozeduren in C geschrieben sind, wirken die Funktionen natürlich ganz einfach. Beispiel: Code: [AUSKLAPPEN] void bbLongMul( BBInt64 *r,BBInt64 x,BBInt64 y ){
*r=x*y; } // oder double bbFloatSgn( double x ){ return x==0 ? 0 : (x>0 ? 1 : -1); } // sogar Int: int bbIntMax( int x,int y ){ return x>y ? x : y; } Hier eine Zusammenfassung (aus blitz_cclib.h): Code: [AUSKLAPPEN] int bbIntAbs( int x ); // Abs für Int
int bbIntSgn( int x ); // Sgn für Int int bbIntMod( int x,int y ); // Mod für Int int bbIntMin( int x,int y ); // Min für Int int bbIntMax( int x,int y ); // Max für Int void bbIntToLong( BBInt64 *r,int x ); // Int nach Long casten double bbFloatAbs( double x ); // Abs für Float und Double double bbFloatSgn( double x ); // Sgn für Float und Double double bbFloatPow( double x,double y ); // Potenzieren für Float und Double (3.14^2.0) double bbFloatMod( double x,double y ); // Mod für Float und Double double bbFloatMin( double x,double y ); // Min für Float und Double double bbFloatMax( double x,double y ); // Max für Float und Double int bbFloatToInt( double x ); // Float/Double nach Int casten void bbFloatToLong( BBInt64 *r,double x ); // Float/Double nach Long casten void bbLongNeg( BBInt64 *r,BBInt64 x ); // Long-Wert negieren (also negatives Vorzeichen) void bbLongNot( BBInt64 *r,BBInt64 x ); // binäres Nicht für Long (also ~, nicht Not) void bbLongAbs( BBInt64 *r,BBInt64 x ); // Abs für Long void bbLongSgn( BBInt64 *r,BBInt64 x ); // Sgn für Long void bbLongAdd( BBInt64 *r,BBInt64 x,BBInt64 y ); // Addition für Long void bbLongSub( BBInt64 *r,BBInt64 x,BBInt64 y ); // Subtraktion für Long void bbLongMul( BBInt64 *r,BBInt64 x,BBInt64 y ); // Multiplikation für Long void bbLongDiv( BBInt64 *r,BBInt64 x,BBInt64 y ); // Division für Long void bbLongMod( BBInt64 *r,BBInt64 x,BBInt64 y ); // Mod für Long void bbLongMin( BBInt64 *r,BBInt64 x,BBInt64 y ); // Min für Long void bbLongMax( BBInt64 *r,BBInt64 x,BBInt64 y ); // Max für Long void bbLongAnd( BBInt64 *r,BBInt64 x,BBInt64 y ); // binäres Und für Long (also &, nicht And) void bbLongOrl( BBInt64 *r,BBInt64 x,BBInt64 y ); // binäres Oder für Long (also |, nicht Or) void bbLongXor( BBInt64 *r,BBInt64 x,BBInt64 y ); // binäres Xor für Long (also auch ~) void bbLongShl( BBInt64 *r,BBInt64 x,BBInt64 y ); // Bitshift nach links (<<) für Long void bbLongShr( BBInt64 *r,BBInt64 x,BBInt64 y ); // Bitshift nach rechts (unsigned >>) für Long void bbLongSar( BBInt64 *r,BBInt64 x,BBInt64 y ); // arithmetischer Bitshift nach rechts (>>) für Long int bbLongSlt( BBInt64 x,BBInt64 y ); // x < y für Long int bbLongSgt( BBInt64 x,BBInt64 y ); // x > y für Long int bbLongSle( BBInt64 x,BBInt64 y ); // x <= y für Long int bbLongSge( BBInt64 x,BBInt64 y ); // x >= y für Long int bbLongSeq( BBInt64 x,BBInt64 y ); // x = y für Long int bbLongSne( BBInt64 x,BBInt64 y ); // x <> y für Long double bbLongToFloat( BBInt64 x ); // Long nach Float/Double casten Die meisten davon sind Operationen, von denen man meiner Meinung nach erwarten könnte, dass sie direkt implementiert sind, sie stehen aber in einer eigenen Funktion. Wahrscheinlich ist es übersichtlicher, es so zu implementieren, aber man muss dazusagen, dass bei solchen Int/Long/Float/Double-Operationen dann natürlich der zusätzliche Funktionsaufruf mehr Zeit in Anspruch nimmt, als wenn der Code inline wäre (was eventuell aber in mehr Maschinencode resultieren würde). Zusatz vom 10.02.2016 Konstruktoren und Finalizer Da ich gerade (gewollt) viel mit C++ zu tun habe, habe ich mir Gedanken darüber gemacht, wie BlitzMax Konstruktor und Finalizer handhabt (im Fall von Sprachen mit Garbage Collector spricht man nicht von Destruktoren, habe ich gemerkt). Also habe ich diese untersucht. BlitzMax: [AUSKLAPPEN] Type A Wichtige Erkenntnisse 0. Finalizer sind nicht deterministisch Das ist der nullte Punkt, weil ich denke, dass er bekannt ist. Aber zur Sicherheit will ich es erwähnen, dass es nicht bestimmt ist, wann der Finalizer eines Objekts aufgerufen wird. Ein Objekt wird vom GC dann freigegeben, wenn alle Referenzen auf es verschwunden sind. Das merkt der GC aber typischerweise nicht sofort nach dem es passiert ist. Also ist es nicht klar, wann der Aufruf passiert. 1. Keine Garantie für Finalizer Aufruf Finalizer oder Destruktoren sind dazu da, um ein Objekt anständig wegzuräumen. Also z.B. freigeben von Speicher, der nicht vom GC gemanaget wird. Allerdings gibt es in BlitzMax keine Garantie, dass der Finalizer eines Objektes jemals aufgerufen wird. Es kann durchaus passieren, dass Finalizer von bestimmten Objekten überhaupt nicht aufgerufen werden. Also sollte man sich nicht auf den Finalizer verlassen. 2. Die Finalizer nicht-konstruierter Objekte werden möglicherweise aufgerufen Wenn ein Objekt nicht konstruiert werden konnte (weil der Konstruktor gethrowt hat), ist das Objekt nicht valide. Dennoch ruft BlitzMax möglicherweise den Finalizer auf, was nicht dem entspricht, was ein C++ Entwickler erwarten würde. Also auch hier ist Vorsicht geboten. Wenn der Konstruktor throwt, oder eine Funktion aufruft, die throwt, dann ist das Objekt ungültig, es beginnt das stack unwinding, aber der Finalizer des Objekts wird möglicherweise noch aufgerufen und dann bekommt man möglicherweise einen Speicherzugriffsfehler um die Ohren. 3. Finalizer dürfen niemals throwen Wenn irgendein Finalizer im Laufe des BlitzMax-Programms aufgerufen wird und dieser oder eine Funktion, die er aufruft, throwt, dann wird an der Stelle, wo das Programm gerade ist (wo man es möglicherweise nicht erwartet), diese Exception rausgeworfen. Nochmal: An irgendeiner stelle im Programm, gerade dann, wenn der GC entscheidet, es wäre gut, ein bisschen Speicher freizumachen, und er ruft den Finalizer des Objekts auf, throwt er und die Exception ist da. Und wo kein Catch, dort führt das zum Beenden des Programms. Daher sollte man (das gilt auch in C++) immer darauf achten, dass Finalizer nicht throwen können. Schlusswort Das sind großteils Dinge, die ich mir selbst erarbeitet habe. Wenn ihr einen Fehler findet oder etwas, das suboptimal erklärt ist, würde ich gerne eure Meinung dazu hören! ![]() Wenn ihr selber noch etwas habt, das ihr beitragen wollt, wenn euch etwas fehlt, freue ich mich natürlich auch über Anregungen! Erweiterungen von meiner Seite folgen eventuell auch noch, wenn ich selbst auf noch etwas interessantes stoßen sollte. |
||
- Zuletzt bearbeitet von Thunder am Mi, Feb 10, 2016 2:10, insgesamt einmal bearbeitet
![]() |
DAK |
![]() Antworten mit Zitat ![]() |
---|---|---|
Das ist mal was wirklich interessantes. Danke für's Teilen!
Bist du zufällig auch drauf gekommen, warum Funktionsaufrufe so extrem langsam sind? |
||
Gewinner der 6. und der 68. BlitzCodeCompo |
![]() |
Thunder |
![]() Antworten mit Zitat ![]() |
---|---|---|
Danke für die Rückmeldung! ![]() Habe mir das angesehen mit den Funktionen (hast übrigens eine PN von mir) und ehrlich gesagt... die Funktionsaufrufe generell sind nicht das Problem. Die Optimierungen des BlitzMax-Compilers sind teilweise extrem gut (er lastet zuerst alle Prozessor-Register aus, bevor er eine lokale Variable auf dem Stack anlegt), aber teilweise generiert er auch Mist. Ich gehe von folgendem BlitzMax-Code aus: BlitzMax: [AUSKLAPPEN] SuperStrict Das gleiche in C: Code: [AUSKLAPPEN] #include <stdio.h>
#include <stdlib.h> #include <time.h> static int i; int getI(void){ return i; } void setI(int x){ i = x; } int main(int argc, char **argv){ int x, y; clock_t time; i = 0; time = clock(); for (x = 0; x < 8192; x++) { for (y = 0; y < 8192; y++) { setI(getI()+1); } } time = clock()-time; printf("Zeit: %d\n", time*1000/CLOCKS_PER_SEC); i = 0; time = clock(); for (x = 0; x < 8192; x++) { for (y = 0; y < 8192; y++) { i++; } } time = clock()-time; printf("Zeit: %d\n", time*1000/CLOCKS_PER_SEC); return 0; } Die beiden Funktionen getI() und setI() kompilieren bei BlitzMax zu: Code: [AUSKLAPPEN] _bb_geti:
push ebp mov ebp,esp mov eax,dword [_bb_i] jmp _16 ; unnötiger Sprung _16: mov esp,ebp pop ebp ret _bb_seti: push ebp mov ebp,esp mov eax,dword [ebp+8] mov dword [_bb_i],eax mov eax,0 ; unnötiges 'Return 0' jmp _19 ; unnötiger Sprung _19: mov esp,ebp pop ebp ret Die Zeilen, die weggelassen werden könnten, sind eingezeichnet. Zusätzlich könnte man auf einfache Weise zumindest in der Funktion getI() den Stackframe entfernen (also die ersten zwei Befehle push und mov sowie die vorletzten Befehle mov und pop). Außerdem legt gcc den Parameter für setI() anders auf den Stack: Code: [AUSKLAPPEN] ; gcc:
movl %eax, (%esp) ; vor der Schleife wird esp entsprechend angepasst call _setI ; BlitzMax: push eax call _bb_seti add esp,4 Das sind (meiner Meinung nach) die gravierendsten Unterschiede zwischen dem unoptimierten Assemblercode von gcc sowie dem Assemblercode von BlitzMax. Okay folgendes... Zitat: BlitzMax direkter Zugriff 2^26 Mal: ~ 220 ms
BlitzMax Zugriff über Funktion 2^26 Mal: ~ 650 ms gcc (unoptimiert) direkter Zugriff 2^26 Mal: ~260 ms gcc (unoptimiert) Zugriff über Funktion 2^26 Mal: ~440 ms Wenn man den Code von BlitzMax nimmt und ihn etwas von Hand optimiert und den Mist raushaut, der nur bremst und keinen Sinn macht, gewinnt man etwa 200 ms und liegt ungefähr beim gcc. Lässt man noch den Stackframe von getI() weg, kommt man auf etwa 350 ms und liegt vor dem gcc. gcc generiert zwar auch keinen optimalen Code (wenn man Optimierung einschaltet berechnet er das Ergebnis von i zur Compilezeit und lässt beide Schleifen weg!), aber man kann denke ich sagen, die Geschwindigkeit von unoptimiertem gcc Code ist in Ordnung. Es liegt einfach vieles daran, dass BlitzMax da und dort suboptimalen Code erzeugt. Und btw: es geht zwar hier um zwei- bis dreifache Geschwindigkeiten, aber um es nochmal hervorzuheben: 2^26 Aufrufe sind 67.108.864. Ich bin ehrlich gesagt richtig froh, dass Computer so schnell sind - immerhin: 67 Millionen Aufrufe brauchen wesentlich weniger als eine Sekunde. Was mir übrigens noch aufgefallen ist (und vielleicht editiere ich dazu noch was in meinen ersten Post, wenn ich mehr weiß): BlitzMax kompiliert If-Ausdrücke seeehr komisch! BlitzMax: [AUSKLAPPEN] If getI() = x Then If getI() = y Then blabla Das sehe ich mir noch genauer an! |
||
Meine Sachen: https://bitbucket.org/chtisgit https://github.com/chtisgit |
Übersicht


Powered by phpBB © 2001 - 2006, phpBB Group