WebAssembly用アセンブラを作ってみた


https://github.com/ikiuo/wasmgen

伝統的な形式で WebAssembly を記述したい方向けです。


# サンプル(1)

簡単な関数の例です。

```asm:test.s
;# i32cpy(x) = x
                .export         i32cpy      ;# i32cpy 関数を外部参照可能に
i32cpy:         .code           i32         ;# 返値型が i32 の関数 i32cpy
x:              .param          i32         ;# 最初の引数型は i32
                local.get       x           ;# スタックに x を積む
                end                         ;# スタックの上端(x)が返値

;# i32add(x, y) = x + y
                .export         i32add      ;# i32cpy 関数を外部参照可能に
i32add:         .code           i32         ;# 返値型が i32 の関数 i32add
x:              .param          i32         ;# 最初の引数型は i32
y:              .param          i32         ;# 2番目の引数型は i32
                local.get       x           ;# スタックに x を積む
                local.get       y           ;# スタックに y を積む
                i32.add                     ;# スタックから x,y を取り崩し、(x+y) を積む
                end                         ;# スタックの上端(x+y)が返値

;# f32vlen(x, y, z) = sqrt(x * x + y * y + z * z)
                .export         f32vlen     ;# f32vlen 関数を外部参照可能に
f32vlen:        .code           f32         ;# 返値型が f32 の関数 f32vlen
x:              .param          f32         ;# 最初の引数型は f32
y:              .param          f32         ;# 2番目の引数型は f32
z:              .param          f32         ;# 3番目の引数型は f32
                local.get       x           ;# stack: [x]
                local.get       x           ;# stack: [x, x]
                f32.mul                     ;# stack: [(x * x)]
                local.get       y           ;# stack: [(x * x), y]
                local.get       y           ;# stack: [(x * x), y, y]
                f32.mul                     ;# stack: [(x * x), (y * y)]
                f32.add                     ;# stack: [(x * x + y * y)]
                local.get       z           ;# stack: [(x * x + y * y), z]
                local.get       z           ;# stack: [(x * x + y * y), z, z]
                f32.mul                     ;# stack: [(x * x + y * y), z * z]
                f32.add                     ;# stack: [(x * x + y * y + z * z)]
                f32.sqrt                    ;# stack: [sqrt(x * x + y * y + z * z)]
                end                         ;# スタックの上端 sqrt(...) が返値

;# f32vnrm(x, y, z) : normalize(x,y,z) → (x',y',z')
                .export         f32vnrm     ;# f32vlen 関数を外部参照可能に
f32vnrm:        .code           f32,f32,f32 ;# 返値型が [f32,f32,f32] の関数 f32vlen
x:              .param          f32         ;# 最初の引数型は f32
y:              .param          f32         ;# 2番目の引数型は f32
z:              .param          f32         ;# 3番目の引数型は f32
n:              .local          f32         ;# 一時的な変数 f32 型
                local.get       x           ;# stack: [x]
                local.get       y           ;# stack: [x, y]
                local.get       z           ;# stack: [x, y, z]
                call            f32vlen     ;# f32vlen 関数を呼び出す / stack: [sqrt(...)]
                local.set       n           ;# スタックから取り出して n へ / stack: []
                local.get       x           ;# stack: [x]
                local.get       n           ;# stack: [x, n]
                f32.div                     ;# stack: [x/n]
                local.get       y           ;# stack: [x/n, y]
                local.get       n           ;# stack: [x/n, y, n]
                f32.div                     ;# stack: [x/n, y/n]
                local.get       z           ;# stack: [x/n, y/n, z]
                local.get       n           ;# stack: [x/n, y/n, z, n]
                f32.div                     ;# stack: [x/n, y/n, z/n]
                end                         ;# スタックの上端3つが返値
```


## テスト

**アセンブル**

```console:
$ wasmgen test.s --base64+q,w=64,c -
'AGFzbQEAAAABHARgAX8Bf2ACf38Bf2ADfX19AX1gA319fQN9fX0DBQQAAQIDBycE',
'BmkzMmNweQAABmkzMmFkZAABB2YzMnZsZW4AAgdmMzJ2bnJtAAMKQQQEACAACwcA',
'IAAgAWoLFAAgACAAlCABIAGUkiACIAKUkpELHQEBfSAAIAEgAhACIQMgACADlSAB',
'IAOVIAIgA5UL',
```

**HTML+JavaScriptでテスト**

