x86 codice macchina a 32 bit (numeri interi a 32 bit): 17 byte.
(vedi anche le altre versioni seguenti, inclusi 16 byte per 32-bit o 64-bit, con una convenzione di chiamata DF = 1).
Il chiamante passa args nei registri, incluso un puntatore alla fine di un buffer di output (come la mia risposta C ; guardalo per giustificazione e spiegazione dell'algoritmo.) Glibc interno _itoa
fa questo , quindi non è solo concepito per il code-golf. I registri di passaggio arg sono vicini al sistema V x86-64, tranne per il fatto che abbiamo un arg in EAX anziché EDX.
Al ritorno, EDI punta al primo byte di una stringa C con terminazione 0 nel buffer di output. Il normale registro del valore restituito è EAX / RAX, ma nel linguaggio assembly è possibile utilizzare qualunque convenzione di chiamata sia conveniente per una funzione. ( xchg eax,edi
alla fine aggiungerebbe 1 byte).
Se lo desidera, il chiamante può calcolare una lunghezza esplicita da buffer_end - edi
. Ma non credo che possiamo giustificare l'omissione del terminatore a meno che la funzione non restituisca effettivamente entrambi i puntatori inizio + fine o puntatore + lunghezza. Ciò consentirebbe di risparmiare 3 byte in questa versione, ma non credo sia giustificabile.
- EAX = n = numero da decodificare. (Per
idiv
. Gli altri argomenti non sono operandi impliciti.)
- EDI = fine del buffer di output (la versione a 64 bit utilizza ancora
dec edi
, quindi deve essere in basso 4GiB)
- ESI / RSI = tabella di ricerca, alias LUT. non ostruito.
- ECX = lunghezza della tabella = base. non ostruito.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Modificato a mano per ridurre i commenti, la numerazione delle righe è strana.)
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
È sorprendente che la versione più semplice senza praticamente compromessi di velocità / dimensione sia la più piccola, ma std
/ cld
costata 2 byte da usare stosb
per andare in ordine decrescente e seguire comunque la convenzione di chiamata DF = 0 comune. (E decrementi STOS dopo archiviazione, lasciando il puntatore che punta a un byte troppo basso all'uscita del loop, costandoci byte extra per aggirare.)
versioni:
Ho escogitato 4 trucchi di implementazione significativamente diversi (usando il semplice mov
caricamento / archivio (sopra), usando lea
/ movsb
(pulito ma non ottimale), usando xchg
/xlatb
/ stosb
/ xchg
, e uno che entra nel ciclo con un hack di istruzioni sovrapposte. Vedi codice sotto) . L'ultimo ha bisogno di un trailing 0
nella tabella di ricerca per copiarlo come terminatore della stringa di output, quindi lo conto come +1 byte. A seconda di 32/64 bit (1 byte inc
o meno) e se possiamo supporre che il chiamante imposta DF = 1 ( stosb
decrescente) o qualsiasi altra cosa, le diverse versioni sono (legate per) più brevi.
DF = 1 da memorizzare in ordine decrescente lo rende una vittoria per xchg / stosb / xchg, ma il chiamante spesso non lo vorrà; Sembra scaricare il lavoro al chiamante in un modo difficile da giustificare. (A differenza dei registri personalizzati di passaggio arg e valore restituito, che in genere non costano ad un chiamante asm alcun lavoro extra.) Ma nel codice a 64 bit,cld
/ scasb
funziona come inc rdi
, evitando di troncare il puntatore di output a 32 bit, quindi a volte scomodo conservare DF = 1 in funzioni pulite a 64 bit. . (I puntatori a codice / dati statici sono 32 bit negli eseguibili x86-64 non PIE su Linux e sempre nell'ABI x32 Linux, quindi in alcuni casi è utilizzabile una versione x86-64 che utilizza puntatori a 32 bit.) Comunque, questa interazione rende interessante esaminare diverse combinazioni di requisiti.
- IA32 con un DF = 0 sulla convenzione di chiamata in entrata / uscita: 17B (
nostring
) .
- IA32: 16B (con una convenzione DF = 1:
stosb_edx_arg
o skew
) ; o con DF = dontcare in entrata, lasciandolo impostato: 16 + 1Bstosb_decode_overlap
o 17Bstosb_edx_arg
- x86-64 con puntatori a 64 bit e un DF = 0 sulla convenzione di chiamata di entrata / uscita: 17 + 1 byte (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
o skew
)
x86-64 con puntatori a 64 bit, altra gestione DF: 16B (DF = 1 skew
) , 17B ( nostring
con DF = 1, usando scasb
invece di dec
). 18B ( stosb_edx_arg
preservando DF = 1 con 3 byte inc rdi
).
O se consentiamo di restituire un puntatore a 1 byte prima della stringa, 15B ( stosb_edx_arg
senza il inc
alla fine). Tutto pronto per richiamare nuovamente ed espandere un'altra stringa nel buffer con base / tabella diversa ... Ma ciò avrebbe più senso se non memorizzassimo nemmeno una terminazione 0
, e potresti mettere il corpo della funzione in un ciclo, quindi è davvero un problema separato.
x86-64 con puntatore di output a 32 bit, DF = 0 convenzione di chiamata: nessun miglioramento rispetto al puntatore di output a 64 bit, ma 18B (nostring
) ora si collega.
- x86-64 con puntatore di output a 32 bit: nessun miglioramento rispetto alle migliori versioni di puntatore a 64 bit, quindi 16B (DF = 1
skew
). O per impostare DF = 1 e lasciarlo, 17B per skew
con std
ma non cld
. O 17 + 1B per stosb_decode_overlap
con inc edi
alla fine invece di cld
/ scasb
.
Con una convenzione di chiamata DF = 1: 16 byte (IA32 o x86-64)
Richiede DF = 1 sull'input, lascia impostato. A malapena plausibile , almeno per una funzione. Fa la stessa cosa della versione precedente, ma con xchg per ottenere il resto dentro / fuori di AL prima / dopo XLATB (ricerca tabella con R / EBX come base) e STOSB ( *output-- = al
).
Con un normale DF = 0 alla convenzione di entrata / uscita, il std
/ cld
/scasb
versione è di 18 byte per il codice a 32 e 64 bit ed è a 64 bit pulita (funziona con un puntatore di output a 64 bit).
Si noti che gli argomenti di input si trovano in registri diversi, incluso RBX per la tabella (per xlatb
). Si noti inoltre che questo ciclo inizia con la memorizzazione di AL e termina con l'ultimo carattere non ancora memorizzato (quindi il mov
alla fine). Quindi il ciclo è "distorto" rispetto agli altri, da cui il nome.
;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
Una versione analoga non inclinata sovrascrive EDI / RDI e quindi lo risolve.
; 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
Ho provato una versione alternativa di questo con lea esi, [rbx+rdx]
/ movsb
come il corpo del loop interno. (RSI viene reimpostato ad ogni iterazione, ma decrementi RDI). Ma non può usare xor-zero / stos per il terminatore, quindi è 1 byte più grande. (E non è pulito a 64 bit per la tabella di ricerca senza un prefisso REX sul LEA.)
LUT con lunghezza esplicita e un terminatore 0: 16 + 1 byte (32 bit)
Questa versione imposta DF = 1 e la lascia così. Sto contando il byte LUT extra richiesto come parte del conteggio dei byte totali.
Il trucco interessante qui è che gli stessi byte decodificano in due modi diversi . Cadiamo nel mezzo del ciclo con remainder = base e quotient = input number e copiamo il terminatore 0 in posizione.
Al primo passaggio della funzione, i primi 3 byte del loop vengono consumati come byte alti di un disp32 per un LEA. Che LEA copia la base (modulo) su EDX, idiv
produce il resto per successive iterazioni.
Il secondo byte di idiv ebp
è FD
, che è il codice std
operativo per l' istruzione che questa funzione deve funzionare. (Questa è stata una scoperta fortunata. Lo avevo già visto in div
precedenza, che si distingue idiv
dall'uso dei /r
bit in ModRM. Il secondo byte di div epb
decodifica come cmc
, che è innocuo ma non utile. Ma con idiv ebp
possiamo effettivamente rimuovere std
dall'alto della funzione.)
Notare che i registri di input sono ancora una volta differenza: EBP per la base.
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
Questo trucco di decodifica sovrapposto può essere utilizzato anche con cmp eax, imm32
: bastano solo 1 byte per saltare in avanti efficacemente di 4 byte, solo bloccando i flag. (Questo è terribile per le prestazioni sulle CPU che segnano i limiti delle istruzioni nella cache L1i, BTW.)
Ma qui, stiamo usando 3 byte per copiare un registro e saltare nel ciclo. Ciò richiederebbe normalmente 2 + 2 (mov + jmp) e ci lascerebbe saltare nel loop proprio prima della STOS invece che prima della XLATB. Ma allora avremmo bisogno di un STD separato, e non sarebbe molto interessante.
Provalo online! (con un _start
chiamante che utilizza sys_write
il risultato)
È consigliabile strace
eseguirne il debug o eseguirne il dump dell'output, in modo da poter verificare che sia presente un \0
terminatore nel posto giusto e così via. Ma puoi vedere che funziona davvero e produrre AAAAAACHOO
per un input di
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(In realtà xxAAAAAACHOO\0x\0\0...
, perché stiamo scaricando da 2 byte precedenti nel buffer fino a una lunghezza fissa. Quindi possiamo vedere che la funzione ha scritto i byte che avrebbe dovuto e non ha fatto alcun passo sui byte che non avrebbe dovuto. start-pointer passato alla funzione era il penultimo x
carattere, seguito da zero.)