ウェブがドキュメントだけでなくアプリのプラットフォームになって以来、一部の高度なアプリはウェブブラウザを限界まで押し上げています。パフォーマンスを向上させるために、低レベル言語とインターフェースを介して「メタルに近づく」アプローチは、多くの高レベル言語で使用されています。たとえば、Java には Java Native Interface があります。JavaScript の場合、この低レベル言語は WebAssembly です。この記事では、アセンブリ言語と、ウェブでアセンブリ言語が有用な理由について説明します。また、asm.js という暫定的なソリューションを通じて WebAssembly がどのように作成されたかについても説明します。
アセンブリ言語
アセンブリ言語でプログラミングした経験はありますか?コンピュータ プログラミングにおいて、アセンブリ言語(単にアセンブリとも呼ばれ、通常は ASM または asm と略されます)は、言語内の命令とアーキテクチャのマシンコード命令との間に非常に強い対応関係がある任意の低レベル プログラミング言語です。
たとえば、Intel® 64 および IA-32 アーキテクチャ(PDF)では、MUL
命令(乗算)は、第 1 オペランド(宛先オペランド)と第 2 オペランド(ソース オペランド)の符号なし乗算を実行し、結果を宛先オペランドに格納します。mul非常に簡単に説明すると、宛先オペランドはレジスタ AX
にある暗黙のオペランドで、ソースオペランドは CX
などの汎用レジスタにあります。結果はレジスタ AX
に再度保存されます。次の x86 コードの例について考えてみましょう。
mov ax, 5 ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx ; Multiply the value of register AX (5)
; and the value of register CX (10), and
; store the result in register AX.
比較のために、5 と 10 を掛けるタスクを JavaScript で行う場合、次のようなコードを記述します。
const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;
アセンブリ ルートを利用するメリットは、このような低レベルで機械に最適化されたコードは、高レベルで人間に最適化されたコードよりもはるかに効率的であるということです。前述のケースでは問題ありませんが、より複雑なオペレーションでは、差が大きくなることが考えられます。
名前が示すとおり、x86 コードは x86 アーキテクチャに依存しています。特定のアーキテクチャに依存せず、アセンブリのパフォーマンス上の利点を継承するアセンブリ コードを記述する方法があるとしたらどうでしょうか。
asm.js
アーキテクチャに依存しないアセンブル コードを記述するための最初のステップは、asm.js でした。これは、コンパイラ用の低レベルで効率的なターゲット言語として使用できる JavaScript の厳密なサブセットです。このサブ言語は、C や C++ など、メモリ安全でない言語のサンドボックス化された仮想マシンを効果的に記述しました。静的検証と動的検証を組み合わせることで、JavaScript エンジンは有効な asm.js コードに対して事前(AOT)最適化コンパイル戦略を採用できました。手動メモリ管理で静的型付け言語(C など)で記述されたコードは、初期の Emscripten(LLVM ベース)などのソースツーソース コンパイラによって変換されました。
言語機能は AOT に対応するものに限定することで、パフォーマンスが改善されました。Firefox 22 は、asm.js をサポートする最初のブラウザで、OdinMonkey という名前でリリースされました。Chrome バージョン 61 では、asm.js のサポートが追加されました。asm.js はブラウザで引き続き機能しますが、WebAssembly に置き換えられています。現時点で asm.js を使用する理由は、WebAssembly をサポートしていないブラウザの代替として使用することです。
WebAssembly
WebAssembly は、ネイティブに近いパフォーマンスで実行されるコンパクトなバイナリ形式の低レベルのアセンブリ ライクな言語です。C/C++、Rust などの言語をコンパイル ターゲットとして提供し、ウェブ上で実行できるようにします。Java や Dart などのメモリ管理言語のサポートは現在開発中であり、まもなく利用可能になる予定です。Kotlin/Wasm の場合はすでにリリースされています。WebAssembly は JavaScript と並行して実行するように設計されており、両方を連携させることができます。
WebAssembly プログラムは、ブラウザ以外にも、WebAssembly 用のモジュラー システム インターフェースである WASI(WebAssembly System Interface)により、他のランタイムでも実行できます。WASI は、安全性とサンドボックス環境での実行を目的として、オペレーティング システム間で移植可能に作成されています。
WebAssembly コード(バイナリコード、バイトコード)は、ポータブル仮想スタックマシン(VM)で実行することを目的としています。バイトコードは、JavaScript よりも解析と実行が速く、コード表現がコンパクトになるように設計されています。
命令の概念的な実行は、命令を進める従来のプログラム カウンタによって行われます。実際には、ほとんどの Wasm エンジンは Wasm バイトコードをマシンコードにコンパイルしてから実行します。指示は次の 2 つのカテゴリに分類されます。
- コントロール構造を形成し、引数の値をスタックからポップする制御命令は、プログラム カウンタを変更し、結果値をスタックにプッシュすることがあります。
- 引数の値をスタックからポップし、値に演算子を適用して、結果の値をスタックにプッシュし、プログラム カウンタを暗黙的に進める単純な命令。
前の例に戻ると、次の WebAssembly コードは、この記事の冒頭の x86 コードと同じです。
i32.const 5 ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul ; Pop the two most recent items on the stack,
; multiply them, and push the result onto the stack.
asm.js はすべてソフトウェアで実装されているため、コードは(最適化されていない場合でも)任意の JavaScript エンジンで実行できますが、WebAssembly には、すべてのブラウザ ベンダーが合意した新しい機能が必要でした。2015 年に発表され、2017 年 3 月に初めてリリースされた WebAssembly は、2019 年 12 月 5 日に W3C 勧告になりました。W3C は、すべての主要なブラウザ ベンダーやその他の関係者からの貢献を得て、この標準を維持しています。2017 年以降、ブラウザのサポートはユニバーサルです。
WebAssembly には、テキストとバイナリの 2 つの表現があります。上記はテキスト表現です。
テキスト表現
テキスト表現は S 式に基づいており、通常はファイル拡張子 .wat
(WebAssembly text 形式)を使用します。どうしても手書きで作成したい場合は、上記の乗算の例を、係数をハードコードしないように変更してより有用なものにすると、次のコードが理解できると思います。
(module
(func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
local.get $factor1
local.get $factor2
i32.mul)
(export "mul" (func $mul))
)
バイナリ表現
ファイル拡張子 .wasm
を使用するバイナリ形式は、人間が作成することはもとより、人間が使用することを目的としたものではありません。wat2wasm などのツールを使用して、上記のコードを次のバイナリ表現に変換できます。(コメントは通常バイナリ表現の一部ではありませんが、わかりやすくするために wat2wasm ツールによって追加されます)。
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01 ; section code
0000009: 00 ; section size (guess)
000000a: 01 ; num types
; func type 0
000000b: 60 ; func
000000c: 02 ; num params
000000d: 7f ; i32
000000e: 7f ; i32
000000f: 01 ; num results
0000010: 7f ; i32
0000009: 07 ; FIXUP section size
; section "Function" (3)
0000011: 03 ; section code
0000012: 00 ; section size (guess)
0000013: 01 ; num functions
0000014: 00 ; function 0 signature index
0000012: 02 ; FIXUP section size
; section "Export" (7)
0000015: 07 ; section code
0000016: 00 ; section size (guess)
0000017: 01 ; num exports
0000018: 03 ; string length
0000019: 6d75 6c mul ; export name
000001c: 00 ; export kind
000001d: 00 ; export func index
0000016: 07 ; FIXUP section size
; section "Code" (10)
000001e: 0a ; section code
000001f: 00 ; section size (guess)
0000020: 01 ; num functions
; function body 0
0000021: 00 ; func body size (guess)
0000022: 00 ; local decl count
0000023: 20 ; local.get
0000024: 00 ; local index
0000025: 20 ; local.get
0000026: 01 ; local index
0000027: 6c ; i32.mul
0000028: 0b ; end
0000021: 07 ; FIXUP func body size
000001f: 09 ; FIXUP section size
; section "name"
0000029: 00 ; section code
000002a: 00 ; section size (guess)
000002b: 04 ; string length
000002c: 6e61 6d65 name ; custom section name
0000030: 01 ; name subsection type
0000031: 00 ; subsection size (guess)
0000032: 01 ; num names
0000033: 00 ; elem index
0000034: 03 ; string length
0000035: 6d75 6c mul ; elem name 0
0000031: 06 ; FIXUP subsection size
0000038: 02 ; local name type
0000039: 00 ; subsection size (guess)
000003a: 01 ; num functions
000003b: 00 ; function index
000003c: 02 ; num locals
000003d: 00 ; local index
000003e: 07 ; string length
000003f: 6661 6374 6f72 31 factor1 ; local name 0
0000046: 01 ; local index
0000047: 07 ; string length
0000048: 6661 6374 6f72 32 factor2 ; local name 1
0000039: 15 ; FIXUP subsection size
000002a: 24 ; FIXUP section size
WebAssembly へのコンパイル
ご覧のとおり、.wat
も .wasm
も、特に人間に優しいものではありません。ここで、Emscripten などのコンパイラが役立ちます。C や C++ などの高水準言語からコンパイルできます。Rust などの他の言語用のコンパイラもあります。次の C コードについて考えてみましょう。
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
通常、この C プログラムはコンパイラ gcc
でコンパイルします。
$ gcc hello.c -o hello
Emscripten がインストールされている場合は、emcc
コマンドとほぼ同じ引数を使用して、WebAssembly にコンパイルします。
$ emcc hello.c -o hello.html
これにより、hello.wasm
ファイルと HTML ラッパー ファイル hello.html
が作成されます。ウェブサーバーからファイル hello.html
を配信すると、DevTools コンソールに "Hello World"
が出力されます。
HTML ラッパーを使用せずに WebAssembly にコンパイルする方法もあります。
$ emcc hello.c -o hello.js
前回と同様に、hello.wasm
ファイルが作成されますが、今回は HTML ラッパーではなく hello.js
ファイルが作成されます。テストするには、Node.js などのツールで生成された JavaScript ファイル hello.js
を実行します。
$ node hello.js
Hello World
その他の情報
WebAssembly の簡単な紹介は、氷山の一角にすぎません。WebAssembly の詳細については、MDN の WebAssembly のドキュメントと Emscripten のドキュメントをご覧ください。実際のところ、WebAssembly の使用は、フクロウの描き方に関するミームに似ているかもしれません。特に、HTML、CSS、JavaScript に精通しているウェブ デベロッパーは、C などのコンパイル対象言語に精通しているとは限りません。幸い、StackOverflow の webassembly
タグなどのチャンネルでは、丁寧に質問すれば、専門家が喜んでサポートしてくれることがよくあります。
謝辞
この記事は、Jakob Kummerow、Derek Schuff、Rachel Andrew が確認しました。