ウェブがドキュメントだけでなくアプリのためのプラットフォームにもなったことで、最先端のアプリケーションの中にはウェブブラウザを限界まで追い込んだものがあります。パフォーマンスを向上させるために低水準言語と連携することで「金属に近づける」というアプローチは、多くの高水準言語で見られます。たとえば、Java には Java ネイティブ インターフェースがあります。JavaScript の場合、この下位言語は WebAssembly です。この記事では、アセンブリ言語の概要とウェブで役立つ理由について説明した後、asm.js の暫定ソリューションを使用して WebAssembly がどのように作成されたかについて説明します。
アセンブリ言語
アセンブリ言語でプログラミングしたことはありますか?コンピュータ プログラミングでは、アセンブリ言語は単にアセンブリと呼ばれ、一般に ASM または asm と略されるものですが、その言語の命令とアーキテクチャのマシンコード命令との間に強い対応関係がある任意の低水準プログラミング言語です。
たとえば、インテル® 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 は 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 プログラムは、ブラウザとは別に、WASI(WebAssembly 用のモジュラー システム インターフェース)である 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
が使用されます(ebアセンブリ t 形式の場合)。本当に必要な場合は、手動で記述することもできます。上の乗算の例を応用し、因数をハードコードするのをやめてより有用なものにすると、次のコードの意味が理解できるはずです。
(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 がレビューしました。