Code machine x86 32 bits (entiers 32 bits): 17 octets.
(voir également les autres versions ci-dessous, y compris 16 octets pour 32 bits ou 64 bits, avec une convention d'appel DF = 1.)
L' appelant passe args dans les registres, y compris un pointeur vers la fin d'un tampon de sortie (comme ma C réponse , voir pour la justification et l' explication de l'algorithme.) Interne de glibc _itoa
fait cela , il est donc pas seulement arrangea pour le code-golf. Les registres de passage d'argument sont proches du système V x86-64, sauf que nous avons un arg dans EAX au lieu d'EDX.
En retour, EDI pointe vers le premier octet d'une chaîne C terminée par 0 dans le tampon de sortie. Le registre de valeur de retour habituel est EAX / RAX, mais en langage assembleur, vous pouvez utiliser n'importe quelle convention d'appel convenant à une fonction. ( xchg eax,edi
à la fin ajouterait 1 octet).
L'appelant peut calculer une longueur explicite s'il le souhaite, à partir de buffer_end - edi
. Mais je ne pense pas que nous puissions justifier l'omission du terminateur à moins que la fonction ne retourne réellement les deux pointeurs de début + fin ou le pointeur + la longueur. Cela économiserait 3 octets dans cette version, mais je ne pense pas que ce soit justifiable.
- EAX = n = nombre à décoder. (Pour
idiv
. Les autres arguments ne sont pas des opérandes implicites.)
- EDI = fin du tampon de sortie (la version 64 bits utilise toujours
dec edi
, donc doit être dans le bas 4GiB)
- ESI / RSI = table de recherche, également appelée LUT. pas clobber.
- ECX = longueur de la table = base. pas clobber.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Modifié à la main pour réduire les commentaires, la numérotation des lignes est bizarre.)
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
Il est surprenant que la version la plus simple sans compromis de vitesse / taille soit la plus petite, mais std
/ cld
coûte 2 octets à utiliser stosb
pour aller dans l'ordre décroissant et toujours suivre la convention d'appel DF = 0 commune. (Et STOS diminue après le stockage, laissant le pointeur pointant un octet trop bas à la sortie de la boucle, ce qui nous coûte des octets supplémentaires pour contourner.)
Versions:
J'ai trouvé 4 astuces d'implémentation significativement différentes (en utilisant un mov
chargement / stockage simple (ci-dessus), en utilisant lea
/ movsb
(soigné mais pas optimal), en utilisant xchg
/ xlatb
/ stosb
/ xchg
et un qui entre dans la boucle avec un hack d'instruction à chevauchement. Voir le code ci-dessous) . Le dernier a besoin d'une fin 0
dans la table de recherche pour copier en tant que terminateur de chaîne de sortie, donc je compte cela comme +1 octet. Selon 32/64 bits (1 octet inc
ou non), et si nous pouvons supposer que l'appelant définit DF = 1 ( stosb
décroissant) ou autre, différentes versions sont (à égalité) les plus courtes.
DF = 1 à stocker dans l'ordre décroissant en fait une victoire pour xchg / stosb / xchg, mais souvent, l'appelant n'en voudra pas; C'est comme décharger le travail de l'appelant d'une manière difficile à justifier. (Contrairement aux registres de passage d'argument et de valeur de retour personnalisés, qui ne coûtent généralement pas de travail supplémentaire à un appelant asm.) Mais dans le code 64 bits, cld
/ scasb
fonctionne comme inc rdi
, évitant de tronquer le pointeur de sortie sur 32 bits, il est donc parfois gênant de conserver DF = 1 dans les fonctions de nettoyage 64 bits. . (Les pointeurs vers du code / des données statiques sont 32 bits dans les exécutables non PIE x86-64 sous Linux, et toujours dans l'ABI Linux x32, donc une version x86-64 utilisant des pointeurs 32 bits est utilisable dans certains cas.) Quoi qu'il en soit, cette interaction rend intéressant d'examiner différentes combinaisons d'exigences.
- IA32 avec un DF = 0 à la convention d'appel d'entrée / sortie: 17B (
nostring
) .
- IA32: 16B (avec une convention DF = 1:
stosb_edx_arg
ou skew
) ; ou avec DF entrant = dontcare, en le laissant réglé: 16 + 1Bstosb_decode_overlap
ou 17Bstosb_edx_arg
- x86-64 avec pointeurs 64 bits, et un DF = 0 sur la convention d'appel d'entrée / sortie: 17 + 1 octets (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
ou skew
)
x86-64 avec pointeurs 64 bits, autre gestion DF: 16B (DF = 1 skew
) , 17B ( nostring
avec DF = 1, en utilisant à la scasb
place de dec
). 18B ( stosb_edx_arg
préservant DF = 1 avec 3 octets inc rdi
).
Ou si nous permettons de renvoyer un pointeur à 1 octet avant la chaîne, 15B ( stosb_edx_arg
sans le inc
à la fin). Tous ensemble pour appeler à nouveau et développer une autre chaîne dans le tampon avec une base / table différente ... Mais cela aurait plus de sens si nous ne stockions pas de terminaison 0
non plus, et vous pourriez mettre le corps de la fonction dans une boucle, donc c'est vraiment un problème distinct.
x86-64 avec pointeur de sortie 32 bits, convention d'appel DF = 0: aucune amélioration par rapport au pointeur de sortie 64 bits, mais 18B ( nostring
) est maintenant lié.
- x86-64 avec pointeur de sortie 32 bits: aucune amélioration par rapport aux meilleures versions de pointeur 64 bits, donc 16B (DF = 1
skew
). Ou pour définir DF = 1 et le laisser, 17B pour skew
avec std
mais pas cld
. Ou 17 + 1B pour stosb_decode_overlap
avec inc edi
à la fin au lieu de cld
/ scasb
.
Avec une convention d'appel DF = 1: 16 octets (IA32 ou x86-64)
Nécessite DF = 1 en entrée, laisse-le défini. À peine plausible , au moins sur une base par fonction. Fait la même chose que la version ci-dessus, mais avec xchg pour obtenir le reste dans / hors de AL avant / après XLATB (recherche de table avec R / EBX comme base) et STOSB ( *output-- = al
).
Avec un DF = 0 normal sur la convention d'entrée / sortie, la version std
/ cld
/ scasb
est de 18 octets pour le code 32 et 64 bits, et est propre 64 bits (fonctionne avec un pointeur de sortie 64 bits).
Notez que les arguments d'entrée sont dans différents registres, y compris RBX pour la table (pour xlatb
). Notez également que cette boucle commence par stocker AL et se termine par le dernier caractère non encore enregistré (d'où le mov
à la fin). La boucle est donc "biaisée" par rapport aux autres, d'où le nom.
;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
Une version similaire non biaisée dépasse EDI / RDI, puis la corrige.
; 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
J'ai essayé une autre version de ceci avec lea esi, [rbx+rdx]
/ movsb
comme corps de boucle interne. (RSI est réinitialisé à chaque itération, mais RDI diminue). Mais il ne peut pas utiliser xor-zero / stos pour le terminateur, c'est donc 1 octet de plus. (Et ce n'est pas propre en 64 bits pour la table de recherche sans préfixe REX sur le LEA.)
LUT avec longueur explicite et terminateur 0: 16 + 1 octets (32 bits)
Cette version définit DF = 1 et le laisse de cette façon. Je compte l'octet LUT supplémentaire requis dans le cadre du nombre total d'octets.
L'astuce ici consiste à décoder les mêmes octets de deux manières différentes . Nous tombons au milieu de la boucle avec reste = base et quotient = numéro d'entrée, et copions le terminateur 0 en place.
Lors de la première utilisation de la fonction, les 3 premiers octets de la boucle sont consommés en tant qu'octets élevés d'un disp32 pour un LEA. Que LEA copie la base (module) sur EDX, idiv
produit le reste pour les itérations ultérieures.
Le 2ème octet de idiv ebp
is FD
, qui est l'opcode de l' std
instruction dont cette fonction a besoin pour fonctionner. (Ce fut une découverte chanceuse. J'avais déjà regardé cela avec div
plus tôt, qui se distingue de l' idiv
utilisation des /r
bits dans ModRM. Le 2ème octet de div epb
décodage as cmc
, ce qui est inoffensif mais pas utile. Mais avec idiv ebp
peut-on réellement supprimer le std
du haut de la fonction.)
Notez que les registres d'entrée sont à nouveau différents: EBP pour 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
Cette astuce de décodage qui se chevauchent peut également être utilisée avec cmp eax, imm32
: il ne faut que 1 octet pour avancer efficacement de 4 octets, seulement les drapeaux qui tapent. (C'est terrible pour les performances sur les processeurs qui marquent les limites des instructions dans le cache L1i, BTW.)
Mais ici, nous utilisons 3 octets pour copier un registre et sauter dans la boucle. Cela prendrait normalement 2 + 2 (mov + jmp), et nous permettrait de sauter dans la boucle juste avant le STOS au lieu d'avant le XLATB. Mais nous aurions alors besoin d'une MST distincte, et ce ne serait pas très intéressant.
Essayez-le en ligne! (avec un _start
appelant qui utilise sys_write
le résultat)
Il est préférable pour le débogage de l'exécuter sous strace
ou hexdump la sortie, afin que vous puissiez voir qu'il y a un \0
terminateur au bon endroit et ainsi de suite. Mais vous pouvez voir que cela fonctionne réellement et produire AAAAAACHOO
pour une entrée de
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(En fait xxAAAAAACHOO\0x\0\0...
, parce que nous vidons de 2 octets plus tôt dans le tampon vers une longueur fixe. Nous pouvons donc voir que la fonction a écrit les octets qu'elle était censée faire et n'a pas marché sur les octets qu'elle ne devrait pas avoir. le pointeur de début transmis à la fonction était l'avant-dernier x
caractère, suivi de zéros.)