JavaScriptCore: パーサーから最適化JITパイプラインへ
前提知識
- ›記事2:WTFとメモリ管理(GCブリッジの概念)
- ›コンパイラの基礎知識:字句解析、構文解析、AST、中間表現
- ›JITコンパイルの基礎と、段階的コンパイルが必要とされる理由の理解
- ›SSA形式と制御フローグラフへの理解(あると望ましいが必須ではない)
JavaScriptCore: パーサーから最適化JITパイプラインへ
JavaScriptCore(JSC)は WebKit の JavaScript および WebAssembly エンジンです。本質的に動的で弱い型付けの JavaScript を、4段階のコンパイルパイプラインをとおして最適化されたマシンコードへと変換する、非常に洗練されたコンパイラ基盤です。タイトなループにおいては、静的型付け言語にも引けを取らないパフォーマンスを発揮します。
JSC の設計の核心にある考え方は、コンパイル速度と実行速度はトレードオフの関係にあるという点です。最適化コンパイラは高速なコードを生成しますが、コンパイル自体に時間がかかります。JSC はこの問題を段階的コンパイルによって解決しています。まず高速なインタープリターで即座に実行を開始し、実行時の動作をプロファイリングしながら、より頻繁に実行されるコードを段階的により積極的な最適化でコンパイルしていきます。
この記事では、ソーステキストからマシンコードに至るまでのパイプライン全体を順に追っていきます。
字句解析と構文解析:ソースから AST へ
JavaScript ソーステキストを構造化された表現へと変換する最初のステップは、字句解析(トークン化)と構文解析(AST 構築)の2フェーズで構成されます。
flowchart LR
SRC["JavaScript Source<br/>'function add(a,b) { return a+b; }'"] --> LEX["Lexer<T>"]
LEX --> TOKENS["Token Stream<br/>FUNCTION, IDENT, LPAREN, ..."]
TOKENS --> PARSE["Parser<LexerType>"]
PARSE --> AST["AST<br/>(Nodes.h types)"]
Lexer<T> は文字型をテンプレートパラメーターとして受け取るテンプレートクラスです(8ビット文字には LChar、16ビット文字には UChar)。ASCII のみのソースファイルで常に16ビット文字を使うオーバーヘッドを避けるための最適化で、WebKit 全体でよく見られる手法です。クラスに付与された CACHE_LINE_ALIGNED アノテーションは、ホットなレキサーフィールドが他のデータとキャッシュラインを共有しないようにするためのものです。
Parser<LexerType> は、トークンストリームから AST を構築する再帰下降パーサーです。AST のノード型は Nodes.h で定義されており、JavaScript のあらゆる構文要素(式、文、宣言、パターンなど)の型階層が含まれた大きなファイルです。
JavaScript の文法は、パーサーにとって扱いが難しいことで知られています。自動セミコロン挿入、アロー関数の曖昧さ((a) => a と (a) に続く => a の区別)、テンプレートリテラルのネスト、その他多くのコンテキスト依存の構文を処理する必要があります。JSC のパーサーはこれらをすべてシングルパスで処理します。
ヒント: スタンドアロンの JSC シェル
jsc.cppを使うと、JSC の内部動作を手軽に試すことができます。コマンドラインフラグを使って、バイトコードのダンプ、JIT コンパイル判断の出力、ティアアップイベントのトレースが可能です。
バイトコードのコンパイルと CodeBlock
AST は直接実行されません。代わりに、バイトコードコンパイラーが AST を CodeBlock に格納されたバイトコードへと変換します。
flowchart TD
AST["AST Nodes"] --> BC["BytecodeGenerator"]
BC --> CB["CodeBlock"]
CB --> |"contains"| INST["Bytecode instructions"]
CB --> |"contains"| PROF["Profiling metadata"]
CB --> |"contains"| CONST["Constant pool"]
CB --> |"will contain"| JIT["JIT code (later)"]
CodeBlock は JSC における中心的なコンパイル成果物です。以下の情報を保持しています:
- バイトコード命令 — プログラムの操作をコンパクトにエンコードしたもの。
- 定数プール — バイトコードから参照される文字列リテラル、数値、その他の定数。
- プロファイリングメタデータ — インタープリター実行中に収集されるカウンターと型情報。後の JIT ティアアップ判断に使用される。
- JIT コード — 関数が JIT コンパイルされると、生成されたマシンコードが同じ
CodeBlockに紐付けられる。
バイトコード命令セットは豊富で、基本的な操作だけでなく、よく使われるパターン向けの特化した高速パスも含まれています(例:プロパティアクセスの op_get_by_id、関数呼び出しの op_call)。各命令には、インタープリターとベースライン JIT が型プロファイルを格納するためのメタデータスロットが用意されています。
LLInt:低レベルインタープリター
最初の実行ティアは LLInt(Low-Level Interpreter)です。関数が初めて呼び出されると、そのバイトコードは LLInt によって実行されます。
LLInt の「スローパス」は LLIntSlowPaths.cpp に実装されており、インタープリターのメインディスパッチループにインライン展開できない複雑な操作を処理します。キャッシュミスが発生するプロパティルックアップ、未知のターゲットへの関数呼び出し、そして特に重要なティアアップチェックがこれに該当します。
ティアアップの仕組みは、各関数とループの実行回数をカウントすることで動作します。実行カウントがしきい値を超えると、LLInt は次のティア(ベースライン JIT)での関数コンパイルをトリガーします。この処理は透過的に行われます。ベースライン JIT がバックグラウンドでコンパイルしている間も、関数はインタープリターで実行し続けます。
flowchart TD
START["Function call"] --> LLINT["LLInt Interpreter"]
LLINT --> |"execution count > threshold"| BL_COMP["Baseline JIT compilation"]
BL_COMP --> BL["Baseline JIT code"]
BL --> |"+ profiling data"| DFG_COMP["DFG JIT compilation"]
DFG_COMP --> DFG["DFG optimized code"]
DFG --> |"+ more profiling"| FTL_COMP["FTL compilation (via B3)"]
FTL_COMP --> FTL["FTL maximum optimization"]
DFG --> |"speculation fails"| OSR_EXIT["OSR Exit"]
FTL --> |"speculation fails"| OSR_EXIT
OSR_EXIT --> |"deoptimize"| BL
段階的 JIT コンパイル:Baseline → DFG → FTL
JSC にはインタープリターの上に3つの JIT ティアがあり、それぞれコンパイル時間と実行速度のトレードオフが異なります。
Baseline JIT — 最初の JIT ティアです。最小限の最適化でバイトコードをマシンコードにコンパイルします。その役割は二つあります:インタープリターより高速に実行することと、型プロファイルを収集することです。Baseline JIT はプロパティアクセス、関数呼び出し、算術演算を計測し、各操作を流れる型を記録します。
DFG(Data Flow Graph)JIT — 最初の最適化ティアです。DFG はグラフベースの中間表現を構築し、Baseline JIT が収集したプロファイルをもとに型推論を行います。DFGAbstractInterpreter は DFG グラフ上で抽象解釈を実行し、投機的最適化を可能にするために型情報を伝播させます。
「投機的(Speculative)」というのがキーワードです。DFG は、将来の実行でも過去と同じ型が現れると仮定します。a + b が常に整数で演算されてきたなら、DFG はガード(型チェック)付きの整数加算としてコンパイルします。実行時にガードが失敗した場合(たとえば文字列が渡された場合)、JIT は OSR exit(On-Stack Replacement)を実行し、ベースライン JIT へと逆最適化します。
FTL(Faster Than Light)JIT — 最大最適化ティアです。FTL は DFG グラフを B3 中間表現(後述)に変換し、ループ不変コードの移動、グローバル値番号付け、定数畳み込み、デッドコード除去、レジスタ割り当てといった積極的な古典的コンパイラ最適化を適用します。
FTL コンパイルのエントリーポイントは FTLCompile.cpp で、B3 コンパイルパイプライン全体を調整し、生成されたマシンコードをインストールする役割を担います。
B3 バックエンドと Air
B3 は JSC のコンパイラバックエンドです。効率的なマシンコードを生成するために設計された低レベルの IR で、FTL JIT(JavaScript 用)と OMG ティア(WebAssembly 用)の両方で使用されています。
flowchart TD
DFG_IR["DFG IR (high-level)"] --> LOWER["FTL Lowering"]
LOWER --> B3_IR["B3 IR (SSA form)"]
B3_IR --> OPT["B3 Optimizations<br/>(strength reduction, CSE, DCE)"]
OPT --> AIR["Air (Assembly IR)"]
AIR --> RA["Register Allocation"]
RA --> MC["Machine Code"]
WASM["Wasm bytecode"] --> OMG["OMG tier"]
OMG --> B3_IR
B3::BasicBlock は B3 IR の基本単位で、制御フローグラフを形成する前後継リストを持った Value(操作)のシーケンスです。B3 は SSA 形式を採用しており、各値がちょうど一度だけ定義されるため、データフロー解析が容易になっています。
Air(Assembly IR)は B3 と最終的なマシンコードの間に位置します。マシン固有の命令を仮想レジスタで表現し、レジスタアロケーターが物理レジスタを割り当てて最終的なマシンコードを出力します。
この設計は意図的に LLVM に似ています(B3 は LLVM の IR にインスパイアされました)が、はるかにシンプルで高速なコンパイルが可能です。JSC はかつて LLVM を FTL バックエンドとして使用していましたが、コンパイル時間の短縮とより緊密な統合を実現するために B3 へ移行しました。
ガベージコレクション:Riptide コレクター
JSC は Riptide と呼ばれるガベージコレクターで JavaScript オブジェクトのライフタイムを管理しています。Riptide は後退波面型の並行 GC です。
「後退波面(retreating wavefront)」とは、コレクションサイクルを完了するために JavaScript の実行をすべて停止する必要がないことを意味します。トレース中に発生するミューテーションを追跡するためにライトバリアを使いながら、JavaScript の実行と並行してライブオブジェクトをトレースします。
JSObject は GC ヒープ上のすべての JavaScript オブジェクトの基底クラスです。JSCell を継承しており、マークビット、構造ポインター、型情報といった GC インフラを提供します。
GC は、パート3で説明したバインディングレイヤーをとおして WebCore の参照カウントオブジェクトと連携する必要があります。JSDocument ラッパー(GC 管理)が Document(参照カウント)への参照を保持している場合、GC は Document が C++ からまだ到達可能である間はラッパーを回収してはなりません。この調整を担うのが DOMWrapperWorld と不透明ルートメカニズムです。
stateDiagram-v2
[*] --> Idle
Idle --> ConstraintFixpoint : allocation threshold
ConstraintFixpoint --> Concurrent_Mark : begin marking
Concurrent_Mark --> Concurrent_Mark : trace objects (concurrent)
Concurrent_Mark --> Reloop : found new objects
Reloop --> Concurrent_Mark : continue marking
Concurrent_Mark --> Sweep : marking complete
Sweep --> Idle : collection done
WebAssembly:BBQ と OMG ティア
JSC の WebAssembly サポートは、JavaScript パイプラインを反映した2つのコンパイルティアを使用しています。
- BBQ(Build Bytecode Quickly)— WebAssembly モジュールが最小限の遅延で実行を開始できるよう、高速なベースラインコンパイラで妥当なコードを素早く生成します。
- OMG(Optimized Machine code Generator)— JavaScript の FTL ティアと同じ B3 バックエンドを使用する最適化ティアで、頻繁に実行される WebAssembly 関数に対して完全な最適化を適用します。
JavaScript の FTL と WebAssembly の OMG が B3 バックエンドを共有しているのは、エンジニアリング上の大きな利点です。B3 のコード生成への改善が、両言語のパフォーマンス向上に直結します。
WebAssembly の実装は Source/JavaScriptCore/wasm/ に置かれています。WebAssembly は静的型付けのため、コンパイルはある面では単純(投機最適化が不要)ですが、別の面では複雑です(SIMD 操作、メモリ境界チェック、テーブル間接参照など)。
次のステップ
このシリーズの最終記事では、アーキテクチャから実践へと視点を移します。WebKit のビルド方法、包括的なテストスイートの実行方法、そして変更のコントリビュート方法を取り上げます。2つのビルドシステム(Xcode と CMake)、ユニファイドソース最適化、レイアウトテストインフラ、そして git-webkit のコントリビューターワークフローについて解説します。アーキテクチャの理解を実際のコードベースへの貢献に変えるには、こうした実践的な知識が欠かせません。