WebAssembly を速くするには?

この記事は WebAssembly と何が速くしたのかのシリーズの5部です。もしまだ前の記事を読んでいない場合、最初から読むことをお勧めします。前の記事では WebAssembly や JavaScript を使ったプログラミングは、どちらか一方を選択するものではないことを説明しました。多くの開発者が完全な WebAssembly コードベースを記述しているとは考えていません。開発者はアプリケーション用に WebAssembly と JavaScript のどちらかを選択する必要はありません。 しかし開発者は JavaScript コードの一部を WebAssembly に取り替えようとしています。

例えば React に取り組んでいるチームは、調整コード (別名仮想 DOM) を WebAssembly バージョンに置き換えることができます。React を使う人は WebAssembly を使用するメリットを除いて、これらのアプリはまったく同じように動作し、何もする必要はありません。

React チームのメンバーのような開発者がこの転換を行う理由は、WebAssembly が高速であるためです。しかし何が高速にしているのでしょうか?

JavaScript のパフォーマンスは現在どうなっているのか?

JavaScript と WebAssembly の間のパフォーマンスの差を理解する前に、私たちは JS エンジンがどのように動くかを理解する必要があります。

この図は今日のアプリケーションの起動時のパフォーマンスがどうなっているかの概略図です。

JS エンジンがこれらのタスクを実行する時間は、ページが使用する JavaScript により異なります。 この図は正確なパフォーマンスの数値を表すものではありません。代わりに JS と WebAssembly で同じ機能のパフォーマンスがどのように異なるかについて、高度なモデルを提供することを目的としています。

Diagram showing 5 categories of work in current JS engines

各バーには、特定のタスクを行うのに費やされた時間が表示されます。

  • 構文解析 — ソースコードをインタプリタが実行できるものに処理するまでにかかる時間。
  • コンパイル + 最適化 — ベースラインコンパイラと最適化コンパイラで費やされる時間。最適化コンパイラの作業の幾つかはメインスレッドに含まれていないため、ここでは加えていません。
  • 再最適化 — JITが前提条件が満たされていないときに再調整を行う時間。コードの再最適化と最適化されたコードからのベースラインコードへの復帰。
  • 実行 — コードを実行するのにかかる時間。
  • ガベージコレクション — メモリのクリーンアップに費やされた時間。

注目すべき重要な点:これらのタスクが個別のチャンクまたは特定の順序で実行されないことです。代わりにインターリーブされます。ほんの少しの解析が行われ、次にいくつかの実行が行われ、次にコンパイルされ、さらに解析され、さらに実行されるなどします。

このような内訳によるパフォーマンスのは JavaScript の初期段階からの大きく改善され、以下のようになっています:

Diagram showing 3 categories of work in past JS engines (parse, execute, and garbage collection) with times being much longer than previous diagram

初期の JavaScript 実行時の変換処理だけだった頃は実行速度は非常に遅かったです。JIT が導入されると、実行時間が大幅に短縮されました。

トレードオフはコードの監視とコンパイルです。もし JavaScript の開発者が今までと同じように JavaScript を書き続けた場合、解析とコンパイル時間が非常に短くなります。改善されたパフォーマンスにより開発者はより大きな JavaScript アプリケーションを作成することができるようになりました。

これはまだ改善の余地があることを意味します。

WebAssembly をどのように比較しますか?

典型的な Web アプリケーションにおける WebAssembly の比較方法を概説します。

Diagram showing 3 categories of work in WebAssembly (decode, compile + optimize, and execute) with times being much shorter than either of the previous diagrams

ブラウザがこれらのフェーズをどのように処理するかは、ブラウザにより若干の違いがあります。ここでは SpiderMonkey を使用します。

フェッチ

これは図には表示していませんが、時間がかかるのはサーバーからファイルを取得することだけです。

