Código de máquina x86 de 32 bits (enteros de 32 bits): 17 bytes.
(vea también otras versiones a continuación, incluidos 16 bytes para 32 bits o 64 bits, con una convención de llamada DF = 1).
La persona que llama pasa argumentos en los registros, incluido un puntero al final de un búfer de salida (como mi respuesta C ; míralo para justificar y explicar el algoritmo). El interno de glibc _itoa
hace esto , por lo que no solo está diseñado para el golf de código. Los registros de paso de argumentos están cerca de x86-64 System V, excepto que tenemos un argumento en EAX en lugar de EDX.
Al regresar, EDI apunta al primer byte de una cadena C terminada en 0 en el búfer de salida. El registro de valor de retorno habitual es EAX / RAX, pero en lenguaje ensamblador puede usar cualquier convención de llamada que sea conveniente para una función. ( xchg eax,edi
al final agregaría 1 byte).
La persona que llama puede calcular una longitud explícita si lo desea, desde buffer_end - edi
. Pero no creo que podamos justificar la omisión del terminador a menos que la función realmente devuelva punteros de inicio + fin o puntero + longitud. Eso ahorraría 3 bytes en esta versión, pero no creo que sea justificable.
- EAX = n = número para decodificar. (Para
idiv
. Los otros argumentos no son operandos implícitos).
- EDI = fin del búfer de salida (la versión de 64 bits todavía se usa
dec edi
, por lo que debe estar en los 4GiB bajos)
- ESI / RSI = tabla de búsqueda, también conocida como LUT. No golpeó.
- ECX = longitud de la tabla = base. No golpeó.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Editado a mano para reducir los comentarios, la numeración de líneas es extraña).
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 sorprendente que la versión más simple, básicamente sin compensaciones de velocidad / tamaño, sea la más pequeña, pero std
/ cld
costó 2 bytes para usar stosb
en orden descendente y seguir la convención de llamadas DF = 0 común. (Y STOS disminuye después del almacenamiento, dejando el puntero apuntando un byte demasiado bajo en la salida del bucle, lo que nos cuesta bytes adicionales para evitar).
Versiones
Se me ocurrieron 4 trucos de implementación significativamente diferentes (usando simple mov
load / store (arriba), usando lea
/ movsb
(ordenado pero no óptimo), usando xchg
/ xlatb
/ stosb
/ xchg
, y uno que ingresa al ciclo con un hack de instrucciones superpuestas. Ver código a continuación) . El último necesita un seguimiento 0
en la tabla de búsqueda para copiar como terminador de cadena de salida, por lo que lo cuento como +1 byte. Dependiendo de 32/64 bits (1 byte inc
o no), y si podemos asumir que la persona que llama establece DF = 1 ( stosb
descendente) o lo que sea, las diferentes versiones son (empatadas) más cortas.
DF = 1 para almacenar en orden descendente lo convierte en una victoria para xchg / stosb / xchg, pero la persona que llama a menudo no querrá eso; Se siente como descargar el trabajo a la persona que llama de una manera difícil de justificar. (A diferencia de los registros personalizados de paso de argumento y valor de retorno, que generalmente no cuestan a un llamador de asm ningún trabajo adicional). Pero en el código de 64 bits, cld
/ scasb
funciona como inc rdi
, evitando truncar el puntero de salida a 32 bits, por lo que a veces es inconveniente para preservar DF = 1 en funciones de limpieza de 64 bits. . (Los punteros a código / datos estáticos son de 32 bits en x86-64 ejecutables no PIE en Linux, y siempre en Linux x32 ABI, por lo que en algunos casos se puede usar una versión x86-64 que usa punteros de 32 bits). Esta interacción hace que sea interesante observar diferentes combinaciones de requisitos.
- IA32 con un DF = 0 en la convención de llamada de entrada / salida: 17B (
nostring
) .
- IA32: 16B (con una convención DF = 1:
stosb_edx_arg
o skew
) ; o con DF entrante = no importa, dejándolo configurado: 16 + 1Bstosb_decode_overlap
o 17Bstosb_edx_arg
- x86-64 con punteros de 64 bits y un DF = 0 en la convención de llamada de entrada / salida: 17 + 1 bytes (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
o skew
)
x86-64 con punteros de 64 bits, otro manejo de DF: 16B (DF = 1 skew
) , 17B ( nostring
con DF = 1, usando en scasb
lugar de dec
). 18B ( stosb_edx_arg
preservando DF = 1 con 3 bytes inc rdi
).
O si permitimos devolver un puntero a 1 byte antes de la cadena, 15B ( stosb_edx_arg
sin inc
el final). Todo listo para llamar de nuevo y expandir otra cadena en el búfer con una base / tabla diferente ... Pero eso tendría más sentido si tampoco almacenamos una terminación 0
, y podría poner el cuerpo de la función dentro de un bucle, así que eso es realmente un problema separado
x86-64 con puntero de salida de 32 bits, convención de llamada DF = 0: no hay mejora con respecto al puntero de salida de 64 bits, pero nostring
ahora se vincula 18B ( ).
- x86-64 con puntero de salida de 32 bits: no hay mejora con respecto a las mejores versiones de puntero de 64 bits, por lo que 16B (DF = 1
skew
). O para establecer DF = 1 y dejarlo, 17B para skew
con std
pero no cld
. O 17 + 1B para stosb_decode_overlap
con inc edi
al final en lugar de cld
/ scasb
.
Con una convención de llamada DF = 1: 16 bytes (IA32 o x86-64)
Requiere DF = 1 en la entrada, lo deja configurado. Apenas plausible , al menos en función de cada función. Hace lo mismo que la versión anterior, pero con xchg para obtener el resto de entrada / salida de AL antes / después de XLATB (búsqueda de tabla con R / EBX como base) y STOSB ( *output-- = al
).
Con un DF = 0 normal en la convención de entrada / salida, la versión std
/ cld
/ scasb
tiene 18 bytes para el código de 32 y 64 bits, y está limpia en 64 bits (funciona con un puntero de salida de 64 bits).
Tenga en cuenta que los argumentos de entrada están en diferentes registros, incluido RBX para la tabla (para xlatb
). También tenga en cuenta que este ciclo comienza almacenando AL y termina con el último carácter que aún no está almacenado (de ahí mov
el final). Entonces el bucle está "sesgado" en relación con los demás, de ahí el nombre.
;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 versión similar no sesgada supera el EDI / RDI y luego lo arregla.
; 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
Intenté una versión alternativa de esto con lea esi, [rbx+rdx]
/ movsb
como el cuerpo del bucle interno. (RSI se restablece cada iteración, pero RDI disminuye). Pero no puede usar xor-zero / stos para el terminador, por lo que es 1 byte más grande. (Y no está limpio para 64 bits para la tabla de búsqueda sin un prefijo REX en la LEA).
LUT con longitud explícita y un terminador 0: 16 + 1 bytes (32 bits)
Esta versión establece DF = 1 y lo deja así. Estoy contando el byte LUT adicional requerido como parte del recuento total de bytes.
El truco genial aquí es tener los mismos bytes decodificar de dos maneras diferentes . Caemos en el medio del bucle con resto = base y cociente = número de entrada, y copiamos el terminador 0 en su lugar.
En la primera vez a través de la función, los primeros 3 bytes del bucle se consumen como los bytes altos de un disp32 para un LEA. Esa LEA copia la base (módulo) a EDX, idiv
produce el resto para iteraciones posteriores.
El segundo byte de idiv ebp
es FD
, que es el código de operación para la std
instrucción que esta función necesita para funcionar. (Este fue un descubrimiento afortunado. Había estado mirando esto div
antes, que se distingue del idiv
uso de los /r
bits en ModRM. El segundo byte de div epb
decodifica como cmc
, que es inofensivo pero no útil. Pero con idiv ebp
eso podemos eliminarlo std
de la parte superior de la función.)
Tenga en cuenta que los registros de entrada son una vez más diferencia: EBP para 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
Este truco de decodificación superpuesta también se puede usar con cmp eax, imm32
: solo se necesita 1 byte para avanzar de manera efectiva 4 bytes, solo banderas de clobbering. (Esto es terrible para el rendimiento en las CPU que marcan los límites de las instrucciones en la caché L1i, por cierto).
Pero aquí, estamos usando 3 bytes para copiar un registro y saltar al bucle. Eso normalmente tomaría 2 + 2 (mov + jmp), y nos permitiría saltar al bucle justo antes del STOS en lugar de antes del XLATB. Pero entonces necesitaríamos una ETS por separado, y no sería muy interesante.
Pruébalo en línea! (con una _start
persona que llama que usa sys_write
en el resultado)
Es mejor para la depuración ejecutarlo strace
o reducir la salida de forma hexadecimal, para que pueda verificar que haya un \0
terminador en el lugar correcto y así sucesivamente. Pero puede ver que esto realmente funciona y producir AAAAAACHOO
para una entrada de
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(En realidad xxAAAAAACHOO\0x\0\0...
, porque estamos volcando desde 2 bytes antes en el búfer a una longitud fija. Entonces podemos ver que la función escribió los bytes que se suponía que debía y no pisó ningún byte que no debería tener. El puntero de inicio que se pasó a la función fue el segundo y último x
carácter, seguido de ceros).