Tips & Tricks: Assembler-Optimierung (1)

An Assembler-Quelltexten kann man gar nicht genug feilen. Die erste Folge unseres Kurses soll Ihnen helfen, ein Optimum an Geschwindigkeit und Programmkürze in Ihren Sourcen zu erreichen.

Das Bessere ist des Guten Feind. Assembler ist zwar mit Abstand die schnellste Programmiersprache, trotzdem gibt es von Lösung zu Lösung himmelweite Unterschiede -- je nachdem, ob der Code auf Schnelligkeit oder Länge optimiert wurde. Außerdem kommt bei geschwindigkeitsorientierter Optimierung erschwerend hinzu, daß man teilweise mit recht undurchsichtigen Tricks arbeiten muß. Am besten überarbeitet man erst nach der Fertigstellung eines Listings einzelne Routinen und fügt erst dann den optimierten Quelltext ein. Ansonsten wird die ganze Sache zu unübersichtlich und hinterher fällt die Programmpflege unnötig schwer. Weiterhin ist der ST von Natur aus schwer berechenbar, wenn es um die Endgeschwindigkeit von Programmen geht: Ein 6-Taktzyklen-Befehl benötigt nämlich meist acht Taktzyklen statt sechs.

Als Einstieg dient eine Routine aus dem »täglichen Leben«: CLS (Clear Screen). Ein wenig erfahrener Programmierer würde die Aufgabe etwa so lösen:

    move.l  scr_adr,a0  ; Adresse des Screens in aO
    move.l  #7999,d0    ; 8000 Longs=32000 Byte löschen
cls_lp: move.l  #0,(a0)+    ; 4 Byte löschen
    dbra    d0,cls_lp   ; Schleifenkehrpunkt
    rts         ; zurück

Diese Routine ist zwar relativ kurz (6+6+6+4+2=24 Byte), auf Länge optimiert geht's aber noch kürzer:

    move.l  scr_adr(pc),a0  ; wenn scr_adr <32K entfernt
    move.w  #7999,d0    ; dbra interessiert nur .w
cls_lp: clr.l   (a0)+       ; wie oben, aber 4B kürzer
    dbra      d0,cls_lp ; macht zusammen
    rts         ; 4+4+2+4+2=16 Byte

Die Kommentare verraten: Die erste Routine ist um 50 Prozent länger als die neue Lösung -- das spart RAM. In einem Spiel wie »Starglider II« allerdings würde sie wie eine Fahrt mit angezogener Handbremse wirken. Geschwindigkeitszuwachs ist jederzeit möglich, allerdings nur auf Kosten der Quelltextlänge. Ein CLR-Befehl z. B. in einer Schleife wirkt wenig effektiv, wenn man ein freies Register hat.

    ...
    clr.l d1        ; zusätzlich
cls_lp: move.l  d1,(a0)+    ; statt "clr.1 (a0)+"
    ...

Durch diese Lösung lassen sich 8000 x 8, also 64000 Taktzyklen im Schleifenkörper einsparen, abzüglich der sechs Taktzyklen zum Nullsetzen von D1. Außerdem ist »CLR« ein Befehl, den man meist durch effektiveren Code ersetzen kann. Ein »moveq #0,d1« z. B. kostet nur vier Taktzyklen und ist ebenfalls long. Das ergibt eine feine Routine, die man noch weiter optimieren kann, indem man die Anzahl der Schleifendurchläufe verkleinert und den Schleifeninhalt vergrößert, also beispielsweise:

    ...
    moveq   #0,d1
    move.w  #799,d0     ; nur noch 800mal
cls_lp: rept10          ; rept n und endr schließen
    move.l  d1,(a0)+    ; Befehle ein, die n-mal
    endr            ; wiederholt werden
    dbra    d0,cls_lp   ; pro Durchlauf 40B löschen
    rts

Den 8000 x 12+8000 x 10=176000 Taktzyklen (fürs »move.l« und »dbra«) stehen jetzt 800 x 10 x 12 + 800 x 10=104000 Taktzyklen in der Schleife gegenüber. Weitere Geschwindigkeitssteigerung kann man durch Nullsetzen mehrerer Register und Kopieren durch »movem«-Befehle erzielen.

