什么是gcc -O2对递归Fibonacci函数做什么?

时间:2012-04-07 21:29:05

标签: gcc assembly recursion x86

我正在学习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中的值。首先推送ediesiebx。然后它声称堆栈帧的另外16个字节,足够4个临时的int。然后从edi读取[esp+32],这是参数。如果参数为<=1,那么它会跳转到L4,然后跳回(?)esi,然后再跳回L2eax=esi+edi设置edi这只是参数>1。如果参数为ebx,那么参数将被复制到esi并将L3归零,然后再进入L3。在eax=ebx-1中,它设置esp并将结果(n-1)存储在堆栈帧中fib(n-1),然后再递归计算esi。结果已添加到ebxn-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)。那是公平的吗?

3 个答案:

答案 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

这会从ebxx)中减去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的情况。

相关问题