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 的有效類型。在實務上,這會執行兩件事
- 用兩個分別代表低位元和高位元的 32 位元整數取代 64 位元整數參數。
- 用代表低位元的 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 發布),因此你可以立即試用!