JIT コンパイル

プログラムの実行時にコードをネイティブコードにコンパイルし、パフォーマンスを向上させる技術

ランタイムパフォーマンス

JIT コンパイルとは

JIT (Just-In-Time) コンパイルは、プログラムの実行時にバイトコードをネイティブコード (機械語) にコンパイルし、パフォーマンスを向上させる技術である。V8 (Node.js)、JVM (Java)、.NET CLR が JIT を使用する。

インタプリタ vs JIT vs AOT

インタプリタと JIT vs AOT の違いを以下にまとめる。

方式コンパイル時期速度
インタプリタ実行時に逐次解釈起動は速いが実行は遅いPython (CPython)
JIT実行時にコンパイル起動は遅いが実行は速いV8, JVM
AOT事前にコンパイル起動も実行も速い (実行時の最適化は無い)Rust, Go, GraalVM

V8 の JIT パイプライン

V8 の JIT パイプラインを図で示す。

JavaScript ソースコード
  ↓ パース
AST (抽象構文木)
  ↓ Ignition (インタプリタ)
バイトコード → 実行 (起動は速いが実行は遅い)
  ↓ 何度か呼ばれたら
Sparkplug (ベースラインコンパイラ)
  ↓ さらに呼ばれ、扱う型が安定していたら
Maglev (中位の最適化コンパイラ)
  ↓ ホットコードとして残り続けたら
TurboFan (最上位の最適化コンパイラ)
  → 最適化されたネイティブコード → 実行 (高速)
コンポーネント役割
Ignitionバイトコードインタプリタ (起動が速い)
Sparkplug最適化を省いたベースラインコンパイラ (2021 年の V8 v9.1 で追加)
Maglev中位の最適化コンパイラ (Chrome 117 で追加)
TurboFanピーク性能を狙う最適化コンパイラ (実行が速い)

段が増えたのは、起動の速さとピーク性能のどちらかを捨てずに済ませるためである。1 度しか呼ばれない関数に TurboFan の最適化時間を払うのは損で、逆にループの中心にある関数をインタプリタで回し続けるのも損になる。V8 は呼び出し回数と型の安定度を見ながらコードを上の段へ昇格させる。Ignition と TurboFan の 2 段だけという解説も広く流通しているが、間の 2 段が入ったのは 2021 年 (Sparkplug) と 2023 年 (Maglev) である。

最適化と脱最適化

最適化と脱最適化のコード例を示す。

function add(a, b) { return a + b; }

// 何度も呼ばれ、a, b が常に number → 型を number と決め打ちしたコードが生成される
add(1, 2);  // 高速 (最適化済み)

// 突然 string を渡す → 決め打ちが崩れて最適化コードが破棄される (脱最適化)
add("hello", "world");  // 遅い (バイトコードの実行に巻き戻る)

これが単相性 (monomorphism) が効く理由である。同じ型・同じ形のオブジェクトだけが渡される呼び出し箇所なら、JIT は型の判定を省いた短いコードを吐ける。渡される型が数種類に増えると判定の分岐が入り、種類が多すぎると最適化を諦めて汎用の呼び出しに落ちる。

実務上の指針は 2 つに絞れる。1 つの引数に数値も文字列も配列も来る関数を作らないこと、同じ役割のオブジェクトはプロパティの追加順まで揃えて生成することである。ただしこれが意味を持つのはホットループの中だけで、毎秒数回しか通らないコードで型を揃える労力を払う価値はない。

Lambda でのコールドスタートと JIT

Lambda でのコールドスタートと JIT を図で示す。

コールドスタート:
  1. 実行環境の初期化
  2. コードのロード
  3. JIT コンパイル (初回は遅い)
  → 初回リクエストのレイテンシが増加

ウォームスタート:
  1. JIT コンパイル済みコードを再利用
  → 高速

SnapStart

初期化フェーズを終えた実行環境のメモリとディスクの状態を Firecracker microVM のスナップショットとして保存しておき、以降の呼び出しではそこから復元することでコールドスタートを縮める仕組みである。初期化の途中で行われた JIT コンパイルの結果もメモリ状態の一部として保存されるため、実行時コンパイルを前提とするランタイムほど効きが大きい。

2026 年 8 月時点で使えるのはマネージドランタイムの Java 11 以降、Python 3.12 以降、.NET 8 以降で、コンテナイメージでは使えない。有効にできるのは発行済みのバージョンとそれを指すエイリアスだけで、$LATEST では使えない点も設計時に押さえておく。

AOT との比較

AOT との主な違いを以下に比較する。

観点JIT (V8, JVM)AOT (Rust, Go)
起動速度遅い (コンパイルが必要)速い (コンパイル済み)
実行速度高い (実行時の型情報による投機的最適化が効く)高い (実行時の情報は使えない)
メモリ多い (コンパイラを含む)少ない
Lambda 適性コールドスタートが課題コールドスタートが速い

さらに掘り下げるなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事