Skip to content

✍️ 필사 모드: コンパイラと現代言語ランタイム — LLVM, JIT, GC, V8 TurboFan/Maglev, Inline Caching, Escape Analysis, Rust Monomorphization 完全ガイド (2025)

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

なぜコンパイラ/ランタイムを知るべきか

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選択の哲学

GCpauseスループットメモリオーバーヘッド
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_ATTRLOAD_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
WasmtimeBytecode AllianceサーバWASI
Wasmer多様なバックエンド組み込み
WasmEdgeCNCFエッジコンピューティング
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プロファイリング

  1. Flame Graphで全体像を把握。
  2. ホットスポット関数を特定。
  3. 該当関数のアセンブリを確認 (perf annotate, またはCompiler Explorer)。
  4. 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項目)

  1. ランタイムは最新LTS — V8, JVM, Go, Python全て継続改善。
  2. GCチューニングはベンチ後 — 早すぎる最適化は悪。
  3. JSオブジェクトの形を安定に — Hidden Class安定化。
  4. Goはescape analysisを意識 — -gcflags="-m" で確認。
  5. Rustは#[inline]/PGO — 手動ヒントが必要なことが多い。
  6. JVMはJFR常時ON — 本番常時プロファイル。
  7. CPython 3.13+を検討 — 特化インタプリタの性能改善。
  8. コンテナ内CPU認識 — GOMAXPROCS, -XX:ActiveProcessorCount
  9. 起動に敏感なアプリはAOTを検討 — Native Image, Go AOT。
  10. JITウォームアップ測定 — ベンチ初期値は捨てる。
  11. Allocation hot pathを最小化 — 大半の性能問題の原因。
  12. Compiler Explorer (godbolt.org) — アセンブリ直接確認を習慣に。

Part 13 — 10大アンチパターン

  1. 「言語が速いから速い」 — ランタイム設定・構造の方が影響大。
  2. time ./app で1回測ってベンチ終了 — 分散・ウォームアップ無視。
  3. JITウォームアップ無視 — -Xcompだけで判断。
  4. マイクロベンチにJMHを使わずSystem.nanoTime() — 誤差が数十倍。
  5. Generic乱用 (monomorphization爆発) — バイナリが数十MBに。
  6. Pythonでtight loopをpure Pythonで — NumPy/Cython/Numba検討。
  7. Node.jsでCPUバウンドをmain event loopに — worker_threads必須。
  8. JVMでXms=Xmxを揃えずに本番運用 — ヒープ再割当コスト。
  9. GCログを取らない — 本番障害解析不可。
  10. コンテナ既定メモリで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年の現実:

작성 글자: 0원문 글자: 8,324작성 단락: 0/240