WebAssemblyはどのようにJavaScriptを高速に実行できるのか
(bytecodealliance.org)イントロ
-
ブラウザでJSを動かす場合、ブラウザのJSエンジンがよくチューニングされているため実行が速いが、最近ではほかの環境でもJSが多く使われている。(サーバーレス、ゲーム機、iOSなど)
-
WASMは、こうしたランタイムでJSを高速に動かせるようにする技術である。
動作方式
-
JSコードは、JSエンジンがあればインタープリタやJITコンパイラなどを通じてバイトコードに変換される。
-
JSエンジンがない環境では、JSエンジンをコードと一緒に配布しなければならないが、JSエンジンをWASMモジュールとして配布することで、さまざまな環境でポータブルにできる。
-
JSコードは、WASMエンジン内に隔離されたJSエンジンの中で動作する。
-
WASMエンジンが使用するJSエンジンはSpiderMonkeyで、Firefoxもこれを使用している。
-
WASMはそれ自体でマシンコードを生成できないため、JSへのコンパイルを経る必要がある。
-
ただしJITは使えないので、WASMが遅いのは当然である。では、WASMはいったいどのようにしてJS実行を「高速化」するのだろうか?
どこでWASMを使うのか
iOS(またはJITが使えない)環境でJSを使う
- ゲーム機、権限のないiOSアプリ、スマートTVなどでは、セキュリティ上の理由でJITを使えない。
(→ JITコンパイルにセキュリティ上の問題があるのが当然であるかのように語られているが、その理由は調べてもよく分かりませんでした。)
- したがってこうした環境ではインタープリタを使う必要があるが、実際には、もともとこうしたプラットフォームで動くアプリは非常に長時間動作し、コード量も多いため、インタープリタによる低速化は避けるべきである。
- インタープリタの性能低下の問題を避けつつJSを使うには、どうすればよいのだろうか?
サーバーレスでJSを使う
- サーバーレス環境ではJIT自体は存在するが、コールドスタート時間が長く、レイテンシが大きくなるのが問題である。(エンジンのロードだけでも最低5 ms)
- コールドスタート時間を隠す最適化手法もあるが、ネットワーク層が改善されるほど(e.g., QUIC)その意味は薄れ、さらに複数のサーバーレス関数を同時に実行すると最適化手法はあまり役に立たなくなる。
- インスタンスの再利用でコールドスタート時間を避けることもできるが、これはリクエスト間で状態が共有されることを意味し、セキュリティリスクになる。
- こうした理由から、実務ではベストプラクティスに従わず、1つのサーバーレス関数に多くの内容を詰め込むことも増えている。
- つまりコールドスタート問題さえ解決できれば、それを避けるためのさまざまな手法を使う必要もなくなり、多くの問題が解決する。
- WASMはJSを包み込んで隔離しており、WASM自体のコードは短く単純なので監査もしやすく、セキュリティリスクも減らせる。
JSエンジンはどこに多くの時間を使うのか
初期化フェーズ
- (engine初期化)サーバーレスに相当する。自分自身を準備し、ビルトイン関数を環境に追加しなければならない。これがサーバーレスのコールドスタートが遅い理由の1つである。
- (application初期化)関数をバイトコードにパースし、変数にメモリを割り当て、変数に値を代入する
ランタイムフェーズ
- ここから先のthroughputは、さまざまな条件の影響を受ける。
- どの言語機能が使われているか
- JSエンジンの観点から見てコードが予測可能に振る舞うかどうか
- どのようなデータ構造が使われているか
- JSエンジンの最適化コンパイラの恩恵を受けられるほど十分に長くコードが動作するかどうか
JSエンジンを高速化するというのは、初期化フェーズとランタイムフェーズの両方を高速化するということである。正確には、初期化にかかる時間を減らし、ランタイムではスループット、つまりコードの処理速度を高める。
初期化時間を減らす
-
WASMはWizerというpre-initializerを使って初期化時間を短縮する。(小さなアプリ基準では、JS isolateに比べてJS on WASMはおよそ13倍速い)
-
コードを配布する前のビルド段階で、pre-initializerはすべてのJSコードを一度、初期化段階まで実行してみる。
-
こうすると、JSエンジンのリニアメモリにはJSコードがバイトコードとして保存され、メモリ割り当ても完了した状態になる。
-
これをそのままコピーしてWASMのデータセクションに貼り付ける。
-
-
JSエンジンがinstantiateされるとき、データセクション内のすべてのデータにアクセスできる。特定のメモリが必要であればデータセクションからコピーしてくればよい。したがって起動時間は不要になり、これをpre-initializationと呼ぶ。
-
現在はJSエンジンと同じモジュールにデータセクションを付けているが、将来的にはmodule linkingを利用してデータセクションを別モジュールにし、複数のアプリケーションがJSエンジンを共有できるようにする計画である。
-
そして実際、このpre-initialization技法はJSエンジンに限定される必要はなく、Python、Ruby、Luaなど、どのランタイムにも使える概念である。
スループットを高める
-
JSコードが短時間しか実行されないのであれば、どうせJITを経由しないため、WASMのスループットもブラウザと同じになるだろう。しかし長時間実行されるコードでは、JITの有無が生むスループット差は大きい。
-
WASMはJITを使えないため、その代わりにAOT(ahead-of-time)コンパイルを行いながら、JITから取り入れられる手法は取り入れる方針を採っている。
-
JITの最適化技法の1つがインラインキャッシュである。過去に実行されたコード片を保持しておき、再利用するものだ。
-
WASMでは、JSで頻繁に使われるパターンをstubとして用意している。たとえばオブジェクトのプロパティにアクセスすること。
-
本来、オブジェクトのプロパティアクセスを正しく行うにはshapeとoffsetの情報が必要だが、これらはAOTでは分からない。
-
しかしshapeとoffsetをパラメータとしてプロパティにアクセスするstubは、あらかじめ作成しておける。このstubコードは複数箇所で再利用可能である。
-
-
WASMはこうしたcommon patternsをすべてstubとして用意している。これはJSコードが実際にどのような形であっても関係ない。これにより、JSエンジンが生成するマシンコードを減らし、初期化時間を短縮し、キャッシュ局所性も改善できる。
-
このようなstubを2kb分用意するだけでも、実際のJSコードの約95%をカバーできることが確認された。
-
この手法はahead-of-time、つまりコード内容を知らないまま(プロファイリングなしで)最適化するものなので、さらにプロファイリングを行えば、JITのようにより最適化できる余地があるだろう。
- ただしプロファイリング自体が簡単ではないため、現在取り組んでいるところである。
2件のコメント
JITのセキュリティ問題については、以前ここで紹介されていた MS Edge チームのブログ記事でも関連する内容が言及されていました。基本的に JIT エンジンは複雑であるため攻撃対象領域が広がるだけでなく、JIT で性能向上のために適用される投機的最適化(Speculative Optimization)のような手法が、特定のパターンのセキュリティ問題を繰り返し発生させる傾向があるようです。このため、Web ブラウザーのセキュリティ欠陥のうち JIT 関連のセキュリティ欠陥の割合はかなり高いとされています。
https://ja.news.hada.io/topic?id=4771
https://microsoftedge.github.io/edgevr/posts/Super-Duper-Secure-Mode/
https://docs.google.com/spreadsheets/d/…
ああ、ありがとうございます! 肝心のGeekNewsを見ていませんでしたね