再帰関数をインライン化できますか?


134
inline int factorial(int n)
{
    if(!n) return 1;
    else return n*factorial(n-1);
}

私がこれを読んでいたとき、上記のコードがコンパイラによって正しく処理されない場合、「無限コンパイル」につながることがわかりました。

コンパイラーは関数をインライン化するかどうかをどのように決定しますか?

回答:


137

まず、inline関数の仕様はヒントにすぎません。コンパイラは、inline修飾子の有無を完全に無視できます(多くの場合、無視します)。そうは言っても、コンパイラ無限ループを展開できるのと同じように、再帰関数をインライン化できます。関数を「アンロール」するレベルに制限を設けるだけです。

最適化コンパイラはこのコードを有効にするかもしれません:

inline int factorial(int n)
{
    if (n <= 1)
    {
        return 1;
    }
    else
    {
        return n * factorial(n - 1);
    }
}

int f(int x)
{
    return factorial(x);
}

このコードに:

int factorial(int n)
{
    if (n <= 1)
    {
        return 1;
    }
    else
    {
        return n * factorial(n - 1);
    }
}

int f(int x)
{
    if (x <= 1)
    {
        return 1;
    }
    else
    {
        int x2 = x - 1;
        if (x2 <= 1)
        {
            return x * 1;
        }
        else
        {
            int x3 = x2 - 1;
            if (x3 <= 1)
            {
                return x * x2 * 1;
            }
            else
            {
                return x * x2 * x3 * factorial(x3 - 1);
            }
        }
    }
}

この場合、基本的には関数を3回インライン化しました。一部のコンパイラこの最適化を実行ます。MSVC ++には、再帰関数で実行されるインライン化のレベルを調整する設定があることを思い出します(最大20と思います)。


20
#pragma inline_recursion(on)です。最大深度に関するドキュメントは、一貫していないか、決定的ではありません。値8、16、または#pragma inline_depthの値が可能です。
peterchen 2008年

@peterchenインライン化された関数がその引数の1つの値を変更する場合、何が起こるかについては、mainではなくファクト内で関数をインライン化する方が良いと思います。英語で申し訳ありません
ob_dev

1
@obounaim:そう思うかもしれません。MSVCはサポートしていません。
SecurityMatt

23

実際、コンパイラーがインテリジェントに動作しない場合、inlined関数のコピーを再帰的に挿入して、無限に大きなコードを作成する可能性があります。ただし、最近のほとんどのコンパイラはこれを認識します。次のいずれかを行うことができます。

  1. 関数をまったくインライン化しない
  2. 特定の深さまでインライン化し、それまでに終了していない場合は、標準の関数呼び出し規約を使用して、関数の個別のインスタンスを呼び出します。これにより、多くの一般的なケースを高性能な方法で処理できる一方で、コールデプスが大きいというまれなケースのフォールバックが残されます。これは、関数のコードのインラインバージョンと個別バージョンの両方を保持することも意味します。

ケース2の場合、多くのコンパイラーには#pragma、これを実行する最大深度を指定するために設定できるsがあります。ではGCC、あなたもでコマンドラインからでこれを渡すことができます--max-inline-insns-recursive(詳細を参照してくださいここに)。


7

AFAIK GCCは、可能であれば、再帰関数で末尾呼び出しの削除を行います。ただし、関数は末尾再帰ではありません。


6

コンパイラーは呼び出しグラフを作成します。サイクルがそれ自体を呼び出すことが検出されると、関数は特定の深さ(n = 1、10、100、コンパイラが調整されているもの)の後にインライン化されなくなります。


3

一部の再帰関数はループに変換でき、無限にインライン化されます。gccでこれができると思いますが、他のコンパイラについては知りません。


2

これが通常は機能しない理由については、すでに与えられた回答を参照してください。

「脚注」として、テンプレートメタプログラミングを使用して、(少なくとも例として使用している階乗に対して)探している効果を得ることができます。ウィキペディアからの貼り付け:

template <int N>
struct Factorial 
{
    enum { value = N * Factorial<N - 1>::value };
};

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};

1
それはとてもかわいいですが、元の投稿には可変引数「int n」があったことに注意してください。
Windowsプログラマ

1
確かに、しかし、コンパイル時にnがわからない場合に「再帰的なインライン化」を要求することにはほとんど意味がありません...コンパイラーはこれをどのようにして達成できるでしょうか?だから質問の文脈では、これは関連する選択肢だと思います。
yungchin 2008年

1
デレクパークの方法の例を参照してください。2回インライン化することで、n >> 2回再帰し、結果のコードから2 + 2が返されます。
MSalters 2008年

1

コンパイラーはこれらの種類のものを検出してそれらを防ぐためにコールグラフを作成します。したがって、関数はインラインではなく、それ自体を呼び出すことがわかります。

ただし、主にインラインキーワードとコンパイラスイッチによって制御されます(たとえば、キーワードがなくても小さな関数をインラインで自動インライン化できます)。コールスタックはミラーに保持されないため、デバッグコンパイルはインライン化しないでください。コードで作成した呼び出し。


1

「コンパイラーは関数をインライン化するかどうかをどのように決定するのですか?」

これは、コンパイラ、指定されたオプション、コンパイラのバージョン番号、使用可能なメモリ容量などによって異なります。

プログラムのソースコードは、インライン関数のルールに従う必要があります。関数がインライン化されるかどうかに関係なく、インライン化される可能性に備えて準備する必要があります(回数は不明です)。

再帰マクロは通常違法であるというウィキペディアの声明は、情報が不十分であるように見えます。CおよびC ++は再帰的な呼び出しを防止しますが、再帰的であるように見えるマクロコードを含めることにより、翻訳単位が違法になることはありません。アセンブラでは、通常、再帰マクロは有効です。


0

一部のコンパイラー(すなわちBorland C ++)は、条件付きステートメント(if、case、whileなど)を含むコードをインライン化しないため、例の再帰関数はインライン化されません。

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