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
subtitles created by c3subtitles.de
Join, and help us!