WebAssemblyコンパイルパイプライン

WebAssemblyは、JavaScript以外のプログラミング言語のコードをWeb上で効率的かつ安全に実行できるバイナリフォーマットです。このドキュメントでは、V8のWebAssemblyコンパイルパイプラインを詳しく説明し、優れたパフォーマンスを提供するためにさまざまなコンパイラをどのように使用するかについて説明します。

Liftoff #

V8は、最初はWebAssemblyモジュールの関数をコンパイルしません。代わりに、関数が初めて呼び出されたときに、ベースラインコンパイラであるLiftoffによって遅延コンパイルされます。Liftoffは1パスコンパイラです。つまり、WebAssemblyコードを1回反復処理し、WebAssembly命令ごとにすぐにマシンコードを生成します。1パスコンパイラは高速なコード生成に優れていますが、適用できる最適化は限られています。実際、LiftoffはWebAssemblyコードを非常に高速にコンパイルでき、毎秒数十メガバイトを処理します。

Liftoffのコンパイルが完了すると、生成されたマシンコードがWebAssemblyモジュールに登録されるため、次回の関数呼び出しではコンパイルされたコードをすぐに使用できます。

TurboFan #

Liftoffは、非常に短い時間でそこそこ高速なマシンコードを生成します。ただし、各WebAssembly命令のコードを個別に生成するため、レジスタ割り当ての改善や、冗長ロードの削除、強度削減、関数インライン化などの一般的なコンパイラの最適化を行う余地はほとんどありません。

そのため、頻繁に実行される*ホット*関数は、WebAssemblyとJavaScriptの両方で使用されるV8の最適化コンパイラであるTurboFanで再コンパイルされます。TurboFanはマルチパスコンパイラです。つまり、マシンコードを生成する前に、コンパイルされたコードの複数の内部表現を構築します。これらの追加の内部表現により、最適化とより優れたレジスタ割り当てが可能になり、コードが大幅に高速化されます。

V8は、WebAssembly関数が呼び出される頻度を監視します。関数が特定のしきい値に達すると、関数は*ホット*と見なされ、バックグラウンドスレッドで再コンパイルがトリガーされます。コンパイルが完了すると、新しいコードがWebAssemblyモジュールに登録され、既存のLiftoffコードが置き換えられます。その関数への新しい呼び出しはすべて、Liftoffコードではなく、TurboFanによって生成された新しい最適化されたコードを使用します。ただし、オンスタック置換は行いません。これは、関数が呼び出された後にTurboFanコードが使用可能になった場合、関数呼び出しはLiftoffコードで実行を完了することを意味します。

コードキャッシング #

WebAssemblyモジュールがWebAssembly.compileStreamingでコンパイルされた場合、TurboFanによって生成されたマシンコードもキャッシュされます。同じWebAssemblyモジュールが同じURLから再度フェッチされると、キャッシュされたコードを追加のコンパイルなしですぐに使用できます。コードキャッシングの詳細については、別のブログ記事をご覧ください。

生成されたTurboFanコードの量が特定のしきい値に達すると、コードキャッシングがトリガーされます。これは、大規模なWebAssemblyモジュールの場合、TurboFanコードが段階的にキャッシュされるのに対し、小規模なWebAssemblyモジュールの場合、TurboFanコードがキャッシュされない場合があることを意味します。Liftoffのコンパイルはキャッシュからのコードの読み込みとほぼ同じ速さであるため、Liftoffコードはキャッシュされません。

デバッグ #

前述のように、TurboFanは最適化を適用します。その多くは、コードの並べ替え、変数の削除、さらにはコード全体のセクションのスキップを伴います。これは、特定の命令にブレークポイントを設定する場合、プログラムの実行を実際にどこで停止する必要があるかが明確でない場合があることを意味します。言い換えれば、TurboFanコードはデバッグには適していません。したがって、DevToolsを開いてデバッグを開始すると、すべてのTurboFanコードが再びLiftoffコードに置き換えられます(「ティアダウン」)。これは、各WebAssembly命令がマシンコードの1つのセクションに正確にマッピングされ、すべてのローカル変数とグローバル変数がそのまま残っているためです。

プロファイリング #

少し混乱しやすいですが、DevTools内では、「パフォーマンス」タブを開き、「記録」ボタンをクリックすると、すべてのコードが再びティアアップ(TurboFanで再コンパイル)されます。「記録」ボタンをクリックすると、パフォーマンスプロファイリングが開始されます。Liftoffコードのプロファイリングは、TurboFanが完了していない間にのみ使用され、TurboFanの出力よりも大幅に遅くなる可能性があるため、代表的なものではありません。TurboFanの出力は、ほとんどの時間実行されます。

実験用フラグ #

実験のために、V8とChromeは、LiftoffのみまたはTurboFanのみでWebAssemblyコードをコンパイルするように構成できます。関数が初めて呼び出されたときにのみコンパイルされる遅延コンパイルを試すこともできます。次のフラグは、これらの実験モードを有効にします。

コンパイル時間 #

LiftoffとTurboFanのコンパイル時間を測定する方法はいくつかあります。V8の本番環境構成では、Liftoffのコンパイル時間は、JavaScriptから `new WebAssembly.Module()` が完了するまでの時間、または `WebAssembly.compile()` がPromiseを解決するまでの時間を測定することで測定できます。TurboFanのコンパイル時間を測定するには、TurboFanのみの構成で同じ操作を行うことができます。

Google EarthのWebAssemblyコンパイルのトレース。

コンパイルは、 `chrome://tracing/` で `v8.wasm` カテゴリを有効にすることで、より詳細に測定することもできます。Liftoffのコンパイルは、コンパイルの開始から `wasm.BaselineFinished` イベントまでの時間であり、TurboFanのコンパイルは `wasm.TopTierFinished` イベントで終了します。コンパイル自体は、 `WebAssembly.compileStreaming()` の場合は `wasm.StartStreamingCompilation` イベントで、 `new WebAssembly.Module()` の場合は `wasm.SyncCompile` イベントで、 `WebAssembly.compile()` の場合は `wasm.AsyncCompile` イベントでそれぞれ開始されます。Liftoffのコンパイルは `wasm.BaselineCompilation` イベントで示され、TurboFanのコンパイルは `wasm.TopTierCompilation` イベントで示されます。上の図は、Google Earth用に記録されたトレースを示しており、主要なイベントが強調表示されています。

より詳細なトレースデータは、 `v8.wasm.detailed` カテゴリで使用できます。これには、単一関数のコンパイル時間などが含まれます。