- 原文地址:GET READY: A NEW V8 IS COMING, NODE.JS PERFORMANCE IS CHANGING.
- 原文做者:Node.js Foundation
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Starrier
- 校對者:ClarenceC、moods445
本文由 David Mark Clements 和 Matteo Collina 共同撰寫,負責校對的是來自 V8 團隊的 Franziska Hinkelmann 和 Benedikt Meurer。起初,這個故事被髮表在 nearForm 的 blog 板塊。在 7 月 27 日文章發佈以來就作了一些修改,文章中對這些修改有所說起。前端
更新:Node.js 8.3.0 將會和 Turbofan 一塊兒發佈在 V8 6.0 中 。用 NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/rc nvm i 8.3.0-rc.0
來驗證應用程序node
自誕生之日起,node.js 就依賴於 V8 JavaScript 引擎來爲咱們熟悉和喜好的語言提供代碼執行環境。V8 JavaScipt 引擎是 Google 爲 Chrome 瀏覽器編寫的 JavaScipt VM。起初,V8 的主要目標是使 JavaScript 更快,至少要比同類競爭產品要快。對於一種高度動態的弱類型語言來講,這可不是容易的事情。文章將介紹 V8 和 JS 引擎的性能演變。android
容許 V8 引擎高速運行 JavaScript 的是其中一個核心部分:JIT(Just In Time) 編譯器。這是一個能夠在運行時優化代碼的動態編譯器。V8 第一次建立 JIT 編譯器的時候, 它被稱爲 FullCodegen。以後 V8 團隊實現了 Crankshaft,其中包含了許多 FullCodegen 未實現的性能優化。ios
編輯:FullCodegen 是 V8 的第一個優化編譯器,感謝 Yang Guo 的報告c++
做爲 JavaScript 自 90 年代以來的關注者和用戶,JavaScript(無論是什麼引擎)中快速或者緩慢的方法彷佛每每是違法直覺的,JavaScript 代碼緩慢的緣由也經常難以理解。git
最近幾年,Matteo Collina 和 我 致力於研究如何編寫高性能 Node.js 代碼。固然,這意味着咱們在用 V8 JavaScript 引擎執行代碼的時候,知道哪些方法是高效的,哪些方法是低效的。github
如今是時候挑戰全部關於性能的假設了,由於 V8 團隊已經編寫了一個新的 JIT 編譯器:Turbofan。算法
從更常見的 "V8 Killers"(致使優化代碼片斷的 bail-out--
在 Turbofan 環境下失效) 開始,Matteo 和我在 Crankshaft 性能方面所獲得的模糊發現,將會經過一系列微基準測試結果和對 V8 進展版本的觀察來獲得答案。npm
固然,在優化 V8 邏輯路徑前,咱們首先應該關注 API 設計,算法和數據結構。這些微基準測試旨在顯示 JavaScript 在 Node 中執行時是如何變化的。咱們可使用這些指標來影響咱們的通常代碼風格,以及改進在進行經常使用優化以後性能提高的方法。編程
咱們將在 V8 5.一、5.八、5.九、6.0 和 6.1 中查看微基準測試下它們的性能。
將上述每一個版本都放在上下文中:V8 5.1 是 Node 6 使用的引擎,使用了 Crankshaft JIT 編譯器,V8 5.8 是 Node 8.0 至 8.2 的引擎,混合使用了 Crankshaft 和 Turbofan。
目前,5.9 和 6.0 引擎將在 Node 8.3(也多是 Node 8.4)中,而 V8 6.1 是 V8 最新版本 (在編寫本報告時),它在 node-v8 倉庫 github.com/nodejs/node… 的實驗分支中與 Node 集成。換句話說,V8 6.1 版本將在後繼 Node 版本中使用。
讓咱們看下微基準測試,另外一方面,咱們將討論這對將來意味着什麼。全部的微基準測試都由 benchmark.js](https://www.npmjs.com/package/benchmark) 執行,繪製的值是每秒操做數,所以在圖中越高越好。
最著名的去優化模式之一是使用 try/catch
塊。
在這個微基準測試中,咱們比較了四種狀況:
try/catch
的函數 (帶 try catch 的 sum)try/catch
的函數 (不帶 try catch 的 sum)try
塊中的函數 (sum 在 try 中)try/catch
(sum 函數)咱們能夠看到,在 Node 6 (V8 5.1) 圍繞 try/catch
引起性能問題是真實存在的,可是對 Node 8.0-8.2 (V8 5.8) 的性能影響要小得多。
值得注意的是,在 try
塊內部調用函數比從 try
塊以外調用函數慢得多 - 在 Node 6 (V8 5.1) 和 Node 8.0-8.2 (V8 5.8) 都是如此。
然而對於 Node 8.3+,在 try
塊內調用函數的性能影響能夠忽略不計。
儘管如此,不要掉以輕心。在整理性能工做報告時,Matteo 和我發現了一個性能 bug,在特殊狀況下 Turbofan 中可能會致使出現去優化/優化的無限循環 (被視爲「killer」 — 一種破壞性能的模式)。
多年來,delete
已經限制了不少但願編寫出高性能 JavaScript 的人(至少是咱們試圖爲熱路徑編寫最優代碼的地方)。
delete
的問題歸結於 V8 在原生 JavaScript 對象的動態性質以及(可能也是動態的)原型鏈的處理方式上。這使得查找在實現層面上的屬性查詢更加複雜。
V8 引擎快速生成屬性對象的技術是基於對象的「形狀」在 c++ 層建立類。形狀本質上是屬性所具備的鍵、值(包括原型鏈鍵值)。這些被稱爲「隱藏類」。可是這是在運行時對對象進行優化,若是對象的類型不肯定,V8 有另外一種屬性檢索的模型:hash 表查找。hash 表的查找速度很慢。歷史上, 當咱們從對象中 delete
一個鍵時,後續的屬性訪問將是一個 hash 查找。 這是咱們避免使用 delete
而將屬性設置爲 undefined
以防止在檢查屬性是否已經存在時,致使結果與值相同的問題的產生的緣由。 但對於預序列化已經足夠了,由於 JSON.stringify
輸出中不包含 undefined
(undefined
不是 JSON 規範中的有效值) 。
如今,讓咱們看看更新 Turbofan 實現是否解決了 delete
問題。
在這個微基準測試中,咱們比較以下三種狀況:
undefined
後,序列化對象delete
對象屬性後,序列化對象delete
已被移出對象的最近添加的屬性後,序列化對象在 V8 6.0 和 6.1 (還沒有在任何 Node 發行版本中使用)中,Turbofan 會建立一個刪除最後一個添加到對象中的屬性的快捷方式,所以會比設置 undefined
更快。這是好消息,由於它代表 V8 團隊正努力提升 delete
的性能。然而,若是從對象中刪除了一個不是最近添加的屬性, delete
操做仍然會對屬性訪問的性能帶來顯著影響。所以,咱們仍然不推薦使用 delete
。
編輯: 在以前版本的帖子中,咱們得出結論 elete
能夠也應該在將來的 Node.js 中使用。可是 Jakob Kummerow 告訴咱們,咱們的基準測試只觸發了最後一次屬性訪問的狀況。感謝 Jakob Kummerow!
ARGUMENTS
普通 JavaScript 函數 (相對於沒有 arguments
對象的箭頭函數 )可用隱式 arguments
對象的一個常見問題是它相似數組,實際上不是數組。
爲了使用數組方法或大多數數組行爲,arguments
對象的索引屬性已被複制到數組中。在過去 JavaScripters 更傾向於將 less code和 faster code 相提並論。雖然這一經驗規則對瀏覽器端代碼產生了有效負載大小的好處,但可能會對在服務器端代碼大小遠不如執行速度重要的狀況形成困擾。所以將arguments
對象轉換爲數組的一種誘人的簡潔方案變得至關流行: Array.prototype.slice.call(arguments)
。調用數組 slice
方法將 arguments
對象做爲該方法的this
上下文傳遞, slice
方法從而將對象看作數組同樣。也就是說,它將整個參數數組對象做爲一個數組來分割。
然而當一個函數的隱式 arguments
對象從函數上下文中暴露出來(例如,當它從函數返回或者像 Array.prototype.slice.call(arguments)
時,會傳遞到另外一個函數時)致使性能降低。 如今是時候驗證這個假設了。
下一個微基準測量了四個 V8 版本中兩個相互關聯的主題:arguments
泄露的成本和將參數複製到數組中的成本 (隨後 函數做用域代替了 arguments
對象暴露出來).
這是咱們案例的細節:
arguments
對象暴露給另外一個函數 - 不進行數組轉換 (泄露 arguments)Array.prototype.slice
特性複製 arguments
對象 (數組的 prototype.slice arguments)讓咱們看一下線性圖形中的相同數據以強調性能特徵的變化:
要點以下:若是咱們想要將函數輸入做爲一個數組處理,寫在高性能代碼中 (在個人經驗中彷佛至關廣泛),在 Node 8.3 及更高版本應該使用 spread 運算符。在 Node 8.2 及更低版本應該使用 for 循環將鍵從 arguments
複製到另外一個新的(預分配) 數組中 (詳情請參閱基準代碼)。
在 Node 8.3+ 以後的版本中,咱們不會由於將 arguments
對象暴露給其餘函數而受到懲罰, 所以咱們不須要完整數組並能夠以使用相似數組結構的狀況下,可能會有更大的性能優點。
部分應用(或 currying)指的是咱們能夠在嵌套閉包做用域中捕獲狀態的方式。
例如:
function add (a, b) {
return a + b
}
const add10 = function (n) {
return add(10, n)
}
console.log(add10(20))
複製代碼
這裏 add
的參數 a
在 add10
函數中數值 10
部分應用。
從 EcmaScript 5 開始,bind
方法就提供了部分應用的簡潔形式:
function add (a, b) {
return a + b
}
const add10 = add.bind(null, 10)
console.log(add10(20))
複製代碼
可是咱們一般不用 bind
,由於它明顯比使用閉包要慢 。
這個基準測試了目標 V8 版本中 bind
和閉包之間的差別,並以之直接函數調用做爲控件。
這是咱們使用的四個案例:
bind
部分應用另外一個函數的第一個參數建立的函數 (bind)。基準測試結果的可視化線性圖清楚地說明了這些方法在 V8 或者更高版本中是如何合併的。有趣的是,使用箭頭函數的部分應用比使用普通函數要快(至少在咱們微基準狀況下)。事實上它跟蹤了直接調用的性能特性。在 V8 5.1 (Node 6) 和 5.8(Node 8.0–8.2)中 bind
的速度顯然很慢,使用箭頭函數進行部分應用是最快的選擇。然而 bind
速度比 V8 5.9 (Node 8.3+) 提升了一個數量級,成爲 6.1 (Node 後繼版本) 中最快的方法( 幾乎能夠忽略不計) 。
使用箭頭函數是克服全部版本的最快方法。後續版本中使用箭頭函數的代碼將偏向於使用 bind
,由於它比普通函數更快。可是,做爲警告,咱們可能須要研究更多具備不一樣大小的數據結構的部分應用類型來獲取更全面的狀況。
函數的大小,包括簽名、空格、甚至註釋都會影響函數是否能夠被 V8 內聯。是的:爲你的函數添加註釋可能會致使性能降低 10%。Turbofan 會改變麼?讓咱們找出答案。
在這個基準測試中,咱們看三種狀況:
Code: github.com/davidmarkcl…
在 V8 5.1 (Node 6) 中,sum small function 和 long all together 是同樣的。這完美闡釋了內聯是如何工做的。當咱們調用小函數時,就好像 V8 將小函數的內容寫到了調用它的地方。所以當咱們實際編寫函數的內容 (即便添加了額外的註釋填充)時, 咱們已經手動內聯了這些操做,而且性能相同。在 V8 5.1 (Node 6) 中,咱們能夠再次發現,調用一個包含註釋的函數會使其超過必定大小,從而致使執行速度變慢。
在 Node 8.0–8.2 (V8 5.8) 中,除了調用小函數的成本顯著增長外,狀況基本相同。這多是因爲 Crankshaft 和 Turbofan 元素混合在一塊兒,一個函數在 Crankshaft 另外一個可能 Turbofan 中致使內聯功能失調。(即必須在串聯內聯函數的集羣間跳轉)。
在 5.9 及更高版本(Node 8.3+)中,由不相關字符(如空格或註釋)添加的任何大小都不會影響函數性能。這是由於 Turbofan 使用函數 AST (Abstract Syntax Tree 節點數來肯定函數大小,而不是像在 Crankshaft 中那樣使用字符計數。它不檢查函數的字節計數,而是考慮函數的實際指令,所以 V8 5.9 (Node 8.3+)以後 空格, 變量名字符數, 函數名和註釋再也不是影響函數是否內聯的因素。
值得注意的是,咱們再次看到函數的總體性能降低。
這裏的優勢應該仍然是保持函數較小。目前咱們必須避免函數內部過多的註釋(甚至是空格)。並且若是您想要絕對最快的速度,手動內聯(刪除調用)始終是最快的方法。固然還要與如下事實保持平衡:函數不該該在大小(實際可執行代碼)肯定後被內聯,所以將其餘函數代碼複製到您的代碼中可能會致使性能問題。換句話說,手動內聯是一種潛在方法:大多數狀況下,最好讓編譯器來內聯。
衆所周知,JavaScript 只有一種數據類型:Number
。
可是 V8 是用 C++ 實現的,所以必須在 JavaScript 數值的底層基礎類型上進行選擇。
對於整數 (也就是說,當咱們在 JS 中指定一個沒有小數的數字時), V8 假設全部的數字都是 32 位--直到它們不是的時候。 這彷佛是一個合理的選擇,由於多數狀況下,數字都在 2147483648–2147483647 範圍之間。 若是 JavaScript (整) 數超過 2147483647,JIT 編譯器必須動態地將該數字基礎類型更改成 double (雙精度浮點數) — 這也可能對其餘優化產生潛在的影響。
如下三個基準測試案例:
Code: github.com/davidmarkcl…
咱們能夠從圖中看出,不管是在 Node 6 (V8 5.1) 仍是 Node 8 (V8 5.8) 甚至是 Node 的後繼版本,這些觀察都是正確的。使用大於 2147483647 數字(整數)的操做將致使函數運行速度在一半到三分之二之間。所以,若是您有很長的數字 ID—將他們放在字符串中。
一樣值得注意的是,在 32 位範圍內的數字操做在 Node 6 (V8 5.1) 和 Node 8.1 以及 8.2 (V8 5.8) 有速度增加,可是在 Node 8.3+ (V8 5.9+)中速度明顯下降。然而在 Node 8.3+ (V8 5.9+)中,double 運算變得更快,這極可能是(32位)數字處理速度緩慢,而不是函數或與 for
循環 (在基準代碼中使用)速度有關
編輯: 感謝 Jakob Kummerow 和 Yang Guo 已經 V8 團隊對結果的準確性和精確性的更新。
得到對象的全部值並對它們進行處理是常見的操做,並且有不少方法能夠實現。讓咱們找出在 V8 (和 Node) 中最快的那個版本。
這個基準測試的四個案例針對全部 V8 版本:
for
-in
循環中使用 hasOwnProperty
方法來檢查是否已經得到對象值。 (for in)Object.keys
並使用數組的 reduce
方法迭代鍵,訪問 iterator 函數中提供給的對象值 (函數式 Object.keys)Object.keys
並使用數組的 reduce
方法迭代鍵,訪問 iterator 函數中的對象值,提供給 reduce
的迭代函數中對象值,以減小 iterator 是箭頭函數的位置 (函數式箭頭函數 Object.keys)for
循環從 Object.keys
返回的數組的每一個對象值 (**for 循環 Object.keys **)咱們還爲V8 5.八、5.九、 6.0 和 6.1 增長了三個額外的基準測試案例
Object.values
和數組 reduce
方法遍歷值, (函數式 Object.values)Object.values
和數組 reduce
方法遍歷值,其中提供給 reduce
的 iterator 函數是箭頭函數 (函數式箭頭函數 Object.values)for
循環遍歷從 Object.values
中返回的數組 (for 循環 Object.values)在 V8 5.1 (Node 6)中,咱們不會支持這些狀況,由於它不支持原生 EcmaScript 2017 Object.values
方法。
Code: github.com/davidmarkcl…
在 Node 6 (V8 5.1) 和 Node 8.0–8.2 (V8 5.8) 中,遍歷對象的鍵而後訪問值使用 for
-in
是迄今爲止最快的方法。4 千萬 op/s 比下一個接近 Object.keys
的方法(大約 8 百萬 op/s)快了近5倍。
在 V8 6.0 (Node 8.3) 中 for
-in
發生了改變,它下降至以前版本速度的四分之三,但仍然比任何方法速度都快。
在 V8 6.1 (Node 後繼版本)中,Object.keys
比使用for
-in
的速度有所提高 -但在 V8 5.1 和 5.8 (Node 6, Node 8.0-8.2) 中,仍然不及 for
-in
的速度。
Turbofan 背後的運行原理彷佛是對直觀的編碼行爲進行優化。也就是說,對開發者最符合人體工程學的狀況進行優化。
使用 Object.values
直接獲取值比使用 Object.keys
並訪問對象值要慢。最重要的是,程序循環比函數式編程要快。所以在迭代對象時可能要作更多的工做。
此外,對那些爲了提高性能而使用 for
-in
卻由於沒有其餘選擇而失去大部分速度的人來講,這是一個痛苦的時刻。
咱們始終在建立對象,因此這是一個很好的測量領域。
咱們要看三個案例:
Code: github.com/davidmarkcl…
在 Node 6 (V8 5.1) 中全部方法都同樣。
在 Node 8.0–8.2 (V8 5.8)中,從 EcmaScript 2015 類建立實例的速度不及用對象字面量或者構造函數速度的一半。因此,你知道後要注意這一點。
在 V8 5.9 中,性能再次均衡。
而後在 V8 6.0 (多是 Node 8.3,或者是 8.4) 和 6.1 (目前還沒有發佈在任何 Node 版本) 中對象建立速度 簡直瘋狂!!超過了 500 百萬 op/s!使人難以置信。
咱們能夠看到由構造函數建立對象稍慢一些。所以,爲了對將來友好的高性能代碼,咱們最好的選擇是始終使用對象字面量。這很適合咱們,由於咱們建議從函數(而不是使用類或構造函數)返回對象字面量做爲通常的最佳編碼實踐。
編輯:Jakob Kummerow 在 http://disq.us/p/1kvomfk 中指出,Turbofan 能夠在這個特定的微基準中優化對象分配。考慮這一點,咱們會盡快從新進行更新。
當咱們老是將相同類型的 argument 輸入到函數中(例如,咱們老是傳遞一個字符串)時,咱們就以單態形式使用該函數。一些函數被編寫成多態 -- 這意味着相同的參數能夠做爲不一樣的隱藏類處理 -- 因此它可能能夠處理一個字符串、一個數組或一個具備特定隱藏類的對象,並相應地處理它。在某些狀況下,這能夠提供良好的接口,但會對性能產生負面影響。
讓咱們看看單態和多態在基準測試的表現。
在這裏,咱們研究五個案例:
圖中的可視化數據代表,在全部的 V8 測試版本中單態函數性能優於多態函數。
這進一步說明了在 V8 6.1(Node 後繼版本)中,單態函數和多態函數之間的性能差距會更大。不過值得注意的是,這個基於使用了一種 nightly-build 方式構建 V8 版本的 node-v8 分支的版本 -- 可能最終不會成爲 V8 6.1 中的一個具體特性
若是咱們正在編寫的代碼須要是最優的,而且函數將被屢次調用,此時咱們應該避免使用多態。另外一方面,若是隻調用一兩次,好比實例化/設置函數,那麼多態 API 是能夠接受的。
編輯:V8 團隊已經通知咱們,使用其內部可執行文件 _d8_
沒法可靠地重現此特定基準測試的結果。然而,這個基準在 Node 上是可重現的。所以,應該考慮到結果和隨後的分析,可能會在以後的 Node 更新中發生變化(基於 Node 和 V8 的集成中)。不過還須要進一步分析。感謝 Jakob Kummerow 指出了這一點。
DEBUGGER
關鍵詞最後,讓咱們討論一下 debugger
關鍵詞。
確保從代碼中刪除了 debugger
語句。散亂的 debugger
語句會破壞性能。
咱們看下如下兩種案例:
debugger
關鍵詞的函數 (帶有 debugger)debugger
關鍵詞的函數 (不含 debugger)Code: github.com/davidmarkcl…
是的,debugger
關鍵詞的存在對於測試全部 V8 版本的性能來講都很糟糕。
在沒有 debugger 行的那些 V8 版本中,性能顯著提高。咱們將在總結中討論這一點。
除了微基準測試,咱們還能夠經過使用 Node.js 最流行的日誌(Matteo 和我建立的 Pino 時編寫的)來查看 V8 版本的總體效果。
下面的條形圖代表在Node.js 6.11 (Crankshaft) 中最受歡迎的 logger 記錄1萬行(更低些會更好) 日誌所用時間:
如下是使用 V8 6.1 (Turbofan) 的相同基準:
雖然全部的 logger 基準測試速度都有所提升 (大約是 2 倍),但 Winston logger 重新的 Turbofan JIT 編譯器中得到了最大的好處。這彷佛證實了咱們在微基準測試中看到的各類方法之間的速度趨於一致:Crankshaft 中較慢的方法在 Turbofan 中明顯更快,而在 Crankshaft 的快速方法在 Turbofan 中每每會稍慢。Winston 是最慢的,多是使用了在 Crankshaft 中較慢而在 Turbofan 中更快的方法,然而 Pino 使用最快的 Crankshaft 方法進行優化。雖然在 Pino 中觀察到速度有所增長,可是效果不是很明顯。
一些基準測試代表,隨着 V8 6.0 和 V8 6.1中所有啓用 Turbofan,在 V8 5.1, V8 5.8 和 5.9 中的緩慢狀況有所加速 ,但快速狀況也有所降低,這每每與緩慢狀況的增速相匹配。
很大程度上是因爲在 Turbofan (V8 6.0 及以上) 中進行函數調用的成本。Turbofan 的核心思想是優化常見狀況並消除「V8 Killers」。這爲 (Chrome) 瀏覽器和服務器 (Node)帶來了淨效益。 對於大多數狀況來講,權衡出如今(至少是最初)速度降低。基準日志比較代表,Turbofan 的整體淨效應即便在代碼基數明顯不一樣的狀況下(例如:Winston 和 Pino) 也能夠全面提升。
若是您關注 JavaScript 性能已經有一段時間了,也能夠根據底層引擎改善編碼方式,那麼是時候放棄一些技術了。若是您專一於最佳實踐,編寫通常的 JavaScript,那麼很好,感謝 V8 團隊的不懈努力,高效性能時代即將到來。
本文的做者是 David Mark Clements 和 Matteo Collina, 由來自 V8 團隊的 Franziska Hinkelmann 和 Benedikt Meurer 校對。
本文的全部源代碼和文章副本均可以在 github.com/davidmarkcl… 上找到。
文章的原始數據能夠在docs.google.com/spreadsheet…。
大多數的微基準測試是在 Macbook Pro 2016 上進行的,16 GB 2133 MHz LPDDR3 的 3.3 GHz Intel Core i7,其餘的 (數字、屬性已經刪除) 則運行在 MacBook Pro 2014,16 GB 1600 MHz DDR3的 3 GHz Intel Core i7 。Node.js 不一樣版本之間的測試都是在同一臺機器上進行的。咱們已經很是當心地確保不受其餘程序的干擾。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。