Skilore

V8エンジン

V8はGoogleが開発した高性能JavaScriptエンジンであり、Chrome、Node.js、Denoなど広範なランタイム環境の基盤を形成している。本ガイドではV8の内部アーキテクチャを深掘りし、パーサー、Ignition(インタプリタ)、TurboFan(最適化コンパイラ)、Hidden Class、インラインキャッシュ、ガベージコレクションの各メカニズムを体系的に解説する。

120 分で読めます59,516 文字

V8エンジン

V8はGoogleが開発した高性能JavaScriptエンジンであり、Chrome、Node.js、Denoなど広範なランタイム環境の基盤を形成している。本ガイドではV8の内部アーキテクチャを深掘りし、パーサー、Ignition(インタプリタ)、TurboFan(最適化コンパイラ)、Hidden Class、インラインキャッシュ、ガベージコレクションの各メカニズムを体系的に解説する。

前提知識

本ガイドを効果的に学習するために、以下の知識を前提とする。

  • JavaScriptの基本的な実行モデル --- 変数、関数、クロージャ、プロトタイプチェーンの理解
  • コンパイラとインタプリタの違い --- ソースコードから実行可能コードへの変換過程
    • 参照: CS基礎 - コンパイラ原理
  • ブラウザのアーキテクチャ --- レンダリングエンジン、JavaScriptエンジン、イベントループの関係性
  • メモリ管理の基礎 --- スタック、ヒープ、ガベージコレクションの概念
  • 実行環境の違い --- ブラウザ環境とNode.js環境におけるV8の役割の違い

この章で学ぶこと

  • V8のソースコード処理パイプライン全体像を理解する
  • パーサーの仕組み(Lazy Parsing / Eager Parsing)を把握する
  • Ignition バイトコードインタプリタの動作原理を理解する
  • TurboFan 最適化コンパイラの最適化手法を学ぶ
  • Hidden Class によるオブジェクト表現の内部構造を把握する
  • インラインキャッシュ(IC)の状態遷移を理解する
  • 世代別ガベージコレクションの戦略を学ぶ
  • V8に最適化されたコードの書き方を実践する

1. V8の全体アーキテクチャ

V8は単なるインタプリタではなく、複数のフェーズを持つ高度なコンパイルパイプラインである。JavaScriptのソースコードは以下のステージを経て実行される。

1.1 パイプライン全体図

V8 コンパイルパイプライン
JavaScript ソースコード (.js)
┌─────────────────┐
Scanner字句解析(Tokenizer)
(Lexer)ソースコードをトークン列に分解
└────────┬────────┘
┌─────────────────┐
Parser構文解析
(Full/Lazy)トークン列からAST(抽象構文木)を構築
└────────┬────────┘
┌─────────────────┐
Ignitionバイトコードインタプリタ
(Interpreter)ASTからバイトコードを生成・実行
+ フィードバックベクタでプロファイリング
└────────┬────────┘
ホットスポット検出
(一定回数以上実行された関数)
┌─────────────────┐
TurboFan最適化コンパイラ
(Optimizingバイトコード + 型フィードバック →
Compiler)最適化されたマシンコードを生成
└────────┬────────┘
脱最適化(Deoptimization)
前提条件が崩れた場合、Ignitionに戻る
┌─────────────────┐
実行最適化マシンコード or バイトコードを実行
└─────────────────┘

1.2 歴史的経緯

V8のコンパイルパイプラインは、バージョンを重ねるごとに大きく変遷してきた。

時期 コンパイラ構成 特徴
2008-2010 Full-Codegen + Crankshaft なし 初期の単純なJITコンパイラ
2010-2016 Full-Codegen + Crankshaft 2段階JIT。Crankshaftがホットコードを最適化
2016-2017 Ignition + TurboFan(段階導入) 新パイプラインへの移行期
2017-現在 Ignition + TurboFan 現行パイプライン。バイトコード + 最適化JIT

Full-Codegenは全関数を即座にネイティブコードにコンパイルする方式だったが、起動が遅く、メモリ使用量が多かった。Ignitionの導入により、まずバイトコードとして実行し、本当に必要なコードだけを最適化コンパイルする効率的なアプローチが実現された。


2. パーサー(Parser)

2.1 字句解析(Scanner)

Scannerはソースコードの文字列をトークン(Token)の列に変換する。トークンとはプログラミング言語の最小単位であり、キーワード、識別子、リテラル、演算子などに分類される。

// ソースコード
function add(a, b) { return a + b; }
 
// トークン列に変換
// Token::kFunction  → "function"
// Token::kIdentifier → "add"
// Token::kLeftParen  → "("
// Token::kIdentifier → "a"
// Token::kComma      → ","
// Token::kIdentifier → "b"
// Token::kRightParen → ")"
// Token::kLeftBrace  → "{"
// Token::kReturn     → "return"
// Token::kIdentifier → "a"
// Token::kAdd        → "+"
// Token::kIdentifier → "b"
// Token::kSemicolon  → ";"
// Token::kRightBrace → "}"

V8のScannerはUTF-16エンコーディングのソースコードを処理し、1文字先読み(one-character lookahead)を使ってトークンを識別する。数値リテラルや文字列リテラルのようなマルチキャラクタートークンは、専用のスキャンルーチンで処理される。

2.2 Lazy Parsing と Eager Parsing

V8のパーサーには2つの動作モードがある。これはV8のパフォーマンス戦略の核心部分である。

パーサーの2つのモード
Eager ParsingLazy Parsing (PreParser)
(完全解析)(遅延解析)
・完全なASTを構築・関数の中身をスキップ
・即座に実行される・変数スコープだけ確認
コードに使用・構文エラーだけ検出
・トップレベルコード・実際に呼び出されるまで
に適用される解析を延期
・メモリと起動時間を節約
コスト: 高コスト: 低(完全解析の約半分)
対象: 即実行コード対象: 関数宣言

Lazy Parsing の仕組み:

// トップレベルコード → Eager Parsing(即座に完全解析)
const config = { debug: true };
 
// 関数宣言 → Lazy Parsing(中身はスキップ)
function processData(data) {
  // この中身は最初の呼び出し時まで解析されない
  const result = data.map(item => item.value * 2);
  return result.filter(v => v > 10);
}
 
// IIFE(即時実行関数) → Eager Parsing(即座に実行されるため)
(function() {
  console.log("immediately invoked");
})();
 
// processData が呼び出された時点で初めて完全解析される
processData([{ value: 5 }, { value: 8 }, { value: 15 }]);

Lazy Parsing が有効な理由:

一般的なWebページでは、読み込まれたJavaScriptコードの30-50%は初期表示で実行されない。Lazy Parsingにより、未使用の関数の解析コストを後回しにできるため、ページの初期読み込み速度が向上する。

Lazy Parsing の落とし穴:

// アンチパターン: Lazy Parsing が裏目に出るケース
// 関数がすぐに呼ばれるのに、V8がLazy Parsingしてしまう
 
function heavyComputation() {
  // 大量のコード...
}
 
// すぐに呼ばれる → Lazy Parse + Re-parse で二重コスト
heavyComputation();
 
// 対策: V8にEager Parsingのヒントを与える
// 括弧で囲むと、V8はIIFEパターンと認識してEager Parsingする
const heavyComputation2 = (function() {
  // 大量のコード...
});

2.3 AST(抽象構文木)の構造

パーサーはトークン列からAST(Abstract Syntax Tree)を構築する。ASTはソースコードの構文構造をツリー形式で表現したものである。

// ソースコード
function multiply(x, y) {
  return x * y;
}
 
// 対応するAST(概念的な表現)
//
//  FunctionDeclaration
//  ├── name: "multiply"
//  ├── params: ["x", "y"]
//  └── body: BlockStatement
//      └── ReturnStatement
//          └── BinaryExpression
//              ├── operator: "*"
//              ├── left: Identifier("x")
//              └── right: Identifier("y")

V8のASTノードは内部的にC++のクラスで表現され、各ノードはソースコード上の位置情報(SourcePosition)を保持している。これはエラーメッセージやデバッグ情報の生成に使用される。


3. Ignition バイトコードインタプリタ

3.1 Ignitionの役割

Ignitionは2016年にV8に導入されたレジスタベースのバイトコードインタプリタである。ASTからバイトコードを生成し、そのバイトコードを直接実行する。

Ignition導入の動機:

  1. メモリ使用量の削減 --- Full-Codegenが生成するネイティブコードに比べ、バイトコードはコンパクト
  2. 起動速度の向上 --- バイトコード生成はネイティブコード生成より高速
  3. TurboFanへの情報提供 --- 実行中に収集した型情報をTurboFanに渡す

3.2 バイトコードの例

// JavaScriptソースコード
function add(a, b) {
  return a + b;
}
 
