ウェブがドキュメントだけでなくアプリのプラットフォームにもなって以来、最先端のアプリの中には、ウェブブラウザの限界に挑みつつあります。パフォーマンスを向上させるために下位レベルの言語をインタフェースとして「よりメタルに」するアプローチは、多くの上位レベルの言語で見られます。たとえば、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 プログラムは、ブラウザとは別に、WebAssembly 用のモジュラー システム インターフェースである WebAssembly システム インターフェースである WASI のおかげで、他のランタイムでも実行されます。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 ext 形式)を使用します。どうしても必要な場合は、手動で記述することもできます。上の乗算の例を取り上げ、因数をハードコードしないことでより便利なものにすると、次のコードが理解できるでしょう。
(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 によってレビューされました。