【问题标题】:Which, if any, C++ compilers do tail-recursion optimization?哪些 C++ 编译器(如果有)进行尾递归优化?
【发布时间】:2010-09-07 05:47:25
【问题描述】:

在我看来,在 C 和 C++ 中进行尾递归优化会非常好,但在调试时,我似乎从未看到表明这种优化的帧堆栈。这很好,因为堆栈告诉我递归有多深。不过,优化也不错。

是否有任何 C++ 编译器进行此优化?为什么?为什么不呢?

我该如何告诉编译器这样做?

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

在某种情况下检查编译器是否已经这样做了怎么样?

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

我仍然会就如何确定某个函数是否被编译器这样优化(尽管我觉得康拉德告诉我假设它是令人放心的)提出建议

始终可以通过进行无限递归并检查它是否导致无限循环或堆栈溢出来检查编译器是否执行此操作(我使用 GCC 执行此操作,发现 -O2 就足够了) ,但我希望能够检查我知道无论如何都会终止的某个功能。我很想有一个简单的方法来检查这个:)


经过一些测试,我发现析构函数破坏了进行这种优化的可能性。有时值得更改某些变量和临时变量的范围,以确保它们在返回语句开始之前超出范围。

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

【问题讨论】:

    标签: c++ optimization tail-recursion


    【解决方案1】:

    当前所有主流编译器都执行尾调用优化相当好(并且已经做了十多年),even for mutually recursive calls 例如:

    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 的 diploma thesis 过程中添加到 GCC 的。论文描述了实现中的一些有趣的注意事项。值得一读。

    【讨论】:

    • ICC 会这样做,我相信。据我所知,ICC 生成市场上最快的代码。
    • @Paul 问题是ICC代码的速度有多少是由尾调用优化等算法优化引起的,有多少是由只有英特尔的缓存和微指令优化引起的,他们的知识渊博他们自己的处理器,可以做到。
    • gcc 有更窄的选项 -foptimize-sibling-calls 来“优化同级和尾递归调用”。此选项(根据针对各种平台的版本 4.4、4.7 和 4.8 的 gcc(1) 手册页)在 -O2-O3-Os 级别启用。
    • 另外,在调试模式下运行而不明确请求优化将不会做任何优化。您可以为真正的发布模式 EXE 启用 PDB 并尝试逐步执行,但请注意,在发布模式下调试有其复杂性 - 不可见/剥离变量、合并变量、变量在未知/意外范围内超出范围、变量永远不会进入范围并成为具有堆栈级地址的真正常量,并且 - 很好 - 合并或丢失堆栈帧。通常合并的堆栈帧意味着被调用者是内联的,丢失/反向合并的帧可能是尾调用。
    【解决方案2】:

    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;
    }
    

    【讨论】:

    • 你可以激活链接时优化,我猜想即使是 extern 方法也可能被内联。
    • 奇怪。我刚刚测试了 gcc 4.2.3 (x86, Slackware 12.1) 和 gcc 4.6.2 (AMD64, Debian wheezy) 和 -O1 没有 没有内联没有尾递归优化。您必须为此使用-O2(嗯,在 4.2.x 中,现在相当古老,它仍然不会被内联)。顺便说一句,还值得补充一点的是,gcc 可以优化递归,即使它不是严格的尾递归(比如没有累加器的阶乘)。
    【解决方案3】:

    除了显而易见的(除非你要求,编译器不会做这种优化),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 或类似的。

    【讨论】:

      【解决方案4】:

      大多数编译器不会在调试版本中进行任何类型的优化。

      如果使用 VC,请尝试启用 PDB 信息的发布版本 - 这将让您跟踪优化的应用程序,然后您应该会看到您想要的内容。但是请注意,调试和跟踪优化的构建会让你到处乱跑,而且通常你不能直接检查变量,因为它们只会出现在寄存器中或完全被优化掉。这是一次“有趣”的经历……

      【讨论】:

      • 尝试 gcc 为什么 -g -O3 并在调试版本中获得优化。 xlC 具有相同的行为。
      • 当您说“大多数编译器”时:您会考虑哪些编译器集合?正如所指出的,至少有两个编译器在调试构建期间执行优化——据我所知,VC 也这样做(除非你启用了修改并继续)。
      【解决方案5】:

      正如 Greg 所提到的,编译器不会在调试模式下执行此操作。调试构建比生产构建慢是可以的,但它们不应该更频繁地崩溃:如果您依赖尾调用优化,它们可能会这样做。因此,通常最好将尾调用重写为正常循环。 :-(

      【讨论】:

        猜你喜欢
        • 2015-06-16
        • 1970-01-01
        • 2010-10-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-10-14
        相关资源
        最近更新 更多