web-dev-qa-db-ja.com

再帰関数はインラインにできますか?

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

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

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

130
Ashwin

まず、関数のinline指定は単なるヒントです。コンパイラは、inline修飾子の有無を完全に無視できます(そして多くの場合そうします)。とはいえ、コンパイラcanは、無限ループを展開できるのと同じくらい、再帰関数をインライン化します。関数を「展開」するレベルに制限を設けるだけです。

最適化コンパイラは、このコードを有効にする場合があります。

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回インライン化しました。一部のコンパイラdoは、この最適化を実行します。 MSVC++には、再帰関数で実行されるインライン化のレベルを調整する設定があることを思い出します(最大20個)。

133
Derek Park

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

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

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

23
Matt J

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

7
leppie

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

6
Paul Nathan

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

3
alex strange

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

「脚注」として、 template metaprogramming を使用して、(少なくとも例として使用している階乗に対して)探している効果を達成できます。ウィキペディアからの貼り付け:

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

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

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

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

1
mattlant

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

それは、コンパイラ、指定されたオプション、コンパイラのバージョン番号、利用可能なメモリ量などに依存します。

プログラムのソースコードは、インライン関数のルールに従う必要があります。関数がインライン化されるかどうかにかかわらず、インライン化される可能性に備えなければなりません(いくつかの未知の回数)。

再帰マクロは一般的に違法であるというウィキペディアの声明は、情報が乏しいようです。 CおよびC++は再帰呼び出しを防ぎますが、変換ユニットは、再帰的であるように見えるマクロコードを含むことにより違法になりません。アセンブラーでは、通常、再帰マクロは有効です。

1

一部のコンパイラ(Borland C++など)は、条件文(if、case、whileなど)を含むコードをインライン化しないため、この例の再帰関数はインライン化されません。

0
Roger Nelson