哪些C ++编译器进行尾递归优化?


149

在我看来,在C和C ++中执行尾递归优化将非常有效,但是在调试时,我似乎从未看到指示这种优化的帧堆栈。这很好,因为堆栈告诉我递归的深度。但是,优化也会很好。

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

如何告诉编译器执行此操作?

  • 对于MSVC:/O2/Ox
  • 对于海湾合作委员会:-O2-O3

在某些情况下,检查编译器是否已执行此操作呢?

  • 对于MSVC,启用PDB输出以能够跟踪代码,然后检查代码
  • 对于海湾合作委员会..?

我仍然会建议如何确定某个函数是否通过编译器进行了优化(即使我放心Konrad告诉我假设也是如此)。

总是可以通过进行无限递归检查编译器是否完全做到了这一点,并检查它是否导致无限循环或堆栈溢出(我用GCC这样做并发现-O2足够了),但是我想能够检查某个我知道会终止的功能。我希望有一个简单的方法来检查这个:)


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

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

Answers:


128

当前所有主流的编译器都很好地执行了尾部调用优化(并且已经进行了十多年),即使对于相互递归的调用,例如:

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

检查编译器是否进行了优化的一种简单方法是执行一个调用,否则将导致堆栈溢出—或查看程序集的输出。

作为一个有趣的历史记录,马克·普罗布斯特(Mark Probst)在文凭论文过程中将C的尾调用优化添加到了GCC中。本文描述了实现过程中的一些有趣的警告。值得一读。


我相信ICC会这样做。据我所知,ICC生成市场上最快的代码。
保罗·内森

35
@Paul问题是ICC代码的速度有多少是由算法优化(例如尾部调用优化)引起的,多少是由缓存和微指令优化引起的,只有英特尔对自己的处理器有深入的了解才能做到。
Imagist

6
gcc具有-foptimize-sibling-calls“优化同级和尾部递归调用”的更窄选项。此选项(按gcc(1)4.4版本,4.7和4.8针对各种平台手册页)的水平启用-O2-O3-Os
FooF 2014年

同样,在DEBUG模式下运行而没有明确要求优化的情况根本不会进行任何优化。您可以为真正的发布模式EXE启用PDB并尝试逐步解决,但请注意,在发布模式下进行调试有其复杂性-不可见/剥离的变量,合并的变量,变量在未知/意外范围之外的范围,永远不会进入的变量范围,并成为具有堆栈级地址的真实常量,以及-合并或丢失的堆栈帧。通常合并的堆栈帧意味着被调用者是内联的,丢失/重新合并的帧很可能是尾调用。
ПетърПетров

21

gcc 4.3.2将此函数(糟糕的atoi()实现)完全内联到main()。优化级别为-O1。我注意到我是否在使用它(即使将它从更改staticextern,尾部递归也会很快消失,所以我不会依赖它来实现程序的正确性。

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

1
您可以激活链接时间优化,但我猜想那时甚至extern可能会内联一个方法。
Konrad Rudolph'2

5
奇怪。我只是测试的gcc 4.2.3(X86,Slackware的12.1)和gcc 4.6.2(AMD64,Debian的喘息),并-O1没有内联无尾递归优化。您必须使用-O2(在4.2.x中,它现在已经很古老了,仍然不会被内联)。顺便说一句,值得一提的是,即使gcc并不是严格意义上的尾数(例如不带乘数的累加器),它也可以优化递归。
przemoc 2012年

16

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

给出类似的东西:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

编译器无法(通常)进行尾部调用优化,因为它需要在递归调用返回cls 调用的析构函数。

有时,编译器可以看到析构函数没有外部可见的副作用(因此可以尽早完成),但通常没有。

这种情况的一种特别常见的形式是where Funky实际上是a std::vector或类似位置。


对我不起作用。系统告诉我,在编辑答案之前,我的投票已锁定。
hmuelner '16

只需编辑答案(删除了括号),现在我就可以撤消我的否决票了。
hmuelner '16

11

大多数编译器在调试版本中不做任何优化。

如果使用VC,请尝试在PDB信息打开的情况下发布版本-这将使您能够跟踪经过优化的应用程序,并希望随后可以看到想要的内容。但是请注意,调试和跟踪优化的构建会使您无所适从,并且通常您无法直接检查变量,因为它们只会出现在寄存器中或完全被优化掉。这是一次“有趣”的经历...


2
尝试使用gcc为什么使用-g -O3并在调试版本中进行优化。xlC具有相同的行为。
g24l 2015年

当您说“大多数编译器”时:您考虑使用哪些编译器集合?如前所述,至少有两个编译器在调试构建期间执行优化-据我所知,VC也是如此(除非您启用了“修改并继续”功能)。
凌晨

7

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.