GCC的尾部呼叫优化有多“聪明”?

时间:2017-02-10 12:30:31

标签: c gcc recursion optimization tail-call-optimization

我刚刚讨论了以下两个C代码的讨论:

有关环路:

#include <stdio.h>
#define n (196607)

int main() {
  long loop;
  int count=0;
  for (loop=0;loop<n;loop++) {
    count++;
  }
  printf("Result = %d\n",count);

  return 0;
}

递归:

#include <stdio.h>
#define n (196607)

long recursive(long loop) {
  return (loop>0) ? recursive(loop-1)+1: 0;
}

int main() {
  long result;
  result = recursive(n);
  printf("Result = %d\n",result);
  return 0;
}

看到这段代码后,我看到recursive(loop-1)+1并认为“啊,这不是尾调用递归”,因为它在完成对recursive的调用后有工作要做;它需要增加返回值。

果然,没有优化,递归代码会触发堆栈溢出,正如您所期望的那样。

然而,

使用-O2标志,不会遇到堆栈溢出,我认为这意味着堆栈被重用,而不是越来越多地进入堆栈 - 这就是tco。

GCC显然可以检测到这个简单的情况(+1返回值)并对其进行优化,但它会走多远?

当递归调用不是要执行的最后一个操作时,gcc可以用tco优化的限制是什么?

附录: 我编写了一个完全尾递归return function();版本的代码。 将上面的内容包含在9999999次迭代的循环中,我想出了以下时间:

$ for f in *.exe; do time ./$f > results; done
+ for f in '*.exe'
+ ./forLoop.c.exe

real    0m3.650s
user    0m3.588s
sys     0m0.061s
+ for f in '*.exe'
+ ./recursive.c.exe

real    0m3.682s
user    0m3.588s
sys     0m0.093s
+ for f in '*.exe'
+ ./tail_recursive.c.exe

real    0m3.697s
user    0m3.588s
sys     0m0.077s

所以(基本上是简单而不是非常严格的)基准测试显示它确实看起来确实处于相同的时间顺序。

1 个答案:

答案 0 :(得分:4)

只需反汇编代码,看看发生了什么。没有优化,我明白了:

0x0040150B  cmpl   $0x0,0x10(%rbp)
0x0040150F  jle    0x401523 <recursive+35>
0x00401511  mov    0x10(%rbp),%eax
0x00401514  sub    $0x1,%eax
0x00401517  mov    %eax,%ecx
0x00401519  callq  0x401500 <recursive>

但是使用-O1,-O2或-O3我得到了这个:

0x00402D09  mov    $0x2ffff,%edx

这与尾部优化没有任何关系,而是更激进的优化。编译器只是简单地内联整个函数并预先计算结果。

这可能是您在所有不同的基准测试案例中得出相同结果的原因。