função de código de máquina x86-64, 30 bytes.
Usa a mesma lógica recursividade como a resposta C por @Level River St . (Profundidade máxima de recursão = 100)
Usa a puts(3)
função da libc, à qual os executáveis normais estão vinculados de qualquer maneira. É possível chamar usando o x86-64 System V ABI, ou seja, do C no Linux ou OS X, e não derruba nenhum registro que não deveria.
objdump -drwC -Mintel
saída, comentada com explicação
0000000000400340 <g>: ## wrapper function
400340: 6a 64 push 0x64
400342: 5f pop rdi ; mov edi, 100 in 3 bytes instead of 5
; tailcall f by falling into it.
0000000000400343 <f>: ## the recursive function
400343: ff cf dec edi
400345: 97 xchg edi,eax
400346: 6a 0a push 0xa
400348: 5f pop rdi ; mov edi, 10
400349: 0f 8c d1 ff ff ff jl 400320 <putchar> # conditional tailcall
; if we don't tailcall, then eax=--n = arg for next recursion depth, and edi = 10 = '\n'
40034f: 89 f9 mov ecx,edi ; loop count = the ASCII code for newline; saves us one byte
0000000000400351 <f.loop>:
400351: 50 push rax ; save local state
400352: 51 push rcx
400353: 97 xchg edi,eax ; arg goes in rdi
400354: e8 ea ff ff ff call 400343 <f>
400359: 59 pop rcx ; and restore it after recursing
40035a: 58 pop rax
40035b: e2 f4 loop 400351 <f.loop>
40035d: c3 ret
# the function ends here
000000000040035e <_start>:
0x040035e - 0x0400340 = 30 bytes
# not counted: a caller that passes argc-1 to f() instead of calling g
000000000040035e <_start>:
40035e: 8b 3c 24 mov edi,DWORD PTR [rsp]
400361: ff cf dec edi
400363: e8 db ff ff ff call 400343 <f>
400368: e8 c3 ff ff ff call 400330 <exit@plt> # flush I/O buffers, which the _exit system call (eax=60) doesn't do.
Construído com yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
. Posso postar a fonte NASM original, mas isso pareceu uma bagunça, pois as instruções asm estão ali na desmontagem.
putchar@plt
está a menos de 128 bytes do jl
, então eu poderia ter usado um salto curto de 2 bytes em vez de um salto próximo de 6 bytes, mas isso é verdade apenas em um pequeno executável, não como parte de um programa maior. Portanto, acho que não posso justificar não contar o tamanho da implementação de libc's puts se eu também tirar proveito de uma codificação jcc curta para alcançá-la.
Cada nível de recursão usa 24B de espaço na pilha (2 pushs e o endereço de retorno pressionado por CALL). Qualquer outra profundidade pagará putchar
com a pilha alinhada apenas por 8, e não 16, portanto isso viola a ABI. Uma implementação stdio que usasse armazenamentos alinhados para derramar registros xmm na pilha falharia. Mas o glibc putchar
não faz isso, gravando em um pipe com buffer completo ou gravando em um terminal com buffer de linha. Testado no Ubuntu 15.10. Isso pode ser corrigido com um push / pop fictício no .loop
, para compensar a pilha em mais 8 antes da chamada recursiva.
Prova de que imprime o número certo de novas linhas:
# with a version that uses argc-1 (i.e. the shell's $i) instead of a fixed 100
$ for i in {0..8}; do echo -n "$i: "; ./golf-googol $(seq $i) |wc -c; done
0: 1
1: 10
2: 100
3: 1000
4: 10000
5: 100000
6: 1000000
7: 10000000
8: 100000000
... output = 10^n newlines every time.
Minha primeira versão disso foi 43B e usada puts()
em um buffer de 9 novas linhas (e um byte final de 0), portanto, o put acrescentaria o décimo. Esse caso base de recursão foi ainda mais próximo da inspiração em C.
Fatorar 10 ^ 100 de uma maneira diferente pode ter reduzido o buffer, talvez até 4 novas linhas, economizando 5 bytes, mas o uso de putchar é de longe o melhor. Ele só precisa de um argumento inteiro, não um ponteiro e nenhum buffer. O padrão C permite implementações para as quais é uma macro putc(val, stdout)
, mas na glibc existe como uma função real que você pode chamar do asm.
Imprimir apenas uma nova linha por chamada em vez de 10 significa apenas que precisamos aumentar a profundidade máxima da recursão em 1, para obter outro fator de 10 novas linhas. Como 99 e 100 podem ser representados por um imediato de 8 bits com extensão de sinal, push 100
ainda há apenas 2 bytes.
Melhor ainda, ter 10
um registro funciona como uma nova linha e um contador de loop, economizando um byte.
Ideias para salvar bytes
Uma versão de 32 bits pode salvar um byte para o dec edi
, mas a convenção de chamada stack-args (para funções de biblioteca como putchar) faz com que a chamada de cauda funcione com menos facilidade e provavelmente exigiria mais bytes em mais locais. Eu poderia usar uma convenção register-arg para o privado f()
, apenas chamado por g()
, mas não poderia chamar putchar (porque f () e putchar () levariam um número diferente de stack-args).
Seria possível ter f () preservar o estado do chamador, em vez de salvar / restaurar o chamador. Isso provavelmente é péssimo, porque provavelmente precisaria ficar separadamente em cada lado do galho e não é compatível com o chamado. Eu tentei, mas não encontrei nenhuma economia.
Manter um contador de loop na pilha (em vez de empurrar / colocar rcx no loop) também não ajudou. Foi 1B pior com a versão que usou puts, e provavelmente ainda mais com esta versão que configura o rcx mais barato.