哪些C++编译器支持尾递归优化?

171

对我来说,C和C++都可以很好地进行尾递归优化,但在调试时,似乎从未看到指示此优化的帧堆栈。这有点好,因为堆栈告诉我递归有多深。然而,优化也会很好。

是否有C++编译器执行此优化?为什么?为什么不?

如何告诉编译器执行它?

  • 对于MSVC:/O2/Ox
  • 对于GCC:-O2-O3

怎样检查编译器是否在某种情况下执行了此操作?

  • 对于MSVC,请启用PDB输出以跟踪代码,然后检查代码
  • 对于GCC..?

虽然Konrad告诉我要假设编译器已经执行了此操作,但我仍然希望能够提出一些关于如何确定编译器是否像此优化某个特定函数的建议。

始终可以通过制作无限递归并检查是否导致无限循环或堆栈溢出来检查编译器是否执行此操作(我使用GCC进行了测试,并发现-O2足够),但我希望能够检查我知道将在任何情况下终止的某个函数。我希望有一种简单的方法来检查这一点:)


经过一些测试,我发现析构函数破坏了使此优化成为可能的可能性。有时更改某些变量和临时变量的作用域以确保它们在返回语句开始之前超出作用域可能值得。

如果任何析构函数需要在尾调用之后运行,则无法进行尾调用优化。

5个回答

150

当前所有主流编译器都能相当好地进行尾调用优化(并且已经这样做了十多年),甚至包括互相递归的调用,如下所示:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

让编译器进行优化非常简单:只需打开优化以加快速度:

  • 对于MSVC,请使用/O2/Ox
  • 对于GCC、Clang和ICC,请使用-O3

检查编译器是否进行了优化的简单方法是执行本应导致堆栈溢出的调用,或查看汇编输出。

作为有趣的历史注释,C语言的尾调用优化是由Mark Probst在其学位论文中添加到GCC中的。该论文描述了实现中的一些有趣的警告。值得一读。


我相信ICC可以做到。据我所知,ICC生产的代码是市场上最快的。 - Paul Nathan
40
问题是ICC代码的速度有多少是由算法优化(如尾递归优化)导致的,有多少是由缓存和微指令优化引起的,只有英特尔拥有对其处理器的深入了解才能实现这些优化。 - Imagist
9
gcc有一个更窄的选项-foptimize-sibling-calls,用于"优化兄弟和尾递归调用"。根据针对不同平台版本4.4、4.7和4.8的gcc(1)手册,此选项在级别-O2-O3-Os下启用。 - FooF
2
此外,如果没有明确请求优化,则在DEBUG模式下运行将不会进行任何优化。您可以为真正的发布模式EXE启用PDB并尝试通过它进行步进,但请注意,在Release模式下调试存在其复杂性 - 不可见/剥离变量、合并变量、变量在未知/意外范围内失去作用域、变量永远不会进入作用域并成为具有堆栈级地址的真实常量,以及 - 好吧 - 合并或缺失的堆栈帧。通常,合并的堆栈帧意味着被调用者被内联,而缺失/回归的帧可能是尾调用。 - Петър Петров

26

除了显而易见的(编译器不会进行此类优化,除非您要求),在C ++中进行尾调用优化存在一个复杂性:析构函数。

如果有类似这样的东西:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }
编译器通常无法对此进行尾调用优化,因为它需要在递归调用返回后调用“cls”的析构函数。
有时编译器可以看到析构函数没有外部可见的副作用(因此可以提前执行),但往往无法这样做。
这种情况特别常见的情况是,Funky实际上是std::vector或类似的东西。

23

使用gcc 4.3.2编译时,这个功能简陋/琐碎的atoi()实现被完全嵌入到main()中。优化级别为-O1。我发现如果我对它进行调整(甚至是将其从static更改为extern),尾递归很快就消失了,所以我不会指望它能正确运行程序。

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}

2
虽然您可以激活连接时优化,但我想即使是extern方法也可能会被内联。 - Konrad Rudolph
6
奇怪了。我刚刚测试了gcc 4.2.3(x86,Slackware 12.1)和gcc 4.6.2(AMD64,Debian wheezy),使用-O1时没有内联和尾递归优化。你必须使用-O2才能进行优化(在4.2.x中,即使现在相当古老,它仍不会被内联)。顺便说一下,值得补充的是,即使递归不是严格的尾递归(例如没有累加器的阶乘),gcc也可以进行递归优化。 - przemoc

13

大多数编译器在调试构建中不会执行任何优化。

如果使用VC,请尝试打开PDB信息的发布构建 - 这将让您跟踪优化应用程序,然后您就应该能够看到想要的结果了。但请注意,调试和跟踪优化构建将使您在整个代码中跳来跳去,通常您无法直接检查变量,因为它们只会最终出现在寄存器中或被完全优化掉。这是一个“有趣”的体验...


2
尝试使用 gcc why -g -O3 命令在调试构建中获得优化。 xlC 也具有相同的行为。 - g24l
当你说“大多数编译器”时,你考虑了哪些编译器集合?正如指出的那样,至少有两个编译器在调试构建期间执行优化 - 就我所知,VC也是如此(除非你启用修改和继续功能)。 - skyking

10

正如Greg所提到的,编译器在调试模式下不会进行尾递归优化。调试构建比生产构建慢是可以接受的,但它们不应该更容易崩溃:如果您依赖于尾递归优化,它们可能会出现这种情况。因此,最好将尾递归重写为普通循环。 :-(


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接