Betriebssystemaufrufe kommen oft vor und sind daher eher für die Optimierung auf Kürze statt Geschwindigkeit prädestiniert (Optimierung auf Geschwindigkeit macht wenig Sinn, da das »lahme« TOS in zeitkritischen Fragen grundsätzlich übergangen werden sollte). Eine nachfolgende Stackkorrektur (diesmal wird das Pferd von hinten aufgezäumt) kann zwar so aussehen,

    adda.l  #8,sp   ;sp=a7,

ist aber um satte 4 Byte länger (und deutlich langsamer) als

    addq.l  #8,sp.

Bei der Addition auf ein Adreßregister ist es daher gleichgültig, ob man Word- oder Longwordlänge angibt, da das Word gegebenenfalls auf Longwordlänge erweitert wird -- Adressen sind nämlich immer long. Anders sieht es bei den Datenregistern aus: Wenn Sie hier angeben, daß ein Word addiert werden soll, dann geschieht das auch und ein eventueller Übertrag fällt weg. Soll eine Routine z. B. einen eigenen Screen anlegen, muß sie dafür sorgen, daß das unterste Byte auf $00 gesetzt wird. Eine Routine wie

    move.l  #32255,-(sp)    ; Anzahl Bytes
    move.w  #$49, -(sp) ; anfordern
    trap    #1      ; GEMDOS
    addq.l  #6,sp       ; Stackkorrektur
    add.w   #$ff,d0     ; im angeforderten bleiben
    and.l   #$ffffff00,d0   ; unterstes Byte $00 setzen
    move.l  d0,scr_adr  ; merken

führt zu einem Fehler, wenn der Übergabewert z. B. $4FFFO lautet: Die Addition mit Wordlänge führt dazu, daß der Übertrag ins obere Word ignoriert wird. Dann ermittelt die Routine $40000 statt $50000 als Screenadresse — und das kann zum Systemabsturz führen. Wenn Sie Murphies Gesetz kennen, dann wissen Sie, daß das genau dann passiert, wenn Sie Ihren Source seit mehreren Stunden nicht gespeichert haben. Also muß der Befehl

    add.w   #$ff,d0

durch ein

    add.l   #$ff,d0

ersetzt werden. Es schmerzt natürlich, daß die Routine dadurch um ein Word länger wird, deshalb auch hier gleich die Suche nach Verbesserungsmöglichkeiten: Der erste Befehl bringt den Wert 32255 auf den Stack. Ein

    move.l  #x,-(sp)

läßt sich bekanntlich durch ein

    pea x

ersetzen (hier fehlt das Immediate-Kreuz, da Motorola leider keine einheitliche Syntax zustande gebracht hat). Bis jetzt er gibt sich durch die Aktion aber noch keine Optimierung, erst mit einem weiteren Trick läßt sich ein Word einsparen. Die Adressierung mit »absolute short«:

    pea      32255.w

. Dieser Trick läßt sich natürlich nur dann einsetzen, wenn -32768 < =X< =32767 gilt. Damit ist die Routine aber noch nicht optimal, denn

    and.l     #$ffffff00,d0

läßt sich von 6 Byte auf 2 schrumpfen. Um das unterste Byte zu kürzen, bedient man sich schließlich doch des CLR-Befehls:

    clr.b      dO

Bei Betriebssystemaufrufen läßt sich außerdem eine weitere Optimierung einsetzen:

    move.w  #'A',-(sp)  ; 'A'=65=$004l
    move.w  #2,-(sp)    ; 2=$2 (wer hätt's gedacht?)
    trap    #1      ; GEMDOS
    addq.l  #4,sp

Zwei Befehle treten in Aktion, um zwei Words auf den Stack zu bringen. Sie müssen lediglich umgedreht werden, schon er- gibt sich

    move.l  #$2004l,-(sp)
bzw.    pea $20041

