x86-32-Bit-Computercode (32-Bit-Ganzzahlen): 17 Byte.
(Siehe auch die folgenden Versionen, einschließlich 16 Byte für 32-Bit- oder 64-Bit-Versionen mit einer DF = 1-Aufrufkonvention.)
Der Aufrufer übergibt Args in Registern, einschließlich eines Zeigers auf das Ende eines Ausgabepuffers (wie meine C-Antwort ; zur Begründung und Erläuterung des Algorithmus siehe). Die interne Funktion von glibc _itoa
tut dies , sodass sie nicht nur für Code-Golf entwickelt wurde. Die Arg-Passing-Register befinden sich in der Nähe von x86-64 System V, mit der Ausnahme, dass wir in EAX anstelle von EDX ein Arg haben.
Bei der Rückkehr zeigt EDI auf das erste Byte einer 0-terminierten C-Zeichenfolge im Ausgabepuffer. Das übliche Rückgabewertregister ist EAX / RAX. In der Assemblersprache können Sie jedoch jede für eine Funktion geeignete Aufrufkonvention verwenden. ( xchg eax,edi
Am Ende würde 1 Byte hinzufügen).
Der Anrufer kann eine explizite Länge berechnen, wenn er möchte buffer_end - edi
. Ich glaube aber nicht, dass wir es rechtfertigen können, das Abschlusszeichen wegzulassen, wenn die Funktion nicht tatsächlich beide Start- + Endzeiger oder Zeiger + Länge zurückgibt. Das würde in dieser Version 3 Bytes einsparen, aber ich halte es nicht für vertretbar.
- EAX = n = zu decodierende Zahl. (For
idiv
. Die anderen Argumente sind keine impliziten Operanden.)
- EDI = Ende der Ausgangspuffer (64-Bit - Version noch Verwendungen
dec edi
, so muss in dem niedrigen 4GiB sein)
- ESI / RSI = Nachschlagetabelle, auch bekannt als LUT. nicht verstaut.
- ECX = Länge der Tabelle = Basis. nicht verstaut.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Hand bearbeitet, um Kommentare zu verkleinern, Zeilennummerierung ist seltsam.)
32-bit: 17 bytes ; 64-bit: 18 bytes
; same source assembles as 32 or 64-bit
3 %ifidn __OUTPUT_FORMAT__, elf32
5 %define rdi edi
6 address %define rsi esi
11 machine %endif
14 code %define DEF(funcname) funcname: global funcname
16 bytes
22 ;;; returns: pointer in RDI to the start of a 0-terminated string
24 ;;; clobbers:; EDX (tmp remainder)
25 DEF(ascii_compress_nostring)
27 00000000 C60700 mov BYTE [rdi], 0
28 .loop: ; do{
29 00000003 99 cdq ; 1 byte shorter than xor edx,edx / div
30 00000004 F7F9 idiv ecx ; edx=n%B eax=n/B
31
32 00000006 8A1416 mov dl, [rsi + rdx] ; dl = LUT[n%B]
33 00000009 4F dec edi ; --output ; 2B in x86-64
34 0000000A 8817 mov [rdi], dl ; *output = dl
35
36 0000000C 85C0 test eax,eax ; div/idiv don't write flags in practice, and the manual says they're undefined.
37 0000000E 75F3 jnz .loop ; }while(n);
38
39 00000010 C3 ret
0x11 bytes = 17
40 00000011 11 .size: db $ - .start
Es ist überraschend , dass die einfachste Version mit grundsätzlich keine Geschwindigkeit / Größe Kompromissen ist die kleinste, aber std
/ cld
Kosten 2 Bytes zu verwenden , stosb
in absteigender Reihenfolge zu gehen und immer noch dem gemeinsame DF = 0 Aufrufkonvention folgen. (Und STOS wird nach dem Speichern dekrementiert , wobei der Zeiger beim Beenden der Schleife ein Byte zu niedrig zeigt, was uns zusätzliche Bytes zum Umgehen kostet.)
Versionen:
Ich fand 4 signifikant unterschiedliche Implementierungstricks (mit einfachem mov
Laden / Speichern (oben), mit lea
/ movsb
(ordentlich, aber nicht optimal), mit xchg
/ xlatb
/ stosb
/ xchg
und einem, der mit einem überlappenden Anweisungs-Hack in die Schleife eintritt. Siehe Code unten) . Das letzte muss 0
in der Nachschlagetabelle nachgestellt werden , damit es als Abschlusszeichen für die Ausgabezeichenfolge kopiert werden kann. Daher zähle ich das als +1 Byte. Abhängig von 32/64-Bit (1 Byte inc
oder nicht) und der Annahme, dass der Aufrufer DF = 1 ( stosb
absteigend) oder was auch immer setzt, sind verschiedene Versionen (gebunden für) am kürzesten.
Wenn DF = 1 in absteigender Reihenfolge gespeichert wird, gewinnt xchg / stosb / xchg, aber der Anrufer möchte das oft nicht. Es fühlt sich an, als würde man dem Anrufer die Arbeit auf eine schwer zu rechtfertigende Weise überlassen. (Im Gegensatz zu benutzerdefinierten Arg-Passing- und Return-Value-Registern, die einen asm-Aufrufer normalerweise keinen zusätzlichen Arbeitsaufwand verursachen.) Im 64-Bit-Code funktioniert cld
/ scasb
as jedoch so inc rdi
, dass der Ausgabezeiger nicht auf 32-Bit gekürzt wird, was manchmal der Fall ist Es ist unpraktisch, DF = 1 in 64-Bit-Bereinigungsfunktionen beizubehalten. . (Zeiger auf statischen Code / Daten sind 32-Bit-Dateien in x86-64-Nicht-PIE-Programmen unter Linux und immer in Linux-x32-ABI. In einigen Fällen kann daher eine x86-64-Version mit 32-Bit-Zeigern verwendet werden.) Dieses Zusammenspiel macht es interessant, verschiedene Anforderungskombinationen zu betrachten.
- IA32 mit einer DF = 0 bei Eingabe / Ausgabe-Aufrufkonvention: 17B (
nostring
) .
- IA32: 16B (mit einer DF = 1-Konvention:
stosb_edx_arg
oder skew
) ; oder mit eingehendem DF = nicht kümmern, lassen Sie es gesetzt: 16 + 1Bstosb_decode_overlap
oder 17Bstosb_edx_arg
- x86-64 mit 64-Bit-Zeigern und einer DF = 0 bei der Eingabe / Ausgabe-Aufrufkonvention: 17 + 1 Byte (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
oder skew
)
x86-64 mit 64-Bit-Zeigern, andere DF-Verarbeitung: 16B (DF = 1 skew
) , 17B ( nostring
mit DF = 1 scasb
anstelle von dec
). 18B ( stosb_edx_arg
Erhalt von DF = 1 mit 3 Byte inc rdi
).
Oder wenn wir zulassen, dass ein Zeiger auf 1 Byte vor der Zeichenfolge 15B ( stosb_edx_arg
ohne das inc
am Ende) zurückgegeben wird. Alles bereit, erneut aufzurufen und einen weiteren String mit einer anderen Basis / Tabelle in den Puffer zu erweitern ... Aber das wäre sinnvoller, wenn wir auch keinen Abschluss speichern würden 0
, und Sie könnten den Funktionskörper in eine Schleife einfügen, das ist also wirklich ein separates Problem.
x86-64 mit 32-Bit-Ausgabezeiger, DF = 0-Aufrufkonvention: Keine Verbesserung gegenüber 64-Bit-Ausgabezeiger, aber 18B ( nostring
) wird jetzt verwendet.
- x86-64 mit 32-Bit-Ausgabezeiger: Keine Verbesserung gegenüber den besten 64-Bit-Zeigerversionen, also 16B (DF = 1
skew
). Oder DF = 1 setzen und es lassen, 17B für skew
mit std
aber nicht cld
. Oder 17 + 1B für stosb_decode_overlap
mit inc edi
am Ende anstelle von cld
/ scasb
.
Mit einer DF = 1-Aufrufkonvention: 16 Byte (IA32 oder x86-64)
Benötigt DF = 1 am Eingang, lässt es gesetzt. Zumindest funktionsbezogen kaum plausibel . Funktioniert genauso wie die obige Version, aber mit xchg, um den Rest in / aus AL vor / nach XLATB (Tabellensuche mit R / EBX als Basis) und STOSB ( *output-- = al
) zu bekommen.
Bei einem normalen DF = 0 am Eintritts- / Austritt Konvention, die std
/ cld
/ scasb
Version ist 18 Bytes für 32 und 64-Bit - Code, und ist 64-bit sauber (Arbeiten mit einem 64-Bit - Ausgangs - Hinweiszeigern).
Beachten Sie, dass sich die Eingabeargs in verschiedenen Registern befinden, einschließlich RBX für die Tabelle (für xlatb
). Beachten Sie auch, dass diese Schleife mit dem Speichern von AL beginnt und mit dem letzten noch nicht gespeicherten Zeichen endet (daher mov
das Ende). Die Schleife ist also relativ zu den anderen "schief", daher der Name.
;DF=1 version. Uncomment std/cld for DF=0
;32-bit and 64-bit: 16B
157 DEF(ascii_compress_skew)
158 ;;; inputs
159 ;; O in RDI = end of output buffer
160 ;; I in RBX = lookup table for xlatb
161 ;; n in EDX = number to decode
162 ;; B in ECX = length of table = modulus
163 ;;; returns: pointer in RDI to the start of a 0-terminated string
164 ;;; clobbers:; EDX=0, EAX=last char
165 .start:
166 ; std
167 00000060 31C0 xor eax,eax
168 .loop: ; do{
169 00000062 AA stosb
170 00000063 92 xchg eax, edx
171
172 00000064 99 cdq ; 1 byte shorter than xor edx,edx / div
173 00000065 F7F9 idiv ecx ; edx=n%B eax=n/B
174
175 00000067 92 xchg eax, edx ; eax=n%B edx=n/B
176 00000068 D7 xlatb ; al = byte [rbx + al]
177
178 00000069 85D2 test edx,edx
179 0000006B 75F5 jnz .loop ; }while(n = n/B);
180
181 0000006D 8807 mov [rdi], al ; stosb would move RDI away
182 ; cld
183 0000006F C3 ret
184 00000070 10 .size: db $ - .start
Eine ähnliche, nicht verzerrte Version übersteuert EDI / RDI und behebt es dann.
; 32-bit DF=1: 16B 64-bit: 17B (or 18B for DF=0)
70 DEF(ascii_compress_stosb_edx_arg) ; x86-64 SysV arg passing, but returns in RDI
71 ;; O in RDI = end of output buffer
72 ;; I in RBX = lookup table for xlatb
73 ;; n in EDX = number to decode
74 ;; B in ECX = length of table
75 ;;; clobbers EAX,EDX, preserves DF
76 ; 32-bit mode: a DF=1 convention would save 2B (use inc edi instead of cld/scasb)
77 ; 32-bit mode: call-clobbered DF would save 1B (still need STD, but INC EDI saves 1)
79 .start:
80 00000040 31C0 xor eax,eax
81 ; std
82 00000042 AA stosb
83 .loop:
84 00000043 92 xchg eax, edx
85 00000044 99 cdq
86 00000045 F7F9 idiv ecx ; edx=n%B eax=n/B
87
88 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
89 00000048 D7 xlatb ; al = byte [rbx + al]
90 00000049 AA stosb ; *output-- = al
91
92 0000004A 85D2 test edx,edx
93 0000004C 75F5 jnz .loop
94
95 0000004E 47 inc edi
96 ;; cld
97 ;; scasb ; rdi++
98 0000004F C3 ret
99 00000050 10 .size: db $ - .start
16 bytes for the 32-bit DF=1 version
Ich habe eine alternative Version davon mit lea esi, [rbx+rdx]
/ movsb
als innerem Schleifenkörper ausprobiert . (RSI wird bei jeder Iteration zurückgesetzt, RDI wird jedoch dekrementiert.) Aber es kann nicht xor-zero / stos als Abschlusszeichen verwenden, daher ist es 1 Byte größer. (Und es ist nicht 64-Bit-sauber für die Nachschlagetabelle ohne ein REX-Präfix auf der LEA.)
LUT mit expliziter Länge und einem 0-Terminator: 16 + 1 Byte (32 Bit)
Diese Version setzt DF = 1 und belässt es so. Ich zähle das zusätzliche LUT-Byte, das als Teil der Gesamtbytezahl benötigt wird.
Der coole Trick dabei ist , dieselben Bytes auf zwei verschiedene Arten dekodieren zu lassen . Wir fallen in die Mitte der Schleife mit Rest = Basis und Quotient = Eingangsnummer und kopieren den 0-Terminator an die richtige Stelle.
Beim ersten Durchlauf der Funktion werden die ersten 3 Bytes der Schleife als High-Bytes eines Disp32 für eine LEA verbraucht. Diese LEA kopiert die Basis (Modul) nach EDX und idiv
erzeugt den Rest für spätere Iterationen.
Das 2. Byte von idiv ebp
ist FD
der Opcode für den std
Befehl, den diese Funktion benötigt, um zu funktionieren. (Dies war eine glückliche Entdeckung. Ich hatte dies mit div
früher betrachtet, was sich von der idiv
Verwendung der /r
Bits in ModRM unterscheidet. Das 2. Byte von div epb
decodiert als cmc
, was harmlos, aber nicht hilfreich ist. Aber mit idiv ebp
können wir tatsächlich das std
von oben entfernen der Funktion.)
Beachten Sie, dass die Eingangsregister wieder einen Unterschied darstellen: EBP für die Basis.
103 DEF(ascii_compress_stosb_decode_overlap)
104 ;;; inputs
105 ;; n in EAX = number to decode
106 ;; O in RDI = end of output buffer
107 ;; I in RBX = lookup table, 0-terminated. (first iter copies LUT[base] as output terminator)
108 ;; B in EBP = base = length of table
109 ;;; returns: pointer in RDI to the start of a 0-terminated string
110 ;;; clobbers: EDX (=0), EAX, DF
111 ;; Or a DF=1 convention allows idiv ecx (STC). Or we could put xchg after stos and not run IDIV's modRM
112 .start:
117 ;2nd byte of div ebx = repz. edx=repnz.
118 ; div ebp = cmc. ecx=int1 = icebp (hardware-debug trap)
119 ;2nd byte of idiv ebp = std = 0xfd. ecx=stc
125
126 ;lea edx, [dword 0 + ebp]
127 00000040 8D9500 db 0x8d, 0x95, 0 ; opcode, modrm, 0 for lea edx, [rbp+disp32]. low byte = 0 so DL = BPL+0 = base
128 ; skips xchg, cdq, and idiv.
129 ; decode starts with the 2nd byte of idiv ebp, which decodes as the STD we need
130 .loop:
131 00000043 92 xchg eax, edx
132 00000044 99 cdq
133 00000045 F7FD idiv ebp ; edx=n%B eax=n/B;
134 ;; on loop entry, 2nd byte of idiv ebp runs as STD. n in EAX, like after idiv. base in edx (fake remainder)
135
136 00000047 92 xchg eax, edx ; eax=n%B edx=n/B
137 00000048 D7 xlatb ; al = byte [rbx + al]
138 .do_stos:
139 00000049 AA stosb ; *output-- = al
140
141 0000004A 85D2 test edx,edx
142 0000004C 75F5 jnz .loop
143
144 %ifidn __OUTPUT_FORMAT__, elf32
145 0000004E 47 inc edi ; saves a byte in 32-bit. Makes DF call-clobbered instead of normal DF=0
146 %else
147 cld
148 scasb ; rdi++
149 %endif
150
151 0000004F C3 ret
152 00000050 10 .size: db $ - .start
153 00000051 01 db 1 ; +1 because we require an extra LUT byte
# 16+1 bytes for a 32-bit version.
# 17+1 bytes for a 64-bit version that ends with DF=0
Dieser überlappende Decodiertrick kann auch verwendet werden mit cmp eax, imm32
: Es wird nur 1 Byte benötigt, um effektiv 4 Byte vorwärts zu springen, nur um Flags zu blockieren. (Dies ist für die Leistung auf CPUs, die Befehlsgrenzen im L1i-Cache markieren, schrecklich.)
Aber hier verwenden wir 3 Bytes, um ein Register zu kopieren und in die Schleife zu springen. Das würde normalerweise 2 + 2 (mov + jmp) dauern und uns in die Schleife direkt vor dem STOS anstatt vor dem XLATB springen lassen. Aber dann bräuchten wir eine separate Geschlechtskrankheit, und das wäre nicht sehr interessant.
Probieren Sie es online! (mit einem _start
Anrufer, der sys_write
das Ergebnis verwendet)
Für das Debuggen ist es am besten, es unter auszuführen strace
oder die Ausgabe zu verhexen, damit Sie überprüfen können, ob sich \0
an der richtigen Stelle ein Abschlusszeichen befindet und so weiter. Aber Sie können sehen, dass dies tatsächlich funktioniert, und AAAAAACHOO
für eine Eingabe von produzieren
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(Eigentlich xxAAAAAACHOO\0x\0\0...
, weil wir von 2 Bytes früher in dem zu einer festen Länge Puffer aus Dumping. So wir , dass die Funktion sehen , schrieb den Bytes es sollte zu und tat nicht Schritt auf jedem Bytes sollte es nicht haben. Die Der an die Funktion übergebene Startzeiger war das vorletzte x
Zeichen, gefolgt von Nullen.)