Ginza.js #12 — V8の最適化を覗いてみよう!

289 Views

October 21, 26

スライド概要

Ginza.js #12 の発表のスライドです

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Ginza.js #12 / 2026.10.19 V8の最適化を覗いてみよう! 綺麗なコードで⾼速化……!? Katsumi Kobayashi

2.

⼩林 克海 所属 株式会社プレイド Core Platform(基盤) 新卒2年⽬ 早稲⽥⼤学基幹理⼯学研究科表現⼯学専攻 橋⽥研究室(HCI) 修⼠2年 twitter: @takabayap JS歴 12年⽬(中学1年⽣〜)

3.

JavaScript は⾃由だ‼

4.
[beta]
こんな Array も書ける

JavaScript

const a = [1, 2, 3];
a.push(0.5);
a.push("hello");
a.push({ x: 1 });

異なる種類の値を混在できる

5.

こんな 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

6.

こんな 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

7.

こんな 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

8.

速度⽐較 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

9.

速さを⽀える仕組み

11.

実⾏環境と JavaScript エンジン 実⾏環境 JavaScript エンジン Chrome V8 Node.js V8 Safari JavaScriptCore Bun JavaScriptCore

12.

実⾏環境と JavaScript エンジン 実⾏環境 JavaScript エンジン Chrome V8 Node.js V8 Safari JavaScriptCore こっちも速いよ! Bun JavaScriptCore

13.
[beta]
TypeScript

type Item = number | string | { x: number };
const a: Item[] = [1, 2, 3];
a.push(0.5);
a.push("hello");
a.push({ x: 1 });

14.
[beta]
JavaScript

const a = [1, 2, 3];
a.push(0.5);
a.push("hello");
a.push({ x: 1 });

消えちゃう!

15.
[beta]
JavaScript

const a = [1, 2, 3];

V8 実⾏時に分類

a.push(0.5);
a.push("hello");
a.push({ x: 1 });

16.

V8 の最適化 01 02 03 値と形を観測 規則性を記録 処理を特化

17.

Elements Kind 値の表現 SMI / DOUBLE / ELEMENTS ⽳の扱い PACKED / HOLEY

18.

SMI: ⼩さい整数を格納する JavaScript const a = [1, 2, 3]; // PACKED_SMI_ELEMENTS 整数をそのまま格納する

19.

DOUBLE: 数値を倍精度で格納する JavaScript const a = [1, 2, 3]; a.push(0.5); // PACKED_DOUBLE_ELEMENTS 64bit の数値を直接格納

20.

汎⽤のELEMENTS: いろいろな値を持てる JavaScript const a = [1, 2, 3]; a.push(0.5); a.push("hello"); // PACKED_ELEMENTS 値や保存先のアドレスを格納

21.

PACKEDとHOLEY: 途中に⽳があるか JavaScript const a = [1, 2, 3]; // PACKED_SMI_ELEMENTS a[5] = 6; // HOLEY_SMI_ELEMENTS ⽳を確認する処理が増える

22.

要素の変化に応じて Elements Kind が遷移する ⼩さい整数 倍精度の数値 汎⽤の値 PACKED_SMI_ELEMENTS PACKED_DOUBLE_ELEMENTS PACKED_ELEMENTS HOLEY_DOUBLE_ELEMENTS HOLEY_ELEMENTS ⽳を作る HOLEY_SMI_ELEMENTS V8 Blog “Elements kinds in V8”

23.

内部表現による実⾏時間の差

24.

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固定

25.

全要素を⾜す処理の実⾏時間 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

26.

全要素を⾜す処理の実⾏時間 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

27.

Arrayの範囲外に⼀度でもアクセスすると そこから先は遅くなる……!?

28.

https://v8.dev/blog/elements-kinds#avoid-reading-beyond-the-length-of-the-array

29.

JavaScript for (let i = 0, item; (item = items[i]) != null; i++) { doSomething(item); } V8 Blog “Elements kinds in V8”(2017)の例 null / undefined が出るまで読む 最後に範囲外も読んでしまう

30.

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)の例

31.

JavaScript const elements = document.getElementsByTagName("input"); elements.length; // IEでは要素 他では数値 elements.namedItem("length"); //現代はこっちで要素が取れる https://bugs.jquery.com/ticket/945/ <input name="length"> がある場合に昔のIEでバグったかららしい

32.

範囲外アクセスを計測前に1回だけ⼊れる 01 02 03 範囲内で繰り返す 1回だけ条件を変える 範囲内へ戻して計測 全要素を同じ関数で処理 1秒間繰り返す 実験群:末尾の次まで読む 対照群:末尾まで読む 範囲内だけで1秒間反復 その後に実⾏時間を測る

33.
[beta]
合計を求める処理と最⼤値を探す処理
合計

最⼤値

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;

34.

速度⽐較 同じ呼び出し箇所・同じ値 中央値 合計:範囲内だけ 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

35.

配列の範囲外でも 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

36.

配列の分類は同じでも IC の情報は変わる 観測項⽬ 範囲外を読む前 1回読んだ後 Elements Kind PACKED_SMI PACKED_SMI IC の範囲外アクセス対応 なし あり IC = Inline Cache

37.

Object の形と最適化

38.

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

39.

違う形の 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

40.

同じプロパティでも作成順序で形が変わる 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

41.

Hidden Class から x の位置が分かる Hidden Class と x の格納位置 JavaScript function readX(o) { return o.x; } Hidden Class プロパティの順序 x の位置 A { x, y } 1番⽬ B { y, x } 2番⽬

42.

同じ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種類のキー構成 準備・ウォームアップの後に計測

43.

逆順⾃体は同程度 同じ値の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

44.
[beta]
JavaScript

Object.keys({ x: 1, y: 2 });
// ["x", "y"]
Object.keys({ y: 2, x: 1 });
// ["y", "x"]

通常の⽂字列キーは作成順

45.
[beta]
JavaScript

Object.keys({ x: 1, y: 2 });
// ["x", "y"]

通常の⽂字列キーは作成順

Object.keys({ y: 2, x: 1 });
// ["y", "x"]

→トランスパイル時に並びかえれば⾼速になるのでは?

46.
[beta]
順序を変えると計算結果が変わる

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

47.

プロパティを並べ替えると評価順も変わる JavaScript const obj = { y: trace("y"), x: trace("x"), }; 評価順は y → x 並べ替えると副作⽤の順も変わる

48.

まとめ ● JavaScript は⾃由な⾔語 ● V8 は様々な⼯夫で⾼速に実⾏する ● 仕様と互換性の制約があり、書き⼿が⼯夫できる余地もまだある

49.

綺麗なコードで速くなる……かもしれない!