LabHub

ブログ

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

한국어English日本語

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

2025年の現実:

本稿は「コードが実行されるまでに何が起きるか」を追う。

Part 1 — Compiler vs Interpreter vs JIT — スペクトラム

伝統的区分

現実はハイブリッド

「compilerかinterpreterか」は無意味な問い。「どのレイヤで構成されているか」が実質的な問いだ。

Part 2 — LLVMの支配

なぜLLVMが標準になったか

2000年、Chris Lattnerの博士プロジェクト。2024年現在:

LLVM IR

define i32 @add(i32 %a, i32 %b) {
  %sum = add i32 %a, %b
  ret i32 %sum
}

言語中立な中間表現。数百の最適化パスがこの上で走る。

MLIR (2019, Chris Lattner再登場)

「多段IR」。LLVM IR一段ではなく、ドメイン別の中間表現を複数レベルで。

必要な理由: MLフレームワーク(TensorFlow/PyTorch)は高レベルグラフから低レベル演算まで複数の抽象を扱う。LLVM IRだけでは表現力が不足。

MLIR採用 (2024):

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):

Inline Cache (IC):

実務教訓: 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が通るコードは遥かに速い。

Tiered Compilation

JVM: C1 (高速JIT) → C2 (攻撃的JIT) → (OpenJDK 17+) GraalVM。

LuaJIT: トレースベースJIT。Mike Pallの傑作。2020年代でも最高の動的言語JITとして引用される。

Part 4 — Garbage Collector 系譜

Mark & Sweep (1960)

Copying GC

Generational GC

Concurrent & Incremental GC

GCをアプリと同時にor分割実行してSTWを最小化。

G1 GC (JDK 9+ デフォルト)

ZGC (JDK 11+, 2023 Production-Ready)

Shenandoah (RedHat, JDK 12+)

GoのGC

CPython

V8

GC選択の哲学

GCpauseスループットメモリオーバーヘッド
Parallel (JDK, 非デフォルト)長い
G1
ZGC極低
Shenandoah極低
Go低-中
CPython RCほぼ0低 (カウンタ)

Part 5 — Goスケジューラ

G-M-Pモデル

各PはローカルGキューを持つ。空なら他のPからwork-stealing。

特徴

弱点

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

「抽象を使ったコードが手書きコードより遅くなることはない」。

RustのBorrow Checker

ランタイムコスト0でメモリ安全性を達成。コンパイル時に所有権・生存期間を検証。

2024-2025 Rustの進展

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の追加変化

PyPy

Part 8 — JavaScriptランタイムの地形

V8 (Chrome, Node.js, Deno)

JavaScriptCore (Safari/WebKit)

SpiderMonkey (Firefox)

BunのJSC選択

BunはV8ではなくJSCを採用。Nodeより速いベンチの一部はこの選択による。

ランタイムAPIの違い

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)

長所:

短所:

JIT (V8, JVM C2)

長所:

短所:

GraalVM — 両者の橋渡し

Part 11 — 性能解析ワークフロー

CPUプロファイリング

  1. Flame Graphで全体像を把握。
  2. ホットスポット関数を特定。
  3. 該当関数のアセンブリを確認 (perf annotate, またはCompiler Explorer)。
  4. JIT出力を見るには: Node --print-opt-code, V8 logging。

メモリプロファイリング

トレース

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 — 学習資料

終わりに — 言語はランタイムである

「どの言語を使うか」という問いは、しばしば「どのランタイム特性が欲しいか」の問題だ。

あらゆるランタイムは妥協。同じ言語でも設定・バージョン・GC選択で性能プロファイルは変わる。エンジニアの武器は「V8のHidden Classを壊さない」「Goのescape analysisを確認する」といった具体的なランタイム理解だ。

LLMがコードを巧みに書く時代に、「なぜこのコードは速い/遅いのか」を説明できるエンジニアの価値はより大きくなる。その答えの多くはコンパイラとランタイムにある。

次回予告 — 「AIエンジニアリング実戦」 — LLM APIアーキテクチャ、RAG、エージェント、ファインチューニング、ベクトルDB、評価、本番運用

14編のコンピュータシステムの旅の末、次はそれらすべての上に立つAIアプリケーションだ。

「AIプロダクトを実際に作る方法」。次回で。

コメント

まだコメントはありません。

ログインするとコメントできます