< Return to Video

Matthias Koch: Compileroptimierungen für Forth im Microcontroller

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

more » « less
Video Language:
German
Duration:
26:57

German subtitles

Revisions