コンパイラを作成するためにx86アセンブラを学習しています。特に、さまざまなコンパイラーによって生成されるアセンブラーの種類をよりよく理解するために、さまざまな単純な再帰関数を取得し、それらをさまざまなコンパイラー(OCaml、GCCなど)にフィードしています。
簡単な再帰整数フィボナッチ関数があります。
int fib(int x) { return (x < 2 ? x : fib(x-1)+fib(x-2)); }
私の手書きのアセンブリは次のようになります。
fib:
cmp eax, 2
jl fin
push eax
dec eax
call fib
push eax
mov eax, [esp+4]
add eax, -2
call fib
add eax, [esp]
add esp, 8
fin:
ret
を使用してその関数をIntel-syntaxアセンブラにコンパイルすると、次のgcc -O2
謎めいたコードが生成されます。
_fib:
push edi
push esi
push ebx
sub esp, 16
mov edi, DWORD PTR [esp+32]
cmp edi, 1
jle L4
mov ebx, edi
xor esi, esi
L3:
lea eax, [ebx-1]
mov DWORD PTR [esp], eax
call _fib
sub ebx, 2
add esi, eax
cmp ebx, 1
jg L3
and edi, 1
L2:
lea eax, [esi+edi]
add esp, 16
pop ebx
pop esi
pop edi
ret
L4:
xor esi, esi
jmp L2
[esp+4]
したがって、呼び出し規約はでの引数であり、の戻り値だと思いますeax
。edi
、、esi
を押すことから始まりebx
ます。次に、スタックフレームに対してさらに16バイトを要求します。これは、4つの一時的なintに十分です。次に、引数である、edi
から読み取られます。[esp+32]
引数がの場合、引数だけであるセットにジャンプする前に、ゼロになる(?)<=1
にジャンプします。引数がだった場合、引数はにコピーされ、ゼロになってからになります。では、計算のために再帰する前に、結果(n-1)をスタックフレームに設定して格納します。結果はに追加されますL4
esi
L2
eax=esi+edi
edi
>1
ebx
esi
L3
L3
eax=ebx-1
esp
fib(n-1)
esi
、ebx
に設定されn-2
、ループバックします。そうでないL3
場合は、にフォールスルーする前にebx>1
の下位ビットを抽出します。edi
L2
このコードが非常に複雑なのはなぜですか(たとえば、実行された最適化の名前が表示されていないのですか?)?
再帰呼び出しfib(n-2)
は、蓄積するループに置き換えられたようですesi
が、その呼び出しはテール位置になかったので、これはどのように行われましたか?
の目的は何and edi, 1
ですか?
の目的は何mov DWORD PTR [esp], eax
ですか?
スタックフレームがこんなに大きいのはなぜですか?
このアルゴリズムをCに分解して、何が起こっているのかを明確にすることはできますか?
私の予備的な印象は、GCCがかなり貧弱なx86アセンブラーを生成するということです。この場合、同等のパフォーマンスを実現するために2倍以上のコードが必要です(両方のプログラムでこの1.6GHz Atomのfib(40)の場合は3.25秒)。それは公平ですか?