【问题标题】:How 'smart' is GCC's Tail-Call-Optimisation?GCC 的尾调用优化有多“聪明”?
【发布时间】:2017-02-10 12:30:30
【问题描述】:

我刚刚讨论过以下两种 C 代码:

For循环:

#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

所以一个(诚然简单但不是很严格的)基准测试表明,它确实看起来确实是在相同的时间顺序中。

【问题讨论】:

  • 编译器可能只是内联了函数而不是使用尾递归。使用-S标志编译程序,看看汇编代码是什么样子的。
  • 我同意@squeamishossifrage。不要启用优化,然后假设编译器做了什么。你可能会感到惊讶,这有点毫无意义。

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


【解决方案1】:

只需反汇编代码,看看发生了什么。没有优化,我得到这个:

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

这与尾部优化无关,而是更激进的优化。编译器只是内联了整个函数并预先计算了结果。

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

【讨论】:

    猜你喜欢
    • 2021-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-30
    • 2011-11-24
    • 1970-01-01
    相关资源
    最近更新 更多