gcc -O2 对递归斐波那契函数做了啥?
Posted
技术标签:
【中文标题】gcc -O2 对递归斐波那契函数做了啥?【英文标题】:What is gcc -O2 doing to the recursive Fibonacci function?gcc -O2 对递归斐波那契函数做了什么? 【发布时间】:2012-04-07 21:29:04 【问题描述】:我正在学习 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
使用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 个临时整数。然后从[esp+32]
中读取edi
,也就是参数。如果参数是<=1
,那么它会跳转到L4
,它会清零(?)esi
,然后再跳转回L2
,它会设置eax=esi+edi
,这只是参数edi
。如果参数是>1
,那么参数会被复制到ebx
,并在进入L3
之前将esi
归零。在L3
中,它设置eax=ebx-1
并将结果(n-1)存储在堆栈帧中的esp
,然后递归计算fib(n-1)
。结果被添加到esi
,ebx
被设置为n-2
,如果ebx>1
,它循环回到L3
,否则它提取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 秒)。这公平吗?
【问题讨论】:
更长的 x86 代码并不一定意味着更糟糕的 x86 代码。一些短序列实际上在时间上比长序列效率低得多。在对两个版本进行概要分析之前,请勿敲击 GCC 版本。 你比较过运行时间吗? 其实 GCC 很擅长优化 ;) 代码看起来不太好,但这是 x86 “糟糕”实现的错误:P “你能把这个算法反汇编成 C [...] 吗?”那不就是组装吗? @OliCharlesworth 是的,我比较了运行时,它们是相同的(在这个 1.6GHz Intel Atom 上是 3.25s)。 【参考方案1】:除了上面的 cmets,注意递归已经通过替换被展开为尾调用:
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 优化(现在内部完全不同了!)。
【讨论】:
【参考方案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)
的调用,这对递归函数来说是一件非常聪明的事情。
【讨论】:
【参考方案3】:这第一部分是确保根据调用约定应保留的寄存器不会被丢弃。我猜这里使用的调用约定是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,所以edi
只是在0 和1 的情况下加载。这对应于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
相当于对x
执行%2
。
从高层次来看,这就是代码的作用:
设置堆栈帧并保存寄存器 如果x < 2
,则返回x
。
继续调用和求和 fib(x-...)
直到 x
小于 2。在这种情况下,转到 x < 2
的情况。
优化之处在于 GCC 通过循环而不是递归来解开 x >= 2
的情况。
【讨论】:
以上是关于gcc -O2 对递归斐波那契函数做了啥?的主要内容,如果未能解决你的问题,请参考以下文章