TL

インライン化と関数間最適化(LTO)

関数呼び出しの壁を壊すとなぜ速くなるのか。インライン展開の損益判断、仮想呼び出しを直呼びに変えるデバート化、翻訳単位を越えて効くLTOの仕組みを原理から押さえます。

応用コンパイラ最適化インライン化LTO関数間最適化最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.インライン化は呼び出しを本体で置換し、呼び出しコストの除去だけでなく定数伝播・デッドコード除去など後続最適化の連鎖を生むのが本質的な利得です。
  • 2.展開可否はコスト/利益モデルで決まり、呼び出し回数・本体サイズ・コードサイズ膨張(および命令キャッシュ圧迫)を天秤にかけた閾値で判定します。
  • 3.LTOはオブジェクトに中間表現を埋め込み、リンク時に全翻訳単位を見渡して関数間最適化(デバート化・跨ぎインライン化)を行います。

なぜインライン化が最適化の要なのか

関数呼び出しには、引数の準備・スタックフレームの確保・制御の移動・戻り値の受け渡しという固定的なコストが伴います。しかしインライン化(inlining)の真価は、この呼び出しオーバーヘッドの除去そのものではありません。本質は、呼び出し先の本体を呼び出し側へ展開することで、関数の境界というデータフローの壁が消える点にあります。

int square(int x) { return x * x; }
int f() { return square(3) + 1; }

square(3) をインライン展開すると f()return 3 * 3 + 1 となり、ここで初めて定数伝播が 9 + 1、さらに 10 へと畳み込めます。展開前は引数 x が呼び出し側で何になるかが見えず、これらの最適化は関数の壁で止まっていました。インライン化はSSA形式とコンパイラ最適化で扱う定数伝播・デッドコード除去・共通部分式除去といったスカラ最適化を駆動する触媒であり、これがコンパイラがインライン化を最重要パスの一つに据える理由です。

コスト/利益モデルと閾値判断

すべての呼び出しを展開すればよいわけではありません。展開はコードを複製するため、無制限に行うと総コードサイズが膨張し、命令キャッシュ(I-cache)のミスが増えてかえって遅くなる——これがインライン化の中心的なトレードオフです。そこでコンパイラは各呼び出し地点について、利益とコストを見積もるコスト/利益モデルで判断します。

要因向き理由
呼び出し先の本体サイズ小さいほど展開複製コストが小さく、I-cache圧迫が軽い。getter/setterなど極小関数はほぼ常に展開
呼び出し頻度(ホット度)高いほど展開ホットパスでは呼び出しコスト除去と後続最適化の利得が累積する
定数引数の有無あれば展開を後押し展開後に定数伝播が連鎖し、本体の大部分が畳み込まれる見込みが立つ
呼び出し回数(call site数)少ないほど展開1か所からしか呼ばれない関数は展開してもサイズ増が一度きり
ループ内か内なら展開を後押し実行回数が多く、ループ不変式の巻き上げなど更なる最適化が効く

実装上は、本体を擬似命令単位でコスト化し、展開で消える命令(定数化される分岐や引数移動)を「ボーナス」として差し引いた純コストを閾値と比較します。例えば本体サイズが閾値未満なら展開、というのが素朴な形です(不等号でいえば「コスト < 閾値」のとき展開)。最適化レベルが上がるほど閾値は緩み、-Os/-Oz のようなサイズ優先では厳しくなります。

再帰とインライン化

再帰関数は無限に展開できないため、通常は展開を打ち切る深さ上限を設けます。末尾再帰は末尾呼び出しとCPS変換で扱うように呼び出しを反復へ変換できますが、一般の再帰では「上位数段だけ展開して残りは通常呼び出し」とするのが定石です。

直接呼び出しと間接呼び出し — インライン化の前提

インライン化はどの関数本体を展開するかが静的に確定して初めて行えます。直接呼び出し(コンパイル時に呼び先が分かる)はそのまま候補になりますが、関数ポインタ経由や動的ディスパッチとvtableによる仮想呼び出しは、実行時まで呼び先が定まらないためそのままではインライン化できません。オブジェクト指向コードで仮想呼び出しが多用されると、関数の壁が解けず最適化が連鎖しない——この障壁を崩すのが次のデバート化です。

デバート化(devirtualization)

デバート化(devirtualization)は、仮想呼び出し(間接呼び出し)を直接呼び出しに変換する最適化です。直呼びに変われば、その先をインライン化できるようになります。手法は確実性の度合いで段階的です。

  • 投機なしのデバート化:型解析で呼び先が一意に確定する場合。例えばあるクラスが派生を持たない(final 宣言や全プログラム解析で派生不在が判明)とき、その型のメソッド呼び出しは一意なので直呼びに置換できます。クラス階層解析(CHA: Class Hierarchy Analysis)がこれを支えます。
  • 投機的デバート化(speculative):プロファイルや型推測から「ほぼこの型」と予測し、ガード(実行時の型チェック)付きで直呼び+インライン化を挿入し、予測外れ時のみ元の仮想呼び出しへ落とすコードを残します。