und 2 Byte sind gespart. Welche der beiden Versionen sie benutzen, bleibt Ihnen überlassen, wenn das obere Word allerdings gleich Null ist, sollten Sie »pea« mit absolute short (s.o.) benutzen. Wenn bei einem Betriebssystemaufruf mehr als 8 Byte übergeben werden, ist eine Stackkorrektur mit »addq« nicht mehr möglich. Hier sollten Sie auf

    LEA x(sp),sp

zurückgreifen. Dieser Befehl erweitert x auch auf long, ist aber vier Taktzyklen schneller. So lassen sich natürlich alle Wordadditionen auf Adreßregister optimieren. Insbesondere in Grafikroutinen kann das sehr nützlich sein. Falls Sie nun doch einmal einen Betriebssystemaufruf flott über die Bühne bringen müssen, können Sie dies durch

    moveq   #6,d0
    move.w  d0,-(sp)

»moveq« erreichen. Besonders in INIT-Routinen folgen viele Betriebssystemaufrufe aufeinander. Da normalerweise genügend Platz auf dem Stack vorhanden ist, läßt er sich genausogut erst nach mehreren Aufrufen korrigieren. Dadurch spart man 2 bis 4 (»addq« oder »lea«) Byte pro Aufruf. Wollen Sie einzelne Bits setzen oder löschen, können Sie dazu entweder »OR/AND« oder »BSET/BCLR« benutzen. Die ersten beiden Befehle setzt man ein, wenn das Ziel ein Datenregister ist und das betreffende Bit im unteren Word liegt. Greifen Sie dagegen auf ein Longword zu, eignet sich »BSET/BCLR« von Länge und Geschwindigkeit her besser. Wollen Sie mehrere Bits setzen bzw. löschen, sollten Sie natürlich »OR/AND« benutzen (oder auch CLR...). Falls Sie ein Adreßregister mit einem Wert füllen, greifen Sie am besten auf den dafür vorhergesehenen Befehl zurück (»LEA«). Den können Sie dann auch noch (meistens wenigstens) PC-relativ verwenden:

    lea cosinus(pc),a0

ist nun mal schneller und zwei Byte kürzer als

    move.l  #cosinus,a0

. Grundsätzlich empfiehlt sich PC-relative Adressierung, wo immer sie möglich ist. Sie gewinnen dadurch jeweils 2 Byte und sind um vier Taktzyklen schneller, außerdem sparen Sie noch einen Eintrag in der Relokationstabelle.

Falls irgendwann zuwenig Datenregister frei bleiben: Dieses Problem ist allgemein bekannt. Es gibt eine Routine, die fast ausnahmslos mit schnellen Registeroperationen auskommt, dennoch muß man zwischendurch einige Datenregister auf dem Stack retten, was natürlich einiges an Rechenzeit kostet. Ein einfacher Trick ist hierbei, die oft brachliegenden Adreßregister als Saveregister zu benutzen.

    move.l    d0,a6
    move.l    a6,d0

kostet je vier Taktzyklen, eine Rettung auf dem Stack 14+12, also 26 Taktzyklen. In einer Schleifenroutine, die der Computer oft abarbeiten muß, macht sich dieser Gewinn schon deutlich bemerkbar.

Auch bei der Modifikation eines Datenregisters läßt sich noch einiges optimieren: Verwenden Sie z. B stets »moveq«, wenn Sie einen Longword-Wert zwischen -128 und 127 in ein Datenregister schreiben wollen. Hier wird sehr oft »move.w« eingesetzt, obwohl »moveq« möglich wäre. Damit werden 2 Byte und vier Taktzyklen verschenkt. Den Wert $200000 erhalten Sie am schnellsten und kürzesten mit folgenden Befehlen in dO:

    moveq   #$20,d0
    swap    dO

Wie sie vielleicht wissen, können Sie durch Links- bzw. Rechtsrotieren eines Registers dessen Inhalt verdoppeln oder halbieren. Wußten Sie aber auch, daß das gar nicht die effektivste Methode ist? Wie's geht - im nächsten Heft. (hu) Download


Thomas Plümpe
Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]