289 Views
October 21, 26
スライド概要
Ginza.js #12 の発表のスライドです
Ginza.js #12 / 2026.10.19 V8の最適化を覗いてみよう! 綺麗なコードで⾼速化……!? Katsumi Kobayashi
⼩林 克海 所属 株式会社プレイド Core Platform(基盤) 新卒2年⽬ 早稲⽥⼤学基幹理⼯学研究科表現⼯学専攻 橋⽥研究室(HCI) 修⼠2年 twitter: @takabayap JS歴 12年⽬(中学1年⽣〜)
JavaScript は⾃由だ‼
こんな Array も書ける
JavaScript
const a = [1, 2, 3];
a.push(0.5);
a.push("hello");
a.push({ x: 1 });
異なる種類の値を混在できる
こんな Array も書ける JavaScript const a = [1, 2, 3]; a.label = "hello"; a["3"] = 4; a[10] = 5; a.length; // 11 a["3"] と a[3] は同じ 10番への代⼊で length は11
こんな Array も書ける JavaScript const a = [1, 2, 3]; a.label = "hello"; a["3"] = 4; a[10] = 5; a.length; // 11 console.log(JSON.stringify(a)); // [1,2,3,4,null,null,null, null,null,null,5] a["3"] と a[3] は同じ 10番への代⼊で length は11
こんな Array も書ける JavaScript const a = [1, 2, 3]; a.label = "hello"; a["3"] = 4; a[10] = 5; でも速い! a.length; // 11 console.log(JSON.stringify(a)); // [1,2,3,4,null,null,null, null,null,null,5] a["3"] と a[3] は同じ 10番への代⼊で length は11
速度⽐較 n-body / 50,000,000 steps / 単⼀スレッド 相対速度 C = 1 CPU時間 C (gcc) 5.54 s Go 9.64 s Node.js 9.64‒9.99 s Python 3 528.16 s 0 0.25 0.5 Benchmarks Game / n-body #8 / 速度 = CのCPU時間 ÷ 各⾔語のCPU時間 0.75 1
速さを⽀える仕組み
実⾏環境と JavaScript エンジン 実⾏環境 JavaScript エンジン Chrome V8 Node.js V8 Safari JavaScriptCore Bun JavaScriptCore
実⾏環境と JavaScript エンジン 実⾏環境 JavaScript エンジン Chrome V8 Node.js V8 Safari JavaScriptCore こっちも速いよ! Bun JavaScriptCore
TypeScript
type Item = number | string | { x: number };
const a: Item[] = [1, 2, 3];
a.push(0.5);
a.push("hello");
a.push({ x: 1 });
JavaScript
const a = [1, 2, 3];
a.push(0.5);
a.push("hello");
a.push({ x: 1 });
消えちゃう!
JavaScript
const a = [1, 2, 3];
V8 実⾏時に分類
a.push(0.5);
a.push("hello");
a.push({ x: 1 });
V8 の最適化 01 02 03 値と形を観測 規則性を記録 処理を特化
Elements Kind 値の表現 SMI / DOUBLE / ELEMENTS ⽳の扱い PACKED / HOLEY
SMI: ⼩さい整数を格納する JavaScript const a = [1, 2, 3]; // PACKED_SMI_ELEMENTS 整数をそのまま格納する
DOUBLE: 数値を倍精度で格納する JavaScript const a = [1, 2, 3]; a.push(0.5); // PACKED_DOUBLE_ELEMENTS 64bit の数値を直接格納
汎⽤のELEMENTS: いろいろな値を持てる JavaScript const a = [1, 2, 3]; a.push(0.5); a.push("hello"); // PACKED_ELEMENTS 値や保存先のアドレスを格納
PACKEDとHOLEY: 途中に⽳があるか JavaScript const a = [1, 2, 3]; // PACKED_SMI_ELEMENTS a[5] = 6; // HOLEY_SMI_ELEMENTS ⽳を確認する処理が増える
要素の変化に応じて Elements Kind が遷移する ⼩さい整数 倍精度の数値 汎⽤の値 PACKED_SMI_ELEMENTS PACKED_DOUBLE_ELEMENTS PACKED_ELEMENTS HOLEY_DOUBLE_ELEMENTS HOLEY_ELEMENTS ⽳を作る HOLEY_SMI_ELEMENTS V8 Blog “Elements kinds in V8”
内部表現による実⾏時間の差
65,536個の整数を⾜す時間を⽐べる 01 02 03 同じ整数列を⽤意 ウォームアップ 実⾏時間を計測 ⼩数・⽂字列を⼀度代⼊ または要素を⼀度削除 計測前に同じ処理を反復 300ms間の反復を2セット 全要素を⾜す処理を反復 7プロセスで各7回測定 元の整数列に戻して⽐較 Node 24.19.0 / V8 13.6.233.17-node.51 / Linux x64 / EPYC 9V74 / CPU 0固定
全要素を⾜す処理の実⾏時間 0〜127の同じ整数列を使⽤ PACKED_SMIを1とした時間⽐ 中央値 PACKED_SMI 1.00 SMI + a.label 1.01 PACKED_DOUBLE 1.19 PACKED_ELEMENTS 1.21 HOLEY_SMI 1.20 0 0.5 ⼩さいほど短時間 Node 24.19.0 / V8 13.6.233.17-node.51 / Linux x64 / EPYC 9V74 / CPU 0固定 1 1.5
全要素を⾜す処理の実⾏時間 0〜127の同じ整数列を使⽤ PACKED_SMIを1とした時間⽐ 中央値 1.00 PACKED_SMI SMI + a.label 1.01 PACKED_DOUBLE 1.19 PACKED_ELEMENTS 1.21 HOLEY_SMI(値は復元) 1.20 同じ内部型でもアクセス履歴は変わる 0 0.5 ⼩さいほど短時間 Node 24.19.0 / V8 13.6.233.17-node.51 / Linux x64 / EPYC 9V74 / CPU 0固定 1 1.5
Arrayの範囲外に⼀度でもアクセスすると そこから先は遅くなる……!?
https://v8.dev/blog/elements-kinds#avoid-reading-beyond-the-length-of-the-array
JavaScript for (let i = 0, item; (item = items[i]) != null; i++) { doSomething(item); } V8 Blog “Elements kinds in V8”(2017)の例 null / undefined が出るまで読む 最後に範囲外も読んでしまう
JavaScript for (let i = 0, item; (item = items[i]) != null; i++) { doSomething(item); } null / undefined が出るまで読む 最後に範囲外も読んでしまう “jQuery uses this pattern in a few places.” V8 Blog “Elements kinds in V8”(2017)の例
JavaScript const elements = document.getElementsByTagName("input"); elements.length; // IEでは要素 他では数値 elements.namedItem("length"); //現代はこっちで要素が取れる https://bugs.jquery.com/ticket/945/ <input name="length"> がある場合に昔のIEでバグったかららしい
範囲外アクセスを計測前に1回だけ⼊れる 01 02 03 範囲内で繰り返す 1回だけ条件を変える 範囲内へ戻して計測 全要素を同じ関数で処理 1秒間繰り返す 実験群:末尾の次まで読む 対照群:末尾まで読む 範囲内だけで1秒間反復 その後に実⾏時間を測る
合計を求める処理と最⼤値を探す処理
合計
最⼤値
1+2+3=6
max(1, 2, 3) = 3
JavaScript
let sum = 0;
for (let i = 0; i < end; i++) {
const value = a[i];
sum += value === undefined
? 0 : value;
}
return sum;
JavaScript
let max = -1;
for (let i = 0; i < end; i++) {
const value = a[i];
if (value > max) max = value;
}
return max;
速度⽐較 同じ呼び出し箇所・同じ値 中央値 合計:範囲内だけ 1.00 合計:範囲外を1回 2.49 最⼤値:範囲内だけ 1.00 最⼤値:範囲外を1回 1.94 0 1 Node 24.19.0 / V8 13.6.233.17-node.51 / Linux x64 / EPYC 9V74 / CPU 0固定 2 3
配列の範囲外でも prototype の値を読む JavaScript const a = [10, 20, 30]; const p = { 3: 99 }; Object.setPrototypeOf( p, Array.prototype); Object.setPrototypeOf(a, p); a[a.length]; // 99 式 評価結果 a.length 3 a[a.length] 99
配列の分類は同じでも IC の情報は変わる 観測項⽬ 範囲外を読む前 1回読んだ後 Elements Kind PACKED_SMI PACKED_SMI IC の範囲外アクセス対応 なし あり IC = Inline Cache
Object の形と最適化
Hidden Class は Object の形を表す JavaScript 同じ Hidden Class const a = { x: 1, y: 2 }; const b = { x: 3, y: 4 }; Object プロパティ function readX(o) { return o.x; } readX(a); readX(b); a x, y b x, y
違う形の Object も同じ関数に渡せる JavaScript const point = { x: 1, y: 2 }; const box = { x: 3, width: 4, height: 5 }; function readX(o) { return o.x; } readX(point); readX(box); 異なる Hidden Class Object プロパティ point x, y box x, width, height
同じプロパティでも作成順序で形が変わる JavaScript 異なる Hidden Class const a = { x: 1, y: 2 }; const b = { y: 2, x: 1 }; Object プロパティ function readX(o) { return o.x; } readX(a); readX(b); a x, y b y, x
Hidden Class から x の位置が分かる Hidden Class と x の格納位置 JavaScript function readX(o) { return o.x; } Hidden Class プロパティの順序 x の位置 A { x, y } 1番⽬ B { y, x } 2番⽬
同じo.xに異なる作り⽅のObjectを渡す Hidden Class 1種 Hidden Class 2種 Hidden Class 5種 各順序を個別に計測 2順序を交互に渡す キー構成を増やす { x, y } のみ { y, x } のみ { x, y } / { y, x } { x, y } / { x, z } など 5種類のキー構成 準備・ウォームアップの後に計測
逆順⾃体は同程度 同じ値のo.xを合計 2順序を混ぜると約1割差 {x, y}だけを1とした時間⽐ 中央値 {x, y}:Hidden Class 1種 1.00 {y, x}:Hidden Class 1種 1.00 2順序:Hidden Class 2種 1.12 5構成:Hidden Class 5種 3.49 0 1 2 Node 24.19.0 / V8 13.6.233.17-node.51 / Linux x64 / EPYC 9V74 / CPU 0固定 3 4
JavaScript
Object.keys({ x: 1, y: 2 });
// ["x", "y"]
Object.keys({ y: 2, x: 1 });
// ["y", "x"]
通常の⽂字列キーは作成順
JavaScript
Object.keys({ x: 1, y: 2 });
// ["x", "y"]
通常の⽂字列キーは作成順
Object.keys({ y: 2, x: 1 });
// ["y", "x"]
→トランスパイル時に並びかえれば⾼速になるのでは?
順序を変えると計算結果が変わる
JavaScript
const steps = {
add: n => n + 1,
double: n => n * 2,
};
Object.values(steps).reduce(
(n, step) => step(n), 1
); // 4
プロパティの順序
計算結果
add → double
4
double → add
3
プロパティを並べ替えると評価順も変わる JavaScript const obj = { y: trace("y"), x: trace("x"), }; 評価順は y → x 並べ替えると副作⽤の順も変わる
まとめ ● JavaScript は⾃由な⾔語 ● V8 は様々な⼯夫で⾼速に実⾏する ● 仕様と互換性の制約があり、書き⼿が⼯夫できる余地もまだある
綺麗なコードで速くなる……かもしれない!