Assembler coder's size optimizing handbook (v. 1.04)
Hallo Coder!
Dies ist die erste Version des A.C.S.O.H, einem Handbuch für x86-Assembler-Coder, eine Art "Sammelstelle" für Größenoptimierung. Dieses Dokument soll keineswegs dem Coder die Arbeit abnehmen, einen guten Algorithmus für sein Programmierproblem zu finden, welcher sich auch entsprechend sparsam implementieren läßt, es soll vielmehr ein Ideenhandbuch zur Source-Optimierung sein. Dies läßt sich z.B. bei 4k-Intros wunderbar anwenden. Sollte noch jemand einen zusätzlichen Trick auf Lager haben, so kann sie/er ihn mir gerne schicken (an fti@gmx.net). Ich nehme ihn dann in die nächste Version mit auf.
Leider haben es die meisten Optimierungstricks an sich, daß sie den Code mehr oder weniger langsamer machen. Deshalb sollte man diese Tricks möglichst bei der Initialisierung anwenden. Weiterhin sind die meisten Tricks nur in Spezialfällen anwendbar.
Da diese Version des A.C.S.O.H nur wenige Tricks enthält (ich hoffe, es werden bald mehr), gibt es noch keinen Index.
Außerdem sollte gesagt werden, daß sich die Optimierungen auf 16-Bit-Code beziehen. Die meisten Tricks sind allerdings auch auf 32-Bit-Code anzuwenden.
Die folgenden Personen haben ebenfalls ihren Teil zu diesem Dokument beigetragen:
Adok/Hugi
Ich habe ein paar Tricks aus seinem Tutorial in Hugi 11.
Allgemeine Tips
COM-Dateien
Wenn man möglichst kleine Programme schreiben will, sollte man besser .COM-
als .EXE-Dateien erstellen. Das geht natürlich nur, wenn man für DOS
programmiert. COM-Dateien haben nämlich keinen Header. Zu beachten ist,
daß die Daten dann alle im Codesegment liegen.
Executable-Packer
Executable-Packer packen Dateien so, daß sie danach trotzdem noch ausführbar
sind. Damit sie funktionieren, muß das Programm aber erst einmal eine gewisse
Größe erreicht haben. Die bekanntesten Executable-Packer sind:
PKLITE | Der Klassiker, schlecht, DOS, WIN31, Shareware |
RJCRUSH | Nur für DOS-Exes, APACK und UPX sind besser, Shareware |
TINYPROG | Download lohnt sich nicht, Shareware |
XPACK | Relativ schlecht, Shareware |
LZEXE | Nur für .EXE-Dateien unter DOS |
SHRINK | Nur für .COM-Dateien unter DOS, niedrige Leistung |
WWPACK | Sehr guter .EXE-Packer für DOS, leider Shareware |
APACK | Guter Packer |
624 | Sehr guter Packer für .COM-Dateien unter DOS |
UPX | Hervorragender Packer, mehrere Betriebssysteme |
Links zum Download findet man unter http://acsoh.home.pages.de.
Segmentregister
Man sollte FS und GS nur benutzen, wenn es unbedingt notwendig ist, denn
Kommandos wie PUSH oder POP benötigen mit diesen Segmentregistern mehr
Platz.
Einsparungen durch Vertauschungen
Oft kann man die Spartricks nur anwenden, nachdem man die Reihenfolge von
Befehlen vertauscht hat.
Beispiel:
; Initialisieren des Graphikmodus
MOV AX,13h
INT 10h
; Überschreiben von 64000 Byte mit 0 an ES:DI
XOR AX,AX
MOV CX,32000
REP STOSW
Hier kann man ein Byte einsparen, indem man erst die 64000 Byte überschreibt und dann den Graphikmodus initialisiert. Da AX nach dem Überschreiben 0 ist, kann man MOV AX,13h durch MOV AL,13h ersetzen.
Das Beenden eines Programmes
Allgemein
Der normale Weg, ein Programm zu beenden, lautet:
mov ah,4ch
int 21h
Durch
int 20h
kann man aber 2 Bytes sparen.
Bei .COM-Dateien
Hier sollte ein einfaches RET das Programm beenden, sofern man nichts
auf dem Stack "vergessen" hat.
Array-Element ansprechen
LEA+ADD -> MOV
Um die Adresse eines Array-Elementes zu berechnen, gibt es zwei
Möglichkeiten:
1. LEA+ADD
Beispiel:
LEA DI,Data
ADD DI,BX
2. MOV
Beispiel:
MOV DI,Data[BX]
Die zweite Methode ist kleiner und schneller.
Register := 0 (AX,BX,CX,DX,SI,DI,BP,SP)
Allgemein
Das Setzen von Registern auf 0 kommt wohl in so ziemlich jedem Programm vor.
Die Standard-Lösung für dieses Problem ist:
mov ax,0
Diese Lösung ist aber schlecht, sie verbraucht 3 Bytes. Besser ist:
xor ax,ax
Diese Lösung verbraucht nur zwei Bytes.
Wenn ein anderes Register auf 0 steht, es aber nicht zu sein braucht
Wenn AX auf 0 gesetzt werden soll und z.B. CX nach einer Schleife auf
0 steht, kann durch
XCHG CX,AX
der Inhalt der beiden Register vertauscht werden. Das bringt natürlich nur etwas, wenn CX später einen Wert ungleich 0 zugewiesen kriegt. Außerdem läßt sich dieser Trick natürlich nicht zweimal hintereinander anwenden, da CX danach nicht mehr auf 0 steht.
Dieser Trick klappt nur, wenn AX=0 und BX, CX, DX, BP, SI, DI oder BP auf 0 gestellt werden sollen oder AX auf 0 gestellt werden soll und BX, CX, DX, BP, SI, DI oder BP auf 0 stehen.
Dieser Befehl verbraucht nur ein Byte!
Wenn das auf 0 zu stellende Register = 1 oder = -1 beträgt
In einem solchen Fall kann man per INC oder DEC das Register auf
0 stellen.
DX := 0 bei AX < 8000h
In diesem Fall kann man einfach das CWD-Kommando benutzen.
Wichtig hierbei ist, daß dieser Trick nicht im 32-Bit-Modus angewendet
werden kann.
Register := 0 (AL,AH,BL,BH,CL,CH,DL,DH)
Allgemein
XOR Reg,Reg ist wohl die beste Lösung. Platzverbrauch: 2 Byte.
AH auf 0 setzen, wenn AL<128 bzw. AL>0 In diesem Sonderfall kann man per CBW ein Byte sparen.
Segmentregister (DS,ES,FS,GS,SS) := Konstante
Allgemein
Der beste Weg, ein Segmentregister auf einen Wert zu stellen, ist das
PUSHen eines Words auf den Stack und das POPpen des Segmentregisters, z.B.
push word 0a000h
pop es
Platzverbrauch: 4 Bytes.
Wenn Konstante = 0
Hier ist der beste Weg ein
xor ax,ax
mov es,ax
Platzverbrauch: 4 Bytes, wird aber etwas schneller als die allgemeine Methode ausgeführt.
Wenn Konstante = 0 und das Programm eine .COM-Datei ist.
Für diese Technik ist es wichtig, daß keine Werte auf dem Stack liegen, d.h.
ein RET das Programm beenden würde.
In diesem Fall ist
POP ES
PUSH ES
der beste Weg.
Platzverbrauch: 2 Bytes.
MOVSB/MOVSW/MOVSD/STOSB/STOSW/STOSD/SCASB/SCASW/SCASD
Mehr ist oft weniger
Sofern möglich, sollte man anstatt MOVSD MOVSW verwenden. Dieser Befehl
benötigt ein Byte weniger, dafür muß der Schleifenzähler aber verdoppelt
werden. Beispiel:
Unoptimierte Version
MOV CX,16000
REP MOVSD
Optimierte Version
MOV CX,32000
REP MOVSW
Da mit diesem einen Byte Einsparung aber eine Menge Geschwindigkeit verloren geht, sollte man diese Optimierung nur bei der Initialisierung vornehmen.
Überflüssige CMPs
Bei CMP Register,0 nach ADD, SUB, INC oder DEC mit dem betreffenden Register
Ein CMP ist hier überflüssig, weil diese Befehle das
Zero-Flag bei Veränderung des Registers entsprechend setzen.
So wird
DEC AX
CMP AX,0
jne @label
zu
DEC AX
jne @label
Bei CMP Register,0
Man spart ein Byte, indem man anstatt
CMP AX,0
folgendes schreibt:
OR AX,AX
Bei CMP Register,0 mit anschließendem JNE/JNZ und Setzen der Variable auf
0, wenn der Inhalt des Register beim Ausführen des Sprunges nicht mehr
gebraucht wird.
In einem solchen Fall kann man z.B. anstatt
CMP AX,Konstante
JNE @label
MOV AX,0
eine Subtraktion benutzen:
SUB AX,Konstante
JNE @label.
Wegen der Subtraktion ist bei nicht ausgeführtem Sprung AX=0.
Konstanten/Code-Vereinigung
RET einsparen
Sollte man irgendwo im Codesegment eine Konstante mit dem Wert 200 bzw.
0C8h haben, kann man die am besten nach einem RET positionieren und das RET
streichen. Da 0C8h einem RET entspricht, hat man den Platz also doppelt
ausgenutzt, einmal für die Konstante und einmal für den Befehl. Es spielt
keine Rolle, ob die Konstante eine Byte, Word oder DWord ist. Eine
Fließkommazahl ist allerdings nicht erlaubt.
Überflüssige Befehle
NOPs killen
Löschen Sie alle NOPs aus Ihrem Sourcecode. ;-)
FINIT durch FNINIT ersetzen
Einsparung: 1 Byte
Byte/Word-Registertricks
Word-Register := Konstante
Wenn man einem Word-Register einen Wert <256 zuordnen will und man sicher
ist, daß der aktuelle Registerwert <256 ist, so kann man anstatt dem ganzen
Register nur dem Low-Byte den Wert zuordnen.
So wird
MOV AX,13h
zu
MOV AL,13h
INC und DEC mit Byte-Registern
Man sollte darauf achten, daß es ein Byte weniger Code ergibt, wenn man
anstatt einem niederwertigen Byte-Register (z.B. AL) das ganze Word-Register
(z.B. AX) verwendet.
Voraussetzung hierfür ist natürlich, daß das höherwertige Byte-Register (z.B. AH) = 0 ist. Außerdem können folgende bedingte Sprünge sich eventuell anders verhalten (Zero-Flag und Overflow-Flag werden nach einem Overflow des niederwertigen Bytes nicht gesetzt).
Optimierungstricks zum Selberstricken: Die Ein-Byte-Opcode-Befehle
Die folgenden Befehle haben eine Opcodelänge von nur einem Byte und sind daher für Programmgrößenoptimierung bestens geeignet.
AAA
AAS
CBW
CLC
CLD
CLI
CMC
CMPSB
CMPSW
CWD
DAA
DAS
DEC
AX;BX;CX;DX;SI;DI;BP;SP
HLT
IN AL,DX
INSB
INSW
INC AX;BX;CX;DX;SI;DI;BP;SP
INT 3 Aufruf des Step-Debugging-Interrupts
INTO
IRET
LAHF
LEAVE
LOCK
LODSB
LODSW
MOVSB
MOVSW
NOP Für Programmgrößenoptimierung weniger geeignet ;-)
OUT DX,AL
OUTSB
OUTSW
PUSH AX;BX;CX;DX;SI;DI;BP;SP;CS;DS;ES;SS
PUSHA
PUSHF
POP AX;BX;CX;DX;SI;DI;BP;SP;DS;ES;SS
POPA
POPF
REP
RET
RETF
SAHF
SCASB
SCASW
STC
STD
STI
STOSB
STOSW
XCHG AX,BX ; AX,CX ; AX,DX ; AX,SP ; AX,BP ; AX,SI ; AX,DI
XLATB