この記事はHacks blogの記事"Why WebAssembly is Faster Than asm.js"の抄訳です。
WebAssemblyはWeb向けの新しい実行可能なバイナリフォーマットで、リリース版のブラウザで既にサポートされています。その主な目的は実行速度です。この記事では、速度向上がどのように行われているのか技術的に解説します。
ご存知のように「速い」とは、何かと比較しての言葉です。JavaScriptや他の動的言語と比較して、WebAssemblyは速く実行されます。それは最適化のために静的に型づけされ、単純なものとなっているからです。しかしWebAssemlbyはネイティブコードと同等の実行速度を目指しています。asm.jsによって、ネイティブコードとの差は縮まっていますが、WebAssemblyはその差を更に縮めます。この記事では、なぜasm.jsよりも高速にできたかに焦点を当てて解説をします。
本題に入る前に、よくある警告を書いておきます:性能というものは計測し難いもので、いくつもの側面があります。また新しい技術には、常に最適化しきれていない部分が存在します。そのため、ここに掲載するベンチマークの結果よりも今日測定したものの方が良い結果を示すでしょう。この記事で、「WebAssemblyによって速くなる」と書かれているもののうち、実際はそのようになっていないものに関しては、修正が必要なバグと認識ください。
それはさておき、本題に入りましょう。
起動の高速化
WebAssemblyはダウンロードと構文解析を高速に行うため、小さくなるように設計されています。そのため大きなアプリケーションでも素早く起動します。
ミニファイされ、gzip圧縮された、JavaScriptコードのサイズを更に小さくすることは簡単なことではありません。なぜならネイティブコードと比較して十分に小さくなっているからです。インデックスにLEB128を利用している点に見られるように、WebAssemblyのバイナリフォーマットはサイズのことを念頭に置いて設計されています。その結果、gzip圧縮されたものと比較して10-20%のサイズダウンを実現しました。
構文解析のスピードはより大きく改善されています。JavaScriptのそれと比べると、10倍以上高速になっています。バイナリフォーマットの構文解析は、テキストのそれよりも高速に行える上に、その特性を活かすように設計されてもいます。関数の構文解析(及び最適化)を簡単に並列化できるため、マルチコアのマシンではその速度は大きく向上します。
もちろんダウンロードや構文解析以外にも起動時間を左右する要素があります。例えば仮想マシンによる最大の最適化や実行のために必要なデータのダウンロードといったものです。しかしプログラムのダウンロードと構文解析を避けることはできません。そのため、これれらの可能な限りの高速化は重要なのです。そのほかの要素の最適化や、それらによる速度低下の緩和を図ることは可能です。例えばWebAssemblyの実行に、最初の数フレームはベースラインコンパイラや、インタプリタを利用することで、完全な最適化を避けられます。他の要素による速度低下の最適化/緩和はブラウザでも、アプリでも行えます。
CPU機能の利用
JavaScriptでは数値を全てdouble型の値として扱います。一方asm.jsでは、足し算には続けてビット演算子が追加されます。これにより、asm.jsの処理はCPUが単純な整数型の足し算の際に行なっていることと、論理的に等しくなります。そしてこのような処理をCPUは高速に行えるのです。そのためasm.jsを利用するVMはCPUの能力を十分に引き出せるのです。これがasm.jsが高速に処理できる秘密です。
その反面asm.jsの能力はJavaScriptで表現できるものに限定されていました。WebAssemblyはJavaScriptの表現能力には制限されないため、CPUの機能をより十全に利用できます。例を挙げると:
- 64ビットの整数を扱えます。それらに関する処理は最大で4倍高速になります。その結果、ハッシュや暗号のアルゴリズムなどを高速化できます。
- loadやstoreの際にオフセットを指定できます。C言語の構造体のような、固定長の属性を持つメモリ上のオブジェクトを利用するものは、基本的にこの恩恵を受けます。
- アラインされていないloadとstoreを、マスクなしで行えます(asm.jsではTyped Arrayの互換性のため、マスクを行なっています)。実際にはすべてのloadとstoreで、この機能は利用されます。
- popcountやcopysignといった多様なCPUの命令を利用できます。これらはそれぞれ特定の状況下で利用されます。例えばpopcountは暗号解析に用いられます。
個々のベンチマークがどの程度高速になるかは、上述した機能をどの程度利用できているかに依存します。それでもasm.jsと比べて5%の高速化が行われています。今後もSIMDのような機能を利用することで、さらなる高速化が望まれます。
ツールチェーンの改良
WebAsseblyは主にコンパイラのターゲットアーキテクチャです。そのため実行性能は、WebAssemblyを生成するツールであるコンパイラと、それをブラウザ上で実行する仮想マシンの両者に依存しています。
これはasm.jsでも同様です。そのためEmscriptenは内部で多くの最適化処理を行っています。例えばLLVMの最適化エンジンも利用しますし、Emscripten自身のもつasm.jsに対する最適化も行います。WebAssemblyは、それらに大きな改良を加えたものを利用しています。asm.jsもWebAssemblyも、典型的なコンパイラのターゲットではありません。そしてどちらも同じような手法を採用しています。そのためasm.jsで学んだことをWebAssemblyのために生かせているのです。具体的には以下の項目が変更されています:
- Emscriptenの持つasm.js最適化エンジンをBinaryenに置き換えています。これは速度を念頭に置いた設計がなされているため、よりコストの高い最適化を行えます。例えば最適化時のデフォルトの動作として、重複している関数は削除されます。これによってC++のコンパイルによって得られた結果を5%程度縮小できました。
- Relooperアルゴリズムを改良することで、畳み込み済みの縮小できないコントロールフローに対して、より最適化できるようになりました。これによりコンパイルされたインタプリタ型のループの速度が向上します。
- Binaryenは設計時に実験を行うことを念頭に置いています。超最適化の実験は結果として細々とした小さい改善であることがわかりました。これらはasm.jsの時に済ませており、一度は考えたものでもありました。
総合的に、ツールチェーンに対する改良によって、asm.jsからWebAssemblyに変えるだけで性能が向上します。
Box2Dにおいては、それぞれ7%と5%の速度向上が実現されました。
性能を予測可能なものとするために
asm.jsは基本的にネイティブと同じ速度で実行できます。しかし全てのブラウザでいつもそう、とは言えないのが実際です。ブラウザごとに最適化手法が異なり、その結果もそれぞれ異なるのが、その理由です。時間の経過とともに広がり始めたとは言え、asm.jsは事実上の標準ではありません。1ブラウザベンダが策定したJavaScriptの一部分に対する非公式な仕様でしかなく、他のベンダは徐々に興味を持ち対応したにすぎません。これが問題の根本です。
一方WebAssemblyは、主なブラウザベンダが設計してきました。独創的な手法でのみ高速化できるJavaScriptやasm.jsと異なり、WebAssemblyは単純な方式で高速に実行できます。しかし全てのブラウザがその方式を採用しているわけではないため、最適化に関するいくつかの合意もなされています。コンパイル階層や、AOTとJITのどちらを採用するかなど、仮想マシンを差別化する余地を大きく残しつつも、全てのWebでその性能を予測可能にするためのよいベースラインを提供しています。
Alon Zakai について
Mozillaの研究チームで、C/C++をJavaScriptにコンパイルするEmscriptenを主に開発しています。
EmscripenはAlonによって2010年に始められたプロジェクトです。
Webサイト: mozakai.blogspot.com/