// Ignitionが生成するバイトコード(概念的な表現)
// ※ 実際のバイトコードは --print-bytecode フラグで確認できる
//
// Parameter count: 3 (receiver + a + b)
// Register count: 0
// Frame size: 0
//
//   Ldar a1          // レジスタa1(引数a)をアキュムレータにロード
//   Add a2, [0]      // アキュムレータにa2(引数b)を加算
//                     // [0]はフィードバックスロットのインデックス
//   Return            // アキュムレータの値を返す

Node.jsでバイトコードを確認する方法:

# --print-bytecode フラグで Ignition のバイトコードを出力
node --print-bytecode --print-bytecode-filter="add" script.js
 
# 出力例:
# [generated bytecode for function: add (0x...)]
# Bytecode length: 6
# Parameter count 3
# Register count 0
# Frame size 0
#    0 : 25 02             Ldar a1
#    2 : 39 03 00          Add a2, [0]
#    5 : aa                Return

3.3 レジスタベースとスタックベースの比較

バイトコードインタプリタには大きく2つの方式がある。Ignitionはレジスタベース方式を採用している。

特性 レジスタベース(Ignition) スタックベース(旧Java VM等)
命令の形式 Add r1, r2, r3 Push a; Push b; Add
命令数 少ない(オペランドに直接指定) 多い(Push/Pop操作が必要)
バイトコードサイズ やや大きい(オペランド指定分) 小さい
実行速度 高速(メモリアクセス削減) やや遅い(スタック操作のオーバーヘッド)
ディスパッチ回数 少ない 多い
採用例 V8 Ignition, Lua VM JVM, Python VM, .NET CLR

3.4 フィードバックベクタ(Feedback Vector)

Ignitionは実行中に型情報やプロパティアクセスパターンなどのプロファイル情報を収集する。これはフィードバックベクタと呼ばれるデータ構造に保存される。

フィードバックベクタの構造
function calculate(obj) {
return obj.x + obj.y; // 2つのプロパティアクセス
}
フィードバックベクタ:
┌─────────┬──────────────────────────────────┐
Slot 0LoadIC: obj.x
→ Map: 0x1234 (Hidden Class)
→ Offset: 12
→ State: monomorphic
├─────────┼──────────────────────────────────┤
Slot 1LoadIC: obj.y
→ Map: 0x1234 (Hidden Class)
→ Offset: 16
→ State: monomorphic
├─────────┼──────────────────────────────────┤
Slot 2BinaryOp: +
→ Hint: SignedSmall
└─────────┴──────────────────────────────────┘
この情報がTurboFanに渡され、最適化の判断材料になる

フィードバックベクタに蓄積された型情報は、TurboFanが最適化コンパイルを行う際の根拠となる。たとえば「この加算は常にSMI(Small Integer)同士で行われている」という情報があれば、TurboFanは整数加算に特化したマシンコードを生成できる。


4. TurboFan 最適化コンパイラ

4.1 TurboFanの概要

TurboFanはV8の最適化コンパイラであり、IgnitionのバイトコードとフィードバックベクタをもとにSSA(Static Single Assignment)形式の中間表現を構築し、数多くの最適化パスを適用した後、高効率なマシンコードを生成する。

4.2 最適化のトリガー条件

TurboFanが関数を最適化コンパイルする条件は以下の通りである。

// 最適化のトリガー例
function hotFunction(arr) {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}
 
// ループの反復回数やバイトコード実行回数が閾値を超えると
// TurboFanによる最適化が開始される
//
// 閾値の目安(V8内部で動的に調整される):
// - 関数の呼び出し回数
// - ループのバックエッジカウント(ループ1回転ごとにカウント)
// - OSR(On-Stack Replacement)の判定
//
// Node.jsで最適化の状況を確認:
// node --trace-opt --trace-deopt script.js

4.3 主要な最適化手法

TurboFanが適用する代表的な最適化手法を以下に列挙する。

インライン展開(Inlining):

// 最適化前
function square(x) {
  return x * x;
}
 
function sumOfSquares(a, b) {
  return square(a) + square(b);
}
 
// TurboFanによるインライン展開後(概念的)
function sumOfSquares_optimized(a, b) {
  return a * a + b * b;  // 関数呼び出しのオーバーヘッドを排除
}

インライン展開により、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の受け渡し、リターンアドレスの管理)が排除される。TurboFanは関数のサイズ、呼び出し頻度、呼び出しの深さなどを考慮してインライン展開の判断を行う。

定数畳み込み(Constant Folding):

// 最適化前
const TIMEOUT = 60 * 1000;  // 60秒をミリ秒に変換
 
// TurboFanによる最適化後
const TIMEOUT = 60000;  // コンパイル時に計算済み

デッドコード除去(Dead Code Elimination):

// 最適化前
function process(x) {
  const unused = x * 2;  // この結果は使われない
  return x + 1;
}
 
// TurboFanによる最適化後
function process_optimized(x) {
  return x + 1;  // 不要な計算を除去
}

ループ不変式の外出し(Loop-Invariant Code Motion):

// 最適化前
function processArray(arr, factor) {
  const results = [];
  for (let i = 0; i < arr.length; i++) {
    const multiplier = factor * 2;  // ループ内で毎回同じ計算
    results.push(arr[i] * multiplier);
  }
  return results;
}
 
// TurboFanによる最適化後(概念的)
function processArray_optimized(arr, factor) {
  const results = [];
  const multiplier = factor * 2;  // ループの外に移動
  const len = arr.length;         // 長さの取得もループ外へ
  for (let i = 0; i < len; i++) {
    results.push(arr[i] * multiplier);
  }
  return results;
}

型特殊化(Type Specialization):

// フィードバックベクタの情報:
// → add関数は常にSMI(Small Integer)引数で呼ばれている
 
function add(a, b) {
  return a + b;
}
 
// TurboFanが生成するマシンコード(概念的な疑似コード):
//
// 1. a が SMI か確認(型ガード)
// 2. b が SMI か確認(型ガード)
// 3. SMI同士の整数加算(1命令で完了)
// 4. オーバーフローチェック
// 5. 結果を返す
//
// もし型ガードが失敗したら → 脱最適化

4.4 脱最適化(Deoptimization)

TurboFanが生成した最適化コードは、型の前提条件に基づいている。この前提が実行時に崩れた場合、V8は脱最適化を行い、Ignitionのバイトコード実行に戻す。

// 脱最適化が起きるシナリオ
 
function add(a, b) {
  return a + b;
}
 
// Phase 1: SMI(整数)で呼び続ける → TurboFanが整数加算に最適化
for (let i = 0; i < 100000; i++) {
  add(i, i + 1);  // 常に整数
}
 
// Phase 2: 突然文字列を渡す → 脱最適化が発生!
add("hello", " world");
// → TurboFanの最適化コードは整数加算前提なので使えない
// → Ignitionのバイトコードに戻って文字列結合を実行
// → 再度プロファイリングを行い、新たな最適化を検討

脱最適化の種類:

脱最適化の分類
Eager Deopt型ガードの失敗など、即座に検出される
例: 整数を期待した箇所に文字列が来た
Lazy Deoptコードの実行後、副作用の処理中に検出
例: マップの変更が検出された
Soft Deopt最適化コードが非効率と判断された場合
例: 多態的な呼び出しサイトの検出

4.5 OSR(On-Stack Replacement)

通常の最適化は関数の次の呼び出し時から適用されるが、OSRは実行中のループの途中で最適化コードに切り替える技術である。

function longRunningLoop() {
  let sum = 0;
  for (let i = 0; i < 10000000; i++) {
    sum += i;
    // ループのバックエッジで最適化判定
    // 閾値を超えた時点で、ループ実行中に最適化コードに切り替え(OSR)
    // → ループ変数 i, sum の状態を最適化コードに引き継ぐ
  }
  return sum;
}
 
// この関数は1回しか呼ばれないが、ループ内で
// OSRにより最適化される
longRunningLoop();

OSRは長時間実行されるループに対して特に有効である。関数が1回しか呼ばれなくても、ループの反復回数が閾値を超えれば最適化が適用される。


5. Hidden Class(Maps)

5.1 Hidden Classの基本概念

JavaScriptのオブジェクトは動的であり、実行時にプロパティの追加・削除が自由にできる。しかし、この柔軟性はプロパティアクセスのパフォーマンスに悪影響を及ぼす。V8はこの問題をHidden Class(V8の内部用語ではMap)という仕組みで解決している。

Hidden Classはオブジェクトの「形状(Shape)」を記述するメタデータであり、以下の情報を含む:

  • プロパティの名前
  • プロパティのオフセット(メモリ上の位置)
  • プロパティの属性(writable、enumerable、configurable)
  • プロトタイプチェーンの参照

5.2 Hidden Classの遷移チェーン

オブジェクトにプロパティが追加されるたびに、新しいHidden Classが作成され、遷移チェーンが形成される。

