ウェブがドキュメントだけでなくアプリのプラットフォームにもなって以来、最先端のアプリのいくつかはウェブブラウザの限界を押し広げてきました。パフォーマンスを向上させるために下位レベルの言語とインターフェースをとることで「ハードウェアに近い」アプローチは、多くの高水準言語で採用されています。たとえば、Java には Java Native Interface があります。JavaScript の場合、この低レベル言語は WebAssembly です。この記事では、アセンブリ言語とは何か、ウェブでアセンブリ言語が役立つ理由、asm.js の暫定的なソリューションを通じて WebAssembly がどのように作成されたかについて説明します。
アセンブリ言語
アセンブリ言語でプログラミングしたことはありますか?コンピュータ プログラミングにおいて、アセンブリ言語(単にアセンブリと呼ばれることも多く、ASM または asm と略されることも多い)は、言語の命令とアーキテクチャのマシンコード命令との間に非常に強い対応関係がある低水準プログラミング言語です。
たとえば、Intel® 64 および IA-32 アーキテクチャ(PDF)を見ると、MUL
命令(乗算用)は、第 1 オペランド(宛先オペランド)と第 2 オペランド(ソースオペランド)の符号なし乗算を実行し、結果を宛先オペランドに格納します。非常に簡略化すると、宛先オペランドはレジスタ 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 は、OdinMonkey という名前でリリースされた、asm.js をサポートした最初のブラウザでした。Chrome では、バージョン 61 で asm.js のサポートが追加されました。asm.js はブラウザで引き続き動作しますが、WebAssembly に置き換えられました。現時点で asm.js を使用する理由は、WebAssembly をサポートしていないブラウザの代替手段としてです。
WebAssembly
WebAssembly は、ネイティブに近いパフォーマンスで実行され、C/C++ や Rust などの言語にコンパイル ターゲットを提供してウェブ上で実行できるようにする、コンパクトなバイナリ形式の低レベルのアセンブリのような言語です。Java や Dart などのメモリ管理言語のサポートは現在開発中で、まもなく利用可能になる予定です。Kotlin/Wasm のように、すでに実装されているものもあります。WebAssembly は JavaScript と並行して実行するように設計されており、両方を連携して動作させることができます。
ブラウザ以外にも、WebAssembly プログラムは WASI(WebAssembly System Interface)という WebAssembly のモジュラー システム インターフェースのおかげで、他のランタイムでも実行されます。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
ファイルが作成されます。テストするには、生成された JavaScript ファイル hello.js
を Node.js などで実行します。
$ node hello.js
Hello World
その他の情報
WebAssembly の簡単な紹介は、氷山の一角にすぎません。WebAssembly の詳細については、MDN の WebAssembly ドキュメントをご覧ください。また、Emscripten ドキュメントもご覧ください。WebAssembly の操作は、フクロウの描き方ミームに似ていると感じるかもしれません。特に、HTML、CSS、JavaScript に精通しているウェブ デベロッパーが、C などのコンパイル対象言語に精通しているとは限らないためです。幸いなことに、StackOverflow の webassembly
タグなどのチャンネルでは、丁寧に質問すれば、専門家が喜んで手助けしてくれることがよくあります。
謝辞
この記事は、Jakob Kummerow、Derek Schuff、Rachel Andrew によってレビューされました。