JavaScript BigInt との WebAssembly 統合

公開日 · タグ: WebAssembly ECMAScript

JS-BigInt-Integration 機能により、JavaScript と WebAssembly 間で 64 ビット整数を簡単に渡せるようになります。 この投稿では、その意味と利点について説明します。開発者にとって簡素化、コードの実行速度の向上、ビルド時間の短縮などが含まれます。

64 ビット整数 #

JavaScript の数値は倍精度浮動小数点数、つまり 64 ビット浮動小数点値です。 この値には、すべての 32 ビット整数を完全な精度で含めることができますが、64 ビット整数はすべて含めることはできません。 一方、WebAssembly は 64 ビット整数型である i64 型を完全にサポートしています。 問題は、この 2 つを接続する場合に発生します。たとえば、Wasm 関数が i64 を返す場合、JavaScript から呼び出すと VM は次のような例外をスローします。

TypeError: Wasm function signature contains illegal type

エラーメッセージが示すように、i64 は JavaScript の有効な型ではありません。

これまで、この問題に対する最良の解決策は、Wasm の「合法化」でした。 合法化とは、Wasm のインポートとエクスポートを JavaScript の有効な型を使用するように変換することです。 実際には、これにより 2 つのことが行われました。

  1. 64 ビット整数パラメーターを、それぞれ下位ビットと上位ビットを表す 2 つの 32 ビットパラメーターに置き換えます。
  2. 64 ビット整数の戻り値を、下位ビットを表す 32 ビットの値に置き換え、上位ビットには別に 32 ビットの値を使用します。

たとえば、次の Wasm モジュールを考えてみましょう。

(module
(func $send_i64 (param $x i64)
..))

合法化により、これは次のように変換されます。

(module
(func $send_i64 (param $x_low i32) (param $x_high i32)
(local $x i64) ;; the real value the rest of the code will use
;; code to combine $x_low and $x_high into $x
..))

合法化は、VM で実行される前に、ツール側で行われます。 たとえば、Binaryen ツールチェーンライブラリには、LegalizeJSInterface と呼ばれるパスがあり、この変換を実行します。これは、Emscripten で必要な場合に自動的に実行されます。

合法化の欠点 #

合法化は多くの場合うまく機能しますが、64 ビット値に 32 ビット部分を結合または分割するための追加作業など、欠点があります。 ホットパスで発生することはまれですが、発生した場合、速度の低下が目立つことがあります。後ほど数値で確認します。

もう 1 つの煩わしさは、合法化によって JavaScript と Wasm の間のインターフェースが変更されるため、ユーザーに目に見えることです。次に例を示します。

// example.c

#include <stdint.h>

extern void send_i64_to_js(int64_t);

int main() {
send_i64_to_js(0xABCD12345678ULL);
}
// example.js

mergeInto(LibraryManager.library, {
send_i64_to_js: function(value) {
console.log("JS received: 0x" + value.toString(16));
}
});

これは、JavaScript ライブラリ 関数を呼び出す小さな C プログラムです(つまり、C で extern C 関数を定義し、それを JavaScript で実装します。これは、Wasm と JavaScript 間で呼び出すためのシンプルで低レベルな方法です)。 このプログラムが行うのは、i64 を JavaScript に送信し、そこでそれを出力しようとすることだけです。

これを次のようにビルドできます。

emcc example.c --js-library example.js -o out.js

実行すると、期待した結果が得られません。

node out.js
JS received: 0x12345678

0xABCD12345678 を送信しましたが、0x12345678 しか受信できませんでした 😔。 ここで起こっているのは、合法化によって i64 が 2 つの i32 に変換され、コードは下位 32 ビットのみを受信し、送信された別のパラメーターを無視したことです。 これを適切に処理するには、次のようなことを行う必要があります。

  // The i64 is split into two 32-bit parameters, “low” and “high”.
send_i64_to_js: function(low, high) {
console.log("JS received: 0x" + high.toString(16) + low.toString(16));
}

これで実行すると、次のようになります。

JS received: 0xabcd12345678

ご覧のとおり、合法化に対応することは可能です。 しかし、少し面倒です!

解決策: JavaScript BigInts #

JavaScript には、任意のサイズの整数を表す BigInt 値が導入されたため、64 ビット整数を適切に表すことができます。 Wasm からの i64 を表すためにこれらを使用したいと思うのは当然のことです。 JS-BigInt-Integration 機能はまさにそれを行います!

Emscripten は Wasm BigInt 統合をサポートしており、-s WASM_BIGINT を追加するだけで、元の例を(合法化のためのハックなしで)コンパイルするために使用できます。

emcc example.c --js-library example.js -o out.js -s WASM_BIGINT

次に、実行できます(現在、BigInt 統合を有効にするには、Node.js にフラグを渡す必要があることに注意してください)。

node --experimental-wasm-bigint a.out.js
JS received: 0xabcd12345678

完璧です。まさに私たちが望んでいたとおりです!

そして、これは単に簡素化されるだけでなく、高速化もされます。 前述のように、実際には i64 変換がホットパスで発生することはまれですが、発生した場合、速度の低下が目立つことがあります。 上記の例をベンチマークに変換し、send_i64_to_js を何度も呼び出すと、BigInt バージョンは 18% 高速になります。

BigInt 統合のもう 1 つの利点は、ツールチェーンが合法化を回避できることです。 Emscripten が合法化する必要がない場合、LLVM が出力する Wasm に対して何も処理を行う必要がないため、ビルド時間が短縮されます。 -s WASM_BIGINT でビルドし、変更が必要な他のフラグを指定しない場合、この高速化を実現できます。 たとえば、-O0 -s WASM_BIGINT は機能します(ただし、最適化されたビルドでは、Binaryen オプティマイザが実行されます。これはサイズにとって重要です)。

結論 #

WebAssembly BigInt 統合は、Chrome 85(2020年8月25日リリース)を含む 複数のブラウザ に実装されているため、今すぐ試すことができます!