WebAssembly
序章
WebAssembly はバイナリ命令形式で、開発者がJavaScript以外の言語で書かれたコードをコンパイルし、効率的で移植性の高いパッケージでWebに持ってくることを可能にするものです。既存のユースケースは、再利用可能なライブラリやコーデックから完全なGUIアプリケーションまで多岐にわたります。2017年から4年間、すべてのブラウザで利用できるようになり、それ以来、採用が進み、今年からWeb Almanacで利用状況を追跡する良いタイミングだと判断しました。
方法論
この分析では、2021-09-01にHTTP Archiveをクロールし、Content-Type
(application/wasm
) またはファイル拡張子 (.wasm
) にマッチするすべてのWebAssemblyレスポンスを選択しました。そして、それらすべてをダウンロードし、その過程でさらにURL、レスポンス サイズ、圧縮前のサイズ、コンテンツ ハッシュをCSVファイルに保存する script を使用しました。サーバーエラーで何度も応答が得られないリクエストや、コンテンツが実際にWebAssemblyらしくないものは除外しました。たとえば、いくつかの Blazor ウェブサイトでは、.NET DLLs に Content-Type: application/wasm
を付けて提供していましたが、これらは実際にはフレームワークコアでパースされるDLLであって、WebAssemblyモジュールではありません。
WebAssemblyのコンテンツ解析では、BigQueryを直接利用することができませんでした。その代わりに、与えられたディレクトリ内のすべてのWebAssemblyモジュールを解析し、カテゴリごとの命令数、セクションサイズ、インポート/エクスポート数などを収集し、すべての統計情報を stats.json
ファイルに格納する ツール を作成しました。前のステップでダウンロードしたファイルのあるディレクトリで実行した後、結果のJSONファイルを BigQueryにインポートし、対応する summary_requests
と summary_pages
テーブルと一緒に httparchive.almanac.wasm_stats
へ結合して、それぞれのレコードが自己完結しWebAssembly要求、応答、モジュールコンテンツに関するすべての必要情報を含むようにします。この最終的なテーブルは、本章のすべての分析に使用されました。
クローラーリクエストを解析のソースとして使用することは、この記事の数字を見る際に注意すべきトレードオフがあります。
- まず、ユーザーとのインタラクションをきっかけに発生するリクエストの情報がありませんでした。ページロード中に収集されたリソースのみを対象としました。
- 次に、ウェブサイトには人気のあるものとそうでないものがありますが、正確な訪問者データがなかったため、それを考慮せず、検出されたWasmの利用状況をそれぞれ同等に扱いました。
- 最後に、サイズのようなグラフでは、ユニークなファイルだけを比較するのではなく、複数のウェブサイトで使用されている同じWebAssemblyモジュールをユニークな使用量としてカウントしています。これは、ライブラリ同士を比較するのではなく、Webページ全体におけるWebAssemblyの使用状況の全体像にもっとも興味があるためです。
これらのトレードオフは他の章で行われた分析ともっとも一致していますが、もし他の統計を集めることに興味があるなら、httparchive.almanac.wasm_stats
のテーブルに対して独自のクエリを実行することを歓迎します。
モジュール数は?
デスクトップで3854、モバイルで3173のWebAssemblyリクエストが確認されました。これらのWasmモジュールはデスクトップでは2724ドメイン、モバイルでは2300ドメインで使用されており、デスクトップとモバイルの全ドメインのそれぞれ0.06%と0.04%に相当します。
興味深いことに、もっとも人気のあるMIMEタイプを見ると、Content-Type: application/wasm
が圧倒的に人気がある一方で、すべてのWasmレスポンスをカバーしているわけではないことがわかります。.wasm
の拡張子を持つ他のURLも含めてよかったです。
そのうちのいくつかは application/octet-stream
という任意のバイナリデータの一般的な型を使い、いくつかは Content-Type
ヘッダーを持たず、他のものはplainやHTMLなどのテキスト型、あるいは binary/octet-stream
のように不正な型を使っています。
WebAssemblyの場合、正しい Content-Type
ヘッダーを提供することは、セキュリティ上の理由だけでなく、WebAssembly.compileStreaming
や WebAssembly.instantiateStreaming
によるストリーミング、コンパイルやインスタンス化の高速化も可能になるので、重要です。
Wasmのライブラリはどのくらいの頻度で再利用されているのでしょうか?
また、これらの応答をダウンロードする際にその内容をハッシュ化し、そのハッシュをディスク上のファイル名として使用することで、重複排除を行いました。その後、デスクトップでは656個、モバイルでは534個の一意のWebAssemblyファイルが残りました。
ユニークなファイルの数と総レスポンス数の差が激しいことから、さまざまなWebサイトでWebAssemblyライブラリが高度に再利用されていることがすでに示唆されています。クロスオリジン/同一オリジンのWebAssemblyリクエストの分布を見ると、さらに確認できます。
では、その再利用されるライブラリは何なのか、もっと掘り下げて考えてみましょう。まず、コンテンツ・ハッシュだけでライブラリの重複排除を試みましたが、残ったものの多くが、ライブラリのバージョンだけが異なる重複ライブラリであることがすぐに判明しました。
そこで、URLからライブラリ名を抽出することにしました。名前の衝突の可能性があるため、理論的にはより問題がありますが、実際にはトップライブラリのためのより信頼性の高いオプションであることが判明しました。URLからファイル名を抽出し、拡張子、マイナーバージョン、コンテンツハッシュのように見えるサフィックスを削除し、結果を繰り返し回数でソートして、各クライアントの上位10個のモジュールを抽出しました。残ったモジュールについては、どのライブラリから来たものなのか、手作業で調べました。
デスクトップとモバイルの両方で使用されるWebAssemblyのほぼ3分の1は、Amazon Interactive Video Service プレーヤー ライブラリに属しています。オープンソースではありませんが、関連するJavaScriptのグルーコードを調べると、Emscripten でビルドされていることがわかります。
次に、Hyphenopolyは、さまざまな言語のテキストをハイフンでつなぐためのライブラリで、デスクトップとモバイルでそれぞれ13%と19%のWasmリクエストを占めています。JavaScriptとAssemblyScriptで構築されています。
デスクトップとモバイルのトップ10リストにある他のライブラリは、それぞれWebAssemblyリクエストの最大5%を占めています。上記のライブラリの完全なリストと、推測されるツールチェーン、および詳細情報を含む対応するホームページへのリンクはこちらです。
- Amazon IVS (Emscripten)
- Hyphenopoly (AssemblyScript)
- Blazor (.NET)
- ArcGIS (Emscripten)
- Draco (Emscripten)
- CanvasKit (Emscripten)
- Playa Games (Unity via Emscripten)
- Tableau (Emscripten)
- Xat (Emscripten)
- Tencent Video (Emscripten)
- Nimiq (Emscripten)
- Scandit (Emscripten)
方法論と結果について、もう少しだけ注意点があります。
- Hyphenopolyはさまざまな言語の辞書を小さなWebAssemblyファイルとしてロードしますが、これらは技術的に別のライブラリではなく、Hyphenopoly自体のユニークな使用法でもないため、上のグラフからは除外しています。
- Playa GamesのWebAssemblyファイルは、似たようなドメインでホストされている同じゲームで使用されているようです。私たちは、クエリでそれらを個々の使用として数えますが、リストの他の項目とは異なり、再利用可能なライブラリとして数えるべきかどうかは明らかではありません。
送るサイズはどのくらいですか?
WebAssemblyにコンパイルされた言語は、通常、独自の標準ライブラリを持っています。言語によってAPIや値の型が大きく異なるため、JavaScriptのビルトインを再利用することができないのです。その代わりに、独自のコードだけでなく、標準ライブラリのAPIもコンパイルして、単一のバイナリとしてまとめてユーザーに提供する必要があります。その結果、ファイルサイズはどうなるのだろうか?見てみよう。
サイズもさまざまで、単純なヘルパーライブラリからWebAssemblyでコンパイルされた完全なアプリケーションまで、さまざまな種類のコンテンツをきちんとカバーしていることが分かります。
最大で81MBのサイズが確認され、かなり気になるところですが、これは非圧縮のレスポンスであることを念頭に置いてください。RAMの使用量や起動時のパフォーマンスも重要ですが、Wasmバイトコードの利点の1つは高圧縮であり、ダウンロード速度や課金上の理由から、通信上のサイズが重要になります。
代わりに、サーバーから送信された生のレスポンスボディのサイズを確認してみましょう。
中央値は約290KBで、半分が290KB以下、半分がそれ以上のダウンロード量ということになります。Wasmの全レスポンスの90%は、デスクトップで2.6MB未満、モバイルで1.4MB未満に留まっています。
HTTP Archiveの最大レスポンスでは、デスクトップで約44MB、モバイルで約28MBのWasmがダウンロードされます。
圧縮しても、世界の多くの地域ではまだ高速インターネット接続ができないことを考えると、この数字はかなり極端です。アプリケーションやライブラリの範囲を狭める以外に、Webサイトがこの数字を改善するためにできることはあるのでしょうか?
Wasmは、実際どのように圧縮されているのですか?
まず、Content-Encoding
ヘッダーに基づいて、これらの生のレスポンスで使用される圧縮方法について見てみましょう。モバイルでは帯域幅がさらに重要なので、ここではモバイルのデータセットを表示しますが、デスクトップの数値もかなり似ています。
残念ながら、モバイルにおけるWebAssemblyのレスポンスの40%は、圧縮されずに送信されていることがわかります。
また、46%はgzipを使用しています。gzipは長い間、ウェブにおける事実上の標準的な方法であり、今でもきちんとした圧縮率を提供していますが、今日のベストアルゴリズムとは言えません。最後に、14% のみがBrotliを使用しています。この最新の圧縮形式はさらに優れた比率を提供し、 すべてのモダン ブラウザでサポートされています。実際、BrotliはWebAssemblyをサポートしているすべてのブラウザでサポートされているので、これらを一緒に使用しない理由はありません。
圧縮率を改善できないか?
違いがあったのでしょうか?我々は、それを解明するために、すべてのWebAssemblyファイルをBrotli(圧縮レベル9)で再圧縮することにしました。各ファイルで使用したコマンドは
brotli -k9f some.wasm -o some.wasm.br
でき上がったサイズはこちら。
中央値はほぼ290KBからほぼ240KBに下がり、これはもうかなり良い兆候です。上位10パーセンタイルは、2.5MB / 1.4MBから2.2MB / 0.8MBに減少しています。その他のパーセンタイルでも、大幅な改善が見られます。
パーセンタイルはその性質上、データセット間で必ずしも同じファイルに収まるとは限らないので、グラフ間で直接数値を比較し、サイズの節約を理解するのは難しいかもしれません。その代わり、これからは各最適化によってもたらされる節約額そのものを、一歩ずつ見ていくことにしましょう。
中央値は約40KBの節約となった。上位10%は、デスクトップで600KB弱、モバイルで330KBを節約しています。最大の削減量は、35MB/21MBに達します。これらの違いは、少なくともWebAssemblyコンテンツについては、可能な限りBrotli圧縮を有効にすることへ有利に働きます。
さらに興味深いことに、グラフの反対側では、もっとも節約できるはずの1.4MBまで後退していることがわかりました。何があったのでしょうか。Brotliの再圧縮が、一部のモジュールで状況を悪化させたというのは、どういうことなのでしょうか?
前述したように今回は圧縮レベル9のBrotliを使用したが、正直、この記事を書くまですっかり忘れていたのだが、レベル10と11もあるです。たとえば、Squashのベンチマークで見られるように、これらのレベルでは、パフォーマンスが急激に低下する代わりに、さらに良い結果が得られます。このようなトレードオフの関係から、一般的なオンザフライ圧縮の候補としては不利になるため、この記事では使用せず、より穏やかなレベル9を採用しました。しかしウェブサイトの作者は、静的リソースを先に圧縮したり圧縮結果をキャッシュしたりすることを選択でき、CPU時間を犠牲にすることなく、さらに帯域を節約できます。このようなケースは、分析の結果、リグレッションとして表示されます。つまりリソースは、この記事で行ったよりもさらに最適化することが可能であり、場合によっては、すでに最適化されていることもあるのです。
どの部分が一番スペースを取っているのでしょうか?
圧縮は別として、WebAssemblyバイナリのハイレベルな構造を分析することで、最適化の機会を探すことも可能でしょう。どのセクションがもっとも大きなスペースを占めているか?これを確認するために、すべてのWasmモジュールのセクションサイズを合計し、バイナリサイズ合計で割りました。ここでもモバイルデータセットの数字を使っていますが、デスクトップの数字もそれほど大きくはずれていません。
当然のことながら、バイナリサイズの大部分(~74%)はコンパイルされたコード自身によるもので、その次に埋め込まれた静的データが~19%となっています。関数型、インポート/エクスポート記述子などは、総サイズのごく一部を構成しているに過ぎません。カスタムセクションは、モバイルデータセットにおける総サイズの約6.5%を占めています。
カスタムセクションは、主にWebAssemblyのサードパーティツールのために使用されます。型結合システム、リンカー、DevToolsなどのための情報を含むかもしれません。どれも正当なユースケースですが、プロダクションコードではほとんど必要ないので、このような大きな比率は疑わしいと言えます。それでは、カスタムセクションがもっとも多いファイルトップ10にあるものを見てみましょう。
URL | カスタムセクションのサイズ | カスタムセクション |
---|---|---|
…/dotnet.wasm | 15,053,733 | name |
…/unity.wasm.br?v=1.0.8874 | 9,705,643 | name |
…/nanoleq-HTML5-Shipping.wasmgz | 8,531,376 | name |
…/export.wasm | 7,306,371 | name |
…/c0c43115a4de5de0/…/northstar_api.wasm | 6,470,360 | name , external_debug_info |
…/9982942a9e080158/…/northstar_api.wasm | 6,435,469 | name , external_debug_info |
…/ReactGodot.wasm | 4,672,588 | name |
…/v18.0-591dd9336/trace_processor.wasm | 2,079,991 | name |
…/v18.0-615704773/trace_processor.wasm | 2,079,991 | name |
…/canvaskit.wasm | 1,491,602 | name |
それらはすべて、基本的なデバッグのための関数名を含む name
セクションにほぼ限定されています。実際、データセットに目を通し続けると、それらのカスタムセクションのほとんどすべてがデバッグ情報のみを含んでいることがわかります。
デバッグ情報を削除することで、どの程度節約できるのか?
デバッグ情報はローカルでの開発には有用ですが、これらのセクションは非常に大きく、上の表では圧縮前に14MB以上あります。もしユーザが体験している問題をデバッグしたいのであれば、送信前に llvm-strip
や wasm-strip
または wasm-opt --strip-debug
を使ってデバッグ情報を取り除き、生のスタックトレースを収集し、オリジナルのバイナリを使ってローカルでソースロケーションにマッチさせるというアプローチがよいかもしれません。
このデバッグ情報を削除することで、前のステップのBrotliだけと比較して、Brotliとの組み合わせでどの程度節約できるかを確認するのは興味深いことです。しかし、データセットに含まれるほとんどのモジュールにはカスタムセクションがないので、90パーセンタイル以下は役に立たないだろう。
代わりに、カスタムセクションを持つファイルにのみ、貯蓄の分配を見てみましょう。
グラフからわかるように、いくつかのファイルのカスタムセクションは無視できるほど小さいですが、中央値は54KBで、90パーセンタイルはデスクトップで247KB、モバイルで118KBです。デスクトップとモバイルで最大のWasmバイナリで2.4MB/1.3MBの節約となり、とくに低速接続ではかなり顕著な改善となりました。
上の表から、カスタムセクションの生のサイズと比べると、その差がかなり小さいことにお気づきだろうか。その理由は、 name
セクションは、その名前が示すように、ほとんどが関数名で構成されているからです。関数名は、繰り返しの多いASCII文字列であり、そのため非常に圧縮されやすいのです。
カスタムセクションを llvm-strip
で削除する過程で、WebAssemblyモジュールに変更が加えられ、圧縮前は小さくても圧縮後はわずかに大きくなるという異常事態がいくつか発生しています。しかし、このようなケースはまれで、圧縮後のモジュールの合計サイズと比較すると、サイズの差は重要でありません。
wasm-opt
を使うとどのくらい節約できるのでしょうか?
Binaryen スイートの wasm-opt
は、結果のバイナリのサイズとパフォーマンスの両方を改善することができる強力な最適化ツールです。Emscripten、wasm-pack、AssemblyScriptなどの主要なWebAssemblyツールチェーンで使用され、基礎となるコンパイラが生成するバイナリを最適化するために使用されます。
非圧縮と圧縮の両方の実世界ベンチマークにおいて、大幅なサイズ削減を実現します。
収集したHTTPアーカイブのデータセットでも wasm-opt
の性能を確認することにしましたが、引っ掛かりがあります。
前述の通り、wasm-opt
はすでにほとんどのコンパイラツールチェーンで使用されているので、データセット内のモジュールのほとんどはすでにその結果の成果物となっています。上記の圧縮解析とは異なり、既存の最適化を逆にしてオリジナルで wasm-opt
を実行する方法はありません。その代わりに、最適化前のバイナリに対して wasm-opt
を再実行することで、結果に歪みを与えています。これは、strip-debugステップの後に生成されたバイナリに対して使用したコマンドです。
wasm-opt -O -all some.wasm -o some.opt.wasm
そして、その結果をBrotliに圧縮して、いつものように前のステップと比較しました。
このデータは実際の使用状況を代表するものではなく、一般消費者は wasm-opt
を通常通り使用すべきですが、CDNのように最適化を大規模に実行したい消費者や、バイナリエンのチーム自身には有用かもしれません。
グラフの結果はまちまちですが、どの変化も26KBまでと比較的小さいものです。もし、外れ値(0と100のパーセンタイル)を含めると、最良側でデスクトップが最大1MB、モバイルが240KB、最悪デスクトップが255KB、モバイルが175KBと、より大幅に改善されることが分かります。
ごく一部のファイルで大幅な節約ができたということは、ウェブで公開する前に最適化されていなかった可能性が高いということです。しかし、なぜ他の結果はこれほどまでにまちまちなのでしょうか?
圧縮しない場合の節約サイズを見てみると、今回のデータセットでも、wasm-opt
は一貫してファイルサイズをほぼ同じにするか、あるいはさらにサイズをわずかに向上させるケースが多く、最適化されていないファイルでは大きな節約となることがより明らかになります。
このことから、圧縮後のグラフに驚くべき分布が見られる理由はいくつか考えられます。
- 前述の通り、我々のデータセットは実際の
wasm-opt
の使用状況とは異なっており、大半のファイルはwasm-opt
によってすでに最適化されています。さらに命令の並べ替えを行い非圧縮サイズを少し改善すると、特定のパターンが他のパターンよりも圧縮されやすくなったり、されにくくなったりすることになり、その結果、統計的なノイズが発生することになります。 - 我々はデフォルトの
wasm-opt
パラメーターを使用していますが、ユーザによってはwasm-opt
フラグを微調整して、特定のモジュールに対してさらに良い節約を実現しているかもしれません。 - 前述したように、ネットワーク(圧縮)サイズがすべてではありません。WebAssemblyのバイナリが小さいと、VMでのコンパイルが速く、コンパイル中のメモリ消費量が少なく、コンパイルしたコードを保持するメモリも少なくなる傾向があります。
wasm-opt
はここでバランスを取る必要があり、圧縮されたサイズが、より良い生のサイズを優先して後退することもあります。 - 最後に、いくつかの回帰は、そのバランスを研究し改善するための貴重な例となる可能性がありそうです。私たちはBinaryenチームに それらを報告し、最適化の可能性についてより深く検討できるようにしました。
人気のあるインストラクションは何ですか?
ここまでで、Wasmをセクションごとに切り分けたときの中身を垣間見ることができました。ここでは、WebAssemblyモジュールの中でもっとも大きく、もっとも重要な部分であるコードセクションの内容をより深く見ていきましょう。
インストラクションをさまざまなカテゴリーに分け、すべてのモジュールでまとめてカウントしています。
この分布から得られる1つの驚くべきことは、ローカル変数の操作、つまり local.get
, local.set
, local.tee
が最大のカテゴリーで36%を占め、次のカテゴリーであるライン定数 (15.2%), ロード/ストア操作 (14.7%) とすべての数学および論理演算 (14.3%) をはるかに上回っていることです。ローカル変数演算は、通常、コンパイラの最適化パスの結果として生成されます。これは、高価なメモリアクセス操作を可能な限りローカル変数にダウングレードし、その後、エンジンがこれらのローカル変数をCPUレジスタに置くことで、より安価にアクセスできるようにするためです。
Wasmにコンパイルする開発者にとって実用的な情報ではありませんが、エンジンやツールの開発者にとっては、さらなるサイズ最適化の可能性がある領域として興味深いものでしょう。
MVP後の拡張機能の使い方は?
もう1つ興味深い指標は、MVP後のWasmの拡張性です。WebAssembly 1.0は数年前にリリースされましたが、今でも活発に開発されており、時間の経過とともに新しい機能が追加されて成長しています。これらの中には、共通の操作をエンジンに移すことでコードサイズを改善するもの、より強力なパフォーマンスプリミティブを提供するもの、開発者の体験やウェブとの統合を改善するものなどがあります。公式の 機能ロードマップ では、人気のあるすべてのエンジンの最新バージョンで、これらの提案のサポートを追跡しています。
Almanacのデータセットでも、その採用状況を見てみよう。
それは、sign-extension operators proposal という機能です。MVPからそれほど時間が経たないうちにすべてのブラウザに提供され、LLVM(Clang / EmscriptenやRustが使用するコンパイラバックエンド)でもデフォルトで有効になったので、その採用率の高さが理解できます。それ以外の機能は、現在のところ開発者がコンパイル時に明示的に有効にする必要があります。
たとえば、non-trapping float-to-int conversions はsign-extension operatorsと非常によく似た精神を持っています。コード サイズを多少節約するために数値型の組み込み変換も提供していますが、つい最近Safari15のリリースで統一的にサポートされました。そのため、この機能はまだデフォルトでは有効になっていません。ほとんどの開発者は、非常に説得力のある理由なしに、異なるブラウザに異なるバージョンのWebAssemblyモジュールを構築して出荷する複雑さを望んでいません。その結果、データセット内のWasmモジュールのどれもが、これらの変換を使用していませんでした。
複数値、参照型、末尾呼び出しなど、使用実績がゼロの機能も同様の状況です。これらはほとんどのWebAssemblyユースケースに恩恵をもたらす可能性がありますが、コンパイラやエンジンのサポートが不完全なためです。
残り、使用されている機能の中で、とくに興味深いのはSIMDとatomicです。どちらも、さまざまなレベルで実行を並列化し、高速化するための命令を提供します。SIMD により、一度に複数の値に対して数学演算を行うことができ、atomicにより Wasmにおけるマルチスレッドの基礎が提供されます。それらの機能はデフォルトでは有効ではなく、特定のユースケースを必要とし、とくにマルチスレッドはソースコードで特別なAPIを使用する必要があり、さらにWebサイトで使用する前 cross-origin isolated にするための追加設定も必要です。そのため、比較的低い利用レベルであることは当然ですが、時間の経過とともに成長していくことが期待されます。
結論
WebAssemblyは比較的新しく、Web上ではややニッチな参加者ですが、単純なライブラリから大規模なアプリケーションまで、さまざまなWebサイトやユースケースで採用されているのは素晴らしいことです。
実際、WebAssemblyはWebのエコシステムに非常によく統合されており、多くのWebサイトのオーナーは、WebAssemblyをすでに使っていることにさえ気づかないかもしれませんし、彼らには他のサードパーティのJavaScript依存のように見えます。
提供サイズに改善の余地があり、さらなる分析により、コンパイラやサーバーの設定を変更することで達成可能なようです。また、エンジン、ツール、CDN開発者がWebAssemblyの利用を理解し、規模に応じて最適化するのに役立つかもしれない、いくつかの興味深い統計や事例を発見しています。
Web Almanacの次版では、これらの統計を長期にわたって追跡し、最新情報をお届けする予定です。