Fonction du code machine x86-64, 30 octets.
Utilise la même logique récursive que la réponse C par @level rivière St . (Profondeur maximale de récursivité = 100)
Utilise la puts(3)
fonction de libc, à laquelle les exécutables normaux sont liés de toute façon. Il est appelable à l'aide de l'ABI System V x86-64, c'est-à-dire à partir de C sous Linux ou OS X, et n'empiète pas sur les registres non prévus.
objdump -drwC -Mintel
sortie, commentée avec explication
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.
Construit avec yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
. Je peux publier la source NASM d'origine, mais cela semblait être une image de fond, car les instructions asm sont exactement là dans le désassemblage.
putchar@plt
est à moins de 128 octets de la jl
, alors j'aurais pu utiliser un saut court de 2 octets au lieu d'un saut de près de 6 octets, mais ce n'est vrai que dans un exécutable minuscule, pas dans le cadre d'un programme plus important. Donc, je ne pense pas pouvoir justifier de ne pas prendre en compte la taille de la mise en œuvre de libc si je profite également d'un codage jcc court pour l'atteindre.
Chaque niveau de récursivité utilise 24B d’espace de pile (2 envois et l’adresse de retour transmise par CALL). Toutes les autres profondeurs appellent putchar
avec la pile alignée uniquement par 8 et non 16, ce qui constitue une violation de l’ABI. Une implémentation stdio qui utilisait des magasins alignés pour renverser des registres xmm sur la pile serait défaillante. Mais glibc's putchar
ne fait pas cela, écrire dans un tube avec mise en mémoire tampon complète ou écrire sur un terminal avec mise en mémoire tampon de ligne. Testé sur Ubuntu 15.10. Cela pourrait être corrigé avec un push / pop factice dans le .loop
, pour décaler la pile de 8 autres avant l'appel récursif.
La preuve qu'il imprime le bon nombre de nouvelles lignes:
# 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.
Ma première version était 43B, et utilisée puts()
sur un tampon de 9 nouvelles lignes (et un octet final 0), donc put ajouter le 10ème. Ce cas de base de la récursivité était encore plus proche de l'inspiration en C.
Factoriser 10 ^ 100 d'une manière différente aurait peut-être pu raccourcir la mémoire tampon, peut-être même réduire à 4 lignes, économiser 5 octets, mais utiliser putchar est de loin meilleur. Il n'a besoin que d'un argument entier, pas d'un pointeur, et pas de tampon du tout. Le standard C autorise les implémentations pour lesquelles c'est une macro putc(val, stdout)
, mais dans glibc, il existe une fonction réelle que vous pouvez appeler depuis asm.
Imprimer seulement un saut de ligne par appel au lieu de 10 signifie simplement que nous devons augmenter la profondeur maximale de récursivité de 1, pour obtenir un facteur supplémentaire de 10 sauts de ligne. Étant donné que 99 et 100 peuvent tous deux être représentés par un signe immédiat étendu sur 8 bits, push 100
n’a encore que 2 octets.
Mieux encore, la présence 10
dans un registre fonctionne à la fois comme une nouvelle ligne et un compteur de boucles, ce qui enregistre un octet.
Idées pour économiser des octets
Une version 32 bits pourrait économiser un octet pour le dec edi
, mais la convention d'appel stack-args (pour les fonctions de bibliothèque telles que putchar) facilite le travail de suivi d'appels et nécessiterait probablement davantage d'octets à plusieurs endroits. Je pouvais utiliser une convention register-arg pour le privé f()
, uniquement appelé par g()
, mais je ne pouvais pas ensuite appeler putchar (car f () et putchar () prendraient un nombre différent d'arguments stack).
Il serait possible que f () conserve l'état de l'appelant au lieu d'effectuer la sauvegarde / restauration dans l'appelant. Cela ne sert probablement à rien, cependant, car il faudrait probablement qu'il se place séparément de chaque côté de la branche et que cela ne soit pas compatible avec le suivi des appels. J'ai essayé mais je n'ai trouvé aucune économie.
Garder un compteur de boucle sur la pile (au lieu de pousser / décompresser rcx dans la boucle) n'a pas aidé non plus. C'était 1B pire avec la version qui utilisait des options de vente, et probablement encore plus d'une perte avec cette version qui met en place rcx à moindre coût.