Upgrade to Pro — share decks privately, control downloads, hide ads and more …

そのJavaScript、V8が泣いてます。V8の気持ちを理解して書くパフォーマンス最適化

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.
Avatar for Riya Amemiya Riya Amemiya
September 20, 2025

 そのJavaScript、V8が泣いてます。V8の気持ちを理解して書くパフォーマンス最適化

このスライドはSlidevで作られており、以下のレポジトリで公開しています
https://github.com/riya-amemiya/amemiya_riya_slide_data/tree/main/frontend_conf_tokyo_2025

Avatar for Riya Amemiya

Riya Amemiya

September 20, 2025

More Decks by Riya Amemiya

Other Decks in Technology

Transcript

  1. V8 とは? V8 は、Google が開発するオープンソースのJavaScript およびWebAssembly エンジンです。現代のウェブとサー バーサイドアプリケーションの根幹を支える技術と言えます。 主な使用例: Google

    Chrome: 実行エンジンとしてV8 を採用 Node.js: V8 をベースにしたJavaScript 実行環境 Electron: VS Code, Slack, Discord など、数多くのデスクトップアプリケーションが内部でV8 を利用 私たちが書くJavaScript コードのパフォーマンスは、このV8 の働きに直接的に依存するため、その内部構造を 理解することは、より高速なアプリケーションを開発する上で極めて重要です。
  2. Ignition の最重要任務 Ignition は単にコードを実行するだけではありません。 後の最適化コンパイラが最高のパフォーマンスを引き出すための偵察として、実行時のコードの振る舞いを詳 細に記録します。 このプロファイル情報は Feedback Vector と呼ばれ、以下のような情報を含みます。

    型情報: 関数に渡された引数や、変数に代入された値がどのような型(数値、文字列、オブジェクトなど) であったか オブジェクト形状: オブジェクトがどのようなプロパティを、どのような順序で持っていたか(Hidden Class ) 実行回数: 関数が何回呼び出されたか、ループが何回繰り返されたか
  3. Ignition のキーテクノロジー①:Hidden Class Hidden Class (V8 内部名: Map )は、V8 がオブジェクトの内部構造(shape

    )を管理する仕組みです。 (一般的な文献では「shape 」と表現されることもあります) JavaScript のオブジェクトは動的にプロパティを追加できますが、V8 はプロパティの 「名前と追加順序」 (お よび属性/ アクセサの種別)が同じオブジェクトを「同じ形状」とみなし、内部的に同じ Map を割り当てま す。 この仕組みにより、V8 はプロパティの格納場所(in-object かプロパティ配列か、そのオフセット)を素早く 特定できます。 オブジェクト生成後にプロパティを追加しても、全インスタンスが“ 同じ順序・同じ有無” で追加される限り は、同じ Map の遷移チェーンを辿るため最適化は維持されます。問題になるのは、インスタンスごとに追加順 序や有無が揺れる場合や、 delete などで形状を壊して辞書モードへ移行する場合で、こうした不安定さが IC を多相化させ、最適化効率を下げます。
  4. Ignition のキーテクノロジー②:Elements Kind Hidden Class がオブジェクトの形状を扱うのに対し、Elements Kind は配列の要素を効率的に扱うための仕組 みです。V8 は配列の中身に応じて、内部的な表現方法(Elements

    Kind )を動的に最適化します。 主な種類と速度順: 1. PACKED_SMI_ELEMENTS : 隙間なく、小さな整数(SMI) のみが格納されている状態 2. PACKED_DOUBLE_ELEMENTS : 浮動小数点数が含まれる状態 3. PACKED_ELEMENTS : 任意のJS 値が格納される状態(数値は HeapNumber として格納) PACKED ( 密) vs HOLEY ( 疎): [1, 2, 3] のような隙間のない配列は PACKED となり高速です [1, , 3] のように要素が抜けている配列は HOLEY となり低速になります 補足: arr[1] = undefined は“ 穴” ではありません。 delete arr[1] 、離れたインデックスへの代入(例: arr[9] = 1 ) 、 length の 先行拡張などが“ 穴” を作ります。 V8 は可能な限り特化度の高い Elements Kind を維持しようとしますが、型の異なる要素が追加されると、より 一般的な種類へと基本的に一方向の遷移(降格)が発生します (同じ配列は原則戻りません。新しい配列を生成すれば PACKED にできます) 。
  5. Ignition のキーテクノロジー③:Inline Cache (IC) Inline Cache (IC) は、Hidden Class を利用してプロパティアクセスを劇的に高速化するキャッシュ機構です。

    仕組み: 初回アクセス時に、オブジェクトのHidden Class とプロパティのメモリ上の位置(オフセット)をセ ットでキャッシュします。2 回目以降は、同じHidden Class であれば検索をスキップし、直接オフセットを参 照します。 状態: Monomorphic ( 単一形状): 最も理想的で最速 Polymorphic ( 多形状): 2 〜4 種類の形状を扱う。少し遅くなる Megamorphic ( 超多形状): 5 種類以上の形状を扱う。IC による最適化を諦め、大幅に低速化する
  6. Sparkplug の設計思想 Sparkplug は、V8 v9.1 で導入された非最適化(ベースライン)コンパイラです。その核心的な思想は、従来の コンパイラの常識を完全に無視することにあります。 通常のコンパイラはソースコードをAST (抽象構文木)やIR (中間表現)に変換し、それを最適化してからマ

    シン語を生成します。しかしSparkplug は、Ignition が生成したバイトコードから直接マシンコードを生成する ことで、これらのプロセスを全て省略し、驚異的なコンパイル速度を実現します。 補足: Sparkplug はIR を生成 せず、最適化は基本的に行いません(ごく局所的なピープホール最適化程度) 。
  7. Sparkplug の内部実装と利点 Sparkplug の実装は、コンパイラ全体が実質的に巨大なswitch 文を含む単一のループとして構成されています。 各バイトコード命令に対し、事前に用意された固定のマシンコード生成関数を呼び出すだけ、という極めてシ ンプルな構造です。 このアプローチが効果的なのは、多くのJavaScript コードは数回しか実行されないという現実があるためで す。複雑な最適化にかける時間が、それによって得られる実行時間の短縮を上回ることが多いため、

    Sparkplug は「変換による高速化」以外の最適化を大胆に切り捨てています。 技術的には、ループ検出のため の軽い前処理パスを経てからコード生成する実装(2 パス)だった時期もありますが、本質はバイトコードの 線形走査による直接コード生成です。 実行速度向上の主因は、インタプリタ特有のオペランドのデコードや次 バイトコードへのディスパッチといったオーバーヘッドを、事前に機械語へ前取りすることで取り除ける点に あります。 また、Sparkplug はインタプリタ互換のスタックフレームを維持し、多くの処理を builtins に委譲 するため、デバッガ/ プロファイラ/ 例外スタック/OSR (最適化・脱最適化)の連携が容易です。
  8. Maglev の主要技術①:SSA と制御フローグラフ SSA ( 静的単一代入) 形式: Maglev はIR としてSSA

    形式を採用しています。この形式では各変数が一度だけ代入 されるため、データフローの解析が劇的に簡単になり、定数伝播や不要コード削除といった基本的な最適化 が、複雑な解析なしに実現できます。 制御フローグラフ (CFG): コードの実行経路(ループや条件分岐)をグラフとして構築することで、基本的な最 適化(例: 定数伝播や不要コード削除)を低コストで適用しやすくします。コンパイル時間を抑えるため、高度 なループ変換は目的外です。
  9. Maglev の主要技術②:型フィードバックと表現選択 型フィードバックによる最適化: Ignition が収集した型情報を活用し、オブジェクトの形状(Hidden Class/Map) が安定しているプロパティアクセスに対しては、形状チェック(例: CheckMap )とオフセット読み取り(例: LoadField

    )に特化した効率的なコードを生成します。 表現選択(Representation Selection ): 数値などの値について最適な内部表現(例: Smi/ 整数、浮動小数点、 タグ付き/ 非タグ値)を選び、必要に応じてアンボックス/ リボックスを行うことで、演算やレジスタ渡しを高 速化します。
  10. TurboFan のコア技術①:中間表現(IR) アーキテクチャ TurboFan は、最高レベルの最適化を実現するために、歴史的な Sea-of-Nodes と、近年本流になりつつある Turboshaft の2 系統のIR

    を背景に持ちます。 Sea-of-Nodes は、データフローと制御フローを単一のグラフで表現するIR です。この構造は、命令の並べ替 えに最大限の柔軟性をもたらし、強力な大域的最適化を可能にしますが、その複雑さからコンパイル時間が長 くなるという性質がありました。 それに対し Turboshaft は、より伝統的な制御フローグラフ(CFG) をベースにしたIR です。最初にコードの実行 経路を固定し、そのブロック内で最適化を行うため、コンパイルのオーバーヘッドが大幅に削減されます。V8 は、JavaScript の動的な性質上、Sea-of-Nodes の理論的な利点が常に活かせるとは限らないという経験則か ら、より実用的で高速なTurboshaft への移行を進めています。現在、Wasm は大部分がTurboshaft 化済みで、 JavaScript 側もLowering や最適化の多くが段階的にTurboshaft へ移行しています。
  11. TurboFan のコア技術②:投機的最適化と脱最適化 投機的最適化は、TurboFan の真骨頂です。Feedback Vector に基づき、 「この変数は常に数値である」 「この条 件分岐は常にtrue である」といった

    予測(賭け) を行います。 この予測が的中している限り、型チェックなどを省略した非常に高速なコードが実行されます。もし予測が外 れた場合、脱最適化 (Deoptimization) という安全装置が作動し、実行中のコードを即座に安全な下位のコンパ イラのコードに戻します。これにより、安全性を担保しつつ、動的言語であるJavaScript で静的言語並みの最 適化を可能にしています。
  12. TurboFan の高度な最適化技術 TurboFan は、前述のコア技術を基盤に、多数の高度な最適化技術を駆使します。 グローバル値番号付け (GVN): プログラム全体で冗長な計算を検出し、削除します。 数値範囲解析: 数値の取り得る範囲を解析し、分岐や境界チェックを削減します。 強度低減:

    除算を乗算に、乗算をシフト演算に置き換えるなど、高コストな演算を低コストなものに変換しま す。 エスケープ解析: オブジェクトの逃避性に基づき、割り当てのスタック化や割り当て自体の削除を行います。 高度なループ最適化: ループ不変式の移動、ループ融合など。JS 一般の自動ベクトル化は限定的で、主にWasm/ 一部の組み込みで有効です。
  13. 2024 年の新技術: Profile-Guided Tiering Intel とGoogle V8 チームが共同開発したProfile-Guided Tiering は、関数ごとに最適なコンパイラ戦略を選択し

    ます。 頻繁に使用される関数は早期にTurboFan へ直接昇格 脱最適化が多い関数は遅延ティアリング戦略を適用 Speedometer 3 で約5% の性能向上を実現(Intel Core Ultra Series 2 )
  14. 1: オブジェクトの形状を保持する なぜ重要か?: オブジェクトの形状(Hidden Class/Map) が安定していると、Inline Cache(IC) が有効に機能し、 プロパティアクセスが高速化されます。生成後にプロパティを追加しても、全インスタンスで“ 同じ順序・同じ

    有無” なら同じMap 遷移を辿れるため最適化は維持されます。問題は、インスタンスごとに追加順序や有無が揺 れる、 delete が混じる等で形状が不安定になる場合で、IC が多相化して性能が低下し、場合によっては辞書 モードへ移行します。
  15. ✅ After: 形状が安定したコード こちらのコードでは、 User インスタンスは常に同じ形状を持つため、V8 は安心して最適化できます。 class User {

    constructor(id, name) { this.id = id; this.name = name; this.isAdmin = false; } } const user1 = new User(1, "Taro"); const user2 = new User(2, "Jiro");
  16. ❌ Before: 型が不安定なコード このコードでは、 add 関数に数値と文字列という異なる型が渡されています。これにより、V8 のInline Cache はPolymorphic またはMegamorphic

    な状態に陥り、型を毎回チェックする必要が生まれるため、最適化の効率 が著しく低下します。 function add(a, b) { return a + b; } add(1, 2); add("a", "b");
  17. 3: 例外による脱最適化を避ける なぜ重要か?: 歴史的に、古いV8 では try...catch があるだけで最適化が阻害されましたが、現代のV8 (TurboFan) ではこの挙動は改善されています。 try...catch

    ブロックの存在自体は、もはや最適化の妨げに はなりません。 しかし、重要なのは、実際に例外がスローされ catch されると、それが「予測不能な事態」と見なされ、高 確率で脱最適化(低速なコードへのフォールバック)を引き起こすという点です。 ループ内など「ホット」な箇所で脱最適化が頻発すると、V8 はその関数の最適化を最終的に諦めてしまう可能 性があります。
  18. ❌ Before: 例外に頼った処理 ループ内でプロパティが存在しない可能性を try...catch で処理すると、 catch に入るたびに脱最適化が 起きるリスクがあります。 function

    getNames(users) { const names = []; for (const user of users) { try { // user.profile が無い場合に TypeError が発生 names.push(user.profile.name); } catch (e) { names.push('Unnamed'); } } return names; }
  19. ❌ Before: 大きく、複数の責任を持つ関数 データ加工、検証、整形といった複数の役割を担っており、V8 にとってインライン化の判断が難しい状態で す。 function processUser(user) { const

    fullName = `${user.lastName} ${user.firstName}`; if (fullName.length > 20) { console.error('Name is too long'); return null; } return { id: user.id, fullName }; }
  20. ✅ After: 小さく、単一責任の関数に分割 役割ごとに小さな関数に分割することで、各関数はV8 にインライン化されやすくなります。特に getFullName のような小さく純粋な関数は、インライン化の最有力候補です。 function getFullName(user) {

    return `${user.lastName} ${user.firstName}`; } function validateName(name) { return name.length <= 20; } function processUser(user) { const fullName = getFullName(user); if (!validateName(fullName)) { console.error('Name is too long'); return null; } return { id: user.id, fullName }; }
  21. 5: 配列の型を固定する (Elements Kind) V8 の気持ち(なぜ重要か?): V8 は、配列の要素の型に応じて内部的に最も効率的な表現( Elements Kind

    ) を選択します。要素がすべて整数であれば非常に高速な整数の配列として扱いますが、途中で1 つでも浮動小 数点数やオブジェクトが混ざると、より汎用的で低速な表現に変換(遷移)せざるを得ません。この遷移はV8 にとって大きな負担です。
  22. ❌ Before: 配列の型が途中で変わる 最初は整数だけだった配列に、後から浮動小数点数を追加しています。この瞬間に Elements Kind のコスト が高い遷移が発生します。 const numbers

    = [1, 2, 3]; // PACKED_SMI_ELEMENTS ( 最速) // この代入により、配列は PACKED_DOUBLE_ELEMENTS ( 低速) に変換される numbers.push(4.5); // さらにオブジェクトを追加すると PACKED_ELEMENTS ( さらに低速) になる numbers.push({});
  23. ✅ After: 配列の型が一貫している 配列の型を一貫させることで、 Elements Kind の遷移を防ぎ、V8 は最速の状態で処理を続けることができま す。 注意:

    -0 、 NaN 、 Infinity を 1 つでも含めると …_DOUBLE_ELEMENTS に遷移します。整数配列を維持した い場合は初期化時に値を正規化/ バリデーションしましょう。 // 整数配列 const integers = [1, 2, 3]; // 浮動小数点数配列 const doubles = [1.1, 2.2, 3.3]; // オブジェクト配列 const objects = [{}, {}];
  24. 「V8 の気持ち」と私たちのコード これまでのTIPS を、V8 の内部動作と結びつけて整理してみましょう。 V8 の気持ち(内部動作) 私たちが書くべきコード Inline Cache

    を効かせたい オブジェクトの形状を一定に保つ 投機的最適化を成功させたい 変数や引数の型を安定させる 脱最適化を避けたい 例外の発生を防ぐ事前チェックを行う インライン化しやすくしたい 小さく単一責任の関数に分割する Elements Kind の遷移を防ぎたい 配列に入れる要素の型を固定する
  25. まとめ V8 は Ignition, Sparkplug, Maglev, TurboFan という多層コンパイラ構造で、起動速度と実行速度を両立させて います。 開発者として重要なのは、V8

    の気持ち、すなわち 「予測しやすいコードを好む」 という性質を理解すること です。 オブジェクトの形状や配列の型を安定させ、関数の引数の型を揃え、例外に頼らない安定した処理を心がけ、 関数を小さく保つこと。これらの積み重ねが、V8 があなたのコードを最大限に高速化するための鍵となりま す。
  26. ご清聴ありがとうございました 本日のスライドは下記のリポジトリで公開しています。 内容の修正・改善など、お気軽にPull Request をお送りください。 11/30 の関西のフロントエンドカンファレンスでも登壇するので、そこでもお会いしましょう! https://github.com/riya-amemiya/amemiya_riya_slide_data/tree/main/frontend_conf_tokyo_2025 X やGitHub

    など: https://riya-amemiya-links.tokidux.com/ このスライドは CC BY-SA 4.0 でライセンスされています。 より自由な翻訳を可能にするため、翻訳は例外的に CC BY 4.0 での配布が許可されています。 Required Attribution: Riya Amemiya (https://github.com/riya-amemiya)