跳至主要内容

在 V8 中新增 BigInts

· 閱讀時間約 8 分鐘
Jakob Kummerow,精確之仲裁者

在過去幾個月中,我們為 V8 中實作了 BigInts 支援,這是目前由 這項提案 規範的一個功能,將納入未來版本的 ECMAScript。本篇文章記錄了我們的冒險故事。

簡而言之

作為一名 JavaScript 程式設計師,現在1 你的工具箱中多了一種具有任意2 精度的整數:

const a = 2172141653n;
const b = 15346349309n;
a * b;
// → 33334444555566667777n // 耶!
Number(a) * Number(b);
// → 33334444555566670000 // 呜!
const such_many = 2n ** 222n;
// → 6739986666787659948666753771754907668409286105635143120275902562304n

關於新功能的詳細信息以及其用法,請參閱 我們關於 BigInt 的深入文章。我們期待看到您用它們創造出的各種精彩成果!

在記憶體中表示 BigInts

通常,計算機會將整數儲存在 CPU 的暫存器中(如今通常是 32 位或 64 位寬度),或者存儲在暫存器大小的記憶體塊中。這帶來了您可能熟悉的最小值和最大值。例如,一個 32 位有符號整數可以保存 -2,147,483,648 到 2,147,483,647 之間的值。然而,BigInts 的思想是不受此類限制的約束。

那麼如何存儲 100 位、或 1000 位、或一百萬位的 BigInt?它無法裝入暫存器,因此我們在記憶體中分配了一個物件。我們使其足夠大,以便容納 BigInt 的所有位元,分成一系列塊,我們稱之為“數位”——因為這在概念上與如何透過更多數位寫出大於“9”的數字(例如“10”)非常相似;但不同於十進位制使用範圍為 0 到 9 的數位,我們的 BigInts 使用範圍為 0 到 4294967295(即 2**32-1)的數位。這是 32 位 CPU 暫存器3 的值範圍,沒有符號位;我們將符號位單獨儲存。在偽代碼中,一個具有 3*32 = 96 位元的 BigInt 物件看起來像這樣:

{
type: 'BigInt',
sign: 0,
num_digits: 3,
digits: [0x12, 0x34, 0x56],
}

回到學校,回到 Knuth

處理儲存在 CPU 暫存器中的整數非常容易:例如,要將兩者相乘,有一個機器指令可以讓軟體告訴 CPU“將這兩個暫存器的內容相乘!”,CPU 會執行此操作。而對於 BigInt 的運算,我們必須想出自己的解決方案。值得慶幸的是,這是一個幾乎每個孩子在某個時候都會學習解決的任務:還記得你在上學時要計算 345 * 678,卻被禁止使用計算器時做了什麼嗎?

345 * 678
---------
30 // 5 * 6
+ 24 // 4 * 6
+ 18 // 3 * 6
+ 35 // 5 * 7
+ 28 // 4 * 7
+ 21 // 3 * 7
+ 40 // 5 * 8
+ 32 // 4 * 8
+ 24 // 3 * 8
=========
233910

這正是 V8 用來相乘 BigInts 的方式:一位數值一位數值地處理,並累加中間結果。此演算法對於 0 到 9,或是 BigInt 的更大數位值,效果都一樣。

Donald Knuth 在 1969 年的經典著作 The Art of Computer Programming 第二卷中,發表了一個具體實作,處理由較小塊組合而成的大數相乘與相除。V8 的實作依照了這本參考書,這表明這是一段相當經得起時間考驗的電腦科學精華。

“更少脫糖” == 更多甜嗎?

或許令人驚訝的是,為了實現像 -x 這樣看似簡單的一元運算,我們不得不花費不少努力。到目前為止,-x 的作用完全等同於 x * (-1),因此為了簡化處理,V8 儘可能早地在處理 JavaScript 時應用該替換,即在解析器中。這種方法稱為“脫糖”,因為它將 -x 表式視為 x * (-1) 的“語法糖”。其他組件(解釋器、編譯器、整個執行時系統)甚至不需要知道一元運算是什麼,因為它們只會看到乘法,這當然是它們必須支援的。

然而,對於 BigInt,這種執行方式突然變得無效,因為將 BigInt 與 Number(例如 -1)相乘必須拋出 TypeError4。如果 x 是 BigInt,解析器必須將 -x 轉換為 x * (-1n)——但解析器無法知道 x 將會計算出什麼值。因此,我們必須停止依賴這種早期的語法糖,而是處處為 Numbers 和 BigInts 添加對一元運算的正確支援。