// 仮想呼び出し  obj.draw()  を、Circle が支配的という推測でこう展開
if (obj.type == Circle) {
    /* Circle::draw の本体をインライン展開 */
} else {
    obj.draw();   // フォールバック(本来の間接呼び出し)
}

投機的デバート化はJITコンパイルとプロファイル誘導最適化の内部で中核を成します。実行時に観測した受信側の型分布(モノモーフィック=単一型なら特に有効)を使い、ガードが外れたら脱最適化(deoptimization)で安全に戻す——この組み合わせが動的言語の高速化の鍵です。静的コンパイラでもプロファイル誘導最適化(PGO)で同様の判断が可能です。

翻訳単位の壁とLTOの必要性

ここまでの最適化は、コンパイラが呼び先の本体を見られることが前提でした。ところがC/C++などの分割コンパイルでは、各ソースファイルが独立した**翻訳単位(translation unit)**としてオブジェクトファイルにコンパイルされ、別ファイルで定義された関数の中身はコンパイル時に見えません。

// util.c
int helper(int x) { return x * x + 1; }
// main.c  ——  helper の宣言だけ見え、本体は不可視
int main() { return helper(7); }

main.c のコンパイル時、helper は宣言(プロトタイプ)しか見えないため、インライン化も定数伝播もできず、通常の呼び出し規約に従った関数呼び出しが残ります。最適化は翻訳単位の内側で頭打ちになる——これが関数間最適化の壁です。

LTO — リンク時最適化の仕組み

LTO(Link-Time Optimization、リンク時最適化)は、この壁をコンパイルとリンクの役割分担を作り替えることで越えます。

  1. IRの埋め込み:各翻訳単位を機械語に落とし切らず、コンパイラの中間表現(AST と IRでいうIR。LLVMならbitcode、GCCならGIMPLE)をオブジェクトファイルに格納します。
  2. リンク時の統合:リンカ(プラグイン経由)が全オブジェクトのIRをかき集め、全プログラムを一つの大きなIRとして再構成します。ここで初めて main から helper の本体が見えます。
  3. 関数間最適化と再生成:統合IR上で跨ぎインライン化・デバート化・関数間定数伝播・未使用関数の除去などを行い、その後で機械語を生成します。

これにより helper(7) は翻訳単位を越えてインライン展開され、49 + 1 = 50 まで畳み込めます。LTOの効果は「翻訳単位の壁を取り払って、これまでの全最適化を全プログラム規模で再実行する」点に集約されます。

ThinLTO — スケールするLTO

全IRを一つに統合するフルLTOは、大規模プログラムでメモリと時間を食い、並列化も難しくなります。ThinLTO(LLVM)は、関数のサマリ(呼び出し関係・サイズ等)だけを集めた軽量なグローバル解析でインライン化候補を決め、実際の変換は翻訳単位ごとに並列実行します。フルLTOに近い効果を、分割コンパイルに近いビルド時間で得るのが狙いです。

LTOのコストと注意点

LTOはビルド時間とメモリを大きく増やし、デバッグ情報の対応も複雑になります。また関数間の定数伝播が進むぶん、別ファイルだから大丈夫という前提のコード(未定義動作・ODR違反・型の不整合)がLTO有効時にだけ表面化することがあります。これは最適化が新たなバグを生むのではなく、潜在的な不正を露呈させるものだと理解するのが正確です。

まとめ

インライン化の本質は呼び出しコストの除去ではなく、関数の壁を壊して定数伝播・デッドコード除去などのスカラ最適化を連鎖的に駆動することにあります。どこを展開するかはコスト/利益モデルで決まり、本体サイズ・呼び出し頻度・定数引数・コードサイズ膨張(I-cache圧迫)を閾値で天秤にかけます。仮想呼び出しはそのままでは展開できないため、デバート化で直呼びへ変換し(投機的なら実行時ガード付きで)インライン化の前提を整えます。そして翻訳単位の壁はLTOがIRをリンク時に統合することで越え、跨ぎインライン化や関数間最適化を全プログラム規模で可能にします——これが現代コンパイラ最適化の最終段です。

プログラミング Article

インライン化と関数間最適化(LTO)を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

コンパイラ

比較で見る軸

難易度: advanced / カテゴリ: プログラミング / タグ数: 5

導入後に効く点

展開可否はコスト/利益モデルで決まり、呼び出し回数・本体サイズ・コードサイズ膨張(および命令キャッシュ圧迫)を天秤にかけた閾値で判定します。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
プログラミング
タグ数
5

判断チェックリスト

  • 自社の用途が「コンパイラ / 最適化」に近いか確認する。
  • 強みである「インライン化は呼び出しを本体で置換し、呼び出しコストの除去だけでなく定数伝播・デッドコード除去など後続最適化の連鎖を生むのが本質的な利得です。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

コンパイラ最適化インライン化LTO関数間最適化コンパイラ最適化インライン化
参考: 公式情報