código de máquina x86 de 32 bits (números inteiros de 32 bits): 17 bytes.
(veja também outras versões abaixo, incluindo 16 bytes para 32 bits ou 64 bits, com uma convenção de chamada DF = 1.)
Chamador passa args em registos, incluindo um ponteiro para o final de um buffer de saída (como minha resposta C ; vê-lo para a justificação e explicação do algoritmo.) Da glibc interna _itoa
faz isso , então não é apenas inventado para o código-golfe. Os registros de passagem de arg estão próximos do x86-64 System V, exceto que temos um arg no EAX, em vez do EDX.
No retorno, o EDI aponta para o primeiro byte de uma cadeia C terminada em 0 no buffer de saída. O registro de valor-retorno usual é EAX / RAX, mas na linguagem assembly você pode usar qualquer convenção de chamada que seja conveniente para uma função. ( xchg eax,edi
no final adicionaria 1 byte).
O chamador pode calcular um comprimento explícito, se desejar, de buffer_end - edi
. Mas acho que não podemos justificar a omissão do terminador, a menos que a função realmente retorne ponteiros de início + fim ou comprimento de ponteiro. Isso economizaria 3 bytes nesta versão, mas não acho que seja justificável.
- EAX = n = número para decodificar. (Para
idiv
. Os outros argumentos não são operandos implícitos.)
- EDI = buffer de final de saída (a versão de 64 bits ainda usa
dec edi
, portanto deve estar com pouco 4GiB)
- ESI / RSI = tabela de pesquisa, também conhecida como LUT. não derrotou.
- ECX = comprimento da tabela = base. não derrotou.
nasm -felf32 ascii-compress-base.asm -l /dev/stdout | cut -b -30,$((30+10))-
(Editado manualmente para reduzir os comentários, a numeração das linhas é estranha.)
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
É surpreendente que a versão mais simples, basicamente sem trocas de velocidade / tamanho, seja a menor, mas std
/ cld
custa 2 bytes para usar stosb
em ordem decrescente e ainda seguir a convenção de chamada DF = 0 comum. (E o STOS diminui após o armazenamento, deixando o ponteiro apontando um byte muito baixo na saída do loop, nos custando bytes extras para contornar.)
Versões:
Eu criei 4 truques de implementação significativamente diferentes (usando mov
carga / armazenamento simples (acima), usando lea
/ movsb
(puro, mas não ideal), usando xchg
/ xlatb
/ stosb
/ xchg
, e um que entra no loop com um hack de instrução sobreposta. Veja o código abaixo) . O último precisa de uma trilha 0
na tabela de pesquisa para copiar como o terminador da cadeia de saída, então estou contando isso como +1 byte. Dependendo de 32/64 bits (1 byte inc
ou não), e se podemos assumir que o chamador define DF = 1 ( stosb
descendente) ou o que for, versões diferentes são (ligadas a) mais curtas.
DF = 1 para armazenar em ordem decrescente torna uma vitória para xchg / stosb / xchg, mas o chamador geralmente não deseja isso; Parece transferir o trabalho para o chamador de uma maneira difícil de justificar. (Diferentemente dos registros arg-pass e de valor-retorno personalizados, que normalmente não custam a um chamador asm nenhum trabalho extra.) Mas no código de 64 bits, cld
/ scasb
funciona como inc rdi
, evitando truncar o ponteiro de saída para 32 bits, então às vezes inconveniente preservar DF = 1 em funções de limpeza de 64 bits. . (Ponteiros para dados / código estático são 32 bits em executáveis não PIE x86-64 no Linux e sempre na ABI do Linux x32, portanto, em alguns casos, uma versão x86-64 usando ponteiros de 32 bits é utilizável.) De qualquer forma, essa interação torna interessante observar diferentes combinações de requisitos.
- IA32 com um DF = 0 na convenção de chamada de entrada / saída: 17B (
nostring
) .
- IA32: 16B (com uma convenção DF = 1:
stosb_edx_arg
ou skew
) ; ou com DF de entrada = não cuidar, deixando-o definido: 16 + 1Bstosb_decode_overlap
ou 17Bstosb_edx_arg
- x86-64 com ponteiros de 64 bits e um DF = 0 na convenção de chamada de entrada / saída: 17 + 1 bytes (
stosb_decode_overlap
) , 18B ( stosb_edx_arg
ou skew
)
x86-64 com ponteiros de 64 bits, outro tratamento de DF: 16B (DF = 1 skew
) , 17B ( nostring
com DF = 1, usando em scasb
vez de dec
). 18B ( stosb_edx_arg
preservando DF = 1 com 3 bytes inc rdi
).
Ou se permitirmos retornar um ponteiro para 1 byte antes da string, 15B ( stosb_edx_arg
sem o inc
no final). Tudo pronto para chamar novamente e expandir outra string no buffer com base / tabela diferente ... Mas isso faria mais sentido se não armazenássemos uma terminação 0
também, e você pode colocar o corpo da função dentro de um loop, então isso é realmente um problema separado.
x86-64 com ponteiro de saída de 32 bits, DF = 0 convenção de chamada: nenhuma melhoria em relação ao ponteiro de saída de 64 bits, mas 18B ( nostring
) está vinculado agora.
- x86-64 com ponteiro de saída de 32 bits: nenhuma melhoria em relação às melhores versões de ponteiro de 64 bits, então 16B (DF = 1
skew
). Ou para definir DF = 1 e deixá-lo, 17B para skew
com std
mas não cld
. Ou 17 + 1B para stosb_decode_overlap
com inc edi
no final em vez de cld
/ scasb
.
Com uma convenção de chamada DF = 1: 16 bytes (IA32 ou x86-64)
Requer DF = 1 na entrada, deixa-o definido. Quase plausível , pelo menos por função. Faz o mesmo que a versão acima, mas com o xchg para obter o restante da entrada / saída do AL antes / depois do XLATB (pesquisa de tabela com R / EBX como base) e STOSB ( *output-- = al
).
Com um DF = 0 normal na convenção de entrada / saída, a versão std
/ cld
/ scasb
é de 18 bytes para códigos de 32 e 64 bits e é limpa de 64 bits (funciona com um ponteiro de saída de 64 bits).
Observe que os argumentos de entrada estão em registradores diferentes, incluindo o RBX para a tabela (para xlatb
). Observe também que esse loop começa armazenando AL e termina com o último caractere ainda não armazenado (portanto, mov
no final). Portanto, o loop é "inclinado" em relação aos outros, daí o 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
Uma versão semelhante não distorcida ultrapassa o EDI / RDI e depois o 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
Eu tentei uma versão alternativa disso com lea esi, [rbx+rdx]
/ movsb
como o corpo do loop interno. (O RSI é redefinido a cada iteração, mas o RDI diminui). Mas ele não pode usar xor-zero / stos para o terminador, por isso é 1 byte maior. (E não está limpo de 64 bits para a tabela de pesquisa sem um prefixo REX no LEA.)
LUT com comprimento explícito e um terminador 0: 16 + 1 bytes (32 bits)
Esta versão define DF = 1 e deixa assim. Estou contando o byte extra de LUT necessário como parte da contagem total de bytes.
O truque legal aqui é ter os mesmos bytes decodificar de duas maneiras diferentes . Caímos no meio do loop com o restante = base e quociente = número de entrada e copiamos o terminador 0 no lugar.
Na primeira vez através da função, os 3 primeiros bytes do loop são consumidos como os bytes altos de um disp32 para um LEA. Esse LEA copia a base (módulo) para o EDX, idiv
produz o restante para iterações posteriores.
O segundo byte de idiv ebp
é FD
, que é o código de operação para a std
instrução que esta função precisa para funcionar. (Esta foi uma descoberta de sorte. Eu já estava examinando isso div
anteriormente, o que se distingue do idiv
uso dos /r
bits no ModRM. O segundo byte de div epb
decodifica como cmc
, o que é inofensivo, mas não é útil. Mas com idiv ebp
podemos remover o std
de cima da função.)
Observe que os registros de entrada são diferentes novamente: EBP para a 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
Esse truque de decodificação sobreposto também pode ser usado com cmp eax, imm32
: são necessários apenas 1 byte para avançar efetivamente 4 bytes, apenas sinalizadores de oscilação. (Isso é terrível para o desempenho em CPUs que marcam os limites de instruções no cache L1i, BTW.)
Mas aqui, estamos usando 3 bytes para copiar um registro e pular para o loop. Isso normalmente levaria 2 + 2 (mov + jmp) e nos permitiria entrar no loop imediatamente antes do STOS em vez de antes do XLATB. Mas então precisaríamos de uma DST separada, e não seria muito interessante.
Experimente online! (com um _start
chamador que usa sys_write
no resultado)
É melhor para a depuração executá-lo strace
ou fazer o hexdump da saída, para que você possa verificar se há um \0
terminador no lugar certo e assim por diante. Mas você pode ver isso realmente funcionar e produzir AAAAAACHOO
para uma entrada de
num equ 698911
table: db "CHAO"
%endif
tablen equ $ - table
db 0 ; "terminator" needed by ascii_compress_stosb_decode_overlap
(Na verdade xxAAAAAACHOO\0x\0\0...
, porque estamos despejando de 2 bytes anteriores no buffer para um comprimento fixo. Assim, podemos ver que a função gravou os bytes que deveria e não pisou em nenhum bytes que não deveria ter. O ponteiro de início passado para a função era o segundo e último x
caractere, seguido por zeros.)