Hier nochmals der P-Code:
Zahl 41 02 00 00 00 00 fp->intpeek int->fp push Zahl 40 01 00 00 00 00 movepull add fp-> int
Im ersten Schritt faßt der Optimierer die Zahl »41 02 00 00 00 00 fp->int
« zusammen, indem er den Wert in die Integer-Darstellung umwandelt. Daraus entsteht:
Izahl 200 peek int->fp push Zahl 40 01 00 00 00 00 movepull add fp->int
Anmerkung: Es gibt zwei Fp-Accus, $D4 bis $D9 und $EO bis $E5. Integerzahlen stehen dabei im Prozessorregister A und Y. Dabei enthält Y das höherwertige und A das niederwertige Byte.
Der Optimierer faßt jetzt »Izahl 200 peek
« zu »lpeek 200
« zusammen. Das erste Beispiel würde »LDA # <200:LDY # >200:JSR PEEK
« als Code erzeugen und das zweite Beispiel »LDA 200:LDY #0
«. Jetzt haben wir also:
Ipeek 200 int->fp push Zahl 40 01 00 00 00 00 movepull add fp-> int
Jetzt wird »push Zahl … movepull add
« zu »add # …
« zusammengefaßt. Daraus ergibt sich:
Ipeek 200 int->fp add # 40 01 00 00 00 00 fp->int
Als nächstes bemerkt der Optimierer die Folge »int->fp Add # … fp->int
«
und versucht diese Befehlsfolge zu vereinfachen. So spart man sich
später eine Umwandlung. Da die Zahl kleiner als 256 ist, gelingt dies
auch.
Jetzt hat der Optimierer seine Arbeit geleistet. Anschließend wird richtiger Maschinencode erzeugt.
Aus »lpeek 200
« entsteht »LDA 200:LDY #0
«, »Iadd # 1
« wird zu »LDX #1:JSR IADD
« (dieser Unterprogrammaufruf ist nötig, da sonst auch ein Overflow stattfinden könnte; Integerwert größer als 65535!)
Das war auch schon alles. An den Befehl COLOR
wird noch »STA $C8
« angehängt. Insgesamt ergibt sich also:
LDA 200 LDY #0 LDX #1 JSR IADD STA $C8
Ohne Optimierung würde wesentlich mehr Programmcode erzeugt werden, der noch dazu viel langsamer ist:
LDA #<Z20O LDY #>Z20O JSR LDOYA JSR FPI? JSR PEEK JSR IFP JSR PUSH LDA #<Z1 LDY #>Z1 JSR MOVEPULL JSR ADD JSR FPI? STA $C8
Außerdem noch im Konstantenbereich:
Z200 .BYTE $41,$02,$00,$00,$00,$00 Z1 .BYTE $40,$01,$00,$00,$00,$00
Durch
Optimierung läßt sich also viel Programmcode sparen. Gegenüber einem
echten Maschinencode sieht das Ergebnis allerdings immer noch recht
umfangreich aus. In Maschinensprache würde nämlich »INC $C8
«,
was nur 2 Byte benötigt, das gleiche bewirken. Hexadezimal $C8
entspricht dabei dem Dezimalwert 200. In unserem Beispiel codiert der
Compiler den Basicbefehl COLOR 1
jedoch optimal, nämlich »LDA #1:STA $C8
«.
Allerdings kann der Compiler nur in den seltensten Fällen optimalen Code erzeugen. Schließlich erübrigt sich nur die Umwandlung von FP nach INT und umgekehrt und dies auch nur bei Verwendung von Konstanten. Bei Variablen ist diese Art der Optimierung nicht zulässig. In den meisten Fällen reicht schon die Einsparung einiger Bytes, weil zum Beispiel das Laden einer Variablen und die Umwandlung in einen statt zwei Integerwerte durch einen einzigen Unterprogrammaufruf erledigt wird.
Auch wird nicht alles, was möglich wäre, optimiert. Zum Beispiel berechnet sich »A=10/3
«
nicht im voraus, obwohl es durchaus sinnvoll wäre. Aber solche
Optimierungen kann der Programmierer gegebenenfalls selbst
vorwegnehmen. Allerdings sind solche Fälle viel zu selten, als daß sich
dies hier lohnen würde. Je länger nämlich der Compiler, desto kürzer
die entsprechenden Basic-Programme. Es gibt jedoch einige Berechnungen,
deren Optimierung sich besonders lohnt: »^2
« erhält eine eigene, schnelle Routine. Im Compiler benötigt »A=B^2
« die gleiche Zeit wie »A=B*B
«.
Es existiert ebenfalls eine schnelle Exponential-Routine, die bereits
im Mathematik-Teil der Runtime-Bibliothek vorhanden ist. So oder
ähnlich werden alle Befehle mit Parametern behandelt. Sind mehrere
Parameter vorhanden, werden diese natürlich zwischengespeichert.
Ein weiterer lohnenswerter Befehl ist der POKE
-Befehl. Beispielsweise wird »POKE 16,64
« durch »LDA #64:STA 16
« übersetzt.
Beim PRINT
-Befehl sind noch die Kommata zu beachten. Auch INPUT
, READ
, GET
und LOCATE
beinhalten noch einige Probleme. Eine ausführliche Erläuterung würde den Rahmen dieses Artikels jedoch sprengen.
Gesondert zu behandeln sind alle Befehle, die den linearen Programmablauf beeinflussen. Der GOTO
-Befehl
läßt sich vergleichsweise einfach übersetzen. Zuerst wird die Routine
aufgerufen, die einen Integer-Ausdruck übersetzt und optimiert (wie
oben); allerdings generiert sie ihn noch nicht in den endgültigen
Maschinencode. Zunächst wird nur der P-Code erzeugt. Dann wird anhand
des P-Codes überprüft, ob eine Zahl folgt. Wenn ja, wird ein JMP
-Befehl compiliert. Zunächst nur »07 Zeile
«, woraus sich nach Paß 2 »4C Adresse
« ergibt. Wenn nein, wird die Routine für die Erzeugung von Maschinencode aufgerufen und »JSR GOTOX
« angehängt. Bei einem berechneten GOSUB
wird beispielsweise entweder »JSR GOSUBX
« oder vor dem »JMP-($07)
«-Befehl der GOTO
-Anweisung
ein Unterprogrammaufruf compiliert. Dieser enthält dann, auf dem
Basic-Stack, die um drei Werte erhöhte Rücksprungadresse des Befehls.
Es wird aber nicht einfach ein JSR
compiliert, da sonst die Anzahl der Unterprogrammebenen durch den zu
kleinen 6502-Stack begrenzt wäre. Außerdern müssen die Parameter für
die FOR
-NEXT
-Schleifen
ebenfalls auf diesem Stapel abgelegt werden. Sie belegen jeweils 16
Byte, so daß sehr schnell ein Overflow des 6502-Stack drohen würde.
Dies könnte katastrophale Folgen haben und die Kompatibilität zum
Interpreter-Basic stark einschränken. Zumindest ist die untere Hälfte
der Seite 1 frei, und es gibt Programme, die mehr als 10 geschachtelte
Schleifen verwenden. Der Zeitverlust durch diesen simulierten Stack
hält sich jedoch in Grenzen.
Einen ähnlichen Stack verwendet der Compiler auch zur Übersetzung von DO
-LOOP
, REPEAT
-UNTIL
, WHILE
-WEND
und FOR
-NEXT
-Schleifen sowie des EXIT
-Befehls.
Bei DO
wird die aktuelle Adresse auf diesem Stapel abgelegt. Sie erhält außerdem noch die DO
-Befehlskennung. Bei Übersetzung des zugehörigen LOOP
wird die Kennung daraufhin überprüft, ob ein DO
fehlt und dann ein JMP
-Befehl auf die zuvor festgestellte Adresse compiliert. Sollte zwischen DO
und LOOP
ein EXIT
gestanden haben, so hat dieser Befehl seine eigene Kennung auf dem
Stack hinterlassen und einen JMP-Befehl mit »Dummy«-Adresse erzeugt. LOOP
ersetzt diese Adresse durch die auf den eigenen JMP-Befehl folgende.
Bei REPEAT
ist der Vorgang ähnlich wie bei DO
. Es unterscheidet sich lediglich die Kennung. Wenn UNTIL
auf eine REPEAT
-Anweisung
folgt, wird zuerst der folgende Ausdruck übersetzt. Erst dann wird ein
bedingter Sprung angehängt. Da der 6502 nur relativ kurze, bedingte
Sprünge kennt, wird mit »BNE *+5:JMP ad
« gearbeitet. Natürlich muß auch UNTIL
eventuelle EXIT
-Befehle berücksichtigen.
WHILE
-WEND
entspricht dem eben Besprochenen, nur steht hier die Bedingung am Schleifenanfang.
Bei FOR
-NEXT
-Schleifen ist noch zu beachten, daß zusätzlich der Basic-Stack benutzt wird. Deshalb wird automatisch ein POP
-Befehl in den EXIT
-Befehl eingeschlossen.
Damit ist die Wirkungsweise des Compilers erklärt. Vielleicht ist Ihnen aber noch nicht ganz klar, wie der Interpreter und der Compiler die Reihenfolge der Berechnungen feststellen. Der Interpreter führt die Berechnungen aus, der Compiler erzeugt einen P-Code. Trotzdem funktionieren beide nach dem gleichen Prinzip. Zur Zwischenspeicherung der Operatoren dient jeweils ein Stack (Stapel). Dazu ein einfaches Beispiel:
8*Sin(A)+16
Der
Compiler liest die Ziffer 8. Als Zahl wird sie sofort compiliert. (Der
Interpreter würde sie auf dem Zahlenstapel ablegen.) Dann wird das
Token für Multiplikation (*) mit der Priorität 5 gelesen. Da der Stapel
für Operatoren noch leer ist (Priorität 0), wird das Token dort
abgelegt. Als nächstes folgt SIN
mit der höchstmöglichen Priorität (9). Weil diese größer ist als die auf dem Stapel (5), wird auch SIN
auf den Stapel gelegt. Dann folgt die geöffnete Klammer. Als
Spezialfunktion ruft die Klammer dieselbe Routine noch einmal,
allerdings rekursiv auf. Es wird also der Wert in Klammern entsprechend
übersetzt oder berechnet. Dann kommt die Variable A an die Reihe.
Variablen werden, genauso wie Zahlen, sofort compiliert. Dann schließt
als Endekennzeichen die Klammer ab. Weiter geht es mit der
ursprünglichen Obersetzung. Es folgt nun das Token für Addition (+),
dessen Priorität (4) kleiner als die auf dem Stapel ist. Deshalb wird
jetzt der SIN
-Befehl compiliert.
Anschließend wird noch die vorliegende Priorität mit der des »*«-Tokens
(5) verglichen. Da sie wieder kleiner ist, wird jetzt die
Multiplikation compiliert.
Der leere Stack hat die
Priorität 0, also wird das Token für Addition auf dem Stack abgelegt.
Dann folgt die Zahl 16 und schließlich das Endekennzeichen mit der
Priorität 0. Der Stapel ist anschließend leer, die Addition wird
compiliert, und der Vorgang ist beendet. Der entstandene P-Code lautet »Zahl 8 Var A Sin Mult Zahl 16 Add
«.
So
einfach wie hier beschrieben, ist das Compilieren von Basic-Programmen
jedoch nicht. Es sollte an dieser Steile vielmehr gezeigt werden, wie
der Turbo-Compiler prinzipiell funktioniert. Sollten alle Vorgänge beim
Compilieren beschrieben werden, würde dies ein Buch füllen. Noch ein
Tip: Das Standard-Atari-Basic ist wegen eines Betriebssystem-Fehlers
nicht in der Lage, »A=NOT NOT A
« zu
berechnen. Dies hängt mit der Stapelverarbeitung zusammen. Probieren
Sie diese Befehlsfolge doch einfach einmal auf Ihrem Atari 800XL aus.
Aufgrund der Syntaxkontrolle ist der Befehl nicht zulässig. Die alten
Atari-Computer reagierten auf »A=NOT NOT A
«
mit einem Systemabsturz. Geben Sie die Programmzeile doch einfach
einmal unter Turbo-Basic-XL ein. Sie werden sehen, daß auch solche
komplexen, logischen Verknüpfungen ordnungsgemäß funktionieren.
(Frank Ostrowski/wb)