WebAssembly は JavaScript よりもコンパクトなため、フェッチはより速くなります。コンパクションアルゴリズムは JavaScript バンドルのサイズを大幅に縮小することができますが、WebAssembly の圧縮バイナリ表現をまだまだ小さくすることは可能です。

これはサーバーとクライアント間の転送時間をより短くできることを意味します。これは特に低速ネットワークでは当てはまります。

解析

ブラウザへ届くと JavaScript ソースが抽象構文木に解析されます。

ブラウザはたいていこれを遅延させ、最初は本当に必要なものを解析し、まだ呼び出されていない関数のスタブを作成するだけです。

そこから、AST はその JS エンジンに固有の中間表現 (バイトコードと呼ばれる) に変換されます。

対照的に、WebAssembly はすでに中間表現であるため、変換を行う必要はありません。デコードしてエラーがないか検証するのみです。

Diagram comparing parsing in current JS engine with decoding in WebAssembly, which is shorter

コンパイル + 最適化

JIT についての記事で説明したように、JavaScript はコードの実行時にコンパイルされます。実行時に使用されるランタイムに応じて、同じコードの複数のバージョンをコンパイルする必要があります。

ブラウザが異なると WebAssembly のコンパイル方法も異なります。WebAssembly を実行する前にベースラインのコンパイルを行うブラウザもあれば、JIT を使用するものもあります。

どちらにしても、WebAssembly は機械語に非常に近い状態で始まります。例えば、型はプログラムの一部です。これはいくつかの理由により高速です:

  1. コンパイラは最適化されたコードのコンパイルを開始する前に、コードにどの型を使用されているかを判断するために時間を費やす必要はありません。
  2. コンパイラは異なる種類のコードに基づいて同じコードの異なるバージョンをコンパイルする必要はありません。
  3. LLVM では、より多くの最適化が事前に行われています。それによりコンパイルと最適化に必要な作業が少なくて済みます。

Diagram comparing compiling + optimizing, with WebAssembly being shorter

再最適化

場合によっては、JIT は最適化されたバージョンのコードをスローして再試行する必要があります。

これは、JIT が実行中のコードに基づいて行う前提が正しくないことが判明した時に発生します。例えばループに入ってくる変数が以前の反復時の変数と異なる場合や、新しい関数がプロトタイプチェーンに挿入された場合に、最適化解除が実施されます。

最適化解除には二つのコストがあります。一つ目は最適化されたコードを取り除いてベースラインのバージョンに戻すのに時間がかかります。二つ目はその関数が依然として多く呼び出されている場合、JIT は最適化コンパイラを介して再度送信する場合があります。そのため2回コンパイルするコストがあります。

WebAssembly では、型のようなものは明白であるため、JIT は実行時に収集するデータに基づいた型の仮定を行う必要はありません。つまり再最適化サイクルを行う必要はありません。

Diagram showing that reoptimization happens in JS, but is not required for WebAssembly

実行

効率よく実行可能な JavaScript を書くことができます。これを行うには JIT が行う最適化について知る必要があります。例えば JIT の記事で説明したように、コンパイラが行う型の特殊化が行う事ができるようなコードを書く方法を知る必要があります。

しかしほとんどの開発者が JIT の内部構造は知りません。このような JIT の内部構造を知っている開発者であってもスイートスポットをヒットすることは難しいでしょう。コードをより読みやすくするために使用する多くのコーディングパターン (一般的なタスクをタイプ間で機能する関数に抽象化するなど) は、コンパイラーがコードを最適化するときに邪魔になります。

さらに、JIT が使用する最適化はブラウザによって異なっているため、あるブラウザの内部構造に合わせてコーディングすると、別のブラウザのでのードのパフォーマンスが低下する可能性があります。

このため、WebAssembly でコードを実行する方が一般的に高速です。WebAssembly では JIT が JavaScript に行う最適化の多く (型の特殊化など) は必要ありません。

また、WebAssembly はコンパイラのターゲットとして設計されています。これはコンパイラーが生成するように設計されており、人間のプログラマーが書くようには設計されていません。

