✍️ 필사 모드: コンパイラと現代言語ランタイム — LLVM, JIT, GC, V8 TurboFan/Maglev, Inline Caching, Escape Analysis, Rust Monomorphization 完全ガイド (2025)
日本語なぜコンパイラ/ランタイムを知るべきか
2025年の現実:
- V8, JVM, Go runtime, CPython, LLVM — 毎日使う道具たち。
- それらの内部最適化が、アプリ性能の70%を決める。
- 「なぜこのコードは遅いのか」の答えはコンパイラの判断に隠れていることが多い。
- 言語選択(Go vs Rust vs Python vs TS)はランタイム特性の選択。
- AI時代、数百万QPSのLLM推論ランタイムはコンパイラ最適化の戦いだ。
本稿は「コードが実行されるまでに何が起きるか」を追う。
Part 1 — Compiler vs Interpreter vs JIT — スペクトラム
伝統的区分
- Compiler: 全コードを機械語へ事前翻訳 (AOT, Ahead-of-Time)。
- Interpreter: ソースを一行ずつ即実行。
- JIT: 実行中に頻出部分を機械語へコンパイル。
現実はハイブリッド
- JVM: interpreter + C1 + C2 JIT + AOT (GraalVM)。
- V8: Ignition (interpreter) + Sparkplug + Maglev + TurboFan (JIT)。
- CPython: 純粋バイトコードinterpreter → 3.13 Specializing Adaptive Interpreter。
- .NET: バイトコード + RyuJIT + AOT (NativeAOT)。
「compilerかinterpreterか」は無意味な問い。「どのレイヤで構成されているか」が実質的な問いだ。
Part 2 — LLVMの支配
なぜLLVMが標準になったか
2000年、Chris Lattnerの博士プロジェクト。2024年現在:
- 言語フロントエンドがLLVM IRを生成し、最適化・コード生成はLLVMに委譲。
- LLVMを使う言語: Rust, Swift, Julia, Zig, Crystal, Kotlin/Native, Mojo, Chapel。
- Apple Silicon移行の中核ツール。
- GPUコード(CUDA/HIP)もLLVM経由。
LLVM IR
define i32 @add(i32 %a, i32 %b) {
%sum = add i32 %a, %b
ret i32 %sum
}
言語中立な中間表現。数百の最適化パスがこの上で走る。
- Constant Folding
- Dead Code Elimination
- Loop Invariant Code Motion
- Inlining
- Vectorization
- Instruction Combining
MLIR (2019, Chris Lattner再登場)
「多段IR」。LLVM IR一段ではなく、ドメイン別の中間表現を複数レベルで。
必要な理由: MLフレームワーク(TensorFlow/PyTorch)は高レベルグラフから低レベル演算まで複数の抽象を扱う。LLVM IRだけでは表現力が不足。
MLIR採用 (2024):
- Mojo (Modular AI) — Pythonスーパーセット、MLIRベース。
- IREE — MLコンパイラ。
- Triton (OpenAI) — GPUカーネル言語。
Part 3 — JITの巨匠たち
V8の4階層
2024年基準:
バイトコード (Ignition interpreter)
↓ (hot)
Sparkplug — バイトコード1:1機械語 (非最適化・高速生成)
↓ (hotter)
Maglev — 中間最適化JIT (2023)
↓ (very hot)
TurboFan — 最大最適化 (Sea of Nodes, 時間がかかる)
各段階は下層で集めたtype feedbackを用い、上位でより攻撃的な最適化を行う。
Deoptimization: 仮定が外れれば下層へ戻る。動的言語最適化の核心メカニズム。
Hidden Class + Inline Caching
JavaScriptオブジェクトは動的。obj.xのアドレスは固定ではない。V8の解:
Hidden Class (別名 Shape, Map):
- 同一プロパティ構造のオブジェクトが同じhidden classを共有。
obj.xは「このhidden classのoffset N」にコンパイルされる。
Inline Cache (IC):
- プロパティアクセス結果を呼び出し地点にキャッシュ。
- 同じhidden classの繰り返しアクセス → cache hit → ネイティブ速度。
- 形状変化 → IC miss → polymorphic → megamorphicへ遷移。
実務教訓: JSオブジェクトの形を安定させよ。
// 悪: 条件付きプロパティ追加
const p = {};
if (cond) p.x = 1;
if (cond2) p.y = 2;
// 良: 生成時に全プロパティを宣言
const p = { x: cond ? 1 : undefined, y: cond2 ? 2 : undefined };
Escape Analysis
「このオブジェクトが関数外へ逃げなければ → スタック割当 or scalar replacement」。
ヒープ割当は高コストでGC圧を生む。escape analysisが通るコードは遥かに速い。
- JVM C2: 強力なescape analysis。
- Goコンパイラ:
go build -gcflags="-m"で結果確認可能。 - V8 TurboFan: 限定的。
Tiered Compilation
JVM: C1 (高速JIT) → C2 (攻撃的JIT) → (OpenJDK 17+) GraalVM。
-XX:+PrintCompilationで観測。- C2は今も業界最強のJITの一つ。
LuaJIT: トレースベースJIT。Mike Pallの傑作。2020年代でも最高の動的言語JITとして引用される。
Part 4 — Garbage Collector 系譜
Mark & Sweep (1960)
- rootから到達可能なオブジェクトをmark。
- markされなかったものをsweep。
- 短所: Stop-the-world, 断片化。
Copying GC
- 2領域に分割、生存オブジェクトを片側へコピー。
- 断片化なし; メモリ2倍必要。
Generational GC
- 多くのオブジェクトは若くして死ぬ (Weak Generational Hypothesis)。
- Young/Old分離、若い領域を頻繁に回収。
Concurrent & Incremental GC
GCをアプリと同時にor分割実行してSTWを最小化。
G1 GC (JDK 9+ デフォルト)
- Regionベース (約2048 heap regions)。
- 予測可能なpause time target。
- 多くのサーバワークロードのデフォルト。
ZGC (JDK 11+, 2023 Production-Ready)
- Colored Pointers — オブジェクト移動を参照自体のカラー bitで管理。
- Sub-millisecond pause保証 (数十TBヒープでも)。
- 2023のGenerational ZGCでスループットも改善。
Shenandoah (RedHat, JDK 12+)
- 同時圧縮。
- ZGCと競合。
GoのGC
- 同時 mark & sweep。
- 3色抽象アルゴリズム + write barrier。
- Stop-the-world 1ms以下目標 (Go 1.5+で達成)。
- Generationalではない — escape analysisで多くをスタックへ上げる戦略。
CPython
- 主にReference Counting + 循環参照用Cycle Collector。
- 長所: 決定的解放。
- 短所: 全alloc/deallocでカウンタ操作、GILが必要。
V8
- Orinoco — 同時・増分・並列。
- 若い世代 (new space): Copying。
- 古い世代 (old space): Mark-Compact。
GC選択の哲学
| GC | pause | スループット | メモリオーバーヘッド |
|---|---|---|---|
| Parallel (JDK, 非デフォルト) | 長い | 高 | 低 |
| G1 | 中 | 中 | 中 |
| ZGC | 極低 | 中 | 中 |
| Shenandoah | 極低 | 中 | 中 |
| Go | 低 | 中 | 低-中 |
| CPython RC | ほぼ0 | 低 (カウンタ) | 低 |
Part 5 — Goスケジューラ
G-M-Pモデル
- G: goroutine
- M: OS thread
- P: processor (論理CPU、通常GOMAXPROCS)
各PはローカルGキューを持つ。空なら他のPからwork-stealing。
特徴
- 協調的preemption + Go 1.14+ 非同期preemption (時間超過でシグナル中断)。
- syscall時: Mがブロックされても、Pは即座に別のMへ再割当 → 他goroutineは走り続ける。
- NetpollerがI/Oをepoll/kqueueと統合。
弱点
- NUMA-aware schedulingは限定的。
- GOMAXPROCS自動検知はcgroups非対応 — コンテナでは手動設定推奨。
- Uberの
automaxprocsライブラリが解決。
Part 6 — RustとAOTの力
Monomorphization
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
let x = max(1i32, 2i32); // max::<i32> 生成
let y = max(1.0f64, 2.0); // max::<f64> 別途生成
型ごとに専用機械語を生成 → 呼び出しオーバーヘッド0、最適なinlining。 短所: バイナリサイズ増大 (code bloat)。
Zero-Cost Abstractions
「抽象を使ったコードが手書きコードより遅くなることはない」。
- Iterator連鎖がfor-loopと同一アセンブリ。
- async/awaitがstate machineへ完全に展開。
- Trait object (
dyn Trait)はコストあり、静的ディスパッチは0。
RustのBorrow Checker
ランタイムコスト0でメモリ安全性を達成。コンパイル時に所有権・生存期間を検証。
2024-2025 Rustの進展
- async traitネイティブ (1.75)。
- Parallel frontend (Nightly) — コンパイル速度改善。
- Cranelift backend — debugビルドでLLVM代替可能。
cargo-nextest— テスト速度改善。
Part 7 — Python 3.13の革命
Specializing Adaptive Interpreter (PEP 659, 3.11+)
CPythonがバイトコードに形状別特化命令をランタイムで差し込む。
LOAD_ATTR → LOAD_ATTR_INSTANCE_VALUE (dictベースobject)
→ LOAD_ATTR_SLOT (__slots__)
→ LOAD_ATTR_MODULE
→ ...
V8のInline CacheをCPythonに持ち込んだもの。
3.13の追加変化
- Experimental JIT (copy-and-patch方式)。実験フラグ。
- Free-Threading (PEP 703) — GILなし実行。別ビルドオプション。
- Incremental GC。
PyPy
- Tracing JIT。数年間CPython比4-10倍速い。
- 互換性問題で普及は限定的。
- HPy (2020+) — C拡張APIの可搬性改善の取り組み。
Part 8 — JavaScriptランタイムの地形
V8 (Chrome, Node.js, Deno)
- 2008年登場。Lars Bak。
- 標準JS最適化の基準。
JavaScriptCore (Safari/WebKit)
- 4階層JIT (LLInt → Baseline → DFG → FTL)。
- 独自のB3 JITコンパイラ。
- メモリ効率はV8より良いとの評。
SpiderMonkey (Firefox)
- IonMonkey JIT。
- WebAssembly実装の品質が高い。
BunのJSC選択
BunはV8ではなくJSCを採用。Nodeより速いベンチの一部はこの選択による。
ランタイムAPIの違い
- Node.js: fs, net, http, CommonJS+ESM。
- Deno: Web API優先 + permissions + TypeScriptネイティブ。
- Bun: Node API + Web API + 高速bundler/test runner内蔵。
- Workerd (Cloudflare): V8 isolate、Node互換は限定的。
Part 9 — WebAssemblyランタイム
既出だが、ランタイム観点で再整理:
| ランタイム | 特徴 | 用途 |
|---|---|---|
| V8 + Wasm | ブラウザ標準 | Web |
| Wasmtime | Bytecode Alliance | サーバWASI |
| Wasmer | 多様なバックエンド | 組み込み |
| WasmEdge | CNCF | エッジコンピューティング |
| Wasmer + Cranelift | 高速コンパイル | 開発 |
| Wasmer + LLVM | 最適コード | 本番 |
Part 10 — AOT vs JIT のトレードオフ
AOT (Rust, Go, Swift, GraalVM Native Image)
長所:
- 高速起動。
- 予測可能な性能。
- ランタイムオーバーヘッド小。
短所:
- 動的最適化不可 (type feedback使えず)。
- バイナリサイズ。
- コンパイル時間。
JIT (V8, JVM C2)
長所:
- 動的型・多態性の最適化。
- ランタイム情報を活用。
短所:
- ウォームアップ (初期遅い)。
- メモリオーバーヘッド。
- 予測困難なpause。
GraalVM — 両者の橋渡し
- Native Image — JavaコードをAOTコンパイル。
- 起動50ms、メモリ90%削減。
- Spring Native, Quarkus, Micronaut — サーバレス・コンテナに適す。
- 短所: ReflectionとDynamic Class Loadingに制約。
Part 11 — 性能解析ワークフロー
CPUプロファイリング
- Flame Graphで全体像を把握。
- ホットスポット関数を特定。
- 該当関数のアセンブリを確認 (
perf annotate, またはCompiler Explorer)。 - JIT出力を見るには: Node
--print-opt-code, V8 logging。
メモリプロファイリング
- Chrome DevTools Heap Snapshot (V8)。
- JFR + Mission Control (JVM)。
- pprof (Go)。
- memray (Python)。
トレース
- Linux perf + flame graph。
- Parca, Pyroscope — continuous profiling。
- async-profiler (JVM) — safepoint問題のないJVMプロファイラ。
Part 12 — チェックリスト (12項目)
- ランタイムは最新LTS — V8, JVM, Go, Python全て継続改善。
- GCチューニングはベンチ後 — 早すぎる最適化は悪。
- JSオブジェクトの形を安定に — Hidden Class安定化。
- Goはescape analysisを意識 —
-gcflags="-m"で確認。 - Rustは
#[inline]/PGO — 手動ヒントが必要なことが多い。 - JVMはJFR常時ON — 本番常時プロファイル。
- CPython 3.13+を検討 — 特化インタプリタの性能改善。
- コンテナ内CPU認識 — GOMAXPROCS,
-XX:ActiveProcessorCount。 - 起動に敏感なアプリはAOTを検討 — Native Image, Go AOT。
- JITウォームアップ測定 — ベンチ初期値は捨てる。
- Allocation hot pathを最小化 — 大半の性能問題の原因。
- Compiler Explorer (godbolt.org) — アセンブリ直接確認を習慣に。
Part 13 — 10大アンチパターン
- 「言語が速いから速い」 — ランタイム設定・構造の方が影響大。
time ./appで1回測ってベンチ終了 — 分散・ウォームアップ無視。- JITウォームアップ無視 —
-Xcompだけで判断。 - マイクロベンチにJMHを使わず
System.nanoTime()— 誤差が数十倍。 - Generic乱用 (monomorphization爆発) — バイナリが数十MBに。
- Pythonでtight loopをpure Pythonで — NumPy/Cython/Numba検討。
- Node.jsでCPUバウンドをmain event loopに —
worker_threads必須。 - JVMでXms=Xmxを揃えずに本番運用 — ヒープ再割当コスト。
- GCログを取らない — 本番障害解析不可。
- コンテナ既定メモリでJVM稼働 — OOM Kill。
-XX:+UseContainerSupportはデフォルトだが確認。
Part 14 — 学習資料
- 書籍: Crafting Interpreters (Robert Nystrom) — 無料公開。最も親切な入門。
- 書籍: Engineering a Compiler (Cooper & Torczon)。
- 書籍: Modern Compiler Implementation in ML/Java/C (Andrew Appel)。
- 書籍: The Garbage Collection Handbook (Jones, Hosking, Moss)。
- ブログ: V8 blog、Chrome V8エンジニア講演 (YouTube)。
- ツール: godbolt.org — アセンブリ実験場。
- 講義: Stanford CS143 Compilers (公開)。
終わりに — 言語はランタイムである
「どの言語を使うか」という問いは、しばしば「どのランタイム特性が欲しいか」の問題だ。
- 極低遅延 → Rust, Go AOT, GraalVM Native。
- 開発生産性優先 → Python, TypeScript, Kotlin。
- 多態性最適化 → JVM C2, V8 TurboFan。
- 起動時間重要 → AOT。
あらゆるランタイムは妥協。同じ言語でも設定・バージョン・GC選択で性能プロファイルは変わる。エンジニアの武器は「V8のHidden Classを壊さない」「Goのescape analysisを確認する」といった具体的なランタイム理解だ。
LLMがコードを巧みに書く時代に、「なぜこのコードは速い/遅いのか」を説明できるエンジニアの価値はより大きくなる。その答えの多くはコンパイラとランタイムにある。
次回予告 — 「AIエンジニアリング実戦」 — LLM APIアーキテクチャ、RAG、エージェント、ファインチューニング、ベクトルDB、評価、本番運用
14編のコンピュータシステムの旅の末、次はそれらすべての上に立つAIアプリケーションだ。
- LLM API呼び出しの実戦 — リトライ、タイムアウト、ストリーミング、コスト
- RAGアーキテクチャ — 単純検索からHybrid Searchまで
- エージェント設計パターン — ReAct, Plan-and-Execute, Tool Use
- ファインチューニングを「いつ、いつしないか」 — LoRA, DPO, RLHF比較
- ベクトルDB実戦 — pgvector vs Qdrant vs Pinecone
- LLM評価 (Eval) — 精度測定の本当の難しさ
- プロンプトエンジニアリングの科学 — Structured Output, Few-shot, Chain-of-Thought
- LLM Observability — OpenTelemetry GenAI, LangSmith, LangFuse
- コスト最適化 — モデル選択、キャッシング、Prompt Compression
- セキュリティ — Prompt Injection, Data Leakage
「AIプロダクトを実際に作る方法」。次回で。
현재 단락 (1/240)
2025年の現実: