x86-64-Maschinencodefunktion, 30 Byte.
Verwendet die gleiche Rekursion Logik wie die C - Antwort von @Level River St . (Maximale Rekursionstiefe = 100)
Verwendet die puts(3)
Funktion von libc, mit der normale ausführbare Dateien sowieso verknüpft sind. Es kann mit x86-64 System V ABI, dh von C unter Linux oder OS X, aufgerufen werden und staut keine Register, die es nicht sollte.
objdump -drwC -Mintel
Ausgabe, kommentiert mit Erklärung
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.
Gebaut mit yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
. Ich kann die Original-NASM-Quelle posten, aber das schien unübersichtlich zu sein, da die ASM-Anweisungen direkt in der Demontage enthalten sind.
putchar@plt
ist weniger als 128 Bytes von der jl
, also hätte ich einen 2-Byte-Kurzsprung anstelle eines 6-Byte-Kurzsprungs verwenden können, aber das ist nur in einer winzigen ausführbaren Datei wahr, nicht als Teil eines größeren Programms. Daher glaube ich nicht, dass ich es rechtfertigen kann, die Größe der Puts-Implementierung von libc nicht zu zählen, wenn ich auch eine kurze JCC-Codierung nutze, um sie zu erreichen.
Jede Rekursionsebene belegt 24B Stapelspeicher (2 Pushs und die von CALL gepushte Rücksprungadresse ). Jede andere Tiefe wird aufgerufen, putchar
wobei der Stapel nur um 8 ausgerichtet ist, nicht um 16, was den ABI verletzt. Eine stdio-Implementierung, bei der ausgerichtete Speicher verwendet wurden, um xmm-Register auf den Stapel zu übertragen, würde einen Fehler verursachen. Aber Glibcs putchar
tun das nicht, sie schreiben in eine Pipe mit voller Pufferung oder in ein Terminal mit Zeilenpufferung. Getestet unter Ubuntu 15.10. Dies könnte mit einem Dummy-Push / Pop im behoben werden .loop
, um den Stapel vor dem rekursiven Aufruf um weitere 8 zu versetzen.
Beweis, dass es die richtige Anzahl von Zeilenumbrüchen druckt:
# 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.
Meine erste Version davon war 43B und wurde puts()
in einem Puffer mit 9 Zeilenumbrüchen (und einem abschließenden 0-Byte) verwendet, also würden Puts das 10. anhängen. Dieser Rekursionsgrundfall war noch näher an der C-Inspiration.
Ein anderer Faktor für 10 ^ 100 hätte den Puffer möglicherweise verkürzen können, möglicherweise auf 4 Zeilen für neue Zeilen, wodurch 5 Byte gespart werden. Die Verwendung von Putchar ist jedoch bei weitem besser. Es wird nur ein ganzzahliges Argument, kein Zeiger und überhaupt kein Puffer benötigt. Der C-Standard erlaubt Implementierungen, für die es ein Makro ist putc(val, stdout)
, aber in glibc existiert es als echte Funktion, die Sie von asm aus aufrufen können.
Das Drucken von nur einer neuen Zeile pro Aufruf anstelle von 10 bedeutet lediglich, dass die maximale Tiefe der Rekursion um 1 erhöht werden muss, um einen weiteren Faktor von 10 neuen Zeilen zu erhalten. Da 99 und 100 beide durch eine vorzeichenerweiterte 8-Bit-Direktdarstellung dargestellt werden können, push 100
sind immer noch nur 2 Bytes.
Noch besser ist, dass 10
ein Register sowohl als Zeilenvorschub- als auch als Schleifenzähler fungiert und ein Byte spart.
Ideen zum Speichern von Bytes
Eine 32-Bit-Version könnte ein Byte für das speichern dec edi
, aber die Aufrufkonvention stack-args (für Bibliotheksfunktionen wie putchar) macht das Aufrufen von Tail-Calls weniger einfach und würde wahrscheinlich mehr Bytes an mehr Stellen erfordern. Ich könnte eine register-arg-Konvention für den privaten f()
, nur von aufgerufenen benutzen g()
, aber dann könnte ich putchar nicht mit einem Tail-Call aufrufen (weil f () und putchar () eine andere Anzahl von Stack-Args annehmen würden).
Es wäre möglich, f () zu veranlassen, den Status des Aufrufers beizubehalten, anstatt das Speichern / Wiederherstellen im Aufrufer durchzuführen. Das ist aber wahrscheinlich nicht gut, weil es wahrscheinlich auf jeder Seite des Zweigs getrennt werden müsste und mit Tailcalling nicht kompatibel ist. Ich habe es versucht, aber keine Einsparungen gefunden.
Es hat auch nicht geholfen, einen Schleifenzähler auf dem Stapel zu belassen (anstatt rcx in der Schleife zu pushen / poppen). Es war 1B schlechter mit der Version, die Puts verwendete, und wahrscheinlich sogar ein Verlust mit dieser Version, die RCX billiger einrichtet.