WebAssembly 與 JavaScript BigInt 整合

發佈於 · 標籤為 WebAssembly ECMAScript

JS-BigInt-Integration 功能讓在 JavaScript 和 WebAssembly 之間傳遞 64 位元整數變得容易。這篇文章說明這代表什麼意思以及為何它很有用,包括讓開發人員的工作更簡單、讓程式碼執行得更快,以及加快建置時間。

64 位元整數 #

JavaScript 數字是雙精度,也就是 64 位元浮點值。此類值可以包含任何 32 位元整數且具有完全精度,但無法包含所有 64 位元整數。另一方面,WebAssembly 完全支援 64 位元整數,也就是 i64 類型。在連接兩者時會發生問題:例如,如果 Wasm 函式傳回 i64,那麼如果您從 JavaScript 呼叫它,VM 會擲回例外狀況,類似這樣

TypeError: Wasm function signature contains illegal type

正如錯誤訊息所說,i64 不是 JavaScript 的合法類型。

歷來,解決這個問題的最佳方法是「合法化」Wasm。合法化是指將 Wasm 匯入和匯出轉換為使用 JavaScript 的有效類型。在實務上,這會執行兩件事

  1. 用兩個分別代表低位元和高位元的 32 位元整數取代 64 位元整數參數。
  2. 用代表低位元的 32 位元整數取代 64 位元整數傳回值,並在側邊使用 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 中自動執行。

合法化的缺點 #

合法化對許多事情來說運作得很好,但它確實有缺點,例如將 32 位元片段組合或拆分為 64 位元值的額外工作。雖然這很少發生在熱路徑上,但當它發生時,減速會很明顯 - 我們稍後會看到一些數字。

另一個令人困擾的是,合法化對使用者來說很明顯,因為它改變了 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 中定義一個 extern C 函式,並在 JavaScript 中實作它,作為在 Wasm 和 JavaScript 之間呼叫的簡單且低階方式)的微小 C 程式。這個程式所做的就是將 i64 傳送至 JavaScript,我們會嘗試在其中列印它。

我們可以用它來建構

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

當我們執行它時,我們沒有得到預期的結果

node out.js
JS received: 0x12345678

我們傳送了 0xABCD12345678 但我們只收到 0x12345678 😔。這裡發生的事情是合法化將 i64 轉換成兩個 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 BigInt #

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

然後我們可以執行它(請注意,我們需要傳遞一個旗標給 Node.js 以目前啟用 BigInt 整合)

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

完美,正是我們想要的!

而且這不僅更簡單,而且更快。如前所述,實際上很少會在熱路徑上發生 i64 轉換,但當發生時,速度變慢會很明顯。如果我們將上述範例轉換成基準測試,執行許多 send_i64_to_js 呼叫,那麼 BigInt 版本會快 18%。

BigInt 整合的另一個好處是工具鏈可以避免合法化。如果 Emscripten 不需要合法化,那麼它可能不需要對 LLVM 發出的 Wasm 進行任何工作,這會加快建置時間。如果你使用 -s WASM_BIGINT 建置,並且沒有提供任何其他需要進行變更的旗標,則可以獲得這種速度提升。例如,-O0 -s WASM_BIGINT 有效(但最佳化建置 執行 Binaryen 最佳化器,這對於大小很重要)。

結論 #

WebAssembly BigInt 整合已在 多個瀏覽器 中實作,包括 Chrome 85(於 2020-08-25 發布),因此你可以立即試用!