*32C3 Vorspannmusik*
Herald: Dann ist es mir jetzt eine ganz besondere Freude Matthias Koch
vorzustellen. Der wird über Compiler Optimierung für Forth im Microcontroller
sprechen. Bitte einen warmen Applaus für Matthias.
*Applaus*
Matthias: Guten Tag, Hallo. Wie gesagt Compileroptimierung für Forth im
Microcontroller und ganz zur Eröffnung habe ich eine Frage: Wer von euch kennt
Forth eigentlich schon? Okay, so etwa die Hälfte. Das bedeutet aber ich sollte
besser noch einmal kurz erklären was diese Sprache eigentlich ausmacht. Es ist
eine Sprache die auf dem Modell eines Stacks basiert. Vielleicht kennt ihr
die umgekehrte polnische Notation wo es so ist das erst die Parameter kommen und dann
die Operatoren. Man legt also Werte auf den Stack und von oben kommen die
Operatoren, nehmen sich ewas und berechnen ewas und legen es wieder darauf. Es gibt
noch einen zweiten Stack, den Return-Stack worüber die Rücksprungadressen
gehandhabt werden. Das bedeutet man kann nacheinander die verschiedenen Operatoren
aufrufen und muss nicht wie bei C den Stackframe hin und her kopieren. Der
Compiler selbst ist sehr simpel aufgebaut. Es basiert darauf, dass man den Eingabestrom
was der Mensch tippt, in Wörter zerhackt, also ein Space, Tabulator, Zeilenumbruch.
Wenn ein Wort gefunden wird, wird es in der Liste der bekannten Wörter gesucht
entweder ausgeführt oder compiliert und wenn es nicht gefunden werden kann wird es
als Zahl interpretiert, auf den Stack gelegt, oder es wird etwas kompiliert um
diese Zahl dann später auf den Stack zu legen. Das war eigentlich schon die
Sprache. Das Besondere daran ist, dass sie klein genug ist, dass sie in einem
Mikrocontroller installiert werden kann. Was dazu führt das man dann mit einem
Terminal mit seinem Chip sprechen kann und von innen heraus ausprobieren, ob der
Hardwarecode funktioniert, weil man dann nicht viele kleine Testprogramme schreiben
muss, sondern ganz einfach von Hand an den Leitungen wackeln kann und alle
Definitionen die man geschrieben hat auch sofort interaktiv ausprobieren kann. Das
führt dann auch dazu, dass die Definitionen natürlich gleich in der
Hardware läuft und auch gleich mit Echtzeit, so dass man die Fehlersuche
stark vereinfachen kann. Das ist so ein bisschen eine Einführung in Forth.
Ich selbst habe diese Sprachen nicht erfunden, die gibt es schon seit mehr als
einem halben Jahrhundert.
Aber ich habe Compiler geschrieben für MSP430, ARM Cortex M0, M3
und M4, M7 ist in Planung und es gibt noch eine Variante, die in Zusammenarbeit mit dem
Bowman gemacht habe, die auf einem FPGA läuft. Aber dazu ein bisschen mehr
später. Eigentlich ist das ungewöhnlich sich selbst vorzustellen aber meine
Freunde meinen das sollte man sagen. Ich bin Diplomphysiker und habe Physik mit
Nebenfach Gartenbau studiert, bin gerade in meiner Doktorarbeit in der Laserspektroskopie
habe gemischte Interessen durcheinander, besonders Radionavigation
und meine Lieblingssprachen kann man hier sehen.
Der Name mag vielleicht etwas ungewöhnlich sein, aber die erste unterstützte
Plattform war der MSP430 von MSP und "écris" aus dem französischen kam der
Name dann. Überschreibt den MSP430 weil es da innen drin ist und man schreibt
direkt hinein. Unterstützt dann alle MSP430-Launchpads, sehr viele ARM Cortex
Architekturen und mit dem FPGA ein bisschen mehr. Die klassischen
Architekturen, also die auf denen Forth normalerweise implementiert worden ist
– so geschichtlich – waren eine virtuelle Maschine. Wo eine Liste von Pointern
drinen war, wo nacheinander diese Pointer genommen worden sind und dann entweder wie
eine Liste von Pointern zählten oder aber eben ein Assemblerprimitive.
Natürlich ist das sehr schön, wenn man dann Fehler suchen möchte im Compiler, es
wird sehr einfach dadurch und es lassen sich auch einige ungewöhnliche Sachen
dabei implementieren. Aber es frisst viele viele Taktzyklen. Die ganz alten
Systeme hatten sogar die Möglichkeit noch einmal über eine Tabelle die ganzen
verschiedenen Pointer umzuleiten, so dass man Definitionen die schon liefen
nachträglich noch ändern konnte, also eine Definition ganz tief im System durch
etwas neues austauschen. Die wird dann sofort auch verwendet. Das ging mal.
Ausserdem lässt sich Forth sehr leicht dekompilieren, zumindest bei der
klassischen Implementation, so das bei einer Jupiter Ace. Man braucht ja keine
Quelltexte, man hatte den Objektcode, man konnte ihn desassemblieren zurück in den
Quelltext, ändern und neu kompilieren fertig. Die Optimierungen, die ich jetzt
vorführen werde, zerstören diesen interessanten Aspekt, weil da Maschinencode
heraus kommt und weil da auch Teile weg optimiert werden.
Es gibt also nicht mehr so den eins zu eins Zusammenhang zwischen dem was man
geschrieben hat und dem was hinterher tatsächlich im Prozessor ausgeführt wird.
Anders herum jedoch, hatte Forth lange auch mit dem Problem zu kämpfen,
dass es immer auch ein bisschen langsam galt. Das habe ich geändert. Und ich möchte
gerne heute vorstellen was für Optimierungen man im Chip selbst
durchführen kann, natürlich aus der Compilertheorie heraus sind das alles alte
Hüte. Aber die meisten Compiler brauchen sehr viel Speicher, laufen auf einem PC wo
es ja fast unbegrenzt davon gibt. Ich möchte aber einmal herausfinden welche
Optimierungen man in den Arbeitsspeichern eines kleinen Microcontrollers
implementieren kann. Dazu gehören Tail-Call, Konstantenfaltung, Inlining,
Opcodierung und die Registerallokation. In welcher Architektur diese jeweils
implementiert sind steht da mit bei. Nun will ich die ganzen einzelnen
Optimierungen einmal vorstellen. Tail-Call ist relativ simpel. Wenn das letzte in
einer Routine ein Call ist und danach direkt ein Return kommt, dann kann man das
auch durch einen Sprungbefehl ersetzen. man braucht dann nicht den Umweg über den
Stack zu nehmen. Das ist eigentlich so weit klar, nichts besonderes.
Konstantenfaltung bedeutet: Manchmal möchte man als Mensch etwas so schreiben,
dass man ein paar Konstanten nimmt, die multipliziert, zusammen verodert; all das
immer mit zu kompilieren wäre ja eigentlich Zeitverschwendung. Es steht ja
schon während der Kompilierung fest, was das Ergebnis dieser Berechnung sein wird,
der Compiler kann also durchaus diese Rechnung schon während dem kompilieren
durchführen, nur noch das Ergebnis einkompilieren. Hier sieht man mal ein
kleines Beispiel, also Sechs auf dem Stack legen, Sieben auf den Stack legen, beide
Werte vertauschen, miteinander multiplizieren und das ganze dann
aufsummieren. Eigentlich stehen die ersten Teile schon fest, das reicht wenn man 42
plus kompilieren würde. Das ist die Konstantenfaltung. Das ist jetzt ein
glasklarer Fall, wo man das direkt sieht, aber manchmal gibt es auch aus anderen
Optimierungen heraus noch die Möglichkeit, dass Konstantenfaltungen
möglich werden kann. Das ist dann nicht mehr ganz so offensichtlich. Dazu, wie das
implementiert wird, möchte ich erst einmal kurz zeigen wie der klassische
Forth Compiler implementiert worden ist. Es war so, dass der Eingabestrom den der
Benutzer eingetippt hat in einzelne Wörter, in Tokens zerhackt worden ist,
dann wurde geprüft ob es in der Liste der bekannten Definitionen auftaucht, oder
eben auch nicht. Ist das der Fall, ist die Frage ob kompiliert werden soll oder
nicht. Es gibt einen Ausführ- und einen Kompiliermodus, der eine interaktive
Sprache. Im Kompiliermodus und was nicht immer geht das – auch eine Spezialität
von Forth. Dann wird ein Aufruf in der Definition einkompiliert, also ein Call
Befehl geschrieben. Immediate bedeutet übrigens, dass etwas das kompiliert
werden soll selbst ausgeführt wird. Das braucht man für Kontrollstrukturen, die
dann noch so Sprünge handhaben müssen und ähnliches und ansonsten ist man im
Ausführmodus, wird die Definition ausgeführt. Nichts gefunden: Versucht man
es als Zahl zu interpretieren und je nachdem ob kompiliert werden soll oder
nicht, wird auf den Stack gelegt oder es wird etwas kompiliert, was die Zahl dann
bei der Ausführung auf den Stack legen wird. Wenn es auch keine gültige Zahl
ist, ist es ein Fehler. Um dort Konstantenfaltung einzfügen sind keine so
großen Änderungen nötig. Wichtig ist jetzt eigentlich nur, dass man die
Konstanten nicht kompiliert, zumindest nicht gleich, sondern erst einmal sammelt
und dann wenn eine Operation kommt die bei konstanten Eingaben auch konstante
Ausgaben produziert, diese dann auszuführen. Die Änderungen sind so,
dass jetzt ganz am Anfang der aktuelle Stackfüllstand gemerkt werden muss, denn
man muss ja auch wissen, wie viele Konstanten gerade zur Verfügung stehen.
Soll es ausgeführt werden, wurde es gefunden, dann brauchen wir keine
Konstantenfaltung machen, dann schmeißen wir den Pointer wieder weg, alles gut,
vergessen wir. Sind wir im Kompiliermodus, wird geprüft ob diese Definition mit
konstanten Eingaben auch eine konstante Ausgabe produzieren kann und wenn ja, sind
genug dafür vorhanden. Eine Invertierung einer Zahl braucht eine Konstante. Eine
Addition braucht zwei Konstanten usw. das muss geprüft werden. Wenn das gut geht
kann sie ausgeführt werden. Sie lässt dann die Ergebnisse dort liegen und
dadurch, dass wir wusten wo der Stackpointer vorher gewesen ist, wissen
wir auch wie viele Konstanten danach noch auf dem Stack liegen geblieben sind. Es
kann also durchaus variabel viele Ausgabekonstanten produzieren. Ist diese
Definition jedoch nicht faltbar, dann bleibt uns nichts anderes übrig, als
alles was dort an Konstanten liegt einzukompilieren und dann einen
klassischen Call Befehl zu machen. Ja, aber man kann den klassischen Callbefehl
auch nochmal ein bisschen abwandeln. Man kann kucken ob da eine sehr kurze
Definition ist und dann Opcodes direkt einfügen und bei Forth natürlich
Imediate überprüfen was bedeutet, dass diese Definition selber irgendwelche
Spezialfälle umsetzen kann. Nicht gefunden, Zahlen bleiben stets auf dem
Stack liegen, damit sie halt später in die Konstantenfaltung rein kommen können.
Wichtig dabei ist zu wissen, dass dabei, während die Zahlen gesammelt werden, ja
schon ein Merker in den Stack gesetzt wurde um den Füllstand zu bestimmen. Ist
es nicht als Zahl zu interpretieren, ist es ein Fehler. Das ist im Prinzip der
Kerngedanke um Konstantenfaltung in Forth zu implementieren. Das hier ist
grundsätzlich auf jeder Architektur möglich wo Forth läuft und ist auch
relativ unabhängig davon wie das Forth innen drin implementiert hat. Ich habe
schon gesehen, dass jemand Matthias Troote (?) von der MForth für AVR, angefangen hat
das auch einzubauen und das noch zusammen recognizern. Also es geht recht
gut, es ist auch Standardkonform. Die nächste Sache: Inlining. Klar macht der
C-Kompiler auch. Kurze Definitionen die nur ein paar Opcodes haben, können mit
einigen Vorsichtsmaßregeln auch direkt eingefügt werden, denn wozu sollte man
einen Call wohin tun, wenn die Opcodes kürzer sind als der Call selbst. Und hier
das Beispiel von Plus. Man ruft nicht die primitive vom Plus auf wenn man den
Plus-Opcode direkt einfügen kann. Das leuchtet eigentlich auch ein.
Opcodierungen - ich nenne es mal so, ich weiß nicht wie es sonst genannt werden
soll – ist, wenn ein Opcode eine Konstante direkt in sich aufnehmen kann. Dann ist es
doch sinnvoller die Konstante direkt mit zu opcodieren, als sie über den Stack zu
legen und dann darüber zu verwenden. Man spart halt ein paar Takte und ein bisschen
Platz. Das hängt davon ab was für einen Prozessor man hat. Beim MSP430 geht das
immer wunderbar, bei einem Cortex manchmal, der hat nur einige Opcodes die
Konstanten können und wenn man einen Stackprozessor hat, geht das gar nicht.
Und der Regsiterallokator schließlich, ist die Überlegung, dass man
Zwischenergebnisse, die bei Forth traditionell auf dem Stack liegen würden,
versucht auf Register abzubilden. Denn klar in der Stacksprache ist das ganz
etwas anderes als einen Prozessor der hauptsächlich mit Registern arbeitet.
Beim ARM Cortex ist das ganz besonders schlimm, denn er kann nicht direkt in den
Speicher zugreifen um da irgend etwas zu berechnen, sondern er muss auf jeden Fall
immer aus dem Speicher in Register holen, im Register etwas machen und in den
Speicher zurück schreiben. Das ist ziemlich aufwendig. Wenn man das abkürzen
kann, die Zwischenergebnisse gleich im Register hält, kann man viel kürzere
Befehlssequenzen nutzen, die direkt zwischen den Registern arbeiten.
Wichtig dabei ist noch, dass das ganze transpartent wird für den Programmierer,
wenn man also etwas macht, wo die logische Struktur des Stacks sichtbar wird oder
sichtbar werden könnte, muss der Compiler auf jeden Fall zurück fallen und alles in
den richtigen Stack rein schreiben, so dass man dann auch direkt im Stack
Manipulation machen kann, wenn das denn mal notwendig ist und das ist bei Forth
ziemlich häufig, weil Forth Programmierer gerne alle möglichen Tricks anwenden.
Das wesentliche für den Registerallokator ist, zu wissen wo welches Element gerade
ist, man muss also während der Kompilierung ein Stackmodell mit laufen
lassen, worin vermerkt ist, wo diese Stack- elemente eigentlich gerade sind. Sind sie
noch auf dem Stack selbst, also im Arbeitsspeicher? Sind sie gerade in einem
Register drin? Wenn ja, in welchem? Oder, wenn neue Zwischenergebnisse auftreten:
Haben wir noch genug Register? Denn wenn mehr Zwischenergebnisse da sind als
Register zur Verfügung stehen, dann müssen die Register wieder in den
Arbeitsspeicher auf den Stack geschrieben werden und das ist es was innen drin das
besondere ausmacht. Man kann es sehr klein implementieren, aber man muss daran
denken, dass das sehr seltsam ist, dass das im Microcontroller läuft,
normalerweise gibt es bei Register- allokatoren viele Algorithmen drum herum,
die überlegen, wie man das möglichst gut über möglichst weite
Strecken im Programm machen kann. Ich habe es sehr einfach gemacht. An den Stellen wo
Kontrollstrukturen verzweigen hört man einfach auf. Man schreibt dann alles in
den Stack und fertig. Ist eine sehr simple Implementation und globale Optimierung
habe ich gar nicht drin. Aber es ist ein Anfang. Das sind jetzt all die
Optimierungen die angesprochen werden sollen. Nun will ich ein paar Beispiele
dafür zeigen. Erst einmal muss ich aber noch sagen: Mercrisp-Ice ist nicht allein
meine Arbeit sondern basiert auf vielen vielen anderen schönen Sachen die auch
vorgestellt worden sind. James Bowman hat den J1 Prozessor entwickelt. Clifford
Wolf, Cotton Seed und Mathias Lasser haben die erste freie Toolchain für FPGAs
entwickelt und darauf basiert das alles. Da drin habe ich die Konstantenfaltung,
automatisches Inline kurzer Definitionen und Tail-Call-Optimierung. Hier ist jetzt
mal ein kleines Beispiel. Das ist noch aus der LEDcom Implementation, wie man
über eine Leuchtdiode kommunizieren kann. Für die die jetzt nicht bei der Assembly
gesehen haben, es ist so, dass man eine Leuchtdiode nicht nur zum Leuchten sondern
auch als Fotodiode nutzen kann und wenn man das schnell hintereinander abwechselt
leuchtet und kucken wie hell es ist, hat man eine serielle Schnittstelle über eine
Leuchtdiode. Was natürlich auch dazu führt, wenn man den Compiler auch noch im
Chip hat, dann kann man über die Power On Lampe seiner Kaffeemaschine neue
Brühprogramme einspeichern und Fehlermeldungen auslesen. Aber das ist
jetzt noch etwas anderes, das nur so nebenbei.
*Gelächter* Kucken wir uns das jetzt einmal genauer an.
Als erstes werden Konstanten definiert für Anode und Kathode, wo die gerade
angeschlossen sind und dann eine Definition, – "shine" soll sie heißen –
wo die Anode und die Kathode beide als Ausgang gesetzt werden und die Anode
"high". Wenn man sich das jetzt einmal disassembliert ansieht ist da schon
einiges passiert. Als erstes "Anode, Kathode or". Ist zu einer einzigen Konstante
Hex F zusammen gefasst worden. Das war die Konstantenfaltung. Dann als
nächstes, ganz unten, das letzte wäre ein Call um etwas zu speichern im io-Teil.
Dort wird jetzt ein Jump und kein Call eingefügt, das war die
Tail-Call-Optimierung. Ist das soweit noch ganz klar?
Hier kann man noch einmal das Inlining sehen,
denn an der Stelle hier, Kathode AND, das "AND" wurde auch direkt
eingefügt als Alu-Opcode und wurde nicht als Call eingefügt und dann darum herum
natürlich wieder die üblichen Verdächtigen. Unten passiert Tail-Call
und für die Konstantenfaltung habe ich nochmal ein kleines Beispiel und zwar das
was ich ganz am Anfang hatte, wie das aussieht. Ganz einfach: Es wird
ausgerechnet vom Compiler schon während des kompilierens, die Konstante wird
geschrieben. Der Stack-Prozessor kann keine Konstante in Opcodes mit einbauen,
also gibt es da keine weitere Optimierung mehr. Dann kommt plus. Plus ist drin im
Prozessor und der J1 hat noch die Möglichkeit auch gleich den Rücksprung
mit im Opcode zu haben. Fertig. Tail-Call im Prinzip auch erledigt. So. Zum J1
Prozessor kann man viel erzählen, ich will nur kurz sagen, er ist sehr klein,
sehr verständlich - das sind 200 Zeilen Verilog, es lohnt sich wirklich sich das
mal anzukucken. Schaut mal rein, wenn ihr euch dafür interessiert. MSP430, das
ist ein Prozessor, der sehr viele verschiedene Adressierungsarten
unterstützt und auch eigentlich recht gut zu Forth passt. Mit Tail-Call gab es so
ein paar Probleme, weil es einige Tricks gibt, die mit den Rücksprungadressen
etwas machen und dann knackst es. Also habe ich keinen Tail-Call drin. Aber
Konstantenfaltung, Inlining und Opcodierung sind drin. Hier noch ein paar
Beispiele. Hier kann man wieder sehen, es werden Konstanten definiert und ganz am
Ende sollen dann wieder Leuchtdioden angesteuert werden, soll der Taster
vorbereitet werden, hat man nur Initialisierung für so ein Launchpad.
Das sieht kompiliert so aus. Es tritt mehreres in Aktion. Die Konstanten kommen wieder
über die Konstantenfaltung und diese Befehle werden über Inlining eingebaut
und dadurch, dass sie direkt Parameter in den Opcode übernehmen können, kriegen
sie auch die Opcodierungen, so, dass das was hinterher heraus kommt eigentlich das
gleiche ist was ich auch in Assembler schreiben würde. Man sieht es auch immer,
die Zahlen immer nur ein Opcode, das war es. Das letzte ist übrigens der
Rücksprung, der sieht immer bisschen komisch aus, aber das funktioniert.
Mecrisp-Stellaris ist eine direkte Portierung von Mecrisp auf einen ARM
Cortex M4 gewesen. Stellaris-Launchpad war die erste Plattform die unterstützt war.
Der Name klingt gut, habe ich so gelassen. Eigentlich identisch mit dem MSP430, wenn
es um Optimierung geht. Aber ich habe jetzt gerade noch (?) müssen noch /
werden noch fertig geworden, einen Registerallokator rein bekommen, den
möchte ich noch kurz zeigen. Hier sieht man ein Beispiel was schon ein bisschen
schwieriger ist. Das oben ist der gray Code, der gray Code ist so eine Sache,
wenn man darin zählt, ändert sich immer nur eine Bitstelle. Na gut, aber darum
soll es jetzt nicht gehen, sondern darum, dass man hier sieht, das keine
Stackbewegungen mehr da sind. Das oberste Stackelement ist im ARM in Register 6 enthalten
und die Zwischenergebnisse, also duplicate legt das oberste Element nochmal auf den
Stack, dann kommt die Eins; man sieht schon, der Schiebebefehl hat als
Zielregister ein anderes Register, also ein reines Zwischenergebnisregister und
exklusiv-oder nimmt es von da und tut es wieder auf das oberste Stackelement,
so dass man gar kein Stackbewegung mehr braucht und das Quadrat genauso. Das ist
eben die Sache, dass man versucht Zwischenergebnisse in Registern zu halten,
soweit möglich. Das hier ist ein klein bisschen aufwendigeres Beispiel. Hier ist
es so, das Variablen geholt werden sollen, zwei Stück, addiert und wieder zurück
geschrieben. Im ARM Cortex kann man übrigens einen Offset an jeden Ladebefehl
daran fügen. Am Anfang wird also die Adresse der Variablen geladen, dann wird
die erste Variable geholt, dann die zweite, beide werden addiert und zurück
geschrieben. Wieder keine Stackbewegungen nötig.
Wer jetzt ein bisschen neugierig geworden ist und sofort loslegen möchte:
Alle Launchpads von Texas Instruments
werden unterstützt, die ARM Cortex, viele davon, von STM, Texas Instruments,
Infineon neuerdings und Freescale und wer andere Systeme benutzt, kann natürlich
auch gerne Forth ausprobieren. Es gibt Gforth für den PC, AmForth für die Atmel
AVR-Reihe, für PIC gibt es FlashForth und für den Z80 und einige andere CamelForth.
Ganz ganz viele verschiedene. Es kommt nämlich daher, das dadurch, dass Forth
recht klein ist und recht leicht implementiert werden kann, dass viele
Leute zum kennen lernen der Sprache einfach selber ihren eigenen Compiler
implementieren. Das macht Spaß und ich denke – auch wenn man mir jetzt dafür den
Kopf abreißen wird, in einigen Kreisen – man möge es tun. Denn dabei lernt man
sehr viel über das Innere kennen. Andere sagen natürlich man soll sich erst einmal
mit der Philosophie der Sprache auseinander setzen. Sei es drum, beide
Seiten haben ihre guten Argumente. Ich muss sage, ich habe direkt mit dem
Schreiben meines ersten Compilers begonnen und stehe nun ja. Ich habe noch einige
Beispiele mitgebracht und ich habe noch ein bisschen Zeit über. Das hier
generiert Zufallszahlen mit dem Rauschen des AD-Wandler der einen Temperatursensor
trägt. Das ist einfach eine kleine Schleife, die 16 mal durchläuft und es
wird jeweils aus dem Analogkanal zehn im MSP430 ein Wert gelesen, das unterste Bit
wird maskiert, dann hinzu getan zu dem was man schon hat und das nächste Bit. Das
ist das wie es kompiliert worden ist. Als erstes werden die Schleifenregister frei
gemacht, dann wird eine Null auf den Stack gelegt, wenn man es sich hier nochmal
ankuckt, eine Null ganz am Anfang wurde da schon hingelegt, also der Wert wo dann
hinterher die Bits aus dem AD-Wandler rein kommen. Dann, der Shiftbefehl wurde per
Inlining eingefügt, dann kommt die Konstante Zehn auf den Stack. Leider gibt
es nur einen Push-Befehl im MSP430, also hier die Kombination aus Stackpointer
erniedrigen, etwas darauf legen, dann wird analog klassisch ausgeführt mit einem
Callbefehl und anschließend wieder Inlining und Opcodierungen. Das Maskieren
des unteren Bits ist nur noch ein Opcode, Xor genauso und kann direkt eingefügt
werden. Dann wird der Schleifenzähler erhöht, verglichen und die Schleife
springt zurück. Wenn die Schleife fertig ist, Register zurück holen, Rücksprung.
Hier hat man mal die ganzen Optimierungen alle in einem gesehen, wie das in einem
echten Beispiel aussieht, weil das davor ja doch so ein bisschen gestellt gewesen ist
um es schön zu zeigen. Das hier ist ein etwas größeres Beispiel auf
dem ARM Cortex. Die Bitexponentialfunktion ist so etwas wie eine Exponentialfunktion,
die aber auf Integer funktioniert. Und hier kann man auch nochmal verschiedene
Sachen sehen wie das im ARM Cortex aussieht und was passiert wenn
Kontrollsturkturen dazwischen kommen. Ganz am Anfang wird verglichen ob der Wert eine
bestimmte Größe erreicht hat. Dann, dieses "push lr" kommt daher, dass im ARM
Cortex so ein Link-Register existiert, der dafür da ist, dass
Unterprogrammeinsprünge die keine weitere Ebene haben direkt im Register bleiben und
nicht auf den Return-Stack gelegt werden. Wenn aber Kontrollstrukturen kommen und
noch nicht klar ist ob in einem der zwei Zweige vielleicht doch noch ein
Unterpgrogramm aufgerufen werden muss, muss er jetzt gesichert werden. Dann zeigt
der Sprung der zum "if" gehört, eine Zahl wird herunter geworfen und eine Neue rein
gelegt, was aber im Endeffekt nur ein Ladebefehl ist, weil ja der Register Top
of Stack ohnehin schon die ganze Zeit bereit gelegen hat. Im else Zweig ist ein
bisschen mehr zu tun. Das duplicate brauchen wir nicht. Ein Registerallokator
dahinter. Dann der Vergleich, wieder das "if", hier bei "1 rshift" kann man wieder
sehen, dass das alles in einen Opcode zusammen gefügt worden ist, das ist
wieder Kombinationen aus Konstantenfaltung und Inlining. Dann, der else-Zweig, so,
hier ist ein bisschen mehr zu tun. Man kann jetzt auch sehen, dass mehrere
Zwischenergebnisse im Register auftreten. Also r3 und r2, beide mit dabei, die Werte
werden jetzt nicht mehr auf den Stack gelegt, sondern zwischen den Registern hin
und her geschoben. Vielleicht wird das jetzt ein bisschen unübersichtlich, aber
ich denke, wenn man das direkt vergleicht, ich habe immer dort wo Assembler steht
auch den Forth Quelltext daneben und das sind die Opcodes die jeweils
für die Zeile generiert werden. Was man hier noch sehen kann,
dass man im ARM Cortex leider nicht eine Konstante in den einzelnen Befehl mit
einfügen kann. Deswegen wird es über ein anderes Register geladen. Aber, andere
Sachen – wie der Shiftbefehl – können Konstanten direkt übernehmen. Das ist
hier passiert und ganz am Ende muss aufgeräumt werden. Bislang war das kein
Problem, weil das oberste Element immer in r6 geblieben ist. Jetzt aber wurden durch
die Zwischenergebnisse und das hin und her jonglieren, der Fall erreicht, dass das
oberste Element auf dem Stack eben nicht mehr in dem Register ist der normalerweise
das oberste Element trägt. Deswegen der vorletzte Befehl, der move-Befehl dient
zum aufräumen. Der hat kein Äquivalent in Forth, aber er dient dazu, das
Stackmodell was gerade in einem Zustand ist wie es sonst nicht sein sollte wieder
auf den kanonischen Stack zurück zu führen, damit an der Schnittstelle alles
sauber übergeben werden kann. Wohl gemerkt, globale Optimierungen gibt es
noch nicht, wird es wohl auch erst einmal nicht geben, aber man kann schon einmal
sehen, dass man die gesamte Sache ohne Stackbewegung geschafft hat, mal davon
abgesehen, das der Returnstack einmal die Adresse zum Rücksprung aufgenommen hat,
was ich aber nicht vermeiden konnte, weil dieser Compiler nicht voraus schauen kann.
Er kann immer nur sehen, wo er gerade ist und versuchen dafür zu generieren. Um das
weglassen zu können, müsste man dann vorausschauen können um zu sehen was
in den Kontrollstrukturen vielleicht doch noch passiert.
Damit bin ich am Ende angelangt, alle Beispiele gezeigt. Kann
ich nur noch wünschen: Alles Gute zum neuen Jahr! Wenn dann noch Fragen sind,
kommt gleich nochmal zu mir, schreibt mir, ich freue mich über viele E-Mails.
Vielen Dank.
*Applaus*
Herald: Okay, alles klar, vielen Dank Matthias.
*Abspannmusik*