Hidden Class 遷移チェーンの例
const point = {}; // Map M0 (空オブジェクト)
point.x = 10; // Map M0 → M1 遷移
point.y = 20; // Map M1 → M2 遷移
Map M0 Map M1 Map M2
┌─────────┐ ┌──────────────┐ ┌──────────────┐
(empty)───▶x: offset 0───▶x: offset 0
xyy: offset 1
└─────────┘ └──────────────┘ └──────────────┘
遷移情報は Map M0 に保存される:
M0.transitions = { "x" → M1 }
M1.transitions = { "y" → M2 }
別のオブジェクトが同じ順序でプロパティを追加すると
既存の遷移チェーンを再利用する:
const point2 = {}; // Map M0(同じ)
point2.x = 30; // Map M0 → M1(再利用)
point2.y = 40; // Map M1 → M2(再利用)
→ point と point2 は同じ Map M2 を共有!

5.3 Hidden Classの共有と分岐

同じ「形状」のオブジェクトはHidden Classを共有するが、プロパティの追加順序が異なると別のHidden Classが作成される。

// ケース1: 同じ順序 → Hidden Classを共有
const a = {};
a.x = 1;
a.y = 2;
 
const b = {};
b.x = 3;
b.y = 4;
// a と b は同じ Hidden Class
 
// ケース2: 異なる順序 → 別の Hidden Class
const c = {};
c.y = 2;  // まず y を追加
c.x = 1;  // 次に x を追加
// c は a, b とは異なる Hidden Class
 
// ケース3: オブジェクトリテラル → 最適化される
const d = { x: 1, y: 2 };
const e = { x: 3, y: 4 };
// d と e は同じ Hidden Class(リテラルの形状が同じ)
 
// ケース4: delete演算子 → Hidden Classが無効化
const f = { x: 1, y: 2 };
delete f.x;
// f は「遅いモード」(辞書モード)に切り替わる
// → Hidden Class の最適化が失われる

5.4 In-Object Properties vs. Backing Store

V8はオブジェクトのプロパティを2つの方法で格納する。

プロパティの格納方式
【In-Object Properties】
オブジェクト本体に直接格納される
┌──────────────────────────┐
Object Header
├── Map pointer
├── Properties pointer
├── Elements pointer
├── In-object prop 0 (x)← 直接アクセス可能
├── In-object prop 1 (y)← 直接アクセス可能
└── In-object prop 2 (z)← 直接アクセス可能
└──────────────────────────┘
→ 最も高速(固定オフセットで直接アクセス)
→ V8は初期プロパティ数を見積もってスペースを確保
【Backing Store(Properties配列)】
In-Objectスロットが不足した場合に使用
┌──────────────────────────┐ ┌──────────────┐
Object HeaderProperties
├── Map pointer├── prop 3
├── Properties pointer ──┼───▶├── prop 4
├── Elements pointer└── prop 5
├── In-object prop 0└──────────────┘
├── In-object prop 1
└── In-object prop 2
└──────────────────────────┘
→ やや遅い(間接参照が1回必要)
【辞書モード(Slow Properties)】
delete演算子使用後や、プロパティ数が非常に多い場合
→ ハッシュテーブルベースの格納
→ Hidden Class の最適化が無効
→ 最も遅い

5.5 Elements(配列要素)の格納

オブジェクトの名前付きプロパティとは別に、数値インデックスのプロパティ(配列要素)はElementsとして別の配列に格納される。

// Elements の種類(V8内部のElementsKind)
 
// PACKED_SMI_ELEMENTS: 全要素が小さな整数
const smiArray = [1, 2, 3, 4, 5];
 
// PACKED_DOUBLE_ELEMENTS: 浮動小数点数を含む
const doubleArray = [1.1, 2.2, 3.3];
 
// PACKED_ELEMENTS: オブジェクトや混合型
const mixedArray = [1, "two", { three: 3 }];
 
// HOLEY_SMI_ELEMENTS: 穴あき配列(整数)
const holeyArray = [1, , 3];  // インデックス1が空
 
// ElementsKind の遷移(一方向のみ、逆戻りしない!)
//
// PACKED_SMI_ELEMENTS
//     │
//     ├──→ PACKED_DOUBLE_ELEMENTS
//     │        │
//     │        ├──→ PACKED_ELEMENTS
//     │        │
//     │        └──→ HOLEY_DOUBLE_ELEMENTS ──→ HOLEY_ELEMENTS
//     │
//     └──→ HOLEY_SMI_ELEMENTS
//              │
//              ├──→ HOLEY_DOUBLE_ELEMENTS ──→ HOLEY_ELEMENTS
//              │
//              └──→ HOLEY_ELEMENTS
 
// 一度でも HOLEY になると、PACKED に戻ることはない
const arr = [1, 2, 3];       // PACKED_SMI_ELEMENTS
arr.push(4.5);               // → PACKED_DOUBLE_ELEMENTS(不可逆遷移)
arr.push("hello");           // → PACKED_ELEMENTS(不可逆遷移)

6. インラインキャッシュ(Inline Cache: IC)

6.1 インラインキャッシュの基本原理

インラインキャッシュはプロパティアクセスの高速化メカニズムである。JavaScriptのプロパティアクセス obj.x は、本来であれば以下のステップが必要となる:

  1. オブジェクトのHidden Classを取得
  2. Hidden Classのプロパティテーブルで "x" を検索
  3. オフセットを取得
  4. そのオフセットでメモリからプロパティ値を読み取る

インラインキャッシュはこの検索結果をキャッシュし、同じHidden Classのオブジェクトが来た場合はステップ2-3をスキップする。

function getX(obj) {
  return obj.x;  // ← このアクセス箇所にICが生成される
}
 
// 1回目の呼び出し: IC miss
// → Hidden Classを確認し、"x"のオフセットを検索
// → 結果をICにキャッシュ(Hidden Class → offset のペア)
const p1 = { x: 10, y: 20 };
getX(p1);
 
// 2回目以降: IC hit
// → Hidden Classがキャッシュと一致 → オフセットを直接使用
// → プロパティテーブルの検索をスキップ
const p2 = { x: 30, y: 40 };  // p1と同じHidden Class
getX(p2);  // 高速アクセス

6.2 ICの状態遷移(State Machine)

インラインキャッシュは以下の状態を持つ有限状態マシンとして動作する。

IC 状態遷移図
┌──────────────┐ 1つ目のMap ┌──────────────┐
Uninitialized────────────▶Monomorphic
(未初期化)(単態)
└──────────────┘ └──────┬───────┘
異なるMap
┌──────────────┐
Polymorphic
(多態: 2-4)
└──────┬───────┘
5つ以上のMap
┌──────────────┐
Megamorphic
(超多態: 5+)
└──────────────┘
パフォーマンス:
Monomorphic ≫ Polymorphic ≫ Megamorphic
(最速) (中程度) (最遅 / 最適化断念)

6.3 各状態の詳細

Monomorphic(単態)--- 最速:

// 常に同じHidden Classのオブジェクトが渡される
function getName(person) {
  return person.name;  // IC: monomorphic
}
 
// 全て同じ形状のオブジェクト
getName({ name: "Alice", age: 30 });
getName({ name: "Bob", age: 25 });
getName({ name: "Charlie", age: 35 });
// → ICは1つのHidden Classだけを記録 → 最速

Polymorphic(多態)--- 中程度:

// 2-4種類のHidden Classが混在
function getArea(shape) {
  return shape.area;  // IC: polymorphic
}
 
getArea({ area: 100, type: "circle" });     // Hidden Class A
getArea({ area: 200, width: 10, height: 20 }); // Hidden Class B
// → ICは2つのHidden Classを記録 → まだ十分高速
// → 線形検索でマッチするHidden Classを探す

Megamorphic(超多態)--- 最遅:

// 5種類以上のHidden Classが混在
function getValue(obj) {
  return obj.value;  // IC: megamorphic
}
 
getValue({ value: 1 });
getValue({ value: 2, a: 1 });
getValue({ value: 3, a: 1, b: 2 });
getValue({ value: 4, a: 1, b: 2, c: 3 });
getValue({ value: 5, a: 1, b: 2, c: 3, d: 4 });
getValue({ value: 6, x: 1 });
// → ICがmegamorphic状態 → キャッシュ無効
// → 毎回Hidden Classの検索が必要 → 遅い
// → TurboFanも型特殊化を断念

6.4 ICの種類

V8にはプロパティアクセス以外にも複数のIC種類がある。

IC種類 対象操作
LoadIC プロパティ読み取り obj.x
StoreIC プロパティ書き込み obj.x = 1
KeyedLoadIC 動的キーによる読み取り obj[key]
KeyedStoreIC 動的キーによる書き込み obj[key] = 1
CallIC 関数呼び出し obj.method()
CompareIC 比較演算 a === b
BinaryOpIC 二項演算 a + b

7. ガベージコレクション(GC)

7.1 V8のメモリ構造

V8のヒープメモリは複数の領域に分割されている。

