JITコンパイルとプロファイル誘導最適化の内部
なぜ実行中に速くなるのか。インタプリタ起動の手軽さと機械語の速度を両取りする JIT の内部を、ホットスポット検出から脱最適化まで原理から解き明かします。
- 1.JIT はまずインタプリタで実行しつつ実行回数や型を計測し、ホットなコードだけを段階的に機械語へコンパイルします。
- 2.プロファイルを前提に分岐除去・インライン化・型特殊化といった投機的最適化を行い、純粋な事前コンパイルより速くなる場合があります。
- 3.投機が外れると脱最適化(deopt)でインタプリタへ戻し、安全性を保ったまま最適化コードを破棄して作り直します。
なぜ実行中にコンパイルするのか
コンパイルとインタプリタ で見たとおり、事前コンパイル(AOT)は速いが起動と配布に手間がかかり、インタプリタは手軽だが遅いというトレードオフがあります。JIT(Just-In-Time)コンパイルはこの二択を崩します。鍵は、AOT が持たない情報を JIT は持っている点です。それは実際の実行プロファイル、すなわち「どのコードが何回通り、各変数に実際どんな型・値が来たか」という動的な事実です。
AOT コンパイラはコードを一度しか見られず、あらゆる入力に正しく動く保守的な機械語しか生成できません。JIT は走らせながら観測できるため、「このループは数百万回回る」「この引数は実測上いつも整数」といった偏りを利用し、起きていないケースを切り捨てた専用コードを生成できます。これが**プロファイル誘導最適化(PGO)**を実行時に常時行うということで、JIT が AOT を上回ることさえある理由です。
ホットスポット検出と階層化
すべてのコードをコンパイルするのは無駄です。多くのプログラムは実行時間の大半をごく一部のコードで消費する(ホットスポット)ためで、起動直後の一度きりの初期化を機械語化しても、コンパイルのコストが回収できません。そこで JIT はまずインタプリタで実行し、各メソッドの呼び出し回数とループのバックエッジ回数(ループ先頭へ戻った回数)をカウンタで計測します。閾値を超えたものだけを「ホット」と判定してコンパイルに回す——これがホットスポット検出です。
実用 JIT は単一段ではなく**階層化(tiered)**します。観測のための軽量段から、本気の最適化段へ昇格させる構成です。
| 段階 | 速度 | 役割 |
|---|---|---|
| インタプリタ | 遅い | 即起動・プロファイル収集 |
| 軽量JIT(C1 / Baseline) | 中 | 素早く機械語化しつつ計測継続 |
| 最適化JIT(C2 / TurboFan) | 速い | プロファイルを使い投機的に最適化 |
HotSpot は C1(クライアントコンパイラ)と C2(サーバーコンパイラ)を備えます。C1 は最適化を控えめにして速くコンパイルし、計測コード(カウンタ・型プロファイル)を埋め込んで実行を続けます。十分なプロファイルが溜まったメソッドを C2 が時間をかけて最適化します。V8 も同様に、Ignition インタプリタ → Sparkplug/Maglev → TurboFan という流れで昇格させます。起動の速さ(インタプリタ/軽量段)とピーク性能(最適化段)を時間軸で両立させるのがこの設計の狙いです。
インライン化が最適化の母
最適化段で最も効くのがインライン化——呼び出し先のコード本体を呼び出し元に展開する変換です。効果は呼び出しオーバヘッド(スタックフレーム生成や分岐)の削減にとどまりません。本質は、展開によって両者のコードが1つの最適化単位になり、定数畳み込み・不要分岐除去・共通部分式の削除といった他の最適化がメソッド境界を越えて働くようになる点にあります。インライン化は「他の最適化を可能にする最適化」と呼ばれます。
ただし無制限には展開できません。展開はコードサイズを増やし、命令キャッシュを圧迫してかえって遅くなり得るため、JIT は呼び出し先のサイズ・呼び出し頻度(プロファイル)・再帰の深さなどを見てヒューリスティックに判断します。小さくホットな呼び出しほどインライン化されやすくなります。
オブジェクト指向の obj.method() は、obj の実型によって飛び先が変わる仮想呼び出しのため、本来そのままではインライン化できません。JIT はプロファイルで「実際に来た型」を観測し、実測上1〜2種類しか来ていなければ、その型を仮定した直接呼び出しへ書き換えてインライン化します(モノモーフィック/バイモーフィック・インライン化)。多くの呼び出し点が実は単一型である、という経験則を突いた最適化です。
投機的最適化と脱最適化(deopt)
JIT の最適化の多くは投機的です。プロファイルが示す「いつもこうだった」を前提に、起きていないケースを省いたコードを作ります。たとえば「この変数は常に整数だった」なら、整数演算だけの高速パスを生成し、ボックス化や型分岐を消します。問題は、過去がそうでも将来そうとは限らないことです。前提が崩れたとき——初めて文字列が渡された、未ロードのクラスが現れた、null が来た——最適化コードは正しくないため、そのまま実行させてはなりません。
ここで脱最適化(deoptimization, deopt)が働きます。最適化コードには前提を検査するガードが埋め込まれており、ガードが失敗すると JIT は実行をその場でインタプリタ(または下位段)へ巻き戻し、最適化コードを破棄します。
最適化コード実行中
→ ガード検査(例: x は本当に整数か?)
成立 → そのまま高速パスを継続
失敗 → deopt: 状態をインタプリタ用に復元 → 安全に実行継続
→ 当該メソッドの最適化コードは無効化
deopt の実装は単純ではありません。最適化により消えていたローカル変数やスタックの値を、インタプリタが期待する形へ正確に復元する必要があり、そのための対応表を JIT はコンパイル時に記録しておきます。これにより、投機が外れても結果の正しさは決して損なわれないことが保証されます。プロファイルが更新された後、メソッドは新しい前提で再コンパイルされます。
JIT は実行を重ねて初めて速くなるため、起動直後は遅いウォームアップ期間があります。ベンチマークでこれを無視すると性能を誤評価します。また、型が安定しない多態的なコード(毎回違う型が来る)は deopt を繰り返し、再コンパイルと巻き戻しのコストでかえって遅くなることがあります。「ホットな経路では型を安定させる」ことが、JIT 言語での実務的な高速化指針になります。
最適化コードの寿命とコードキャッシュ
生成された機械語はコードキャッシュという専用領域に置かれ、deopt や前提の変化で不要になったものは回収されます。この回収は ガベージコレクション がヒープ上のオブジェクトを管理するのと発想が似ていますが、対象は実行コードである点が異なります。キャッシュが枯渇すると新規コンパイルが止まり、性能が静かに劣化するため、長時間稼働するサーバーでは監視対象になります。
なお、ループ実行中のホットなループを、メソッドの途中からその場で最適化コードへ乗り換える仕組みを **OSR(On-Stack Replacement)**と呼びます。1回しか呼ばれないが内部で巨大なループを回すメソッドは、呼び出し回数だけでは「ホット」と判定されないため、バックエッジ回数を見て実行の途中で差し替えるわけです。最適化が効くループほど反復コストが下がる効果は、計算量 が示すアルゴリズム的コストとは別軸の、定数倍の改善として現れます。
まとめ
JIT は、まずインタプリタで実行しながら実行回数と型を計測し、ホットなコードだけを階層的に機械語へコンパイルします。AOT にない実行プロファイルを武器に、インライン化・型特殊化・分岐除去といった投機的最適化を施すため、純粋な事前コンパイルを上回ることさえあります。投機の前提はガードで常時検査され、外れれば**脱最適化(deopt)**でインタプリタへ安全に巻き戻して作り直すため、速度を攻めながら正しさを保てます。HotSpot の C1/C2、V8 の TurboFan はいずれもこの「観測して、賭けて、外れたら戻す」原理の上に立っています。
プログラミング Article
JITコンパイルとプロファイル誘導最適化の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
JIT
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
プロファイルを前提に分岐除去・インライン化・型特殊化といった投機的最適化を行い、純粋な事前コンパイルより速くなる場合があります。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「JIT / 最適化」に近いか確認する。
- 強みである「JIT はまずインタプリタで実行しつつ実行回数や型を計測し、ホットなコードだけを段階的に機械語へコンパイルします。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。