0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-05

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

サンプル(1)

簡単な関数の例です。

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つが返値

テスト

アセンブル

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

HTML+JavaScriptでテスト

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>

結果

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

サンプル(2)

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

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

テスト

アセンブル

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

HTML+JavaScriptでテスト

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

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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?