コンピューティングにおいて、インライン展開(またはインライン化)とは、関数呼び出し箇所を呼び出された関数の本体に置き換える、手動またはコンパイラによる最適化手法です。インライン展開はマクロ展開に似ていますが、コンパイル時に発生し、ソースコード(テキスト)は変更されません。一方、マクロ展開はコンパイル前に発生し、異なるテキストが生成され、それがコンパイラによって処理されます。
インライン化は重要な最適化手法ですが、パフォーマンスに複雑な影響を与えます。[ 1 ]経験則として、ある程度のインライン化はわずかなスペースのコストで速度を向上させますが、過剰なインライン化は、インライン化されたコードが命令キャッシュを過剰に消費するため速度を低下させ、またかなりのスペースを消費します。1980年代と1990年代のインライン化に関するささやかな学術文献の概説は、Peyton Jones & Marlow 1999 に記載されています。[ 2 ]
インライン展開はマクロ展開に似ており、コンパイラは関数が呼び出されるたびにその関数の新しいコピーを配置します。インライン化された関数は、関数呼び出しのオーバーヘッドが削減されるため、通常の関数よりも少し高速に実行されますが、メモリのペナルティがあります。関数が 10 回インライン化されると、コード内に関数のコピーが 10 個挿入されます。したがって、インライン化は、頻繁に呼び出される小さな関数に最適です。C++ では、クラスのメンバ関数は、クラス定義内で定義されている場合、デフォルトでインライン化されます ( inline予約語(キーワード) を使用する必要はありません)。そうでない場合は、キーワードが必要です。コンパイラは、特に大きな関数の場合、プログラマによる関数のインライン化の試みを無視することがあります。
インライン展開は、関数呼び出し時の時間オーバーヘッド(余分な時間)を排除するために使用されます。これは通常、頻繁に実行される関数に使用されます。また、非常に小さな関数ではメモリ使用量の削減にも役立ち、他の最適化を可能にする変換でもあります。
インライン関数を使用しない場合、コンパイラがどの関数をインライン化するかを決定します。プログラマは、どの関数をインライン化するか、しないかをほとんど、あるいは全く制御できません。プログラマにこの程度の制御権を与えることで、アプリケーション固有の知識を活用して、どの関数をインライン化するかを選択できるようになります。
通常、関数が呼び出されると、分岐命令または呼び出し命令によって制御がその定義に渡されます。インライン化では、分岐命令や呼び出し命令を経由することなく、制御が直接関数のコードに渡されます。
コンパイラは通常、インライン展開を用いてステートメントを実装します。ループ条件とループ本体は遅延評価を必要とします。この特性は、ループ条件とループ本体を計算するコードがインライン化されている場合に満たされます。パフォーマンス上の考慮事項も、ステートメントをインライン化するもう1つの理由です。
関数型プログラミング言語の文脈では、インライン展開の後にベータ縮約変換が続くのが一般的である。[ 3 ]
プログラマーは、ソースコードに対して一度限りの操作として、コピー&ペーストプログラミングによって関数を手動でインライン化することがあります。しかし、インライン化を制御する他の方法(下記参照)の方が望ましいです。なぜなら、これらの方法では、インライン化された関数のバグを修正する際に、元の関数本体の(場合によっては変更された)複製バージョンを見落としてしまうことによるバグが発生しないからです。
この最適化の直接的な効果は、(呼び出しオーバーヘッドを排除することで)時間パフォーマンスを向上させることですが、その代償として(関数本体の重複により)スペースの使用が悪化します[ a ] 。単純なケースを除いて、関数本体の重複によるコード拡張が支配的であるため[ b ]、インライン展開の直接的な効果は、スペースを犠牲にして時間を改善することです。
しかし、インライン展開の主な利点は、関数本体のサイズが大きくなることで、さらなる最適化とスケジューリングの改善が可能になることです。これは、関数が大きいほど最適化が進むためです。[ 4 ]インライン展開が速度に及ぼす最終的な影響は複雑です。これは、現代のプロセッサのパフォーマンスを左右するメモリシステム(主に命令キャッシュ)のパフォーマンスに複数の影響があるためです。特定のプログラムとキャッシュによっては、特定の関数をインライン化することでパフォーマンスが向上する場合も低下する場合もあります。[ 1 ]
インライン化の影響は、抽象化の度合いが異なるため、プログラミング言語やプログラムによって異なります。CやFortranのような低レベルの命令型言語では、通常 10 ~ 20% の速度向上となり、コード サイズへの影響はわずかです。一方、より抽象的な言語では、インライン化によって削除されるレイヤーの数が多いため、その影響ははるかに大きくなります。極端な例としてSelfがあり、あるコンパイラではインライン化によって 4 ~ 55 倍の改善が見られました。[ 2 ]
関数呼び出しをなくすことによる直接的なメリットは以下のとおりです。
しかし、インライン化の主な利点は、さらなる最適化が可能になることです。関数境界を越える最適化は、プロシージャ間最適化(IPO)を必要とせずに実行できます。インライン化が実行されると、拡張された関数本体に対して、プロシージャ内最適化(「グローバル最適化」)を追加で実行できるようになります。例:
逆に、言語仕様によっては、プログラムが手続きの引数について追加の仮定を行うことを許可している場合があり、手続きがインライン化された後にはその仮定ができなくなるため、一部の最適化が妨げられることがあります。より高度なコンパイラ(Glasgow Haskell Compiler(GHC)など)はこれを追跡しますが、単純なインライン化ではこの情報が失われます。
メモリシステムにおけるインライン化のさらなる利点は以下のとおりです。
インライン化の直接的なコストは、関数本体が呼び出し箇所ごとに複製されるため、コードサイズが増加することです。しかし、常にそうなるわけではありません。例えば、関数本体が関数呼び出しのサイズ(呼び出し元における引数と戻り値の処理を含む)よりも小さい非常に短い関数(単純なアクセサメソッドやミューテータメソッド(ゲッターとセッター)など)や、1箇所でしか使用されない関数(複製されない)の場合などです。したがって、組み込みシステムでよく見られるように、コードサイズを最適化する場合は、インライン化を最小限に抑えるか、あるいは完全に排除することができます。
インライン化は、コードの拡張(重複による)によって命令キャッシュのパフォーマンスが低下するため、パフォーマンスにもコストがかかります。[ 7 ]これは、拡張前にプログラムのワーキングセット(またはホットセクションのコード)がメモリ階層の1つのレベル( L1キャッシュなど)に収まっていたのに、拡張後に収まらなくなり、そのレベルで頻繁にキャッシュミスが発生する場合に最も顕著になります。階層の異なるレベルでのパフォーマンスの差が大きいため、これはパフォーマンスを著しく低下させます。最上位レベルでは、ページフォルトの増加、スラッシングによる壊滅的なパフォーマンス低下、またはプログラムの実行自体の失敗につながる可能性があります。最後のケースは、コードサイズが利用可能なメモリに対して小さい一般的なデスクトップおよびサーバーアプリケーションではまれですが、組み込みシステムなどのリソース制約のある環境では問題になる可能性があります。この問題を軽減する1つの方法は、関数をより小さなホットインラインパス(高速パス)と、より大きなコールド非インラインパス(低速パス)に分割することです。[ 7 ]
インライン化によるパフォーマンス低下は、主に多くの場所で使用される大規模な関数で問題となりますが、インライン化によってパフォーマンスが低下する損益分岐点を特定するのは難しく、一般的には正確な負荷に依存するため、手動最適化またはプロファイルガイドによる最適化の対象となります。[ 8 ]これは、ループ展開などの他のコード拡張最適化と同様の問題であり、処理される命令数を削減しますが、キャッシュパフォーマンスの低下によりパフォーマンスが低下する可能性があります。
インライン化がキャッシュのパフォーマンスに及ぼす正確な影響は複雑です。キャッシュサイズが小さい場合(拡張前のワーキングセットよりもはるかに小さい場合)、シーケンシャル性の増加が支配的になり、インライン化によってキャッシュのパフォーマンスが向上します。キャッシュサイズがワーキングセットに近い場合、インライン化によってワーキングセットが拡張され、キャッシュに収まらなくなるため、これが支配的になり、キャッシュのパフォーマンスが低下します。キャッシュサイズがワーキングセットより大きい場合、インライン化はキャッシュのパフォーマンスにほとんど影響を与えません。さらに、ロードフォワーディングなどのキャッシュ設計の変更により、キャッシュミスの増加を相殺することができます。[ 9 ]
コンパイラは、どの関数呼び出しをインライン化するかを決定するためにさまざまなメカニズムを使用します。これには、特定の関数に対するプログラマからの手動ヒントや、コマンドラインオプションによる全体的な制御が含まれます。多くの言語の多くのコンパイラは、インライン化が有益かどうかを判断して自動的にインライン化を実行しますが、それ以外の場合は、コンパイラディレクティブを使用して手動で指定できます。これは通常、キーワードまたはコンパイラディレクティブを使用しますinline。通常、これはインライン化が必須ではなく、インライン化が望ましいことを示唆するだけであり、ヒントの強さは言語とコンパイラによって異なります。
通常、コンパイラ開発者は上記のパフォーマンス上の問題を念頭に置き、パフォーマンスを悪化させるのではなく、ほとんどの場合パフォーマンスを向上させるために、どの関数をインライン化するかを選択するヒューリスティックをコンパイラに組み込んでいます。
コンパイラが特定の関数をインライン化することを決定したら、インライン化処理自体は通常簡単です。コンパイラが異なる言語のコード間で関数をインライン化するかどうかによって、コンパイラは高レベルの中間表現(抽象構文木など)または低レベルの中間表現のいずれかに基づいてインライン化を行うことができます。いずれの場合も、コンパイラは引数を計算し、関数の引数に対応する変数に格納した後、関数の本体を呼び出し箇所に挿入するだけです。
リンカは関数のインライン化も行うことができます。リンカが関数をインライン化する場合、ライブラリ関数など、ソースが利用できない関数もインライン化することがあります(リンク時最適化を参照)。ランタイムシステムも関数をインライン化できます。ランタイムインライン化では、Java HotSpotコンパイラのように、動的プロファイリング情報を使用して、どの関数をインライン化するかについてより良い判断を下すことができます。[ 10 ]
以下は、 C言語においてソースコードレベルで「手動で」実行されるインライン展開の簡単な例です。
int pred ( int x ) { if ( x == 0 ) { return 0 ; } else { return x - 1 ; } }インライン化前:
int func ( int y ) { return pred ( y ) + pred ( 0 ) + pred ( y + 1 ); }インライン化後:
int func ( int y ) { int tmp ;// (1) if ( y == 0 ) { tmp = 0 ; } else { tmp = y - 1 ; }// (2) if ( 0 == 0 ) { tmp += 0 ; } else { tmp += 0 - 1 ; }// (3) if ( y + 1 == 0 ) { tmp += 0 ; } else { tmp += ( y + 1 ) - 1 ; }return tmp ; }これはあくまで一例です。実際のC言語アプリケーションでは、パラメータ付きマクロやインライン関数などのインライン化言語機能を使用して、コンパイラにコードをこのように変換するように指示する方が望ましいでしょう。次のセクションでは、このコードを最適化する方法を説明します。
アセンブラマクロは、インライン展開の代替手段を提供します。これにより、通常は単一のマクロソースステートメント(0個以上のパラメータを持つ)からマクロ展開によって一連の命令をインラインで生成できます。パラメータの1つは、代わりにシーケンスを含む個別のサブルーチンを1回生成し、関数へのインライン呼び出しによって処理するオプションである場合があります。例:
MOVE FROM=array1,TO=array2,INLINE=NO
インライン化にはさまざまなヒューリスティックが検討されてきました。通常、インライン化アルゴリズムには一定のコード予算(プログラムサイズの許容増加量)があり、その予算を超えずに最も価値の高い呼び出し箇所をインライン化することを目指します。この意味で、多くのインライン化アルゴリズムは通常、ナップサック問題をモデルにしています。[ 11 ]どの呼び出し箇所がより価値が高いかを判断するために、インライン化アルゴリズムはそれらのメリット、つまり実行時間の期待される減少を推定する必要があります。一般的に、インライン化アルゴリズムは、さまざまなコードパスの実行頻度に関するプロファイリング情報を使用してメリットを推定します。[ 12 ]
プロファイリング情報に加えて、最新のジャストインタイムコンパイラは、次のようなより高度なヒューリスティックをいくつか適用します。[ 5 ]
インライン展開自体も、呼び出しのオーバーヘッドを排除する最適化ですが、それ以上に重要なのは、変換を可能にする機能です。つまり、コンパイラが関数本体を呼び出し箇所のコンテキストで展開すると(多くの場合、固定定数である引数を含む)、以前は不可能だったさまざまな変換を実行できるようになります。たとえば、条件分岐がこの特定の呼び出し箇所で常に真または常に偽になる場合があります。これにより、デッドコードの削除、ループ不変コードの移動、または誘導変数の削除が可能になります。
前のセクションのC言語の例では、最適化の機会が豊富にあります。コンパイラは次のような手順を踏む可能性があります。
tmp += 0(2)と(3)で示された行のステートメントは何も実行しません。コンパイラはこれらを削除できます。0 == 0は常に真であるため、コンパイラは(2)とマークされた行を後件tmp += 0(何も行わない)に置き換えることができます。y+1 == 0を に書き換えることができますy == -1。(y + 1) - 1を に簡略化できますy。yはy+1両方ともゼロになることはできません。これにより、コンパイラはテストを1つ省略できます。if (y == 0) return yの値はy本文中で既知であり、インライン化できます。新しい関数は次のようになります。
int func ( int y ) { if ( y == 0 ) { return 0 ; } if ( y == -1 ) { return -2 ; } return 2 * y - 1 ; }再帰のため、完全なインライン展開が常に可能とは限りません。再帰的にインライン展開された呼び出しは終了しません。制限された量を展開したり、呼び出しグラフを分析して特定のノードでループを中断したりする(つまり、再帰ループ内の一部のエッジを展開しない)など、さまざまな解決策があります。[ 13 ]マクロ展開でも同様の問題が発生します。再帰的な展開は終了しないため、通常は再帰マクロを禁止することで解決されます(C や C++ と同様)。
従来、 C言語などの言語では、インライン展開はパラメータ付きマクロを使用してソースレベルで行われていました。C99で利用可能な真のインライン関数を使用すると、この方法に比べていくつかの利点があります。
多くのコンパイラは一部の再帰関数をインライン展開することもできます。[ 14 ]再帰マクロは通常不正です。
C++の設計者であるビャルネ・ストロヴストルップは、マクロは可能な限り避けるべきであり、インライン関数を積極的に利用すべきだと強調している。
多くのコンパイラは、有利な箇所であれば積極的に関数をインライン化します。インライン化によって実行ファイルが大きくなる可能性はありますが、メモリ容量の増加がCPU速度の増加を上回るにつれて、積極的なインライン化はますます望ましいものとなっています。インライン化は、関数型プログラミングやオブジェクト指向プログラミング言語において重要な最適化手法であり、これらの言語では、通常小さな関数に十分なコンテキストを提供することで、従来の最適化手法を効果的に活用できます。
Javaや関数型言語を含む多くの言語は、インライン関数のための言語構造を提供していませんが、それらのコンパイラやインタプリタは、積極的なインライン展開を実行することがよくあります。[ 5 ]他の言語は、一般的にコンパイラディレクティブ(プラグマ)として、明示的なヒントのための構造を提供しています。
Ada言語には、インライン関数のためのプラグマが存在する。
Common Lispの関数は、次のように宣言することでインラインとして定義できますinline。[ 15 ]
( declaim ( inline dispatch )) ( defun dispatch ( x ) ( funcall ( get ( car x ) 'dispatch ) x ))HaskellコンパイラGHCは、十分に小さい関数や値をインライン化しようとしますが、インライン化は言語プラグマを使用して明示的に指定できます。[ 16 ]
key_function :: Int -> String -> ( Bool , Double ) {-# INLINE key_function #-}CとC++には、インライン化が有益である可能性を示唆するキーワードがありますinlineが、新しいバージョンでは、その主な目的は関数の可視性とリンク動作を変更することです。[ 17 ]
C++では、クラス本体内で定義されたクラスのメソッドは、暗黙的にインライン化されます。
Kotlinでは、inline関数はインライン化され、 を使用してインライン化しないように指示できますnoinline。inlineは、関数のバイトコードを呼び出しサイトにコピーし、ラムダ引数をインライン化し、関数呼び出しとラムダ割り当てのオーバーヘッドを排除します。[ 18 ]同等のJava (またはJava バイトコード)では、関数は呼び出しサイトでロジックとして表現されます。
Rustでは、インライン化はコンパイラによって自動的に行われます。[ 19 ] Rust は、#[inline]関数をインライン化する必要があることをコンパイラに示唆する属性を提供しますが、それを保証するものではありません。コンパイラは、たとえ であっても無視する場合があります#[inline(always)]。デバッグモードでは、コンパイラはインライン化を行いません。[ 20 ]