WebAssembly を速くするには?
例えば React に取り組んでいるチームは、調整コード (別名仮想 DOM) を WebAssembly バージョンに置き換えることができます。React を使う人は WebAssembly を使用するメリットを除いて、これらのアプリはまったく同じように動作し、何もする必要はありません。
React チームのメンバーのような開発者がこの転換を行う理由は、WebAssembly が高速であるためです。しかし何が高速にしているのでしょうか?
JavaScript のパフォーマンスは現在どうなっているのか?
JavaScript と WebAssembly の間のパフォーマンスの差を理解する前に、私たちは JS エンジンがどのように動くかを理解する必要があります。
この図は今日のアプリケーションの起動時のパフォーマンスがどうなっているかの概略図です。
JS エンジンがこれらのタスクを実行する時間は、ページが使用する JavaScript により異なります。 この図は正確なパフォーマンスの数値を表すものではありません。代わりに JS と WebAssembly で同じ機能のパフォーマンスがどのように異なるかについて、高度なモデルを提供することを目的としています。
各バーには、特定のタスクを行うのに費やされた時間が表示されます。
- 構文解析 — ソースコードをインタプリタが実行できるものに処理するまでにかかる時間。
- コンパイル + 最適化 — ベースラインコンパイラと最適化コンパイラで費やされる時間。最適化コンパイラの作業の幾つかはメインスレッドに含まれていないため、ここでは加えていません。
- 再最適化 — JITが前提条件が満たされていないときに再調整を行う時間。コードの再最適化と最適化されたコードからのベースラインコードへの復帰。
- 実行 — コードを実行するのにかかる時間。
- ガベージコレクション — メモリのクリーンアップに費やされた時間。
注目すべき重要な点:これらのタスクが個別のチャンクまたは特定の順序で実行されないことです。代わりにインターリーブされます。ほんの少しの解析が行われ、次にいくつかの実行が行われ、次にコンパイルされ、さらに解析され、さらに実行されるなどします。
このような内訳によるパフォーマンスのは JavaScript の初期段階からの大きく改善され、以下のようになっています:
初期の JavaScript 実行時の変換処理だけだった頃は実行速度は非常に遅かったです。JIT が導入されると、実行時間が大幅に短縮されました。
トレードオフはコードの監視とコンパイルです。もし JavaScript の開発者が今までと同じように JavaScript を書き続けた場合、解析とコンパイル時間が非常に短くなります。改善されたパフォーマンスにより開発者はより大きな JavaScript アプリケーションを作成することができるようになりました。
これはまだ改善の余地があることを意味します。
WebAssembly をどのように比較しますか?
典型的な Web アプリケーションにおける WebAssembly の比較方法を概説します。
ブラウザがこれらのフェーズをどのように処理するかは、ブラウザにより若干の違いがあります。ここでは SpiderMonkey を使用します。
フェッチ
これは図には表示していませんが、時間がかかるのはサーバーからファイルを取得することだけです。
WebAssembly は JavaScript よりもコンパクトなため、フェッチはより速くなります。コンパクションアルゴリズムは JavaScript バンドルのサイズを大幅に縮小することができますが、WebAssembly の圧縮バイナリ表現をまだまだ小さくすることは可能です。
これはサーバーとクライアント間の転送時間をより短くできることを意味します。これは特に低速ネットワークでは当てはまります。
解析
ブラウザへ届くと JavaScript ソースが抽象構文木に解析されます。
ブラウザはたいていこれを遅延させ、最初は本当に必要なものを解析し、まだ呼び出されていない関数のスタブを作成するだけです。
そこから、AST はその JS エンジンに固有の中間表現 (バイトコードと呼ばれる) に変換されます。
対照的に、WebAssembly はすでに中間表現であるため、変換を行う必要はありません。デコードしてエラーがないか検証するのみです。
コンパイル + 最適化
JIT についての記事で説明したように、JavaScript はコードの実行時にコンパイルされます。実行時に使用されるランタイムに応じて、同じコードの複数のバージョンをコンパイルする必要があります。
ブラウザが異なると WebAssembly のコンパイル方法も異なります。WebAssembly を実行する前にベースラインのコンパイルを行うブラウザもあれば、JIT を使用するものもあります。
どちらにしても、WebAssembly は機械語に非常に近い状態で始まります。例えば、型はプログラムの一部です。これはいくつかの理由により高速です:
- コンパイラは最適化されたコードのコンパイルを開始する前に、コードにどの型を使用されているかを判断するために時間を費やす必要はありません。
- コンパイラは異なる種類のコードに基づいて同じコードの異なるバージョンをコンパイルする必要はありません。
- LLVM では、より多くの最適化が事前に行われています。それによりコンパイルと最適化に必要な作業が少なくて済みます。
再最適化
場合によっては、JIT は最適化されたバージョンのコードをスローして再試行する必要があります。
これは、JIT が実行中のコードに基づいて行う前提が正しくないことが判明した時に発生します。例えばループに入ってくる変数が以前の反復時の変数と異なる場合や、新しい関数がプロトタイプチェーンに挿入された場合に、最適化解除が実施されます。
最適化解除には二つのコストがあります。一つ目は最適化されたコードを取り除いてベースラインのバージョンに戻すのに時間がかかります。二つ目はその関数が依然として多く呼び出されている場合、JIT は最適化コンパイラを介して再度送信する場合があります。そのため2回コンパイルするコストがあります。
WebAssembly では、型のようなものは明白であるため、JIT は実行時に収集するデータに基づいた型の仮定を行う必要はありません。つまり再最適化サイクルを行う必要はありません。
実行
効率よく実行可能な JavaScript を書くことができます。これを行うには JIT が行う最適化について知る必要があります。例えば JIT の記事で説明したように、コンパイラが行う型の特殊化が行う事ができるようなコードを書く方法を知る必要があります。
しかしほとんどの開発者が JIT の内部構造は知りません。このような JIT の内部構造を知っている開発者であってもスイートスポットをヒットすることは難しいでしょう。コードをより読みやすくするために使用する多くのコーディングパターン (一般的なタスクをタイプ間で機能する関数に抽象化するなど) は、コンパイラーがコードを最適化するときに邪魔になります。
さらに、JIT が使用する最適化はブラウザによって異なっているため、あるブラウザの内部構造に合わせてコーディングすると、別のブラウザのでのードのパフォーマンスが低下する可能性があります。
このため、WebAssembly でコードを実行する方が一般的に高速です。WebAssembly では JIT が JavaScript に行う最適化の多く (型の特殊化など) は必要ありません。
また、WebAssembly はコンパイラのターゲットとして設計されています。これはコンパイラーが生成するように設計されており、人間のプログラマーが書くようには設計されていません。
人間のプログラマーは直接 WebAssembly を直接プログラミングする必要が無いため、WebAssembly は機械にとって理想的な一連の命令を提供できます。コードが動作する内容次第で、これらの命令は 10% から 800% 速く実行することができます。
ガベージコレクション
JavaScript では開発者は必要なくなった古い変数をメモリからクリアすることについて心配する必要はありません。代わりに JS エンジンは自動的にガベージコレクタと呼ばれるものを使用します。
ただし予測可能なパフォーマンスが必要な場合は問題になる可能性があります。ガベージコレクタがいつ動作するのかは制御できないため、都合の悪い時間に実施することがあります。ほとんどのブラウザではスケジューリングをかなりうまく行っていますが、コードの実行の途中でオーバーヘッドになることがあります。
少なくとも今のところ、WebAssembly はガベージコレクションをサポートしていません。メモリは C や C++ のように手動で管理されます。 これは開発者にとってプログラミングを難しくする一方で、パフォーマンスの一貫性も向上させます。
結論
WebAssembly は JavaScript よりも多くのケースで高速です:
- WebAssembly のフェッチは圧縮された状態であっても JavaScript より時間がかかりません。
- WebAssembly のデコードは JavaScript を解析するよりも時間がかかりません。
- WebAssembly は JavaScript よりも機械語に近いため、コンパイルと最適化に要する時間が短縮され、すでにサーバー側で最適化が行われています。
- WebAssembly には型やその他の情報が組み込まれているため、再最適化を行う必要はありません。そのため JS エンジンは JavaScript を使用方法を最適化するときに推測を行う必要はありません。
- 開発者が効率良いコードを書くために知っておく必要があるコンパイラのトリックや落とし穴が少ないために、実行時間は短く済み、また WebAssembly の命令セットが機械にとってより理想的です。
- ガベージコレクションはメモリーを手動で管理するため必要としません。
このため、多くの場合、WebAssembly は同じタスクを実行するときに JavaScript より優れたパフォーマンスを発揮します。
WebAssembly が期待通りの性能を発揮しない場合もありますし、より速くする範囲にもいくつかの変更があります。水平方向にもいくつかの変更があり、高速化する場合もあります。次の記事でこれらをカバーします。
AnonymousCoward :