關於位運算的一些趣味

目前大多數計算機系統使用一種稱為“二進制補碼”的巧妙技術來存儲有符號整數,它具有以下優點:第一位表示符號,加 1 總是使數字的位模式遞增 1,自動處理符號位。例如,對於 8 位整數:

  • 10000000 是 -128,最小可表示數字,
  • 10000001 是 -127,
  • 11111111 是 -1,
  • 00000000 是 0,
  • 00000001 是 1,
  • 01111111 是 127,最大可表示數字。

這種編碼非常常見,以至於許多程式員期望並依賴這種編碼,而 BigInt 規範反映了這一事實,規定 BigInt 必須如同使用二進制補碼表示。正如上述所描述,V8 的 BigInt 並沒有這麼做!

因此,為了根據規範執行按位運算,我們的 BigInt 必須假裝在底層使用二進制補碼表示。對於正數來說,這沒有什麼區別,但負數必須執行額外的工作來完成這一點。這產生了一個有些令人驚訝的效果,即如果 ab 都是負 BigInt,那麼 a & b 實際上執行了_四_步(而如果它們都是正數只需一步):將兩個輸入轉換為假的二進制補碼格式,然後執行實際操作,然後將結果轉換回我們的實際表示形式。您可能會問,為什麼要來回轉換呢?因為所有非位運算操作以這種方式進行要容易得多。

兩種新的 TypedArray 類型

BigInt 提案包括兩種新的 TypedArray 類型:BigInt64ArrayBigUint64Array。現在,BigInt 提供了一種自然的方式來讀寫這些元素中的所有位,我們可以擁有具有 64 位寬整數元素的 TypedArray。而如果嘗試使用 Number 來操作,可能會遺失某些位。這就是為什麼這些新類型的陣列與現有的 8/16/32 位整數 TypedArray 不大一樣:訪問它們的元素總是用 BigInt;嘗試使用 Number 會拋出異常。

> const big_array = new BigInt64Array(1);
> big_array[0] = 123n; // OK
> big_array[0]
123n
> big_array[0] = 456;
TypeError: 無法將 456 轉換為 BigInt
> big_array[0] = BigInt(456); // OK

正如用這些類型的陣列工作的 JavaScript 程式碼看起來和工作方式與傳統 TypedArray 稍有不同,我們也必須概括我們的 TypedArray 實現,使之對這兩個新類型的陣列表現出不同的行為。

優化考量

目前,我們正在發布 BigInt 的初始實現。它功能齊全,並應提供穩定的效能(比現有的用戶端庫稍快一些),但尚未特別優化。原因是,根據我們優先考慮實際應用而非人工基準的目標,我們首先想看看您會如何使用 BigInt,然後我們可以針對您所關心的用例進行精確優化!

例如,如果我們發現相對較小的 BigInt(最多 64 位)是一個重要的使用案例,我們可以通過使用一種專用表示方式來使其更節省記憶體:

{
type: 'BigInt-Int64',
value: 0x12,
}

要確定的一個細節是,我們是否應該對“int64”的值範圍、“uint64”的範圍或兩者都應如此——要記住,支援較少的快速路徑意味著我們能更快地發布它們,而每增加一個快速路徑,也會讓其他所有操作反而略微變慢,因為受影響的操作必須檢查是否適用。

另一個話題是優化編譯器對 BigInt 的支援。對於在 64 位硬體上操作 64 位值的運算密集型應用程式,將這些值保留在暫存器中會比按目前的方法將它們作為物件分配在堆上高效得多。我們已有計劃如何實現這種支援,但這又是一個我們希望首先搞清楚的情況:這是否真的是您所關心的重點;或者我們是否應該把時間花在其他事情上。

請向我們反饋您使用 BigInt 的經驗以及遇到的任何問題!您可以通過我們的問題追蹤器 crbug.com/v8/new,發送郵件到 [email protected],或通過 Twitter 連接 @v8js

Footnotes

  1. 現在 如果你使用 Chrome Beta、Dev 或 Canary,或者 預覽版本的 Node.js,否則 很快 就會有 (Chrome 67、大約同時的 Node.js 開發版本)。

  2. 任意精度,直到實作定義的限制為止。抱歉,我們尚未找到如何將無限數據壓縮到計算機有限內存的方法。

  3. 在 64 位機器上,我們使用 64 位數位,即值範圍從 0 到 18446744073709551615(即 2n**64n-1n)。

  4. 混合使用 BigIntNumber 操作數類型通常是不允許的。這在 JavaScript 中有些不尋常,但這一決定有解釋