使用 64 位变量的 C++ 尾递归

C++ Tail recursion using 64-bit variables

我用 C++(使用 Visual Studio)编写了一个简单的斐波那契函数作为练习,以测试尾递归并了解它是如何工作的。

这是代码:

int fib_tail(int n, int res, int next) {
  if (n == 0) {
    return res;
  }
  return fib_tail(n - 1, next, res + next);
}

int main()
{
  fib_tail(10,0,1); //Tail Recursion works
}

当我使用 Release 模式编译时,尽管有调用,但我看到优化的程序集使用 JMP 指令。所以我的结论是:尾递归有效。见下图:

我想通过增加斐波那契函数中的输入变量 n 来做一些性能测试。然后我选择将函数中使用的变量类型从 int 更改为 unsigned long long。然后我传递了一个大数字,例如:10e+08

这是现在的新功能:

typedef  unsigned long long ULONG64;

ULONG64 fib_tail(ULONG64 n, ULONG64 res, ULONG64 next) {
   if (n == 0) {
     return res;
   }
   return fib_tail(n - 1, next, res + next);
}

int main()
{
  fib_tail(10e+9,0,1); //Tail recursion does not work
}

当我 运行 上面的代码出现堆栈溢出异常时,这让我认为尾递归不起作用。我查看了程序集,实际上我发现了这个:

如您所见,现在有一个 call 指令,而我只期待一个简单的 JMP。我不明白为什么使用 8 字节变量会禁用尾递归。为什么编译器在这种情况下不执行优化?

这是您必须问那些为 MS 进行编译器优化的人的问题之一 - 实际上没有技术原因可以解释为什么任何 return 类型都应该阻止尾递归成为跳转因此 - 可能还有其他原因,例如 "the code is too complex to understand" 或其他原因。

几周前的 clang 3.7 清楚地解决了这个问题:

_Z8fib_tailyyy:                         # @_Z8fib_tailyyy
    pushl   %ebp
    pushl   %ebx
    pushl   %edi
    pushl   %esi
    pushl   %eax
    movl    36(%esp), %ecx
    movl    32(%esp), %esi
    movl    28(%esp), %edi
    movl    24(%esp), %ebx
    movl    %ebx, %eax
    orl %edi, %eax
    je  .LBB0_1
    movl    44(%esp), %ebp
    movl    40(%esp), %eax
    movl    %eax, (%esp)            # 4-byte Spill
.LBB0_3:                                # %if.end
    movl    %ebp, %edx
    movl    (%esp), %eax            # 4-byte Reload
    addl    $-1, %ebx
    adcl    $-1, %edi
    addl    %eax, %esi
    adcl    %edx, %ecx
    movl    %ebx, %ebp
    orl %edi, %ebp
    movl    %esi, (%esp)            # 4-byte Spill
    movl    %ecx, %ebp
    movl    %eax, %esi
    movl    %edx, %ecx
    jne .LBB0_3
    jmp .LBB0_4
.LBB0_1:
    movl    %esi, %eax
    movl    %ecx, %edx
.LBB0_4:                                # %return
    addl    , %esp
    popl    %esi
    popl    %edi
    popl    %ebx
    popl    %ebp
    retl


main:                                   # @main
    subl    , %esp
    movl    [=10=], 20(%esp)
    movl    , 16(%esp)
    movl    [=10=], 12(%esp)
    movl    [=10=], 8(%esp)
    movl    , 4(%esp)
    movl    10065408, (%esp)     # imm = 0x540BE400
    calll   _Z8fib_tailyyy
    movl    %edx, f+4
    movl    %eax, f
    xorl    %eax, %eax
    addl    , %esp
    retl

同样适用于 gcc 4.9.2,如果你给它 -O2(但不是在 -O1 中,这是所有 clang 所需要的)

(当然还有 64 位模式)