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 つのことが行われました。
- 64 ビット整数パラメーターを、それぞれ下位ビットと上位ビットを表す 2 つの 32 ビットパラメーターに置き換えます。
- 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日リリース)を含む 複数のブラウザ に実装されているため、今すぐ試すことができます!