もしあれば、C ++コンパイラーが末尾再帰の最適化を行いますか?


149

CとC ++の両方で末尾再帰の最適化を行うことは完全にうまくいくように思えますが、デバッグ中は、この最適化を示すフレームスタックが表示されないようです。スタックが再帰の深さを教えてくれるからです。ただし、最適化も同様によいでしょう。

C ++コンパイラーはこの最適化を行いますか?どうして?何故なの?

コンパイラーに指示する方法を教えてください。

  • MSVCの場合:/O2または/Ox
  • GCCの場合:-O2または-O3

コンパイラが特定のケースでこれを行ったかどうかを確認するのはどうですか?

  • MSVCの場合、PDB出力がコードをトレースできるようにしてから、コードを検査します
  • GCC ..?

特定の関数がコンパイラーによってこのように最適化されているかどうかを判断する方法については引き続き提案します(Konradがそれを想定するように指示していることを確信しているにもかかわらず)

無限再帰を行って無限ループまたはスタックオーバーフローが発生するかどうかを確認することで、コンパイラーがこれを実行するかどうかを常に確認することができます(GCCでこれを実行し、それで-O2十分であることがわかりました)。とにかく終了することがわかっている特定の機能をチェックできます。これをチェックする簡単な方法が欲しいです:)


いくつかのテストの後、デストラクタがこの最適化を行う可能性を台無しにしていることを発見しました。特定の変数と一時オブジェクトのスコープを変更して、return-statementが始まる前にスコープから外れるようにすることは、価値がある場合があります。

末尾呼び出しの後にデストラクタを実行する必要がある場合、末尾呼び出しの最適化は実行できません。

回答:


128

現在のすべての主流コンパイラは、次のような相互に再帰的な呼び出しあっても、末尾呼び出しの最適化をかなりうまく実行しています(10年以上も実行しています)。

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は市場で最速のコードを生成します。
ポールネイサン

35
@Paul問題は、ICCコードの速度がテールコール最適化などのアルゴリズム最適化によってどの程度引き起こされているか、および独自のプロセッサに関する深い知識を持つIntelだけが実行できるキャッシュおよびマイクロ命令最適化によってどれほど引き起こされているかです。
イマジスト

6
gcc-foptimize-sibling-calls「兄弟とテール再帰呼び出しを最適化する」ためのより狭いオプションがあります。このオプションは、(に応じてgcc(1)バージョン4.4のマニュアルページ、4.7および4.8様々なプラットフォームをターゲット)のレベルで有効になっています-O2-O3-Os
FooF 2014年

また、最適化を明示的に要求せずにデバッグモードで実行しても、最適化はまったく行われません。真のリリースモードEXEに対してPDBを有効にして、ステップ実行を試みることができますが、リリースモードでのデバッグには複雑さが伴うことに注意してください。スコープとスタックレベルのアドレスを持つ真の定数になりました-よく-マージまたはスタックフレームがありません。通常、マージされたスタックフレームは、呼び出し先がインライン化されることを意味し、欠落/バックマージされたフレームはおそらく末尾呼び出しです。
ПетърПетров

21

gcc 4.3.2は、この関数(crappy / trivial 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;
}

1
ただし、リンク時の最適化をアクティブにすることもexternできます。その場合、メソッドもインライン化される可能性があります。
Konrad Rudolph

5
奇妙な。私は、GCC 4.2.3(x86の、Slackwareの12.1)とGCC 4.6.2(AMD64、喘鳴のDebian)をテストし、-O1はありませんインライン化なし無末尾再帰の最適化が。あなたがする必要が使用し-O2ているため(今はむしろ古代で4.2.xでは、で、まあ、それはまだインライン化されません)。ところで、gccは、厳密に末尾でない(階乗w / oアキュムレータのような)場合でも、再帰を最適化できることも付け加えておきます。
przemoc

16

明白なこと(コンパイラーはこのような最適化を行わない限り、最適化を行わない)と同様に、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類似する場所です。


私には効きません。回答が編集されるまで私の投票はロックされているとシステムから通知されます。
hmuelner

答えを編集しただけで(括弧は削除されました)、今度は自分の反対票を取り消すことができました。
hmuelner

11

ほとんどのコンパイラは、デバッグビルドでいかなる種類の最適化も行いません。

VCを使用している場合は、PDB情報をオンにしてリリースビルドを試してください。これにより、最適化されたアプリをトレースできるようになり、希望どおりに表示されるはずです。ただし、最適化されたビルドのデバッグとトレースを行うと、あちこちにジャンプすることになり、変数がレジスターになってしまうか、完全に最適化されるため、変数を直接検査できないことがよくあります。それは「面白い」経験です...


2
gcc why -g -O3を試して、デバッグビルドで最適化を取得してください。xlCの動作は同じです。
g24l 2015年

「ほとんどのコンパイラ」と言うとき、どのコンパイラのコレクションを検討しますか?指摘したように、デバッグビルド中に最適化を実行するコンパイラが少なくとも2つあります-私が知る限り、VCもそれを行います(おそらく変更と継続を有効にしている場合を除きます)。
skyking

7

Gregが述べているように、コンパイラーはデバッグモードではそれを行いません。デバッグビルドが本番ビルドよりも遅くても問題ありませんが、クラッシュが頻繁に発生することはありません。テールコールの最適化に依存している場合は、まさにそれが可能です。このため、通常はテールコールを通常のループとして書き換えることが最善です。:-(

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.