2019年のWebAssembly事情

2年前に、[「(Learn the Hard Way)nodejs-8でのWebAssembly自体を調べてみた」](https://qiita.com/bellbind/items/a6435b0c8f10306128b8)として、WebAssemblyについて記事にしました。

この記事は、2年前から変わった、2019年6月現在でのWebAssemblyの状況についてまとめたものです。

## WAT: WebAssembly Text Format

2年前は、WebAssemblyのテキスト表現といえば、[S式](https://ja.wikipedia.org/wiki/S%E5%BC%8F)形式であり、フォーマット名は「WAST」と呼ばれていました。そして、ツールbinaryenの、`wasm-as`/`wasm-dis`コマンドでバイナリ形式の`.wasm`ファイルとテキスト形式の`.wast`ファイルの変換を行いました。

2019年現在、WebAssemblyのテキスト表現は、「WebAssembly Text Format」通称**WAT**となっています。

- http://webassembly.github.io/spec/core/text/index.html

大きく変わったのは、命令コード部分の表記法が、木構造のS式形式から、**[FORTH](https://ja.wikipedia.org/wiki/Forth)言語風のスタック操作を行う[後置記法](https://ja.wikipedia.org/wiki/%E9%80%86%E3%83%9D%E3%83%BC%E3%83%A9%E3%83%B3%E3%83%89%E8%A8%98%E6%B3%95)**となった点です。このWAT形式のファイルの拡張子は`.wat`となります。

以下はWAT形式のコードの例です:

```wat:add.wat
(module
 (func (export "add") (param $a i32) (param $b i32) (result i32)
   local.get $a
   local.get $b
   i32.add
   return
 )
)
```

(注: WAT形式では、命令の複数の引数は、スタックの底側から第一引数、第二引数、となります。)

一方で、引数を明記するS式表現もWATの["Folded Instructions"](http://webassembly.github.io/spec/core/text/instructions.html#folded-instructions)として、構文糖が仕様にあり、以前のwast形式のコードとの互換が残っています。

以下は、`add.wat`と同じ結果となるS式形式のコードです:

```wat:add-folded.wat
(module
 (func (export "add") (param $a i32) (param $b i32) (result i32)
   (return (i32.add (local.get $a) (local.get $b)))
 )
)
```

注: 2年前のWAST時代での命令`get_local`は`local.get`に、`set_local`は`local.set`になりました。(binaryenや後述するツールwabtでは、旧来の`get_local`や`set_local`でも、互換解釈してくれるようです。)

そして、S式形式だったWASTの名は、WATモジュールをテストするScriptのファイル名として、割り当てられました。

- https://github.com/WebAssembly/spec/tree/master/interpreter/#scripts

## ツールwabt

以前は、binaryenをテキスト表現のアセンブラとして使いました。
現在でもEmscriptenなどはbinaryenを使用します。

- https://github.com/WebAssembly/binaryen

しかし、binaryenのヘルプメッセージがいまもwastのままだったりしていて、現在のWAT形式のためのコンパイラとしては、新しめのツール実装の**wabt**を使うのが良いようです。

- https://github.com/WebAssembly/wabt

wabtのコマンドには、`wat2wasm`, `wasm2wat`などがあります。使い方は以下のとおりです:

- `wat2wasm add.wat`: モジュールファイル`add.wasm`が生成される
- `wasm2wat add.wasm`: (S式形式などの短縮表記を用いない)WAT形式で**標準出力**する
- `wat-desugar add.wat`: S式形式などの短縮表記を用いないWAT形式で標準出力する

## ECMAScript Module Integration Proposal

"ECMAScript Module Integration Proposal"(es-module integration)とは、ES6の`import`文で、`wasm`モジュールファイルを`import`対象にするための仕様提案です。現在はまだStage 2ですが、現行のnodejs-12では、フラグ付きで実行できる状況にあります。

- 仕様: https://github.com/WebAssembly/esm-integration/tree/master/proposals/esm-integration
- 例: https://github.com/WebAssembly/esm-integration/blob/master/proposals/esm-integration/EXAMPLES.md

この仕様は、jsコードから`.wasm`モジュールファイルをimportできようにするだけでありません。
WebAssemblyの**`import`モジュール名として、(名前でなく)URLを設定可能にする**仕様でもあります。

つまり、ECMAScript/WebAssemblyランタイムでは、`.wasm`モジュールファイルからも`.wasm`モジュールファイルや`.js`(ES6)モジュールファイルを自動解決して利用させることができるようになります。

### 例1: wasmファイルをimportするwasmファイルをimportするjsファイルを実行する

先程の`add.wasm`を使う`sub.wasm`は、以下のように記述できます:

```wat:sub.wat
(module
  (import "./add.wasm" "add" 
    (func $add (param $a i32) (param $b i32) (result i32)))
  (func (export "sub") (param $a i32) (param $b i32) (result i32)
    local.get $a
    i32.const -1
    local.get $b
    i32.mul
    call $add
    return
  )
)
```

ここでは、`import`のモジュール名部分が、`./add.wasm`となっている点がポイントです。

このファイル`sub.wat`を使い、コマンド`wat2wasm sub.wat`を実行し、`sub.wasm`を生成します。
注意点として、`wat2wasm`実行では、importしたファイルのリンクはされません。また、`sub.wat`の変換時には、`add.wasm`ファイルの存在も不要です。

この`sub.wasm`を呼び出すJSコード`main.mjs`は以下のように記述できます:

```js:main.mjs
import {sub} from "./sub.wasm";
console.log(sub(20, 10));
```

`add.wasm`、`sub.wasm`、`main.mjs`を同じディレクトリに置きます。

この実行には、nodejs-12が必要で、オプション`--experimental-modules`と`--experimental-wasm-modules`の双方が必須です:

```shell-session
$ node --experimental-modules --experimental-wasm-modules main.mjs
(node:41782) ExperimentalWarning: The ESM module loader is experimental.
10
```

### 例2: jsファイルをimportするwasmファイルをimportするjsファイルを実行する

まず、以下の`math.mjs`モジュールファイルを用意します:

```js:math.mjs
export function sin(x) {return Math.sin(x);}
export function cos(x) {return Math.cos(x);}
```

この`math.mjs`をimportして使う`tan.wat`を以下のように記述します:

```wat:tan.wat
(module
  (import "./math.mjs" "sin" (func $sin (param $a f64) (result f64)))
  (import "./math.mjs" "cos" (func $cos (param $a f64) (result f64)))
  (func (export "tan") (param $a f64) (result f64)
    local.get $a
    call $sin
    local.get $a
    call $cos
    f64.div
    return
  )
)
```


同様に`wat2wasm tan.wat`コマンドを実行し、ファイル`tan.wasm`を生成します。

この`tan.wasm`を呼び出す`main.mjs`は以下のとおりです:

```js:main.mjs
import {tan} from "./tan.wasm";
console.log(tan(1));
```

同様に、`math.mjs`、`tan.wasm`、`main.mjs`を同一ディレクトリに置き、以下のように実行します:

```shell-session
$ node --experimental-modules --experimental-wasm-modules main.mjs
(node:41782) ExperimentalWarning: The ESM module loader is experimental.
1.557407724654902
```

### ES module integrationについての補足

- WebAssembly側で定数をimportする場合は、`global`を使います
    -  例: `(import "./math.mjs" "PI" (global $pi f64))`
- `.js`モジュールと同様に、複数からimportされた`.wasm`モジュールは1つです
    - wasmから`export`した`memory`があれば、複数箇所から更新されうることになるでしょう
- ダイナミックローディング用のJavaScript API `WebAssembly.instantiate(buffer, imports)`でのwasmモジュールの利用では、引数`buffer`がURL情報を含まないため、wasm内のURL importが機能しなくなります
    - `imports`に、`{"./math.mjs": await import(new URL("./math.mjs", import.meta.url))}` などを渡すことで対処できる。
- 新しいAPIの `WebAssembly.instantiateStreaming(response, imports)`では、おそらくwasm側のURL importも機能するでしょう
   - 注: この第一引数は[fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)の`Response`オブジェクトであり、fetch APiを持たない現状nodejsには存在しないメソッドです。

## その他のWebAssembly extension proposals

WebAssembly extensionのProposal一覧は、以下のURLで列挙されています:

- https://github.com/WebAssembly/proposals

以下のものは、nodejs-12やwabtのwat2wasmで、オプション指定で有効化できるproposalです:

- [multi values](https://github.com/WebAssembly/multi-value/blob/master/proposals/multi-value/Overview.md) (node: `--experimental-wasm-mv`, wat2wasm: `--enable-multi-value`): `(result i32 i32)`のように、関数やブロックで複数の値を返すことを可能にする仕様提案。JS APIには影響しない。
- [reference types](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md) (node: `--experimental-wasm-anyref`, wat2wasm: `--enable-reference-types`): wasm関数のポインタ利用で使う`Table`の利用方法を拡張する仕様提案。`anyref`型と`ref`命令及び`cast-else-end`命令の追加、`funcref`をlocal値で扱えるようにし`call_ref`命令の追加、`Table`の各種操作のWebAssembly命令の追加、など。
- [exception handling](https://github.com/WebAssembly/exception-handling/blob/master/proposals/Exceptions.md) (node: `--experimental-wasm-eh`, wat2wasm: `--enable-exceptions`): `try`-`catch`-`end`や`throw`/ `rethrow`等のWebAssembly命令を追加する仕様提案。`Function`や`Global`と同列のオブジェクトとして、`Event`の導入もする。
- [sign extension operators](https://github.com/WebAssembly/sign-extension-ops/blob/master/proposals/sign-extension-ops/Overview.md) (node: `--experimental-wasm-se`, wat2wasm: `--enable-sign-extension`): 現状は`i32`から`i64`の間のみ存在する符号拡張命令(`i64.extend_i32_s`, `i64.extend_i32_u`)を、型としては存在しない8ビット/16ビット整数に対して符号拡張を行うWebAssembly命令を追加する仕様提案。`i32.extend8_s`など。
- [threads proposal](https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md) (node: `--experimental-wasm-threads`, wat2wasm: `--enable-threads`): `Worker`などで共有できる`SharedArrayBuffer`対応とそのアトミック系命令をWebAssemblyにも入れる仕様提案。(WebAssembly内でスレッドを作れるわけではない)
- [BigInt<->i64 conversion in JS API](https://github.com/WebAssembly/JS-BigInt-integration) (node: `--experimental-wasm-bigint`): JS APIで`BigInt`型をWebAssemblyの`i64`にマッピングさせる仕様提案。`memory`での`BigInt64Array`や`BigUint64Array`との互換性も規定する。
- [Non-trapping Float-to-int Conversions](https://github.com/WebAssembly/nontrapping-float-to-int-conversions/blob/master/proposals/nontrapping-float-to-int-conversion/Overview.md) (node: `--experimental-wasm-sat-f2i-conversions`, wat2wasm: `--enable-saturating-float-to-int`): `f32`->`i32`などでの数値型の切り詰めキャスト命令でのトラップ処理なしバージョンの仕様提案。これらの命令では、切り詰めでオーバーフローする場合はその数値型の最大値になる、などの違いがある。
- [bulk memory](https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md) (node: `--experimental-wasm-bulk-memory`, wat2wasm: `--enable-bulk-memory`): `memcpy`や`memset`相当の`memory`に対する一括操作のためのWebAssembly命令の追加の仕様提案。`memory.copy`や`memory.fill`などが追加される。
- [tail call](https://github.com/WebAssembly/tail-call/blob/master/proposals/tail-call/Overview.md) (node: `--experimental-wasm-return-call`, wat2wasm: `--enable-tail-call`): 末尾呼び出しのための命令`return_call`の追加の仕様提案。
- [SIMD](https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md) (node: `--experimental-wasm-simd`, wat2wasm: `--enable-simd`): SIMD用の128ビットの`v128`型の導入と、`v128`操作および、既存の演算命令の並列版(`i8x16`、`i16x8`、`i32x4`、`i64x2`、`f32x4`、`f64x2`)の追加の仕様提案。数学関数の命令はこの仕様には含まれない。
- [Custom Annotation Syntax for the Wasm Text Format](https://github.com/WebAssembly/annotations/blob/master/proposals/annotations/Overview.md) (wat2wasm: `--enable-annotations`): wasmモジュールに埋め込める任意のカスタムメタデータ記述のためのWAT構文の拡張の仕様提案。


## V8: `--wasm-math-intrinsics`

WebAssemblyの命令セットには、`sin`や`cos`を始めとする数学関数が存在していません。

このため、WebAssemblyの中で数学関数を使うには、`Math`オブジェクトの関数を`import`して、その`import`した関数を`call`して使う必要があります。
しかし、外部モジュールの関数を`call`して結果を受け取るまでの実行コストは、WebAssemblyの計算命令や`local`変数アクセスと比べて、非常に高くなってしまいます。

このため、chromeやnode.jsのJavaScriptランタイムのv8では、WebAssemblyの`import`した`funtcion`が、ビルトインの**`Math`オブジェクトの関数オブジェクトそのもの**である場合、関数呼び出しの`call`が実行されるのではなく、v8内で組み込み処理されるオプション`--wasm-math-intrinsics`が実装されており、デフォルトで有効化されています。

つまり、この`--wasm-math-intrinsics`の恩恵を得るには、上述の`tan.wat`のための`math.mjs`の実装は、

```js:math.mjs
//NOTE: --wasm-math-intrinsicsが機能しないモジュール
export function sin(x) {return Math.sin(x);}
export function cos(x) {return Math.cos(x);}
```

ではなく、

```js:math.mjs
//NOTE: --wasm-math-intrinsicsが機能するモジュール
export const sin = Math.sin;
export const cos = Math.cos;
```

とする必要があります。ダイナミックロードでは、`WebAssembly.instantiate(buffer, {"./math.mjs": Math})`などを行うことでも機能させられます。

`sin`や`cos`を頻繁に呼ぶWebAssemblyコードでは、この2つを入れ替えて実行速度を比べることができ、後者のときに実行速度は格段に向上するでしょう。


### FFTでベンチマーク

- https://gist.github.com/bellbind/36b7e71b3203e868c10405a61cdd2d09

に、`sin`と`cos`を使うFFT実装を用いたベンチマークコードを置きました。

`Float64Array`を使った[ループ版FFT](https://qiita.com/bellbind/items/ba7aa07f6c915d400000#7-javascript%E3%81%AB%E3%82%88%E3%82%8B%E3%83%AB%E3%83%BC%E3%83%97%E7%89%88fft%E5%AE%9F%E8%A3%85%E3%82%B3%E3%83%BC%E3%83%89)のJavaScript実装と、それとほぼ同等の(最適化してない)WATモジュール実装、およびnodejs用とブラウザ用のWASMモジュールのベンチマークスクリプトをおいてあります。

ブラウザでは、

- https://gist.githack.com/bellbind/36b7e71b3203e868c10405a61cdd2d09/raw/index.html

で実行でき、Webコンソール上に、`console.time/timeEnd`による実行時間が出ます。

| browser    | JS FFT             | WASM with Math function | WASM with Wrapped function |
|------------|--------------------|-------------------------|----------------------------|
| node-12    | 873.279ms          | 890.830ms               | 1666.906ms   |
| chrome-75  | 926.402099609375ms | 898.839111328125ms      | 1920.876953125ms    |
| firefox-67 | 747ms              |  944ms                   | 1067ms |


nodejsやchromeだと、`Math`の関数を直接渡すのと、ラップした関数を渡すのとでは、2倍ほど差が出るようです。


## 付録A: WATコード例

### コメント

WATのコメントは2種類あります:

- `(; ... ;)` :ブロックコメント
- `;; ...`: 行コメント

行コメントはセミコロン2つ必須です。

### `local.tee`

`local.tee $a`は、`local.set $a`して直後に`local.get $a`するのと同等です。

つまり、`a = b = c = 0`は、

```wat
   i32.const 0
   local.tee $c
   local.tee $b
   local.set $a
```

と記述できます。

### 戻り値有り条件分岐式`if-else-end`の例

2引数の大きい方を返す`max(a, b)`は、以下のようなコードとなります:

```wat:max.wat`
(module
  (func (export "max") (param $a f64) (param $b f64) (result f64)
     ;; return (a < b) ? b : a
     local.get $a
     local.get $b
     f64.lt
     if (result f64)
       local.get $b
     else
       local.get $a
     end
     return
  )
)
```

条件式の結果をスタックに起き、結果の型指定`(result 型)`を持った`if`命令を呼びます。戻り値がある場合の`if`命令は、`else`部分が必須です。

### ループとブロック`loop`/`block`の例

以下は、ループ版階乗`fact(n)`のWebAssemblyコードです:

```wat:fact.wat
(module
  (func (export "fact") (param $n i32) (result i32)
     (local $i i32)
     (local $r i32)
     
     ;; r = i = 1
     i32.const 1
     local.tee $i
     local.set $r
     block $i-break loop $i-continue 
       ;; if (i > n) break
       local.get $i
       local.get $n
       i32.gt_u
       br_if $i-break
       
       ;; r = r * i
       local.get $r
       local.get $i
       i32.mul
       local.set $r
       
       ;; i = i + 1
       local.get $i
       i32.const 1
       i32.add
       local.set $i
       
       br $i-continue
     end end
     ;; return r
     local.get $r
     return
  )
)
```

制御命令`loop`と`block`は、`br`や`br_if`によるジャンプの扱いが違います。`loop`では`loop`の次の位置に**前方**ジャンプし、`block`では`end`の次の位置に**後方**ジャンプします。

`return`命令で脱出するループでない限りは、繰り返しと脱出のために`loop`と`block`を組み合わせて使うことになるでしょう。命令がループの`end`に到達しても、自動で`loop`まで巻き戻るわけではない点は注意です(`br $i-continue`の行をコメントアウトしたwasmモジュールを実行させてみれば確認できるでしょう)。

`loop`や`block`のラベルは、実際にはその分岐命令からのジャンプ対象までの相対深さの整数値です。このコードでは、`br_if $i-break`が`br_if 1`、`br $i-continue`が`br 0`になります(wasm2watで確認できるでしょう)。このため同じラベルを、並列するループで使うことは可能です。

また、このコードの`loop`と`block`の順序は入れ替えても同じ結果が得られるコードになります。ただ、`loop`を内側にするほうが、nodejsでの実行は、若干早かったです(wasmモジュールの最適化などもあるので、誤差かもしれません)。


### 多重分岐`br_table`

おなじみFizzBuzzとして、3で割り切れるなら1、5で割り切れるなら2、15で割り切れるなら3、それ以外は0を返す`fizzbuzz`関数を多重分岐`br_table`命令で実装したWATコードです。

```wat:fizzbuzz.wat
(module
  (func (export "fizzbuzz") (param $n i32) (result i32)
    block $default
      block $fizz
        block $buzz
          block $fizzbuzz
            ;; switch (n % 15) {...}
            local.get $n
            i32.const 15
            i32.rem_u
            br_table $fizzbuzz $default $default $fizz    $default
                     $buzz     $fizz    $default $default $fizz
                     $buzz     $default $fizz    $default 
          end
          i32.const 3
          return
        end
        i32.const 2
        return
      end
      i32.const 1
      return
    end
    i32.const 0
    return
  )
)
```

`br_table`では、ブロックラベルを複数個指定できます。スタックトップの整数値一つに対し、0,1,2,と左から数えたラベルへとジャンプします。スタックトップ値が範囲外の場合(ラベル数以上、マイナス値、など)は、一番最後のラベルにジャンプします(このコードでは、余り14のとき、最後のラベル`$default`が採用される)。