```html:test.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <p>i32cpy = <span id="tagI32Cpy">[-]</span></p>
    <p>i32add = <span id="tagI32Add">[-]</span></p>
    <p>f32vlen = <span id="tagF32VLen">[-]</span></p>
    <p>f32vnrm = <span id="tagF32VNrm">[-]</span></p>

    <script>

     // アセンブルした test.s を Base64 符号化
     const test_wasm_binary = [
         'AGFzbQEAAAABHARgAX8Bf2ACf38Bf2ADfX19AX1gA319fQN9fX0DBQQAAQIDBycE',
         'BmkzMmNweQAABmkzMmFkZAABB2YzMnZsZW4AAgdmMzJ2bnJtAAMKQQQEACAACwcA',
         'IAAgAWoLFAAgACAAlCABIAGUkiACIAKUkpELHQEBfSAAIAEgAhACIQMgACADlSAB',
         'IAOVIAIgA5UL'
     ].join('');

     // test.s の関数をテストする
     function test(wasm_module)
     {
         const wasm = wasm_module.instance.exports;  // .wasm の Export Section 情報

         tagI32Cpy.innerText = wasm.i32cpy(0);      // = 0
         tagI32Add.innerText = wasm.i32add(1, 2);   // = 3
         tagF32VLen.innerText = wasm.f32vlen(0.1, 0.2, 0.3); // = √0.14
         tagF32VNrm.innerText = wasm.f32vnrm(0.1, 0.2, 0.3);
     }

     /*
      * 引数
      *     wasm: Base64 符号化された .wasm バイナリ
      *     caller: .wasm の Import Section 情報
      * 返値
      *     .wasm をコンパイルする Promise
      */
     function wasmInstantiateStreaming(wasm, caller)
     {
         const bin = Uint8Array.from(atob(wasm), c => c.charCodeAt(0));
         const res = new Response(bin, {headers: {"Content-Type":"application/wasm"}});
         return WebAssembly.instantiateStreaming(res, caller ?? {});
     }

     // .wasm をコンパイルして実行
     wasmInstantiateStreaming(test_wasm_binary).then(test);

    </script>
  </body>
</html>
```

**結果**

```text:
i32cpy = 0
i32add = 3
f32vlen = 0.37416577339172363
f32vnrm = 0.26726120710372925,0.5345224142074585,0.8017836809158325
```


# サンプル(2)

エンディアン(バイト順)を入れ替えます。

```asm:byteswap.s
;; -*- WASM -*-

@option type.unique = true

;;
;; 作業領域
;;

            .import.memory  "module", "memory", 1

;;
;; マクロ
;;

$func       .defmacro       name, table ; 関数<name>をマクロ定義

            .export         name        ; export される関数<name>

name        .code                       ; 関数 <name>
offs        .param          i32         ; 引数 offs : メモリ上のオフセット(16byte境界)
cnt         .param          i32         ; 引数 cnt  : 16byte 単位での処理回数

            block                       ; $end 用
            local.get       cnt         ; if (cnt == 0)
            u32.eqz         $end        ;   goto $end

$cont:      loop

            local.get       offs        ; v128.store 用位置
            local.get       offs        ; v128.load 用位置
            v128.load                   ; offs から 16byte 読み取り
            i64x2.const     0, 0        ; i8x16.shuffle 用ダミー
            i8x16.shuffle   *table      ; table による入れ替え
            v128.store                  ; データの直接更新

            local.get       offs        ; offs += 16
            u32.const       16
            u32.add
            local.set       offs

            local.get       cnt         ; cnt -= 1
            u32.const       1
            u32.sub
            local.tee       cnt
            br_if           $cont       ; if (cnt) goto $cont

            end.loop                    ; loop に対する end
$end:       end.block                   ; block に対する end
            end                         ; 終了

            .endmacro                   ; マクロ定義終了

;; マクロ展開による関数の生成
;;
;;   byteswap16(offs, cnt)
;;   byteswap32(offs, cnt)
;;   byteswap64(offs, cnt)
;;

            $func   byteswap16, [1,0]*8[+][0:16:2:2]  ; [1,0, 3,2, 5,4, 7,6, 9,8, 11,10, 13,12, 15,14]
            $func   byteswap32, [3:-1]*4[+][0:16:4:4] ; [3,2,1,0, 7,6,5,4, 11,10,9,8, 15,14,13,12]
            $func   byteswap64, [7:-1]+[15:7]         ; [7,6,5,4,3,2,1,0, 15,14,13,12,11,10,9,8]

;; end: byteswap.s
```


