Open5

JavaScriptの最適化についてちょっと調べた

t3trat3tra

JavaScript が速い理由 - JIT (Just-In-Time) コンパイラの最適化

現代的なJavaScriptエンジン(V8, SpiderMonkey, JavaScriptCoreなど)は、実行時のJIT(動的コンパイル)を用いて次のようにパフォーマンスを得ている

1. ランタイムの型情報をモニタリング

  • たとえば、arr[i] という配列アクセスが「数値だけが入っている配列ならこの形、混合型なら別の形…」のように、実行時の実際に使われている型や呼び出し先の実際の関数などをプロファイリングしている

2. Hidden Class / Inline Caches / Polymorphic Caches

  • JavaScriptオブジェクトでは「Hidden Class (隠れクラス)」 と呼ばれる仕組みを用いてプロパティ配置を一意に特定し、メモリアクセスを静的に決定できるようにしている
  • さらに「この関数呼び出しは、8割がた引数が int で呼ばれる」などの実行時統計をベースに最適化したコードを生成し monomorphic / polymorphic inline cache などで型チェックを高速に行う

3. 投機的最適化 (Speculative Optimization)

  • 「この変数は実質的に int しか来ないはずだ」と推測してコードを生成し、もし実行時に外れたらデオプティマイズ (deoptimization) してより汎用的なコードに切り替える
  • これにより型が動的な言語でも「大半の場所では特定の型しか来ない」という状況下でネイティブに近い最適化を施すことができる

4. ホットパス重視

  • コードをインタプリタや軽量コンパイラで実行しつつホットな関数を検出 -> 高度な最適化コンパイラ(V8のTurbofanなど)で再コンパイルし高速化
  • 不要になればデオプティマイズするなど実行時に細かくコンパイル戦略を切り替えている

このようにJavaScriptランタイムは実行中に動的な型情報を集めそれに合わせてコードを再コンパイルする「JIT + 投機的最適化」アプローチで高いパフォーマンスを得ている

t3trat3tra

PythonのAOTコンパイラでも同じ最適化を行えるか

結論としては、「完全に同じアプローチを AOT で行うのは難しい」
それでもいくつか類似のアイデアは取り込めそう

1. JITでの動的型最適化は、AOTでは再現しにくい

  • JavaScript エンジンは 実行時 に型や呼び出し先を観測し、それが安定していれば投機的最適化コードを生成する -> 失敗すればデオプティマイズ…というサイクルを繰り返す
  • 一方AOT コンパイラはコンパイル時に「これが int だけ」「ここは string だけ」と確定できる場合を除き、最適化コードを作れない
  • 「実行中に型が変化するかもしれない」場合、AOTでは汎用コードを生成せざるを得ない -> 速度が落ちる

2.1 Type Hint / Profile Guided Optimization (PGO)

Python でも静的型チェックと型ヒントを活用して「ここは int しか来ない」とある程度保証できれば、AOTでもint用の最適化コードを出力できる

  • ただしPythonは歴史的に型ヒントが必須ではなく、常に正しいとも限らないので限定的
  • JavaScriptも本質的には動的型だがJITで実行時情報を使って最適化するためユーザーが型注釈を付けなくても速い

2.2 PGO (Profile Guided Optimization) では

  • まずインタプリタ or 仮のコンパイルで実行 -> ランタイムプロファイル取得
  • それを AOT コンパイラに渡し、「ここの引数の実際の型は 95% が int」と知る
  • そこを specialized code でコンパイルし、外れたらランタイムチェック -> fallback
    PGO にはデオプティマイズの仕組みまでは含まれないが、ホットパスでの速度を上げることができる

3. CPython の Specialized bytecode と似た方向

CPython 3.11+ では一部specialized bytecodeが導入され「属性アクセスが何型か観測」「連続で同じ型なら高速経路」「違えばキャッシュ破棄」といった仕組みを実装している
JIT ほど強力ではないが、投機的最適化の簡易版のようなもの

AOT コンパイラに同様の仕組みを埋め込み、実行時に「型チェック + 高速経路 or fallback」みたいな形にすることは理論上可能だが

  • そのためには実行時に再コンパイル or デオプティマイズする仕組みがないと性能向上は限定的
  • かなり複雑な仕組みになる
t3trat3tra

すべて PyObject* だと遅い理由

Python 的に何でも格納できる PyObject* は、

  • 要素アクセスごとに型チェックやメソッドテーブル参照が走る
  • 「int同士の足し算」でさえPyIntObject を介して関数呼び出し(あるいはC-API呼び出し)になる
  • 型が静的に決まらないためにオーバーヘッドが大きい

JavaScript エンジンはランタイム情報を元に「int 相当の最適化をする」「実は double が混在するときはこう」「オブジェクトになったらデオプティマイズ」といった柔軟な再コンパイルを行うので最終的にほぼネイティブ並みの速度を出せるケースがある

t3trat3tra

概略

JavaScriptの高速化手法

  • 実行時JIT + 投機的最適化 + hidden class + inline caching + デオプティマイズ
  • 動的型でも「実行時の実際の型パターン」を学習し、最適化コードを生成 -> 外れたら撤回

PythonのAOT コンパイル

  • コンパイル時点で型が定まっていなければ汎用 PyObject* ベースのコード生成 -> 遅い
  • JavaScript方式の投機的最適化はJITが前提でありAOTだと困難
  • ただしtype hintsやPGOを用いることで一部最適化は可能
  • PyPyやCPython3.13+のようにJITを実装している系統もあるがAOTで同等の仕組みはさらに困難

よって、
JavaScriptエンジンのような「複雑な型システムでも高速」な実行は「JIT で動的に型を観測・最適化」という強力な仕組みによるものであり、AOTコンパイラでは同じことを完全に再現するのは極めて難しい
もし同様の最適化 (投機的型専用コード + 失敗時のフォールバック) を導入したければ実行時に再コンパイルする仕組みやデオプティマイズなどを組み込む必要があるため、純粋なAOTではJavaScriptレベルの万能最適化は難しい