funzione codice macchina x86-64, 30 byte.
Utilizza la stessa logica di ricorsione della risposta C di @Level River St . (Profondità di ricorsione massima = 100)
Utilizza la puts(3)funzione di libc, a cui sono comunque collegati i normali eseguibili. È richiamabile usando l'ABI System V x86-64, ovvero da C su Linux o OS X, e non ostruisce alcun registro che non dovrebbe.
objdump -drwC -Mintel output, commentato con spiegazione
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.
Costruito con yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o . Posso pubblicare l'origine NASM originale, ma mi è sembrato un disordine poiché le istruzioni asm sono proprio lì nello smontaggio.
putchar@pltè a meno di 128 byte dal jl, quindi avrei potuto usare un salto corto di 2 byte invece di un salto vicino di 6 byte, ma questo è vero solo in un piccolo eseguibile, non come parte di un programma più grande. Quindi non credo di poter giustificare non contare le dimensioni dell'implementazione di libc se approfitto anche di una breve codifica jcc per raggiungerla.
Ogni livello di ricorsione utilizza 24 B di spazio stack (2 push e l'indirizzo di ritorno inviato da CALL). Ogni altra profondità chiamerà putcharcon lo stack allineato solo di 8, non di 16, quindi ciò viola l'ABI. Un'implementazione stdio che utilizzava negozi allineati per versare i registri xmm nello stack avrebbe un difetto. Ma glibc putcharnon lo fa, scrivendo su una pipe con buffering completo o scrivendo su un terminale con buffering di linea. Testato su Ubuntu 15.10. Questo potrebbe essere risolto con un push / pop fittizio in .loop, per compensare lo stack di altri 8 prima della chiamata ricorsiva.
Prova che stampa il giusto numero di newline:
# 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.
La mia prima versione di questo è stata 43B e utilizzata puts() su un buffer di 9 newline (e un byte di terminazione 0), quindi put avrebbe aggiunto il decimo. Quella base di ricorsione era ancora più vicina all'ispirazione C.
Il factoring di 10 ^ 100 in un modo diverso avrebbe forse abbreviato il buffer, forse fino a 4 newline, risparmiando 5 byte, ma usare putchar è di gran lunga migliore. Richiede solo un argomento intero, non un puntatore e nessun buffer. Lo standard C consente implementazioni in cui è una macro putc(val, stdout), ma in glibc esiste come una vera funzione che puoi chiamare da asm.
Stampare solo una nuova linea per chiamata anziché 10 significa solo che dobbiamo aumentare la profondità massima di ricorsione di 1, per ottenere un altro fattore di 10 nuove linee. Poiché 99 e 100 possono entrambi essere rappresentati da un immediato a 8 bit con estensione del segno,push 100 è ancora solo 2 byte.
Ancora meglio, avere 10in un registro funziona sia come newline che come contatore di loop, salvando un byte.
Idee per salvare byte
Una versione a 32 bit potrebbe salvare un byte per la dec edi, ma la convenzione di chiamata stack-args (per funzioni di libreria come putchar) rende meno facile il lavoro di coda e probabilmente richiederebbe più byte in più punti. Potrei usare una convenzione register-arg per il privato f(), chiamata solo da g(), ma poi non posso chiamare tailc putchar (perché f () e putchar () richiederebbero un numero diverso di stack-arg).
Sarebbe possibile avere f () preservare lo stato del chiamante, invece di fare il salvataggio / ripristino nel chiamante. Ciò probabilmente fa schifo, tuttavia, perché probabilmente dovrebbe essere separato in ciascun lato del ramo e non è compatibile con il tailcalling. L'ho provato ma non ho trovato alcun risparmio.
Mantenere un contatore di loop in pila (invece di premere / saltando rcx nel loop) non ha aiutato neanche. Era 1B peggio con la versione che usava put, e probabilmente anche più di una perdita con questa versione che imposta rcx in modo più economico.