我正在学习x86汇编程序以编写编译器。特别是,我正在使用各种简单的递归函数并通过不同的编译器(OCaml,GCC等)提供它们,以便更好地理解不同编译器生成的汇编程序的种类。
我有一个简单的递归整数Fibonacci函数:
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
使用gcc -O2
将该函数编译到Intel-syntax汇编程序会产生这个神秘的代码:
_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
,那么它会跳转到L4
,然后跳回(?)esi
,然后再跳回L2
,eax=esi+edi
设置edi
这只是参数>1
。如果参数为ebx
,那么参数将被复制到esi
并将L3
归零,然后再进入L3
。在eax=ebx-1
中,它设置esp
并将结果(n-1)存储在堆栈帧中fib(n-1)
,然后再递归计算esi
。结果已添加到ebx
,n-2
设置为L3
,如果ebx>1
它会循环回edi
,否则会提取{{1}的低位在落到L2
之前。
为什么这段代码如此错综复杂(例如,是否有一个我未见过的优化名称?)?
递归调用fib(n-2)
似乎已被esi
中累积的循环所取代,但该调用不在尾部位置,所以如何完成?
and edi, 1
的目的是什么?
mov DWORD PTR [esp], eax
的目的是什么?
为什么堆栈帧太大了?
您可以将此算法反汇编回C以使其更清晰吗?
我的初步印象是,GCC会生成相当差的x86汇编程序。在这种情况下,相同性能的代码超过2倍(对于这两个程序,此1.6GHz Atom上的fib(40)为3.25s)。那是公平的吗?
答案 0 :(得分:8)
除了上面的注释之外,请注意通过替换以下内容将递归已展开为尾调用:
return x < 2 ? x : fib(x - 2) + fib(x - 1);
使用:
if ((xprime = x) < 2) {
acc = 0;
} else {
/* at this point we know x >= 2 */
acc = 0; /* start with 0 */
while (x > 1) {
acc += fib(x - 1); /* add fib(x-1) */
x -= 2; /* now we'll add fib(x-2) */
}
/* so at this point we know either x==1 or x==0 */
xprime = x == 1 ? 1 : 0; /* ie, x & 1 */
}
return xprime + acc;
我怀疑这个相当棘手的循环来自多个优化步骤,而不是因为关于gcc 2.3(我现在内部都非常不同),我已经摆弄了gcc优化!。
答案 1 :(得分:2)
很简单,fib(x-2)
等于fib(x-3) + fib(x-4)
,fib(x-4)
等于fib(x-5) + fib(x-6)
等,因此fib(x)
计算为fib(x-1) + fib(x-3) + fib(x-5) + ... + fib(x&1)
(fib(x&1)
等于x&1
)即gcc已经内联fib(x-2)
的调用,这对于递归函数来说是非常聪明的事情。
答案 2 :(得分:2)
第一部分是确保根据调用约定保留的寄存器不会被删除。我猜这里使用的调用约定是cdecl
。
_fib:
push edi
push esi
push ebx
sub esp, 16
DWORD PTR[esp+32]
是您的x
:
mov edi, DWORD PTR [esp+32]
cmp edi, 1
jle L4
如果x
小于或等于1(这对应于您的x < 2
),请转到L4
,即:
L4:
xor esi, esi
jmp L2
这会将esi
归零并分支到L2
:
L2:
lea eax, [esi+edi]
add esp, 16
pop ebx
pop esi
pop edi
ret
这会将eax
(返回值)设置为esi+edi
。由于esi
已经为0,因此在{0}和1的情况下只会加载edi
。这对应于x < 2 ? x
。
现在我们来看x
不是< 2
的情况:
mov ebx, edi
xor esi, esi
L3:
lea eax, [ebx-1]
mov DWORD PTR [esp], eax
call _fib
首先,x
被复制到ebx
,然后esi
被归零。
接下来,eax
设置为x - 1
。此值移动到堆栈顶部并调用_fib
。这相当于fib(x-1)
。
sub ebx, 2
add esi, eax
这会从ebx
(x
)中减去2。然后eax
(来自_fib
调用的返回值被添加到esi
,之前设置为0)。因此,esi
现在保留fib(x-1)
的结果。
cmp ebx, 1
jg L3
and edi, 1
将 ebx
与1.进行比较。如果它大于1,则我们循环回L3
。否则(它是0或1的情况),我们执行and edi, 1
并直至L2
(我们已经分析了之前已做过的事情)。 and edi, 1
相当于在%2
上执行x
。
从高层次来看,这就是代码的作用:
x < 2
,则返回x
。fib(x-...)
,直到x
小于2.在这种情况下,请转到x < 2
案例。优化是GCC通过循环而不是递归来解除x >= 2
的情况。