[譯] 瞭解「多態」JSON 數據的性能問題

結構相同但值類型不一樣的對象如何對 JavaScript 性能產生驚人的影響javascript

照片由 [Markus Spiske](https://unsplash.com/@markusspiske?utm_source=medium&utm_medium=referral) 發佈於 [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)

當我作一些底層性能優化以用於渲染 Wolfram Cloud notebook 時,我注意到一個很是奇怪的問題,就是函數會由於處理浮點數進入較慢的執行路徑,即便全部傳入的數據都是整數的狀況下也會是這樣。具體來講,單元格計數器被 JavaScript 引擎視爲浮點數,這大大減慢了大型 notebook 的渲染速度(至少在 Chrome 裏面是這樣)。html

咱們將單元格計數器 (由 CounterAssignmentsCounterIncrements 進行的定義) 表示爲一個整數數組,它具備從屬性名到索引的一個獨立的映射。這比每組計數器存儲爲一個字典形式更爲高效。舉個例子,它並非下面的這種格式前端

{Title: 1, Section: 3, Input: 7}
複製代碼

而是咱們會存儲一個數組java

[1, 3, 7]
複製代碼

而後再保持一個從值到索引的獨立的(全局)映射關係node

{Title: 0, Section: 1, Input: 2}
複製代碼

當咱們渲染 notebook 時,每一個單元格都保留本身當前計數器值的副本,執行本身的賦值和增量(若是有的話),並將新數組傳遞給下一個單元格。android

我發現—-至少在有些時候--V8(也就是 Chrome 和 Node.js 的 JS 引擎)將數值數組視爲它們包含的是浮點數。這會在不少操做上下降效率,由於浮點數的內存佈局不如(小)整數高效。這很奇怪,由於數組裏面除了 Smi (在正負 31 位之間的整數,也就是從 -2³⁰ 到 2³⁰-1)不包含任何其餘的東西。ios

我找到一個解決辦法,就是在從 JSON 對象讀取數據以後到將他們放到計數數組以前,「強制」對全部的值進行「值 | 0」的按位運算轉變成整數(即便他們已是 JSON 數據中的整數)。然而雖然我有了這個解決辦法,可是我仍是不能徹底理解爲何它會起做用--直到最近...git

說明

Mathias BynensBenedikt MeurerAgentConf 的分享 JavaScript 引擎基礎:好的,壞的和醜陋的終於點醒了我:這都是關於 JS 引擎中對象的內部實現,以及每一個對象如何連接到某個結構github

JS 引擎會跟蹤對象上定義的屬性名稱,而後每當添加或刪除屬性時,隱式使用不一樣的結構。相同結構的對象會在內存的相同位置有相同屬性(相對於對象地址而言),容許引擎顯著地加速屬性的訪問並減小單個對象實例的內存樣板(他們沒必要本身維護一本完整的屬性字典)。npm

我以前不知道的是,結構也區分了不一樣類型的屬性值。特別是,具備小整數值的屬性意味着與(部分時候)包含其餘數值的屬性不一樣的結構。好比在

const b = {};
b.x = 2;
b.x = 0.2;
複製代碼

結構轉換髮生在二次賦值時,從一個具備 Smi 值的屬性 x 轉變到一個多是任意雙精度值的屬性 x。以前的結構隨後被「棄用」,再也不繼續使用。就算其餘對象沒有使用非 smi 的值,可是隻要它的屬性 x 一旦被使用就會被切換到其餘狀態。這個幻燈片對此總結的很好。

因此這正是咱們使用計數器的狀況:CounterAssignments 和 CounterIncrements 定義來自 JSON 值的數據就像這樣

{"type": "MInteger", "value": 2}
複製代碼

可是咱們也會有數據像是這樣

{"type": "MReal", "value": 0.2}
複製代碼

在筆記本的其餘部分。即便沒有將 MReal 對象用於計數器,這些對象的存在自己致使全部 MInteger 對象也會改變它們的結構。將它們的值複製到計數器數組中而後也會致使這些數組切換到性能較低的狀態。

檢查 Node.js 中的內部類型

咱們可使用 natives syntax 來檢查 V8 內部的內容。這是經過命令行參數 --allow-natives-syntax 來啓用的。特殊函數的完整列表尚未官方文檔,可是已經有非官方列表。並且還有一個 v8-natives 包能夠更方便的訪問。

在咱們的例子中,咱們可使用 %HasSmiElements 來肯定指定的數組是否具備 Smi 元素:

const obj = {};
obj.value = 1;
const arr1 = [obj.value, obj.value];
console.log(`arr1 has Smi elements: ${%HasSmiElements(arr1)}`);

const otherObj = {};
otherObj.value = 1.5;

const arr2 = [obj.value, obj.value];
console.log(`arr2 has Smi elements: ${%HasSmiElements(arr2)}`);
複製代碼

運行此程序會輸出下面的內容:

$ node --allow-natives-syntax inspect-types.js
arr1 has Smi elements: true
arr2 has Smi elements: false
複製代碼

在構造具備相同結構但具備浮點值的對象以後,使用原始對象(包含整數值)再次產生非 Smi 數組。

在獨立示例上衡量其形成的影響

爲了說明對性能的影響,讓咱們使用如下 JS 程序(counters-smi.js):

function copyAndIncrement(arr) {
  const copy = arr.slice();
  copy[0] += 1;
  return copy;
}

function main() {
  const obj = {};
  obj.value = 1;
  let arr = [];
  for (let i = 0; i < 100; ++i) {
    arr.push(obj.value);
  }
  for (let i = 0; i < 10000000; ++i) {
    arr = copyAndIncrement(arr);
  }
}

main();
複製代碼

咱們首先構造一個從對象 obj 中提取的 100 個整數的數組,而後咱們調用 copyAndIncrement 一千萬次,它會建立一個數組的副本,而後在副本中改變一個元素,而後返回新的數組。這就是在渲染(體積很大的)notebook 時處理單個計數器時實質上發生的事。

讓咱們稍微改變一下程序並在開頭加入以下代碼(counters-float.js):

const objThatSpoilsEverything = {};
    objThatSpoilsEverything.value = 1.5;
複製代碼

僅僅這個對象的存在自己就將致使另外一個對象改變其結構並減慢根據它的值構造的數組的操做。

請注意,建立空對象後添加屬性與解析 JSON 字符串具備相同的效果:

const objThatSpoilsEverything = JSON.parse('{"value": 1.5}');
複製代碼

如今比較這兩個程序的執行狀況:

$ time node counters-smi.js
node counters-smi.js  0.87s user 0.11s system 103% cpu 0.951 total

$ time node counters-float.js
node counters-float.js  1.22s user 0.13s system 103% cpu 1.309 total
複製代碼

這是使用 Node v11.9.0(運行 V8 版本 7.0.276.38-node.16)。但讓咱們嘗試一下全部的主流 JS 引擎:

$ npm i -g jsvu

$ jsvu

$ v8 -v
V8 version 7.4.221

$ spidermonkey -v
JavaScript-C66.0

$ chakra -v
ch version 1.11.6.0

$ jsc
複製代碼

在 Chrome 中的 V8,在 Firefox 中的 SpiderMonkey,在 IE 和 Edge 中的 Chakra,在 Safari 中的 JavaScriptCore。

並不能理想測量整個過程的執行時間,但咱們能夠經過用 multitime 關注每一個示例的 100 次運行的中位數來減小異常值(按隨機順序,在兩次運行之間休息 1 秒):

$ multitime -n 100 -s 1 -b examples.bat
===> multitime results
1: v8 counters-smi.js
            Mean        Std.Dev.    Min         Median      Max
real        0.767       0.014       0.738       0.765       0.812
user        0.669       0.012       0.643       0.666       0.705
sys         0.086       0.003       0.080       0.085       0.095

2: v8 counters-float.js
            Mean        Std.Dev.    Min         Median      Max
real        0.854       0.016       0.829       0.851       0.918
user        0.750       0.019       0.662       0.750       0.791
sys         0.088       0.004       0.082       0.087       0.107

3: spidermonkey counters-smi.js
            Mean        Std.Dev.    Min         Median      Max
real        1.378       0.024       1.355       1.372       1.538
user        1.362       0.011       1.346       1.360       1.408
sys         0.074       0.005       0.067       0.073       0.101

4: spidermonkey counters-float.js
            Mean        Std.Dev.    Min         Median      Max
real        1.406       0.021       1.385       1.400       1.506
user        1.389       0.011       1.374       1.387       1.440
sys         0.075       0.005       0.068       0.074       0.093

5: chakra counters-smi.js
            Mean        Std.Dev.    Min         Median      Max
real        2.285       0.051       2.193       2.280       2.494
user        2.359       0.044       2.291       2.354       2.560
sys         0.203       0.032       0.141       0.202       0.268

6: chakra counters-float.js
            Mean        Std.Dev.    Min         Median      Max
real        2.292       0.050       2.195       2.286       2.444
user        2.365       0.042       2.284       2.360       2.501
sys         0.207       0.031       0.141       0.209       0.277

7: jsc counters-smi.js
            Mean        Std.Dev.    Min         Median      Max
real        1.042       0.031       1.009       1.034       1.218
user        1.051       0.013       1.030       1.050       1.093
sys         0.336       0.013       0.319       0.333       0.394

8: jsc counters-float.js
            Mean        Std.Dev.    Min         Median      Max
real        1.041       0.025       1.012       1.038       1.246
user        1.054       0.012       1.032       1.056       1.099
sys         0.338       0.014       0.315       0.335       0.397
複製代碼

這裏有幾點須要注意:

  • 僅在 V8 中,兩種方法之間存在着顯著差別(大約 0.08 秒或 10%)。

  • 在 Smi 和浮點數模式下,V8 都比其餘全部的引擎更快。

  • 這裏獨立使用的 V8 比 Node 11.9(它使用的老版本的 V8)要快得多。我猜測這主要是由於最近的 V8 版本的常規性能改進(注意 Smi 和浮點數之間的差別是如何從 0.35s 減小到 0.08s 的),但與 V8 相比,Node 的其餘一些開銷可能也有影響。

你能夠看一下完整的測試文件。全部測試均在 2013 年底 15 英寸款 MacBook Pro 上運行,運行 macOS 10.14.3,配備 2.6 GHz i7 CPU。

總結

V8 中的結構轉換可能會產生一些使人驚訝的性能影響。但一般您沒必要在實踐中擔憂這個問題(主要是由於 V8 即便在「慢速」路徑上,也可能比其餘全部引擎都表現得更快)。可是在一個高性能的應用程序中,最好記住「全局」結構表的效果,由於應用程序的各個相互獨立的部分也能夠相互影響。

若是您正在處理不受您控制的外部 JSON 數據,您可使用按位 OR 將值「轉換」爲整數,如值 | 0,這也將確保其內部表示是一個 Smi。

若是您能夠直接定義 JSON 數據,那麼對於具備相同底層值類型的屬性僅使用相同的屬性名稱沒準是個好主意。例如,在咱們的例子中這可能更好用

{"type": "MInteger", "intValue": 2}
{"type": "MReal", "realValue": 2.5}
複製代碼

而不是在不一樣值類型的狀況下都使用同一個屬性。換句話說:避免使用「多態」對象。

即便在實踐中 V8 場景下對性能的影響能夠忽略不計,可是更深刻的瞭解幕後發生的事情總會頗有趣。就我我的來講,當我發現我一年前作的優化爲何有效的時候我會感到特別開心。

有關更詳細的內容,這裏還有各個資料的連接:

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索