
2026/01/10 1:05
インライン化 ― 究極の最適化
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
インライン化は、関数本体全体を呼び出し箇所にコピーするコアコンパイラ最適化であり、オプティマイザが定数やデッドコードを局所的に可視化できるようにします。これにより、別々の関数では不可能な変換を実行できます。現代のインライン化は呼び出しオーバーヘッドを回避するだけでなく、定数伝搬、デッドブランチ除去、およびその他の最適化を可能にします。
例: •
std::vector::size() は完全にインライン化されるため、アセンブリには関数呼び出しがなく、減算とシフト命令のみが含まれます。• 文字列を大文字に変換するルーチンで、
change_case が make_upper にインライン化されます。上位フラグが常に真であるため、コンパイラは分岐全体を除去し、(c - 'a') & 0xff < 26 ? c - 32 : c を単一の条件付き減算に置き換えます。
利点:
– 定数と未使用経路が局所的に可視化されることで、各呼び出し箇所で積極的な最適化が可能になります。
– インライン化は明示的な分岐を排除し、命令数を削減できます。
欠点:
– 過度のインライン化はコードサイズを膨らませます。コンパイラは利益とコストをヒューリスティックにバランスさせる必要があります。
– まれに、共有ルーチン(インライン化しない)を保持することで、グローバル分岐が非常に予測可能な場合に分岐予測が改善されることがあります。
要件と挙動:
– コンパイラは関数定義全体を完全に把握している必要があります。宣言だけではインライン化が阻止されます。
– インライン化の判断はヒューリスティックで、コンパイラ間で異なるため、小さなコード変更が最適化決定に波及することがあります。
総括すると、インライン化は究極の有効化最適化とみなされます。関数を別々に保つ場合には実現できない定数伝搬、デッドブランチ除去、およびその他の変換を解き放ちます。
本文
私が執筆し、LLM が校正した記事です。
詳細は最後に記載しています。
16 日目になりました。ここでは多くの人が「コンパイラ最適化の基本」として捉える インライン展開(inlining)について考えてきました。複雑だからではなく、実際にはコピー&ペーストという単純な作業よりも、インライン展開が可能にすることの方が興味深いからです。
かつてはインライン展開は関数呼び出し自体のコストを回避するためでしたが、現在ではそれ以外にも多くの最適化を実現できるようになっています。
すでにインライン展開を経験しています(ただし今回は抑えました)。8 日目にはベクターのサイズを取得する際に
.size() メソッドを呼び出しました。アセンブリコードを見ると、size() が std::vector のメンバ関数であることはわかりますが、実際には呼び出し命令は見られず、単に減算とシフトだけが残っています。
では、インライン展開が他の最適化を可能にする仕組みとは? ARMv7‑M を使って文字列を大文字に変換する例を考えてみましょう。文字を上から下へまたはその逆に変えるユーティリティ関数
change_case があるとします。この関数をコードで使用すると、コンパイラは次のように展開し、さらに upper が常に真であることを利用して全体を簡略化できます。
// コンパイラが change_case を make_upper にインライン展開し、 // upper が常に true であることを見てコード全体を簡略化する例: .LBB0_1: ldrb r2, [r0] ; 次の文字 `c` を読み込む; c = *string; sub r3, r2, #97 ; tmp = c - 'a' uxtb r3, r3 ; tmp = tmp & 0xff cmp r3, #26 ; tmp と 26 を比較 sublo r2, r2, #32 ; tmp が 26 未満なら c = c - 32 ; c = ((c - 'a') & 0xff) < 26 ? c - 32 : c; strb r2, [r0], #1 ; `c` を書き戻す; *string++ = c subs r1, r1, #1 ; カウンタを減らす bne .LBB0_1 ; まだ残りがあるならループ
!upper のケースは完全に消え、コンパイラはインライン化されたコードのコピーをさらに修正して真であることを利用できるようになりました。ブランチを使わずに (c - 'a') & 0xff < 26 を判定し、条件付きで 32 を減算することで a を A に変換します。
インライン展開はコンパイラにローカルな変更を行う余地を与えます。呼び出し元が存在しない場所で実装を特別扱いでき、定数として知られる値(上記の
upper のような)を伝搬させたり、使用されていないコードパスを削除したりできます。
ただしインライン展開には欠点もあります。過剰に使うとプログラムのサイズが大幅に増える可能性があります。コンパイラは関数をインライン化するかどうか、またその関数が呼び出す他の関数まで連鎖的に評価し、コードサイズの増加と予想される利益とのバランスで最適化を決定します。結局のところこれは推測です。
まれに共通ルーチンへの呼び出しコストを受け入れることでメリットが生じるケースもあります。例えば、グローバルに予測可能な分岐があるルーチンでは、共有された分岐点の方がブランチプロデクタにとって有利になることがあります。しかし多くの場合は逆で、インライン化されたコード内の分岐ヒストリがよりローカルになり、予測精度を向上させることがあります。これは複雑です。
インライン展開を行う際には呼び出す関数の定義(本体)が可視である必要があります。コンパイラが宣言だけ(例:
char change_case(char c, bool upper);)しか見ていない場合、インライン化はできません。本格的な C++ ではヘッダに多くのコードが入っているためこれは問題になりにくいですが、ビルド時間や依存関係を最小限に抑えたい場合には注意が必要です。
さらにフラストレーションが生じるのは、インライン展開が非常にヒューリスティックに左右されるためです。コンパイラごとに「どの関数をインライン化すべきか」の推測が異なり、ある場所に1 行追加するだけで全体の最適化方針に波及効果を与えることがあります。
結論として インライン展開は究極的な可能性を広げる最適化 です。単純に関数本体を呼び出し箇所へコピーするだけで数サイクル節約になる場合もありますが、コンパイラに新しいコードコピーを与えることで定数伝搬、デッドブランチの除去、そして共有関数本体では不可能な変換を適用できるようになります。コピー&ペーストが常に悪いとは限らないのです。
この投稿には添付動画もあります。
この記事は Advent of Compiler Optimisations 2025 の第17日目で、コンパイラが私たちのコードをどのように変換するかを探る25 日間シリーズの一部です。
← 「呼び出し元」 | 部分的インライン化 →
この記事は人間(Matt Godbolt)によって執筆され、LLM と人間によって校正・レビューが行われました。
Patreon、GitHub、または Compiler Explorer Shop で CE をサポートしてください。
2025 年 12 月 17 日 06:00:00 CST に投稿しました。