## テスト

**アセンブル**

```console:
$ ./wasmgen byteswap.s --deflate --base64+q=2,w=64,c -
"eNqlzL0KwjAYheHzJWn6k6YVr6A6ufpD53Zw9B6s2EUNFatIp16w1xCwWToIguDZ",
"Xjg8qFpDAEjSnvU92JSkaY6PSy1NbZpbx0Bc8OHhL3h06O51+6yuyxwYY70CjZFv",
"wKIX8R1YkdFWgxcZMlgI2Bgfs5rAmSd8GQYq0nGaWCWGdzk5zZBRSec5aSilftQ4",
"I/jSEyoKgzTR8X+ak5zoJCd+0d7JGC1v",
```

**HTML+JavaScriptでテスト**

「パフォーマンス テスト」をクリックすると、16MiB 分の実行時間をミリ秒(ms)で表示します。

```html:test2.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>ByteSwap テスト</title>
    <style>
     body {
         margin: auto;
         width: 750px;
     }
     table {
         border: solid 0px;
         border-left: solid 1px lightgray;
         border-top: solid 1px lightgray;
         border-collaspe: collaspe;
         border-spacing: 0;
     }
     th, td {
         border: solid 0px;
         border-right: solid 1px lightgray;
         border-bottom: solid 1px lightgray;
         padding: 4px;
     }
    </style>
  </head>
  <body>
    <center>
      <h3>WebAssembly を使った ByteSwap テスト</h3>
      <hr>
      <table>
        <tr><th colspan="3">Uint16Array</th></tr>
        <tr>
          <td><code id="tagData16"></code></td>
          <td>&nbsp;→&nbsp;</td>
          <td><code id="tagSwap16"></code></td>
        </tr>
      </table>
      <div>&nbsp;</div>
      <table>
        <tr><th colspan="3">Uint32Array</th></tr>
        <tr>
          <td><code id="tagData32"></code></td>
          <td>&nbsp;→&nbsp;</td>
          <td><code id="tagSwap32"></code></td>
        </tr>
      </table>
      <p><button onclick="test();">更新</button></p>
      <hr>
      <p><button onclick="onperftest();">パフォーマンス テスト</button></p>
      <table>
        <tr><th>JavaScript</th><th>byteswap16</th><td id="tagPerfJS" style="text-align: right">未実行</td></tr>
        <tr><th>WebAssembly</th><th>ByteSwap.byteswap</th><td id="tagPerfWA" style="text-align: right">未実行</td></tr>
      </table>
      <p>&nbsp;</p>
      <p>&nbsp;</p>
      <p>&nbsp;</p>
    </center>

    <script>

     /*
      * ByteSwap クラス
      *
      *   WebAssembly の SIMD 命令を使用する
      *
      */

     class ByteSwap {
         static #compileStart = false;
         static #wasmMemory = new WebAssembly.Memory({initial:1});
         static #wasmModule = null;
         static #wasmExports = null;

         static createModule()
         {
             if (ByteSwap.#compileStart)
                 return;
             ByteSwap.#compileStart = true;

             const imports = {module: {memory: ByteSwap.#wasmMemory}}

             // byteswap.s を wasmgen でアセンブルしたバイナリを deflate 圧縮して Base64 化
             const base64wasm = [
                 "eNqlzL0KwjAYheHzJWn6k6YVr6A6ufpD53Zw9B6s2EUNFatIp16w1xCwWToIguDZ",
                 "Xjg8qFpDAEjSnvU92JSkaY6PSy1NbZpbx0Bc8OHhL3h06O51+6yuyxwYY70CjZFv",
                 "wKIX8R1YkdFWgxcZMlgI2Bgfs5rAmSd8GQYq0nGaWCWGdzk5zZBRSec5aSilftQ4",
                 "I/jSEyoKgzTR8X+ak5zoJCd+0d7JGC1v",
             ].join('');

             const bin = Uint8Array.from(atob(base64wasm), c => c.charCodeAt(0));
             const res = new Response(
                 new Blob([bin]).stream().pipeThrough(
                     new DecompressionStream('deflate')),
                 {headers: {"Content-Type": "application/wasm"}});
             return WebAssembly.instantiateStreaming(res, imports)
                               .then(ByteSwap.#instantiateCallback);
         }

         static #instantiateCallback(mod)
         {
             ByteSwap.#wasmModule = mod;
             ByteSwap.#wasmExports = mod.instance.exports;
         }

         static byteswap(binary) {
             const wasm = ByteSwap.#wasmExports;
             const memory = ByteSwap.#wasmMemory;

             if (!wasm)
                 throw Error("InitializeByteSwap が完了していません");

                 let func;

             if ((binary instanceof Int8Array) ||
                 (binary instanceof Uint8Array) ||
                 (binary instanceof Uint8ClampedArray))
                 return binary; // byteswap 不要
             else if ((binary instanceof Int16Array) ||
                      (binary instanceof Uint16Array))
                 func = wasm.byteswap16;
             else if ((binary instanceof Int32Array) ||
                      (binary instanceof Uint32Array) ||
                      (binary instanceof Float32Array))
                 func = wasm.byteswap32;
             else if ((binary instanceof BigInt64Array) ||
                      (binary instanceof BigUint64Array) ||
                      (binary instanceof Float64Array))
                 func = wasm.byteswap64;
             else
                 throw TypeError(); // 引数は TypeArray オブジェクトのみ

             if (!binary.length)
                 return binary;

             const bsize = (memory.buffer.byteLength + 0xffff) >> 16;
             const msize = (binary.byteLength + 0xffff) >> 16;

             if (bsize < msize)
                 memory.grow(msize - bsize);

             const count = (binary.byteLength + 15) >> 4;
             const view = new binary.constructor(memory.buffer, 0, binary.length);

             view.set(binary);
             func(0, count);
             return new binary.constructor(view);
         }
     }

     /*
      * テスト
      */

     function make_random_binary(array, size) {
         const tmp = new array(1);
         const max = Math.pow(2, tmp.BYTES_PER_ELEMENT << 3);
         return new array([...Array(size)].map(() => Math.trunc(Math.random() * max)));
     }

     function binary_dump(binary, addr) {
         const unit_size = binary.BYTES_PER_ELEMENT;

         const prefix0 = '0000000000000000'
         const hex = (n, l) => (prefix0 + n.toString(16)).slice(-l);

         const lw = 16 / unit_size;
         const aw = (binary.byteLength - 1).toString(16).length;
         const uw = unit_size << 1;

         const lines = Math.trunc((binary.length + lw - 1) / lw);
         const dat = ((y, x) => {
             const p = y * lw + x;
             return p < binary.length ? ' ' + hex(binary[p], uw) : '';
         });

         return [...Array(lines)].map((_, y) =>
             [addr ? hex(y * lw, aw) + ': ' : '',
              [...Array(lw)].map((_, x) => dat(y, x)).join(''),
              '\n'].join('')).join('');
     }

     function test_byteswap(array, size, tags) {
         const data = make_random_binary(array, size);
         const swap = ByteSwap.byteswap(data);
         tags.data.innerText = binary_dump(data, true);
         tags.swap.innerText = binary_dump(swap);
     }

     function test() {
         test_byteswap(Uint16Array, 32, {data:tagData16, swap:tagSwap16});
         test_byteswap(Uint32Array, 16, {data:tagData32, swap:tagSwap32});
     }

     /*
      * パフォーマンス テスト
      */

     function byteswap16(array) {
         return array.map(v => (v >> 8) | (v << 8));
     }

     let test_binary;
     function perftest() {
         const byte_size = 16 * 1024 * 1024; // 16MiB
         if (!test_binary)
             test_binary = make_random_binary(Uint16Array, byte_size);

         const p_sj = performance.now();
         const js_u16a = byteswap16(test_binary)
         const p_ej = performance.now();
         const p_j = p_ej - p_sj;

         const p_sw = performance.now();
         const wa_u16a = ByteSwap.byteswap(test_binary);
         const p_ew = performance.now();
         const p_w = p_ew - p_sw;

         const perfstr = ((v) => {
             const s = '' + Math.trunc(v * 10);
             return s.slice(0, -1) + '.' + s.slice(-1) + 'ms';
         });
         tagPerfJS.innerText = perfstr(p_j);
         tagPerfWA.innerText = perfstr(p_w);

         return;
         for (const index in js_u16a) {
             if (js_u16a[index] != wa_u16a[index]) {
                 console.log(`${js_u16a[index].toString(16)} != ${wa_u16a[index].toString(16)}`);
                 break;
             }
         }
     }

     function onperftest() {
         setTimeout(perftest);
     }

     /*
      * 初期処理
      */

     ByteSwap.createModule().then(test);

    </script>
  </body>
</html>
```

</div></details>