V8 ヒープメモリ構造
┌────────────────────────────────────────────┐
New Space(新世代)
┌──────────────┬───────────────────┐
Semi-spaceSemi-space
(From)(To)
1-16 MB1-16 MB
└──────────────┴───────────────────┘
新しく生成されたオブジェクトが配置される
Minor GC(Scavenge)の対象
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
Old Space(旧世代)
┌──────────────────────────────────┐
Old Pointer Space
→ 他のオブジェクトへの参照を含む
├──────────────────────────────────┤
Old Data Space
→ 参照を含まないデータ(文字列等)
└──────────────────────────────────┘
2回のMinor GCを生き延びたオブジェクト
Major GC(Mark-Sweep-Compact)の対象
サイズ: --max-old-space-size で設定可能
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
Large Object Space
通常のページに収まらない巨大オブジェクト
個別にGC管理される
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
Code Space
JITコンパイルされたコードが配置される
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
Map Space
Hidden Class(Map)オブジェクトが配置される
└────────────────────────────────────────────┘

7.2 Minor GC(Scavenge)

新世代のGCはScavenge(Cheney's algorithm の変種)を使用する。

Scavenge アルゴリズムの流れ:

Step 1: 割り当て
From Space (active)To Space (inactive)
[A][B][C][D][E][ free ][ empty ]
Step 2: GCトリガー(From Space が満杯に近づく)
  → ルートオブジェクトから到達可能なオブジェクトを特定
  → A, C, E が生存、B, D は到達不能(ゴミ)

Step 3: コピー
From SpaceTo Space
[A][B][C][D][E][ free ][A'][C'][E'][ free ]
→ 生存オブジェクトをTo Spaceにコピー
  → コピー先のアドレスで参照を更新

Step 4: 入れ替え
To Space → 新 From SpaceFrom Space → 新 To Space
[A'][C'][E'][ free ][ empty (解放済み) ]
→ From と To を入れ替え
  → 旧From Spaceは丸ごと解放(個別のfreeが不要)

昇格(Promotion):
  → 2回のScavengeを生き延びたオブジェクトはOld Spaceに移動
  → 「長寿命オブジェクト」と判断

7.3 Major GC(Mark-Sweep-Compact)

旧世代のGCはMark-Sweep-Compactアルゴリズムを使用する。

Mark フェーズ:

ルートオブジェクト(スタック、グローバル変数等)から開始し、
到達可能な全オブジェクトを再帰的にマークする。

  Root Set
    │
    ├──▶ Object A (marked ✓)
    │      ├──▶ Object D (marked ✓)
    │      └──▶ Object E (marked ✓)
    │
    └──▶ Object B (marked ✓)
           └──▶ Object F (marked ✓)

  Object C (unmarked ✗) → 到達不能 → ゴミ
  Object G (unmarked ✗) → 到達不能 → ゴミ

Sweep フェーズ:

マークされていないオブジェクトのメモリを解放し、フリーリストに追加する。

Compact フェーズ:

メモリの断片化を解消するため、生存オブジェクトを移動して連続したメモリ領域にまとめる。これにより後続の割り当てが高速になる。

7.4 インクリメンタルマーキングとコンカレントGC

Major GCは旧世代全体を対象とするため、処理に時間がかかる。これによるメインスレッドの停止時間(Stop-the-world pause)を削減するため、V8は以下の手法を採用している。

【従来のStop-the-world GC】
JS実行 ────────┤ GC(長い停止) ├──── JS実行
               └─── 100ms+ ───┘

【インクリメンタルマーキング】
JS実行 ──┤GC├── JS ──┤GC├── JS ──┤GC├── JS実行
         5ms       5ms       5ms
→ GC作業を小さなステップに分割
→ JSの実行と交互に行う

【コンカレントGC】
メインスレッド: JS実行 ──────────────────────── JS実行
バックグラウンド:      ├── GC marking ──┤
→ GC作業の大部分をバックグラウンドスレッドで実行
→ メインスレッドの停止はほぼゼロに近づく

【パラレルGC】
メインスレッド:    ├── GC ──┤
ヘルパースレッド1: ├── GC ──┤
ヘルパースレッド2: ├── GC ──┤
→ 停止は必要だが、複数スレッドで並列処理して時間短縮

V8のOrinoco GCプロジェクトにより、これらの手法が組み合わされ、GCの停止時間は多くの場合数ミリ秒以下に抑えられている。


8. V8の配列最適化

8.1 ElementsKind の詳細

V8は配列の要素型に応じて内部表現を切り替える。適切なElementsKindを維持することで、配列操作のパフォーマンスが大幅に向上する。

// PACKED_SMI_ELEMENTS: 最も高速
// SMI = Small Integer(31ビット符号付き整数、64ビットプラットフォームでは32ビット)
const smiArray = [1, 2, 3, 4, 5];
// → 要素がunboxed(タグなし)で格納される
// → ポインタ追跡やタグチェックが不要
 
// PACKED_DOUBLE_ELEMENTS
const doubleArray = [1.1, 2.2, 3.3];
// → IEEE 754 倍精度浮動小数点数として格納
// → ヒープオブジェクトとしてのオーバーヘッドがない
 
// PACKED_ELEMENTS: 最も汎用的だが最も遅い
const objectArray = [{ x: 1 }, "hello", true];
// → 各要素がヒープオブジェクトへのポインタ
// → GCがポインタを追跡する必要がある

8.2 配列の最適化ガイドライン

// 良い: 型が統一された配列
const numbers = [1, 2, 3, 4, 5];  // PACKED_SMI_ELEMENTS
numbers.push(6);  // OK: SMIのまま
 
// 悪い: 型の混在で不可逆な遷移が発生
const bad = [1, 2, 3];           // PACKED_SMI_ELEMENTS
bad.push(4.5);                   // → PACKED_DOUBLE_ELEMENTS(不可逆)
bad.push("hello");               // → PACKED_ELEMENTS(不可逆)
// 元のPACKED_SMI_ELEMENTSには二度と戻らない
 
// 良い: 事前確保
const preallocated = new Array(1000);
// ただし HOLEY_SMI_ELEMENTS になる点に注意
 
// より良い: fill で初期化
const filled = new Array(1000).fill(0);
// PACKED_SMI_ELEMENTS(穴なし)
 
// 悪い: 穴あき配列
const holey = [1, , 3];  // HOLEY_SMI_ELEMENTS
// インデックス1のアクセス時にプロトタイプチェーンの検索が必要
// → PACKED に比べて遅い

9. パフォーマンス最適化の実践

9.1 型安定性の確保

V8が最も効率的に動作するのは、変数や関数の引数の型が安定している場合である。

// アンチパターン1: 同じ変数に異なる型を代入
function unstable() {
  let value = 42;       // SMI
  value = 3.14;         // → Double
  value = "hello";      // → String
  value = { x: 1 };    // → Object
  return value;
}
// → TurboFanが型特殊化できない → 最適化が困難
 
// 推奨パターン: 変数の型を一貫させる
function stable() {
  const intValue = 42;
  const floatValue = 3.14;
  const strValue = "hello";
  const objValue = { x: 1 };
  return objValue;
}

9.2 オブジェクト初期化のベストプラクティス

// アンチパターン2: 条件付きプロパティ追加
function createUser(name, email, isAdmin) {
  const user = { name, email };
  if (isAdmin) {
    user.role = "admin";        // Hidden Classが分岐
    user.permissions = ["all"];  // さらに分岐
  }
  return user;
}
// → isAdmin=true と isAdmin=false で異なるHidden Class
// → この関数を経由するオブジェクトのICがpolymorphicに
 
// 推奨パターン: 全プロパティを初期化
function createUserOptimized(name, email, isAdmin) {
  return {
    name,
    email,
    role: isAdmin ? "admin" : null,
    permissions: isAdmin ? ["all"] : null,
  };
}
// → 全オブジェクトが同じHidden Class → ICがmonomorphic
 
// 推奨パターン: クラスを使用
class User {
  constructor(name, email, isAdmin) {
    this.name = name;
    this.email = email;
    this.role = isAdmin ? "admin" : null;
    this.permissions = isAdmin ? ["all"] : null;
  }
}
// → コンストラクタで全プロパティ初期化 → 安定したHidden Class

9.3 関数のmonomorphic性を保つ

// アンチパターン: 多様な形状のオブジェクトを同じ関数に渡す
function processItem(item) {
  return item.name + ": " + item.value;
}
 
processItem({ name: "A", value: 1 });
processItem({ name: "B", value: 2, extra: true });       // 別のHidden Class
processItem({ name: "C", value: 3, x: 1, y: 2 });       // また別のHidden Class
processItem({ value: 4, name: "D" });                     // 順序違いで別のHidden Class
// → ICがmegamorphicに → パフォーマンス低下
 
// 推奨パターン: 統一された形状を使用
class Item {
  constructor(name, value) {
    this.name = name;
    this.value = value;
  }
}
 
processItem(new Item("A", 1));
processItem(new Item("B", 2));
processItem(new Item("C", 3));
// → 全て同じHidden Class → ICがmonomorphic → 最速

9.4 delete 演算子の回避

// アンチパターン: delete でプロパティを削除
const obj = { x: 1, y: 2, z: 3 };
delete obj.y;
// → オブジェクトが「辞書モード(slow mode)」に切り替わる
// → Hidden Class の最適化が完全に失われる
// → 以降のプロパティアクセスが全て遅くなる
 
// 推奨パターン: null や undefined を代入
const obj2 = { x: 1, y: 2, z: 3 };
obj2.y = undefined;
// → Hidden Class は維持される
// → プロパティアクセスの最適化は継続
 
// 推奨パターン: 新しいオブジェクトを作成
const { y, ...rest } = { x: 1, y: 2, z: 3 };
// rest = { x: 1, z: 3 }
// → 新しいオブジェクトは新しいHidden Classを持つが、
//   辞書モードにはならない

9.5 数値の型に関する注意

V8は数値を内部的に複数の表現で管理している。

// SMI(Small Integer): 最も効率的
// → 31ビット符号付き整数(32ビットプラットフォーム)
// → 32ビット符号付き整数(64ビットプラットフォーム)
// → タグ付きポインタとして即値で格納(ヒープ割り当て不要)
const smi = 42;
 
// HeapNumber: ヒープに割り当てられる浮動小数点数
// → SMIの範囲外の整数、または小数
// → ヒープオブジェクトとしてのオーバーヘッドがある
const heapNum = 3.14;
const bigInt = 2147483648;  // SMI範囲外
 
// 配列におけるSMI vs Doubleの影響
const smiArr = [1, 2, 3];           // PACKED_SMI_ELEMENTS(最速)
const doubleArr = [1, 2, 3.0];      // PACKED_DOUBLE_ELEMENTS
// 3.0 が含まれるだけでDouble配列になる
 
// 整数演算がオーバーフローすると型が変わる
let counter = 0;
for (let i = 0; i < 1000000; i++) {
  counter += i;
  // counter がSMIの範囲を超えた時点でHeapNumberに変更
  // → ループ内の加算が急に遅くなる可能性
}

10. V8のデバッグとプロファイリング

10.1 V8フラグ一覧

Node.jsやChrome(DevTools Protocol経由)で使用できるV8のデバッグフラグを以下にまとめる。

# 最適化の追跡
node --trace-opt script.js
# → TurboFanが最適化した関数を表示
 
# 脱最適化の追跡
node --trace-deopt script.js
# → 脱最適化が発生した箇所と理由を表示
 
# バイトコードの出力
node --print-bytecode script.js
# → Ignitionが生成したバイトコードを表示
 
# 特定の関数のバイトコードのみ出力
node --print-bytecode --print-bytecode-filter="functionName" script.js
 
# GCの追跡
node --trace-gc script.js
# → GCイベントの発生タイミングと所要時間を表示
 
# 詳細なGC情報
node --trace-gc-verbose script.js
 
# Hidden Class(Map)の遷移を追跡
node --trace-maps script.js
 
# ICの状態を追跡
node --trace-ic script.js
 
# TurboFanの最適化グラフを出力(Turbolizer用)
node --trace-turbo script.js
# → turbo-*.json ファイルが生成される
# → https://v8.github.io/tools/turbolizer/ で可視化

10.2 Chrome DevToolsでのV8分析

Chrome DevTools を使ったV8パフォーマンス分析:

1. Performance タブ
   → CPU Profile を記録
   → 関数ごとの実行時間を確認
   → ホットスポットの特定

2. Memory タブ
   → Heap Snapshot: ヒープの全オブジェクトを一覧
   → Allocation Timeline: メモリ割り当ての時系列変化
   → Allocation Sampling: 低オーバーヘッドなサンプリング

3. Console での確認
   → %HasFastProperties(obj)
      オブジェクトが高速プロパティ(Hidden Class)モードかを確認
      ※ --allow-natives-syntax フラグが必要

   → %OptimizeFunctionOnNextCall(fn)
      関数を次の呼び出し時に強制最適化
      ※ テスト用途。本番環境では使用しないこと

   → %GetOptimizationStatus(fn)
      関数の最適化状態を数値で返す

10.3 最適化状態の確認方法

// Node.jsで --allow-natives-syntax フラグを使用して確認
// ※ このフラグはV8の内部APIを公開するため、開発・テスト目的のみで使用
 
function testFunction(a, b) {
  return a + b;
}
 
// ウォームアップ
for (let i = 0; i < 100000; i++) {
  testFunction(i, i + 1);
}
 
// 最適化状態を確認
// %GetOptimizationStatus(testFunction) の返り値:
// 1 = 関数は最適化可能
// 2 = 関数は最適化されている
// 3 = 関数は常に最適化される
// 4 = 関数は最適化されていない
// 6 = 関数はベースラインコードの可能性

11. エッジケース分析

11.1 エッジケース1: try-catch による最適化への影響

以前のV8(Crankshaft時代)では、try-catch を含む関数は最適化対象から除外されていた。TurboFanではこの制限は大幅に緩和されたが、依然として注意が必要なケースが存在する。

// かつてのアンチパターン(Crankshaft時代)
// → try-catchがあるだけで関数全体が最適化されなかった
function oldPattern() {
  try {
    // ホットなコード
    for (let i = 0; i < 1000000; i++) {
      // 重い処理
    }
  } catch (e) {
    console.error(e);
  }
}
 
// TurboFan時代の現状:
// → try-catch自体は最適化を阻害しない
// → ただし、catch節内のコードは最適化されにくい
//   (例外発生は稀であるべきという前提)
 
// エッジケース: try-catch内での型の不安定さ
function parseJSON(str) {
  try {
    return JSON.parse(str);
    // 返り値の型がstring, number, object, array等、不定
    // → 呼び出し側のICがpolymorphicになりやすい
  } catch (e) {
    return null;
    // さらにnullも返り値に加わる
    // → 呼び出し側のICがさらに複雑に
  }
}
 
// 推奨: 返り値の型を統一する工夫
function parseJSONSafe(str) {
  try {
    const result = JSON.parse(str);
    return { success: true, data: result };
  } catch (e) {
    return { success: false, data: null };
  }
}
// → 常に同じ形状のオブジェクトを返す → Hidden Classが安定

11.2 エッジケース2: arguments オブジェクトのリーク

// arguments オブジェクトは特殊な振る舞いを持ち、
// 最適化に悪影響を与える場合がある
 
// アンチパターン: argumentsを他の関数に渡す(リーク)
function leakyFunction() {
  // arguments がクロージャに捕捉される → 最適化が困難
  return Array.prototype.slice.call(arguments);
}
 
// アンチパターン: argumentsを外部変数に代入
function badPattern() {
  const args = arguments;  // argumentsオブジェクトが「リーク」
  return function() {
    return args[0];  // クロージャ内でargumentsを参照
  };
}
 
// 推奨パターン: レストパラメータを使用
function goodPattern(...args) {
  // argsは通常の配列 → 最適化に問題なし
  return args.slice();
}
 
// 推奨パターン: ES2015+ のデストラクチャリング
function betterPattern(first, second, ...rest) {
  return [first, second, ...rest];
}

11.3 エッジケース3: with文とeval

// with文は V8 の最適化を完全に阻害する
// → スコープチェーンが動的になり、変数解決が静的にできない
 
// アンチパターン: with文
function withExample(obj) {
  with (obj) {
    // x が obj.x なのか外部スコープの x なのか
    // コンパイル時に判断できない
    return x + y;
  }
}
// → 関数全体が最適化対象から除外される可能性
// → strict mode では with文は構文エラー
 
// eval も同様の問題を引き起こす
function evalExample(code) {
  eval(code);
  // eval内で変数が宣言・変更される可能性
  // → 関数のスコープ全体が動的に
  // → Hidden Classやスコープの最適化が不可能
}
 
// 間接的なeval(グローバルスコープで実行)は影響が限定的
const indirectEval = eval;
indirectEval("console.log('hello')");
// → 呼び出し元の関数スコープには影響しない

12. 比較表

12.1 V8 vs 他のJavaScriptエンジン

特性 V8 (Chrome/Node) SpiderMonkey (Firefox) JavaScriptCore (Safari)
開発元 Google Mozilla Apple
インタプリタ Ignition(レジスタベース) Warp Baseline LLInt(Low Level Interpreter)
最適化コンパイラ TurboFan Ion(Warp) DFG + FTL(B3)
JIT段階数 2段階(Ignition → TurboFan) 3段階(Baseline → IC → Ion) 4段階(LLInt → Baseline → DFG → FTL)
GC方式 世代別 Mark-Sweep-Compact 世代別 Incremental GC 世代別 Mark-Sweep(Riptide)
Hidden Class名称 Map Shape Structure
IC実装 フィードバックベクタ CacheIR Polymorphic IC
WebAssembly Liftoff + TurboFan Baseline + Ion BBQ + OMG
使用ランタイム Chrome, Node.js, Deno, Electron Firefox, SpiderNode Safari, Bun

12.2 最適化レベルごとの比較

特性 Ignition (バイトコード) TurboFan (最適化済み)
起動速度 高速(バイトコード生成は軽量) 遅い(コンパイルに時間がかかる)
実行速度 中程度(インタプリタ実行) 高速(ネイティブコード実行)
メモリ使用量 小(バイトコードはコンパクト) 大(マシンコードはサイズが大きい)
コンパイル時間 短い 長い(最適化パス多数)
型特殊化 なし(汎用バイトコード) あり(フィードバックベクタに基づく)
脱最適化 不要(汎用コード) 必要な場合がある(型前提が崩れた時)
適用対象 全関数(初回実行) ホットスポットのみ
デバッグ容易性 高い(バイトコードと1:1対応) 低い(インライン展開等で元コードと乖離)

13. メモリリーク対策

13.1 典型的なメモリリークパターン

V8のGCは到達可能なオブジェクトを自動的に管理するが、プログラマの意図しない参照が残ることで「メモリリーク」が発生する。

// パターン1: イベントリスナーの解除忘れ
class Component {
  constructor() {
    this.data = new Array(10000).fill("large data");
    // イベントリスナーを登録
    window.addEventListener("resize", this.handleResize);
  }
 
  handleResize = () => {
    console.log(this.data.length);
  };
 
  destroy() {
    // リスナーを解除しないと、
    // このComponentインスタンスはGCされない
    // → this.data の巨大配列もリークする
  }
}
 
// 修正版
class ComponentFixed {
  constructor() {
    this.data = new Array(10000).fill("large data");
    this.handleResize = this.handleResize.bind(this);
    window.addEventListener("resize", this.handleResize);
  }
 
  handleResize() {
    console.log(this.data.length);
  }
 
  destroy() {
    window.removeEventListener("resize", this.handleResize);
    this.data = null;  // 明示的に参照を切る
  }
}
// パターン2: クロージャによる意図しない参照保持
function createProcessor() {
  const hugeData = new Array(1000000).fill("x");  // 巨大なデータ
 
  // この関数はhugeDataへの参照を保持し続ける
  return function process(input) {
    // hugeDataを直接使っていなくても、
    // 同じスコープの変数なのでクロージャが参照を保持
    return input.toUpperCase();
  };
}
 
const processor = createProcessor();
// → processor が存在する限り hugeData はGCされない
 
// 修正版: スコープを分離
function createProcessorFixed() {
  const hugeData = new Array(1000000).fill("x");
  const result = processHugeData(hugeData);
 
  // hugeDataを使う処理を完了させてから
  // クロージャを返す
  return function process(input) {
    return input.toUpperCase() + result;
  };
}
// パターン3: タイマーのクリア忘れ
function startPolling() {
  const data = { buffer: new ArrayBuffer(1024 * 1024) }; // 1MB
 
  const intervalId = setInterval(() => {
    // data への参照が維持される
    console.log(data.buffer.byteLength);
  }, 1000);
 
  // intervalIdを返さないと、クリアする手段がない
  // → data は永久にGCされない
}
 
// 修正版
function startPollingFixed() {
  const data = { buffer: new ArrayBuffer(1024 * 1024) };
 
  const intervalId = setInterval(() => {
    console.log(data.buffer.byteLength);
  }, 1000);
 
  // クリーンアップ関数を返す
  return function stop() {
    clearInterval(intervalId);
    // intervalIdをクリアすれば、コールバックへの参照が消え、
    // data もGC対象になる
  };
}

13.2 WeakRef と FinalizationRegistry

ES2021で導入されたWeakRefとFinalizationRegistryは、メモリリーク対策の強力なツールである。

// WeakRef: 弱参照(GCを阻害しない参照)
class Cache {
  constructor() {
    this.cache = new Map();
  }
 
  set(key, value) {
    // WeakRefで値を保持 → GCが必要なら回収可能
    this.cache.set(key, new WeakRef(value));
  }
 
  get(key) {
    const ref = this.cache.get(key);
    if (ref) {
      const value = ref.deref();  // 弱参照を解決
      if (value !== undefined) {
        return value;  // まだ生きている
      }
      // GCされていた → キャッシュエントリを削除
      this.cache.delete(key);
    }
    return undefined;
  }
}
 
// FinalizationRegistry: オブジェクトがGCされた時のコールバック
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with key "${heldValue}" was garbage collected`);
  // クリーンアップ処理(外部リソースの解放など)
});
 
function createManagedObject(key) {
  const obj = { data: new Array(10000) };
  registry.register(obj, key);  // objがGCされたらkeyを引数にコールバック
  return obj;
}
 
// WeakMap: キーが弱参照(キーオブジェクトがGCされるとエントリ自動削除)
const metadata = new WeakMap();
 
function attachMetadata(obj, meta) {
  metadata.set(obj, meta);
  // obj がどこからも参照されなくなれば、
  // WeakMapのエントリも自動的に削除される
}

14. WebAssembly と V8

14.1 V8のWebAssembly実行パイプライン

V8はWebAssemblyも実行でき、専用のコンパイルパイプラインを持っている。

V8 WebAssembly パイプライン
.wasm バイナリ
┌──────────────┐
ValidationWasmバイナリの検証
└──────┬───────┘
├──────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
LiftoffTurboFan
(Baseline)(Optimizing)
高速コンパイル高品質最適化
低品質コード遅いコンパイル
└──────┬───────┘ └──────┬───────┘
▼ ▼
即座に実行開始 TurboFanの完了後に
(レイテンシ重視) Liftoffコードを置換
(スループット重視)
Lazy Compilation:
→ 関数が初めて呼ばれた時にコンパイル
→ 未使用の関数はコンパイルしない
→ 起動時間の短縮に貢献

14.2 JavaScript と WebAssembly の相互運用

// WebAssemblyモジュールの読み込みと実行
async function loadWasm() {
  const response = await fetch("module.wasm");
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module, {
    env: {
      // JavaScriptからWasmに渡す関数
      log: (value) => console.log(value),
    },
  });
 
  // Wasmのエクスポート関数を呼び出す
  const result = instance.exports.add(10, 20);
  // → JavaScript と Wasm の呼び出しにはオーバーヘッドがある
  // → 頻繁な小さな呼び出しは避け、まとまった処理を委譲する
}
 
// パフォーマンスの考慮点:
// ・JS → Wasm 呼び出し: 約10-20ns のオーバーヘッド
// ・Wasm → JS 呼び出し: より大きなオーバーヘッド(型変換等)
// ・大きなデータ: SharedArrayBufferを使ったゼロコピー転送が理想
// ・小さな関数の頻繁な呼び出しは JS 内で完結させた方が速い

FAQ

Q1: V8のHidden ClassとInline Cachingはどのように連携して動作するのか?

A: Hidden ClassとInline Caching(IC)は密接に連携してプロパティアクセスを高速化する。

Hidden Classの役割:

  • オブジェクトの「形状」を記述するメタデータ
  • プロパティ名とメモリオフセットのマッピングを保持
  • 同じ形状のオブジェクトは同じHidden Classを共有

Inline Cachingの役割:

  • プロパティアクセス箇所ごとに最適化情報をキャッシュ
  • Hidden Classとオフセットのペアを記憶
  • 次回の同じアクセスで検索をスキップ

連携の具体例:

function getX(obj) {
  return obj.x;  // ← このアクセス箇所にICが生成される
}
 
// 1回目の呼び出し: IC miss
const p1 = { x: 10, y: 20 };
getX(p1);
// 1. p1のHidden Class(Map M1)を取得
// 2. M1のプロパティテーブルで "x" を検索
// 3. オフセット0を発見
// 4. ICにキャッシュ: { Map: M1, Property: "x", Offset: 0 }
 
// 2回目の呼び出し: IC hit
const p2 = { x: 30, y: 40 };  // p1と同じHidden Class M1
getX(p2);
// 1. p2のHidden ClassがM1であることを確認
// 2. ICのキャッシュから直接オフセット0を使用
// 3. プロパティテーブル検索をスキップ → 高速化

状態遷移:

Uninitialized (未初期化)
    ↓ 1つ目のHidden Class
Monomorphic (単態) --- 常に同じHidden Class → 最速
    ↓ 異なるHidden Class
Polymorphic (多態) --- 2-4種類のHidden Class → 中速
    ↓ 5種類以上のHidden Class
Megamorphic (超多態) --- キャッシュ無効 → 最遅

最適化のポイント:

  • 同じ形状のオブジェクトを使い続ける → Monomorphic状態を維持
  • プロパティの追加順序を統一する → Hidden Classを共有
  • オブジェクトリテラルを使う → 初期化時に形状が確定

Q2: TurboFanのJIT最適化を妨げるコードパターンは何か?

A: 以下のパターンがTurboFanの最適化を阻害または無効化する。

1. 型の不安定性 --- 最適化の最大の敵

// アンチパターン: 変数の型が頻繁に変わる
function unstable(a, b) {
  return a + b;
}
 
unstable(1, 2);        // SMI + SMI
unstable(1.5, 2.5);    // Double + Double
unstable("a", "b");    // String + String
unstable({}, {});      // Object + Object
 
// → TurboFanが型特殊化できない
// → 汎用的な(遅い)加算コードを生成
// → 脱最適化のリスクが高い

推奨パターン:

// 整数専用関数
function addInt(a, b) {
  return (a | 0) + (b | 0);  // ビット演算で整数を強制
}
 
// 浮動小数点専用関数
function addFloat(a, b) {
  return +a + +b;  // 単項プラス演算子でNumber型を強制
}

2. Hidden Classの分岐 --- IC状態の悪化

// アンチパターン: 条件付きプロパティ追加
function createConfig(enableCache) {
  const config = { baseUrl: "/" };
  if (enableCache) {
    config.cache = true;  // Hidden Classが分岐
  }
  return config;
}
 
// → 2つの異なるHidden Classが生成される
// → この関数を使う箇所のICがPolymorphicに

推奨パターン:

function createConfig(enableCache) {
  return {
    baseUrl: "/",
    cache: enableCache || null,  // 常に全プロパティを初期化
  };
}

3. delete演算子 --- 辞書モードへの転落

// アンチパターン: deleteでプロパティ削除
const obj = { x: 1, y: 2, z: 3 };
delete obj.y;
 
// → オブジェクトが「slow mode(辞書モード)」に
// → Hidden Classの最適化が完全に失われる
// → 以降のアクセスがハッシュテーブル検索になる

推奨パターン:

// undefinedを代入
obj.y = undefined;
 
// または新しいオブジェクトを作成
const { y, ...newObj } = obj;

4. 配列の穴(Holey Arrays)

// アンチパターン: 穴あき配列
const arr = [1, 2, , 4];  // インデックス2が空
 
// → HOLEY_SMI_ELEMENTS に遷移
// → アクセス時にプロトタイプチェーンの検索が必要
// → PACKED配列より遅い

推奨パターン:

const arr = [1, 2, 0, 4];  // 穴を埋める
// または
const arr = new Array(4).fill(0);
arr[0] = 1;
arr[1] = 2;
arr[3] = 4;

5. arguments オブジェクトのリーク

// アンチパターン: argumentsを外部に公開
function leaky() {
  const args = arguments;
  return function() { return args[0]; };
}
 
// → クロージャがargumentsを捕捉
// → 最適化が困難

推奨パターン:

function optimized(...args) {
  return function() { return args[0]; };
}

6. 評価不能な動的コード

// アンチパターン: eval、with文
function dynamic(code) {
  eval(code);  // スコープが動的になる
}
 
// → 変数解決が静的にできない
// → 関数全体が最適化対象外

7. 巨大な関数 --- インライン展開の失敗

// アンチパターン: 1000行の巨大関数
function huge() {
  // ... 大量のコード ...
}
 
// → TurboFanがインライン展開できない
// → 呼び出しオーバーヘッドが残る

推奨パターン:

// 小さな関数に分割(10-50行が目安)
function small1() { /* ... */ }
function small2() { /* ... */ }

8. try-catch内での型の不安定さ

// アンチパターン: try-catch内で複数の型を返す
function parse(str) {
  try {
    return JSON.parse(str);  // Object, Array, String, Number等
  } catch (e) {
    return null;  // さらにnull
  }
}
 
// → 返り値の型が多態的
// → 呼び出し側のICがMegamorphicに

推奨パターン:

function parse(str) {
  try {
    return { success: true, data: JSON.parse(str) };
  } catch (e) {
    return { success: false, data: null };
  }
}
// → 常に同じ形状のオブジェクトを返す

Q3: V8以外の主要JavaScriptエンジン(SpiderMonkey、JavaScriptCore)との違いは何か?

A: 主要3エンジンはそれぞれ異なる設計思想と最適化戦略を持つ。

1. アーキテクチャの違い

特性 V8 (Chrome/Node) SpiderMonkey (Firefox) JavaScriptCore (Safari/Bun)
開発元 Google Mozilla Apple
JIT段階数 2段階 3段階 4段階
インタプリタ Ignition(レジスタベース) Warp Baseline LLInt(Low Level Interpreter)
ベースラインコンパイラ なし(Ignition直接) Baseline Interpreter Baseline JIT
最適化コンパイラ TurboFan Ion(Warp) DFG + FTL(B3/Air)
起動戦略 高速起動重視 バランス型 段階的最適化重視

V8の戦略:

  • Ignition(バイトコード)→ TurboFan(最適化)の2段階
  • 起動速度を優先:バイトコードの生成が非常に高速
  • メモリ効率:バイトコードはコンパクト

SpiderMonkeyの戦略:

  • Baseline Interpreter → IC Stub → Ion の3段階
  • CacheIR(Inline Cache IR)による柔軟なIC生成
  • WebAssembly最適化に注力(Firefox Reality等)

JavaScriptCoreの戦略:

  • LLInt → Baseline JIT → DFG → FTL の4段階
  • 長時間実行を想定:最も多段階の最適化
  • FTL(Faster Than Light)は LLVM B3 バックエンド使用
  • Safari等でのバッテリー効率を重視

2. Hidden Class(形状管理)の違い

エンジン 名称 特徴
V8 Map 遷移チェーンをMapに保存。Transition Treeを構築
SpiderMonkey Shape Shape Lineageシステム。ShapeTableで高速検索
JavaScriptCore Structure Structure IDによる識別。Property Tableを共有

V8のMap:

// プロパティ追加順序が重要
const obj1 = {};
obj1.x = 1;  // Map M0 → M1
obj1.y = 2;  // Map M1 → M2
 
// 順序が違うと別のMap
const obj2 = {};
obj2.y = 2;  // Map M0 → M3
obj2.x = 1;  // Map M3 → M4

SpiderMonkeyのShape:

  • BaseShapeとShapeの2層構造
  • プロトタイプ情報をBaseShapeに分離
  • Shapeの共有率がやや高い

JavaScriptCoreのStructure:

  • Structure IDによる高速な等価性チェック
  • Inline Cacheで Structure IDを直接比較
  • Property Tableを複数のStructureで共有可能

3. ガベージコレクションの違い

エンジン 新世代GC 旧世代GC 並行/並列処理
V8 Scavenge(Cheney's) Mark-Sweep-Compact Concurrent Marking, Parallel Scavenging
SpiderMonkey Nursery(Generational) Incremental Mark-Sweep Incremental GC, Parallel Marking
JavaScriptCore Eden(Generational) Full GC(Riptide) Concurrent GC, DFG Safepoints

V8 Orinoco GC:

  • Concurrent Marking:マーキングをバックグラウンドで実行
  • Parallel Scavenging:新世代GCを並列化
  • Idle-time GC:ブラウザのアイドル時間にGC実行

SpiderMonkey:

  • Incremental GC:GCを細かく分割してStop-the-worldを削減
  • Compacting GC:メモリ断片化を積極的に解消
  • Background Sweeping:スイープをバックグラウンド化

JavaScriptCore Riptide:

  • Constraint-based GC:制約ベースのマーキング
  • DFG Safepoint:最適化コード実行中の安全なGCポイント
  • Incremental Marking:少しずつマーキング

4. パフォーマンス特性の違い

ベンチマーク別の傾向(一般的な傾向):

ベンチマーク種別 V8 SpiderMonkey JavaScriptCore
起動速度 ◎ 最速 ○ 中程度 △ やや遅い
短時間実行 ◎ 優秀 ○ 良好 ○ 良好
長時間実行 ○ 良好 ○ 良好 ◎ 最も最適化される
メモリ効率 ◎ バイトコードコンパクト ○ 中程度 △ 多段階JITでメモリ消費
WebAssembly ◎ Liftoff高速 ◎ Ion最適化優秀 ○ 良好

V8の強み:

  • Node.js、Chrome拡張機能など起動頻度の高いワークロード
  • バイトコードのコンパクトさによるメモリ節約
  • TurboFanの強力な最適化(Speculative Optimization)

SpiderMonkeyの強み:

  • asm.js、WebAssembly最適化(Firefoxのゲーム実行等)
  • CacheIRによる柔軟なIC最適化
  • Incremental GCによる応答性

JavaScriptCoreの強み:

  • Safari等での長時間実行(FTLによる高度な最適化)
  • バッテリー効率(モバイルデバイス)
  • 4段階JITによる段階的な最適化

5. 開発者ツールの違い

V8:

  • node --trace-opt:最適化の追跡
  • node --trace-deopt:脱最適化の追跡
  • Turbolizer:TurboFanのIR可視化ツール
  • --allow-natives-syntax:内部API公開

SpiderMonkey:

  • --ion-eager:Ion最適化を即座に適用
  • --baseline-eager:Baseline JITを即座に適用
  • Firefox DevTools:詳細なプロファイラ

JavaScriptCore:

  • --useConcurrentJIT=false:並行JITを無効化
  • Safari Web Inspector:タイムラインプロファイラ
  • --dumpDisassembly:JITコードの逆アセンブル

6. 使い分けの指針

V8を選ぶべき場合:

  • Node.js、Deno、Electron等のサーバー/デスクトップアプリ
  • 起動速度が重要なCLIツール
  • Chrome拡張機能

SpiderMonkeyを選ぶべき場合:

  • Firefox拡張機能(必須)
  • WebAssemblyヘビーなアプリケーション
  • asm.js互換コード

JavaScriptCoreを選ぶべき場合:

  • Safari対応が必須のWebアプリ
  • iOS/macOSネイティブアプリ(必須)
  • Bun(新しいJavaScriptランタイム)

結論:

  • V8: 起動速度とメモリ効率のバランスが優秀。Node.js エコシステムの標準
  • SpiderMonkey: WebAssembly最適化に強み。Firefox専用機能に必須
  • JavaScriptCore: 長時間実行での最適化が優秀。Apple エコシステムの標準

実際の開発では、ブラウザ環境ではエンジンの選択はユーザーに依存するため、すべてのエンジンで良好に動作するコード(型安定性、Hidden Class共有等)を書くことが重要である。


まとめ

V8エンジンの内部構造と最適化メカニズムをまとめる。

重要概念の対応表

概念 役割 最適化への影響 開発者の制御
パーサー ソースコード → AST変換 Lazy Parsingで起動高速化 IIFE パターンでEager Parsingを誘導
Ignition AST → バイトコード生成・実行 フィードバックベクタ収集 制御不可(型安定性で間接的に影響)
TurboFan バイトコード → 最適化コンパイル 型特殊化、インライン展開等 型安定性、monomorphic呼び出しで支援
Hidden Class オブジェクト形状の記述 プロパティアクセス高速化 初期化順序の統一、deleteの回避
Inline Cache アクセス箇所ごとの最適化 Monomorphic → 最速 同一形状オブジェクトの使用
GC メモリ自動管理 Stop-the-world時間削減 参照の早期解放、WeakRef活用
ElementsKind 配列の内部表現 型統一で高速化 穴なし、型統一配列の使用

V8最適化の3つのキーポイント

1. 型の安定性を維持する

// ✅ Good: 型が一貫している
function addNumbers(a, b) {
  return (a | 0) + (b | 0);  // 整数に強制
}
 
// ❌ Bad: 型が変わる
function addAny(a, b) {
  return a + b;  // 整数、浮動小数点、文字列が混在
}

2. Hidden Classを共有する

// ✅ Good: 全オブジェクトが同じ形状
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}
 
// ❌ Bad: オブジェクトごとに形状が異なる
function createPoint(x, y, hasZ) {
  const p = { x, y };
  if (hasZ) p.z = 0;  // Hidden Class分岐
  return p;
}

3. Inline Cacheをmonomorphicに保つ

// ✅ Good: 常に同じHidden Classのオブジェクトを処理
function process(items) {
  for (const item of items) {
    console.log(item.name);  // IC: monomorphic
  }
}
const items = [
  new Item("A"),
  new Item("B"),
  new Item("C"),
];
 
// ❌ Bad: 異なるHidden Classが混在
const mixed = [
  { name: "A" },
  { name: "B", value: 1 },
  { value: 2, name: "C" },  // 順序違い
];
process(mixed);  // IC: megamorphic

パフォーマンス診断のチェックリスト

  • node --trace-optで最適化状況を確認した
  • node --trace-deoptで脱最適化の原因を特定した
  • Chrome DevToolsのPerformanceタブでホットスポットを特定した
  • Memory SnapshotでHidden Classの分岐を確認した
  • --trace-icでIC状態(monomorphic/polymorphic/megamorphic)を確認した
  • 配列のElementsKindが意図通りか確認した(SMI/Double/Objectの遷移)
  • delete演算子を使っていないか確認した
  • argumentsオブジェクトをリークしていないか確認した

V8の進化の方向性

V8は継続的に進化しており、以下の方向性で改善が続いている。

  • 起動速度の向上 --- Lazy Parsingの改善、バイトコードキャッシュ
  • メモリ効率 --- Pointer Compression(64bit環境で32bitポインタ使用)
  • GCの低レイテンシ化 --- Concurrent Marking、Incremental Compaction
  • WebAssembly統合 --- Liftoff(高速ベースラインコンパイラ)、TurboFan最適化
  • モダンJS機能の最適化 --- async/await、Optional Chaining、Nullish Coalescing
  • セキュリティ強化 --- Spectre/Meltdown対策、Sandbox強化

V8の内部を理解することで、「なぜこのコードが速いのか」「なぜ遅いのか」を論理的に説明でき、根拠のあるパフォーマンス改善が可能になる。


次に読むべきガイド

V8エンジンの理解を深めたら、次は実行環境でのイベント駆動モデルを学習する。

推奨学習パス

  1. イベントループ(ブラウザ) 【次のステップ】

    • V8のタスク実行と非同期処理の統合
    • マクロタスク、マイクロタスク、レンダリングのタイミング
    • requestAnimationFrame、setTimeout、Promiseの実行順序
  2. イベントループ(Node.js)

    • libuv統合によるNode.js固有のイベントループ
    • フェーズごとの処理(timers、I/O callbacks、poll等)
    • process.nextTick vs setImmediate
  3. WebWorker

    • 別スレッドでのV8インスタンス実行
    • メインスレッドとの通信(postMessage)
    • SharedArrayBufferによる共有メモリ
  4. メモリ管理とパフォーマンス

    • V8のGC詳細とチューニング
    • メモリリークの検出と修正
    • Chrome DevToolsを使った実践的プロファイリング

関連ガイド

  • Chromeブラウザアーキテクチャ --- V8がどのようにレンダリングエンジンと連携するか
  • JavaScriptコア仕様 --- V8が実装するECMAScript仕様の詳細
  • TypeScript型システム --- 型安定性を静的に保証する方法

参考文献

公式ドキュメント・ブログ

  1. V8 Official Blog https://v8.dev/blog V8チームによる最新機能、最適化技術、パフォーマンス改善の解説。Hidden Class、TurboFan、GC改善などの詳細な技術記事が豊富。

  2. V8 Documentation https://v8.dev/docs V8の公式ドキュメント。ビルド方法、デバッグフラグ、埋め込み方法などの実践的情報。

  3. Chrome DevTools Documentation https://developer.chrome.com/docs/devtools/ Chrome DevToolsの公式ドキュメント。Performance、Memory、Profilerタブの使い方。V8のパフォーマンス分析に必須。

  4. Node.js Performance Measurement APIs https://nodejs.org/api/perf_hooks.html Node.jsのパフォーマンス計測API。V8のGCイベント、タイミング情報の取得方法。

技術記事・解説

  1. "A tour of V8: Full Compiler" --- V8 Blog https://v8.dev/blog/full-compiler V8の初期コンパイラ(Full-Codegen)の解説。現在のIgnitionとの比較に有用。

  2. "Ignition and TurboFan: V8's new interpreter and optimizing compiler" https://v8.dev/blog/ignition-interpreter Ignition導入の背景とTurboFanとの連携。V8の現行パイプラインの決定版解説。

  3. "Fast properties in V8" --- V8 Blog https://v8.dev/blog/fast-properties Hidden Class(Map)、In-Object Properties、Backing Storeの詳細解説。プロパティアクセス最適化の必読記事。

  4. "V8 Garbage Collection" --- V8 Blog https://v8.dev/blog/trash-talk Orinoco GCプロジェクトの解説。Scavenge、Mark-Sweep-Compact、Concurrent Markingの仕組み。

書籍

  1. "JavaScript: The Definitive Guide, 7th Edition" --- David Flanagan JavaScriptの包括的リファレンス。V8の動作原理を理解する前提知識として最適。

  2. "High Performance Browser Networking" --- Ilya Grigorik ブラウザのネットワーク、レンダリング、JavaScriptエンジンの連携を解説。V8をブラウザ環境で理解するために有用。

ツール・可視化

  1. Turbolizer https://v8.github.io/tools/turbolizer/ TurboFanの最適化グラフを可視化するツール。node --trace-turboで生成したJSONファイルを読み込んで、IR(中間表現)の変換過程を視覚的に確認できる。

  2. V8 Heap Snapshot Visualizer Chrome DevTools内蔵。Heap Snapshotを取得してオブジェクトの参照関係、Hidden Class、メモリリークを分析。

コミュニティ・ディスカッション

  1. V8 GitHub Repository https://github.com/v8/v8 V8のソースコード。Issue、Pull Request、Discussionで最新の議論を追える。

  2. Chromium Blog https://blog.chromium.org/ Chromiumプロジェクト全体のブログ。V8以外のレンダリングエンジン(Blink)との連携も理解できる。

これらのリソースを活用して、V8エンジンの理解を深め、実践的なパフォーマンス最適化スキルを習得されたい。