人間のプログラマーは直接 WebAssembly を直接プログラミングする必要が無いため、WebAssembly は機械にとって理想的な一連の命令を提供できます。コードが動作する内容次第で、これらの命令は 10% から 800% 速く実行することができます。
Diagram comparing execution, with WebAssembly being shorter

ガベージコレクション

JavaScript では開発者は必要なくなった古い変数をメモリからクリアすることについて心配する必要はありません。代わりに JS エンジンは自動的にガベージコレクタと呼ばれるものを使用します。

ただし予測可能なパフォーマンスが必要な場合は問題になる可能性があります。ガベージコレクタがいつ動作するのかは制御できないため、都合の悪い時間に実施することがあります。ほとんどのブラウザではスケジューリングをかなりうまく行っていますが、コードの実行の途中でオーバーヘッドになることがあります。

少なくとも今のところ、WebAssembly はガベージコレクションをサポートしていません。メモリは C や C++ のように手動で管理されます。 これは開発者にとってプログラミングを難しくする一方で、パフォーマンスの一貫性も向上させます。

Diagram showing that garbage collection happens in JS, but is not required for WebAssembly

結論

WebAssembly は JavaScript よりも多くのケースで高速です:

  • WebAssembly のフェッチは圧縮された状態であっても JavaScript より時間がかかりません。
  • WebAssembly のデコードは JavaScript を解析するよりも時間がかかりません。
  • WebAssembly は JavaScript よりも機械語に近いため、コンパイルと最適化に要する時間が短縮され、すでにサーバー側で最適化が行われています。
  • WebAssembly には型やその他の情報が組み込まれているため、再最適化を行う必要はありません。そのため JS エンジンは JavaScript を使用方法を最適化するときに推測を行う必要はありません。
  • 開発者が効率良いコードを書くために知っておく必要があるコンパイラのトリックや落とし穴が少ないために、実行時間は短く済み、また WebAssembly の命令セットが機械にとってより理想的です。
  • ガベージコレクションはメモリーを手動で管理するため必要としません。

このため、多くの場合、WebAssembly は同じタスクを実行するときに JavaScript より優れたパフォーマンスを発揮します。

WebAssembly が期待通りの性能を発揮しない場合もありますし、より速くする範囲にもいくつかの変更があります。水平方向にもいくつかの変更があり、高速化する場合もあります。次の記事でこれらをカバーします。

Lin Clark に関して

Lin は Mozilla Developer Relations チームのエンジニアです。 彼女は JavaScript、WebAssembly、Rust、Servo を使っています。また、コードの漫画を描きます。

Lin Clark によるその他の記事はこちら…

1 件のコメント

  1. AnonymousCoward :

    △ 高度なモデル
    モデルが優れているように読めてしまう。
    → 高水準なモデル

    × JavaScript の初期段階からの大きく改善され、以下のようになっています
    逆。
    → 以下のようであったJavaScriptの初期段階から大きく改善されています

    × JavaScript 実行時の変換処理だけだった頃
    意訳? にしても意味不明。
    → JavaScriptを走らせているのがただのインタープリターであった頃

    × 改善されたパフォーマンスにより~
    butが抜けている。
    → しかし、改善されたパフォーマンスにより~

    × WebAssembly の圧縮バイナリ表現をまだまだ小さくすることは可能です。
    違う。
    → WebAssembly の圧縮バイナリ表現はさらに小さいです。

    △ 抽象構文木 … AST
    同じものを挿しているとわからないのだからすべて抽象構文木で通すかあるいは初出に略語を付けるべき。

    × コードをスローして再試行
    そのthrowじゃない。
    → コードを捨てて再試行

    × JS エンジンは JavaScript を使用方法を最適化するときに推測を行う必要はありません。
    日本語がおかしいし、何を言っているのかわからない。
    → JSエンジンは最適化する時、JavaScriptで行うようなやり方で推測を行う必要はありません。