x86-64マシンコード関数、30バイト。
@Level River StによるCの回答と同じ再帰ロジックを使用します。(最大再帰深度= 100)
puts(3)
とにかく通常の実行可能ファイルがリンクされているlibc の関数を使用します。これは、x86-64 System V ABIを使用して呼び出し可能です。つまり、LinuxまたはOS XのCから呼び出され、想定されていないレジスタを上書きしません。
objdump -drwC -Mintel
出力、説明付きのコメント付き
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.
で構築 yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
。元のNASMソースを投稿することはできますが、逆アセンブリのasm命令がそこにあるため、混乱しているように見えました。
putchar@plt
から128バイト未満離れているjl
ため、6バイトのニアジャンプの代わりに2バイトのショートジャンプを使用できましたが、これは小さなプログラムの一部であり、大きなプログラムの一部ではありません。そのため、短いjccエンコーディングも利用してlibcのputs実装のサイズをカウントしないことを正当化できないと思います。
再帰の各レベルは、24Bのスタックスペースを使用します(2回のプッシュと、CALLによってプッシュされたリターンアドレス)。他のすべての深さはputchar
、16ではなく8だけでスタックを呼び出すため、これはABIに違反します。整列ストアを使用してxmmレジスタをスタックにスピルするstdio実装ではエラーが発生します。しかし、glibc putchar
はそれを行いません。完全バッファリングでパイプに書き込むか、行バッファリングで端末に書き込みます。Ubuntu 15.10。でテスト済み。これは、のダミープッシュ/ポップで修正して.loop
、再帰呼び出しの前にスタックをさらに8オフセットすることができます。
適切な数の改行を印刷することの証明:
# 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.
これの最初のバージョンは43Bで、puts()
9個の改行(および終端の0バイト)のバッファーで使用されるため、putsは10番目を追加します。その再帰ベースケースは、Cのインスピレーションにさらに近かった。
別の方法で10 ^ 100を因数分解すると、バッファが短くなり、4改行になり、5バイト節約されたかもしれませんが、putcharを使用する方がはるかに優れています。必要なのは整数の引数のみであり、ポインターは必要ありません。バッファーはまったく必要ありません。C標準では、のマクロである実装が許可されていますputc(val, stdout)
が、glibcでは、asmから呼び出すことができる実際の関数として存在します。
呼び出しごとに10ではなく1つの改行のみを印刷するということは、10個の改行の別の係数を取得するために、再帰最大深度を1増やす必要があることを意味します。99と100は両方とも符号拡張された8ビットの即値で表すことができるため、push 100
2バイトのみです。
さらに良いことに10
は、レジスタに入れると、改行とループカウンタの両方として機能し、バイトを節約できます。
バイトを節約するためのアイデア
32ビットバージョンではのバイトを節約できますdec edi
が、スタック引数呼び出し規則(putcharなどのライブラリ関数の場合)を使用すると、末尾呼び出しの動作が簡単になり、おそらくより多くの場所でより多くのバイトが必要になります。private f()
にregister-arg規則を使用することはできますが、呼び出されるのg()
はputcharだけです(f()とputchar()は異なる数のstack- argを使用するため)。
呼び出し元で保存/復元を行う代わりに、f()に呼び出し元の状態を保存させることができます。ただし、ブランチの両側で個別に取得する必要があり、テールコールとの互換性がないため、これはおそらく面倒です。私はそれを試しましたが、貯金は見つかりませんでした。
(ループ内でrcxをプッシュ/ポップする代わりに)ループカウンターをスタックに保持することも助けにはなりませんでした。putを使用したバージョンでは1B悪化し、おそらくrcxをより安価にセットアップするこのバージョンではさらに損失が大きくなりました。