審閱:來自V8團隊的Franziska Hinkelmann和Benedikt Meurer.css
**更新:Node.js 8.3.0已經發布了V8 6.0和Turbofan.html
Node.js依靠V8 JavaScript引擎來運行代碼,其語言自己也是咱們熟悉和喜好的。V8 JavaScript引擎是Google爲Chrome瀏覽器編寫的JavaScript虛擬機。從一開始,V8的一個主要目標是讓JavaScript運行地更快,或者至少比競爭對手更快。而對於一個高動態的鬆散類型的語言來講,這並不容易。本文介紹了有關V8和JS引擎性能的演變。node
JIT(Just In Time)編譯器是V8引擎的核心部分,它容許高速執行JavaSctipt代碼。它是一個動態編譯器,能夠在運行時對代碼進行優化。在一開始的V8引擎中JIT編譯器被稱爲FullCodegen,後來V8團隊實現了Crankshaft,其中包含了不少在FullCodegen中沒有實現的性能優化。ios
修正:FullCodegen是V8引擎的第一個優化編譯器,感謝Yang Guo提供。git
做爲JavaScript的局外人和用戶,從90年代開始,彷佛JavaSciprt中的快慢路徑(不管何種引擎)看起來都違背常理,而JavaScript代碼很慢的緣由一般也難以理解。github
近幾年,Matteo Collina和我一直關注如何編寫高性能的Node.js代碼,這意味着咱們必須知道在用V8 JavaScript引擎運行代碼時哪些方法要快哪些方法要慢。算法
如今是時候挑戰這些有關性能方面的假設了,由於V8團隊已經編寫了一個新的JIT編譯器:Turbofan.npm
從衆所周知的「V8殺手」(一段會致使optimazation bail-out的代碼——該術語在Turbofan中已經沒有意義)開始,以及Matteo和我圍繞Crankshaft性能方面的一些發現,咱們將對V8版本的進展進行一系列的觀察並給出微基準測試結果。編程
固然,在進行V8的邏輯路徑優化以前,咱們應該首先關注API設計,算法和數據結構。這些微基準測試用來標識JavaScript在Node中的執行過程如何被改變。咱們可使用這些指示器來改變咱們的代碼風格以及在應用優化以後提升性能的方式。數組
咱們將在V8的5.1,5.8,5.9,6.0和6.1版本上查看微基準測試的性能。
咱們將把每一個不一樣的版本放到對應的環境中:V8 5.1引擎使用Node 6和Crankshaft JIT編譯器,V8 5.8使用Node 8.0和8.2並混合使用Crankshaft和Turbofan。
當前的6.0引擎屬於Node 8.3(或者多是Node 8.4),而V8的6.1是最新版(在編寫本文時),它被集成到Node中,能夠查看實驗中的node-v8 repo。也就是說,V8 6.1版本最終將會出如今將來的Node版本中,有多是Node.js 9。
咱們來看看微基準測試,而另外一方面咱們也將討論這些微基準測試對將來都意味着什麼。全部的這些微基準測試都是經過benchmark.js來執行的,而且數值都是按秒繪製的,所以值越高越好。
其中一個比較著名的去優化模式是使用try/catch塊。
在這個微基準測試中,咱們比較瞭如下四種狀況:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/try-catch.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() function sum (base, max) { var total = 0 for (var i = base; i < max; i++) { total += i } } suite.add('sum with try catch', function sumTryCatch () { try { var base = 0 var max = 65535 var total = 0 for (var i = base; i < max; i++) { total += i } } catch (err) { console.log(err.message) } }) suite.add('sum without try catch', function noTryCatch () { var base = 0 var max = 65535 var total = 0 for (var i = base; i < max; i++) { total += i } }) suite.add('sum wrapped', function wrapped () { var base = 0 var max = 65535 try { sum(base, max) } catch (err) { console.log(err.message) } }) suite.add('sum function', function func () { var base = 0 var max = 65535 sum(base, max) }) suite.on('complete', require('./print')) suite.run()
能夠看到,在Node 6(V8 5.1)中圍繞try/catch所產生的性能問題是真實存在的,可是對Node 8.0-8.2(V8 5.8)版本的性能影響要小得多。
另外值得注意的是,從try塊內部調用一個函數要比從try塊外部調用一個函數慢得多——這一點在Node 6(V8 5.1)和Node8.0-8.2(V8 5.8)中都是同樣的。
不過,對於Node 8.3+而言,在try塊內部調用函數的性能能夠忽略不計。
但也別高興得太早。在研究一些性能研討會的材料時,Mattero和我發現了一個性能問題,就是在某個特定的狀況下會致使Turbofan的無限去優化/從新優化循環(這個被稱之爲「殺手」——一種破壞性能的模式)。
多年來,delete限制了不少但願能寫出高性能JavaScript代碼的人(至少對於咱們正試圖編寫一個熱路徑的最優代碼來講是這樣的)。
Delete的問題被歸結爲V8在處理JavaScript objects的動態特性和原型鏈(也多是動態的)時,對於屬性的查找在實現級別上變得更加複雜。
對於快速生成一個屬性對象,V8引擎所採用的技術是在C++層根據對象的「形狀」來建立一個類。形狀本質上是一個屬性的key和value(包括原型鏈的key和value)。它們被稱之爲「隱藏類」。可是,若是對象的形狀存在不肯定性,V8會採用另外一種屬性檢索模式:哈希表查找。這是對運行時對象的一種優化。哈希表查找方式明顯要慢許多。從以往來看,當咱們將一個key從object中delete時,後續的屬性訪問將變成哈希表查找方式。這就是爲何咱們要避免delete一個屬性,而是將值設置爲undefined。就屬性的值而言,這樣操做的結果是同樣的,但在查看屬性是否存在時會有問題。不過,這對於對象的序列化操做來講一般都是沒問題的,由於JSON.stringify在輸出時不會包含undefined值(在JSON規範中undefined不是有效值)。
如今,讓咱們來看看新的Turbofan是否解決了delete問題。
在這個微基準測試中咱們比較瞭如下三種狀況:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/property-removal.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() function MyClass (x, y) { this.x = x this.y = y } function MyClassLast (x, y) { this.y = y this.x = x } // You can tell if an object is in hash table mode by calling console.log(%HasFastProperties(obj)) when the flag --allow-natives-syntax is enabled in Node.JS. // you can convert back to fast properties using // https://www.npmjs.com/package/to-fast-properties suite.add('setting to undefined', function undefProp () { var obj = new MyClass(2, 3) obj.x = undefined JSON.stringify(obj) }) suite.add('delete', function deleteProp () { var obj = new MyClass(2, 3) delete obj.x JSON.stringify(obj) }) suite.add('delete last property', function deleteProp () { var obj = new MyClassLast(2, 3) delete obj.x JSON.stringify(obj) }) suite.add('setting to undefined literal', function undefPropLit () { var obj = { x: 2, y: 3 } obj.x = undefined JSON.stringify(obj) }) suite.add('delete property literal', function deletePropLit () { var obj = { x: 2, y: 3 } delete obj.x JSON.stringify(obj) }) suite.add('delete last property literal', function deletePropLit () { var obj = { y: 3, x: 2 } delete obj.x JSON.stringify(obj) }) suite.on('complete', require('./print')) suite.run()
在V8 6.0和6.1中(還沒有在任何Node的發行版中使用),刪除對象中最後一個添加的屬性會在V8中命中快速路徑,所以這個操做會比直接將屬性值設置爲undefined要快。這是一個好消息,由於這代表V8團隊正在努力提升delete操做的性能。可是,若是刪除的不是最後添加的屬性,delete操做仍然會致使其他屬性的查找性能降低。因此總的來講,咱們仍是要推薦繼續使用delete。
修正:以前咱們認爲delete可能而且應該在將來的Node.js版本中使用。感謝Jakob Kummerow告知咱們,咱們的基準測試只觸發了最後一個屬性被訪問的狀況!
對普通JavaScript函數來講(ES6中的箭頭函數「=>」沒有arguments對象),一個常見的問題是隱式arguments對象爲類數組,它不是一個真正的數組。
爲了使用數組的方法和數組的大部分特性,arguments對象的索引屬性被複制到了數組中。在之前,JavaScripters傾向於將代碼量與運行速度等同起來,即代碼量越少則執行越快。這條規則會有效地減小瀏覽器端的代碼量,但對於服務端來講代碼的執行速度更重要。所以這樣一種簡單有效地將arguments對象轉換成數組的方式變得很流行:Array.prototype.slice.call(arguments). 調用數組的slice方法並將arguments對象做爲該方法的this上下文傳入,該方法會將整個arguments對象做爲一個數組來分割。
可是當一個函數的隱式arguments對象從上下文中被暴露出來時(例如,當它從函數返回或者經過Array.prototype.slice.call(arguments)傳遞給另外一個函數時),一般會致使性能降低。如今是時候來挑戰這個假設了。
在下一個微基準測試中,咱們測試了四個V8版本中的兩個相互關聯的問題:即暴露arguments參數所產生的開銷,以及將arguments參數複製到數組中的開銷(隨後能夠從函數內部訪問該數組,從而替代暴露arguments對象)。
下面是具體的測試用例:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/arguments.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() function leakyArguments () { return other(arguments) } function copyArgs () { var array = new Array(arguments.length) for (var i = 0; i < array.length; i++) { array[i] = arguments[i] } return other(array) } function sliceArguments () { var array = Array.prototype.slice.apply(arguments) return other(array) } function spreadOp(...args) { return other(args) } function other (toSum) { var total = 0 for (var i = 0; i < toSum.length; i++) { total += toSum[i] } return total } suite.add('leaky arguments', () => { leakyArguments(1, 2, 3) }) suite.add('Array.prototype.slice arguments', () => { sliceArguments(1, 2, 3) }) suite.add('for loop copy arguments', () => { copyArgs(1, 2, 3) }) suite.add('spread operator', () => { spreadOp(1, 2, 3) }) suite.on('complete', require('./print')) suite.run()
讓咱們來看看對應的折線圖,以着重觀察性能特徵的變化:
重點是:將函數的輸入處理成一個數組,若是想要提升性能的話(依據個人經驗這個需求應該很常見),在Node 8.3及以上版本中咱們應當使用擴展運算符。而在Node 8.2及如下版本中,應當使用for循環將arguments中的每個值複製到新(預分配的)數組中(詳情可見代碼)。
更進一步,在Node 8.3+中,將arguments暴露給其它函數不會引發任何問題,所以當咱們不須要一個完整的數組並處理類數組結構時,性能還可能有進一步的提高。
例如:
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,由於它比使用閉包要慢。
這個基準測試使用函數的直接調用比較了bind和閉包在目標V8版本中的區別。
下面是咱們的四個測試用例:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/currying.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() function sum (base, max) { var total = 0 for (var i = base; i < max; i++) { total += i } } var bind = sum.bind(null, 0) var curry = function (max) { return sum(0, max) } var fatCurry = (max) => sum(0, max) suite.add('curry', function smallSum () { var max = 65535 curry(max) }) suite.add('fat arrow curry', function bigSum () { var max = 65535 fatCurry(max) }) suite.add('bind', function smallSum () { var max = 65535 bind(max) }) suite.add('direct call', function bigSum () { var base = 0 var max = 65535 sum(base, max) }) suite.on('complete', require('./print')) suite.run()
這個基準測試的折線圖可視化結果清楚地說明了這些方法在V8的更高版本中是如何融合的。有意思的是,使用箭頭函數的偏函數應用要比正常函數快得多(至少在咱們的微基準測試中是這樣的)。事實上它徹底能夠媲美函數直接調用。對比來看在V8 5.1(Node 6)和5.8(Node 8.0-8.2)中bind方法是很慢的,顯然在偏函數應用中箭頭函數是最快的選擇。不過,從V8 5.9(Node 8.3+)開始,在將來的6.1版本中,bind的速度提升了一個數量級,成了最快的方法(幾乎能夠忽略不計)。
在全部的版本中,柯里化最快的方法是使用箭頭函數。在後來的版本中使用箭頭函數的代碼將盡量地接近使用bind方法的代碼,而目前它是比普通函數最快的方法。但須要說明的一點是,咱們可能須要用不一樣的數據結構來測試更多類型的偏函數應用,以得到更全面的瞭解。
函數的大小,包括簽名、空格甚至註釋都會影響函數是否可使用V8內聯。是的,給函數添加註釋可能會致使性能下降10%。Turbofan會改變這個嗎?讓咱們來看看。
在這個基準測試中咱們查看了如下三種狀況:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/function-size.js
'use strict' // inlining example, v8 inlines sum() but cannot inline longSum because it is too long // Use --trace_inlining to show this // Output: "Did not inline longSum called from long (target text too big)." var benchmark = require('benchmark') var suite = new benchmark.Suite() function sum (base, max) { var total = 0 for (var i = base; i < max; i++) { total += i } } function longSum (base, max) { // Lorem ipsum dolor sit amet, consectetur adipiscing elit. // Vestibulum vel interdum odio. Curabitur euismod lacinia ipsum non congue. // Suspendisse vitae rutrum massa. Class aptent taciti sociosqu ad litora torquent // per conubia nostra, per inceptos himenaeos. Morbi mattis quam ut erat vestibulum, // at laoreet magna pharetra. Cras quis augue suscipit, pulvinar dolor a, mollis est. // Suspendisse potenti. Pellentesque egestas finibus pulvinar. // Vestibulum eu rhoncus ante, id viverra eros. Nunc eget tempus augue. var total = 0 for (var i = base; i < max; i++) { total += i } } suite.add('sum small function', function short () { var base = 0 var max = 65535 sum(base, max) }) suite.add('long all together', function long () { var base = 0 var max = 65535 // Lorem ipsum dolor sit amet, consectetur adipiscing elit. // Vestibulum vel interdum odio. Curabitur euismod lacinia ipsum non congue. // Suspendisse vitae rutrum massa. Class aptent taciti sociosqu ad litora torquent // per conubia nostra, per inceptos himenaeos. Morbi mattis quam ut erat vestibulum, // at laoreet magna pharetra. Cras quis augue suscipit, pulvinar dolor a, mollis est. // Suspendisse potenti. Pellentesque egestas finibus pulvinar. // Vestibulum eu rhoncus ante, id viverra eros. Nunc eget tempus augue. var total = 0 for (var i = base; i < max; i++) { total += i } }) suite.add('sum long function', function long () { var base = 0 var max = 65535 longSum(base, max) }) suite.on('complete', require('./print')) suite.run()
在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(雙精度浮點數)——這可能也會對其它的優化產生潛在的影響。
這個基準測試包含了下面三個用例:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/numbers.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() function sum (base, max) { var total = base for (var i = base + 1; i < max; i++) { total += i } return total } suite.add('sum small', function smallSum () { var base = 0 var max = 65535 // 0 + 1 + ... + 65535 = 2147450880 < 2147483647 sum(base, max) }) suite.add('from small to big', function bigSum () { var base = 32768 var max = 98303 // 32768 + 32769 + ... + 98303 = 4294934528 > 2147483647 sum(base, max) }) suite.add('all big', function bigSum () { var base = 2147483648 var max = 2147549183 // 2147483648 > 2147483647 sum(base, max) }) suite.on('complete', require('./print')) suite.run()
從圖中咱們能夠看到,不管是Node 6(V8 5.1)仍是Node 8(V8 5.8),甚至是未來的Node版本,該測試結果都是成立的。操做大於2147483647的整數將致使函數的運行速度爲1/2~2/3。因此,若是你有一個很長的數字ID,將它們放到字符串中。
一樣值得注意的是,對32位之內的數字操做,在Node 6(V8 5.1)和Node 8.1(V8 5.8)之間速度增長,但在Node 8.3+(V8 5.9+)中速度明顯變慢。可是,對於double類型數字的操做在Node 8.3+ (V8 5.9+)中變得更快。這極可能是32位的數字處理速度變慢,而不是與函數調用的速度或者循環(在測試代碼中使用的)有關。
修正:感謝Jakob Kummerow和Yang Guo以及V8團隊給出了精確的測量結果。
獲取一個對象的全部值並進行相關的操做十分常見,並且有不少方法能夠實現。讓咱們來看看在V8(和Node)版本中哪一個是最快的。
這個基準測試針對全部的V8版本包含了如下四個用例:
咱們還對V8 5.8,5.9和6.1作了另外的三個測試:
咱們沒有在V8 5.1(Node 6)中跑這些測試用例,由於不支持原生的EcmaScript 2017 Object.values方法。
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-iteration.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() suite.add('for-in', function forIn () { var obj = { x: 1, y: 1, z: 1 } var total = 0 for (var prop in obj) { if (obj.hasOwnProperty(prop)) { total += obj[prop] } } }) suite.add('Object.keys functional', function forIn () { var obj = { x: 1, y: 1, z: 1 } var total = Object.keys(obj).reduce(function (acc, key) { return acc + obj[key] }, 0) }) suite.add('Object.keys functional with arrow', function forIn () { var obj = { x: 1, y: 1, z: 1 } var total = Object.keys(obj).reduce((acc, key) => { return acc + obj[key] }, 0) }) suite.add('Object.keys with for loop', function forIn () { var obj = { x: 1, y: 1, z: 1 } var keys = Object.keys(obj) var total = 0 for (var i = 0; i < keys.length; i++) { total += obj[keys[i]] } }) if (process.versions.node[0] >= 8) { suite.add('Object.values functional', function forIn () { var obj = { x: 1, y: 1, z: 1 } var total = Object.values(obj).reduce(function (acc, val) { return acc + val }, 0) }) suite.add('Object.values functional with arrow', function forIn () { var obj = { x: 1, y: 1, z: 1 } var total = Object.values(obj).reduce((acc, val) => { return acc + val }, 0) }) suite.add('Object.values with for loop', function forIn () { var obj = { x: 1, y: 1, z: 1 } var vals = Object.values(obj) var total = 0 for (var i = 0; i < vals.length; i++) { total += vals[i] } }) } suite.on('complete', require('./print')) suite.run()
在Node 6(V8 5.1)和Node 8.0-8.2(V8 5.8)中,使用for-in循環來遍歷對象的key和value是迄今爲止最快的方法。每秒大約操做4千萬次,比排第二位的Object.keys方法快5倍,後者每秒大約操做800萬次。
在V8 6.0(Node 8.3)中,for-in循環有時候會出現一些問題,致使其性能會降到以前版本的1/4,但仍比其它方法都快。
在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遍歷對象的key而後再獲取值要慢。重要的是,程序循環比函數式編程要快。所以在對象迭代過程當中可能會作不少事情。
還有,對於那些使用for-in循環來提升程序性能的人而言,若是速度受到影響而又沒有任何可用的替代方法時,那將會很是痛苦。
註解:在V8中for-in循環的性能問題已經被修復,更多細節請參見http://benediktmeurer.de/2017/09/07/restoring-for-in-peak-performance/。這個修改將會被整合進Node 9中。
對象的分配是無可避免的,因此這是一個重要的測試部分。
咱們將查看如下三個測試用例:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-creation.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() var runs = 0 // the for loop is needed otherwise V8 // can optimize the allocation of the object // away var max = 10000 class MyClass { constructor (x) { this.x = x } } function MyCtor (x) { this.x = x } suite.add('noop', function noop () {}) suite.add('literal', function literalObj () { var obj = null for (var i = 0; i < max; i++) { obj = { x: 1 } } return obj }) suite.add('class', function classObj () { var obj = null for (var i = 0; i < max; i++) { obj = new MyClass(1) } return obj }) suite.add('constructor', function constructorObj () { var obj = null for (var i = 0; i < max; i++) { obj = new MyCtor(1) } return obj }) suite.on('cycle', () => runs = 0) suite.on('complete', require('./print')) suite.run()
對象分配在全部V8版本的測試中都有相同的結果,除了Node 8.2(V8 5.8)中的class,它比其它的方式都慢。這是因爲V8 5.8中的混合Crankshaft/Turbofan特性所致,在包含V8 6.0的Node 8.3中將解決這個問題。
修正:Jakob Kummerow在http://disq.us/p/1kvomfk中指出,在特定的微基準測試中Turbofan能夠優化對象分配,從而致使不正確的測試結果,因此本文作了相應的調整。
在對本文的結果進行整理時,咱們發現Turbofan會始終對某一類對象分配進行優化。起初咱們還一直覺得這個優化會針對全部的對象分配,感謝V8團隊的加入,使得咱們可以更好地理解該優化所涉及的部分。
在以前的對象分配微基準測試中,咱們分配了一個變量,將值設置爲null,而後屢次從新分配該變量,以免觸發咱們如今要查看的特殊優化操做。
與上面同樣,這裏的微基準測試也包含如下三個測試用例:
不一樣之處在於,對象的引用不會被其它對象的分配所覆蓋,而是將該對象傳遞給另外一個操做該對象的函數。
咱們來看看測試結果!
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/object-creation-inlining.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() var runs = 0 class MyClass { constructor (x) { this.x = x } } function MyCtor (x) { this.x = x } var res = 0 function doSomething (obj) { res = obj.x } suite.add('literal', function base () { var obj = { x: 1 } doSomething(obj) }) suite.add('class', function allNums () { var obj = new MyClass(1) doSomething(obj) }) suite.add('constructor', function allNums () { var obj = new MyCtor(1) doSomething(obj) }) suite.add('create', function allNums () { var obj = Object.create(Object.prototype) obj.x = 1 doSomething(obj) }) suite.on('cycle', () => runs = 0) suite.on('complete', require('./print')) suite.run()
咱們注意到在這個微基準測試中V8 6.0(Node 8.3)和6.1(Node 9)的速度大大提升,每秒超過5億次,主要由於一旦Turbofan應用優化,沒有其它任何額外的代碼須要執行。在這種特殊狀況下,Turbofan可以優化對象分配,由於它不須要對象實際存在就可以肯定後續的邏輯能夠被執行。
微基準測試的代碼仍然沒有徹底說明如何觸發這個優化,並且這個優化應用的條件很是複雜。
可是咱們知道的其中一個條件是絕對不會讓對象被Turbofan優化掉的:
對象不能超出建立它的函數。意思是說,在堆棧中的每一個函數完成以後,不該該再出現對該對象的引用。對象能夠傳遞給其它函數,可是若是咱們將該對象添加到this上下文中,或者將其分配給一個外部變量,又或者在堆棧完成以後將其添加到另外一個對象,則沒法應用優化。
這個影響很酷,可是很難預測這種優化發生的全部條件。儘管如此,當複雜的條件獲得知足時,它有可能會產生加速。
修正:感謝Jakob Kummerow和V8團隊的其餘成員幫助咱們發現此特定行爲的根本緣由。做爲這項研究的一部分,咱們發現了在V8新GC中的性能迴歸,Orinoco,若是你對此有興趣能夠查看https://v8project.blogspot.it/2016/04/jank-busters-part-two-orinoco.html and https://bugs.chromium.org/p/v8/issues/detail?id=6663
當咱們老是將同一類型的參數傳遞給一個函數時(好比老是傳遞一個string),咱們就是以單態的方式使用這個函數。
有一些函數被寫成是多態的。咱們能夠把多態函數想象成這樣一個函數,它在同一參數位置上能夠接受不一樣類型的值。例如,一個函數的第一個參數能夠接受一個字符串或者一個對象。不過,這裏咱們所說的「類型」不是指string,number和object,而是指對象的形狀(雖然JavaScript的類型實際上也算做不一樣的對象形狀)。
一個對象的形狀由其屬性和值來定義。例如,在下面的代碼片斷中,obj1和obj2是相同的形狀,但obj3和obj4與其他的形狀不一樣:
const obj1 = { a: 1 } const obj2 = { a: 5 } const obj3 = { a: 1, b: 2 } const obj4 = { b: 2 }
用同一段代碼來處理不一樣形狀的對象,在某些狀況下這是很是不錯的代碼接口,可是每每會影響程序性能。
讓咱們來看看在咱們的微基準測試中單態與多態的測試用例。
這裏咱們測試如下兩種狀況:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/polymorphic.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() var runs = 0 suite.add('polymorphic', function polymorphic() { var objects = [{a:1}, {b:1, a:2}, {c:1, b:2, a:3}, {d:1, c:2, b:3, a:4}]; var sum = 0; for (var i = 0; i < 10000; i++) { var o = objects[i & 3]; sum += o.a; } return sum; }) suite.add('monomorphic', function monomorphic() { var objects = [{a:1}, {a:2}, {a:3}, {a:4}]; var sum = 0; for (var i = 0; i < 10000; i++) { var o = objects[i & 3]; sum += o.a; } return sum; }) suite.on('complete', require('./print')) suite.run()
上圖的可視化數據明確地顯示出,在全部測試的V8版本中,單態函數的性能要優於多態函數。不過,從V8 5.9+開始(也就是從使用V8 6.0的Node 8.3開始),多態函數的性能有了必定的改進。
在Node.js的代碼中,多態函數十分廣泛,它們以APIs的形式提供了很大的靈活性。因爲對多態交互的這種改進,咱們能夠看到在更復雜的Node.js應用程序中的性能有所提高。
若是咱們正在編寫的代碼須要優化,函數須要被屢次調用,那麼咱們應該調用具備相同「形狀」參數的函數。另外一方面,若是一個函數只被調用一兩次,例如instantiating function或者setup function,那麼就能夠選擇一個多態的API。
修正:感謝Jakob Kummerow提供了這個微基準測試的可靠版本。
最後,讓咱們來討論一下debugger關鍵字。
確保將debugger語句從你的代碼中去掉。多餘的debugger語句會影響程序的性能。
咱們來看如下兩個測試用例:
代碼:https://github.com/davidmarkclements/v8-perf/blob/master/bench/debugger.js
'use strict' var benchmark = require('benchmark') var suite = new benchmark.Suite() // node --trace_opt --trace_deopt --trace_inlining --code-comments --trace_opt_verbose debugger.js > out // look for [disabled optimization for 0x34e65f73db01 <SharedFunctionInfo withDebugger>, reason: DebuggerStatement] suite.add('with debugger', function withDebugger () { var base = 0 var max = 65535 var total = 0 for (var i = base; i < max; i++) { debugger total += i } }) suite.add('without debugger', function withoutDebugger () { var base = 0 var max = 65535 var total = 0 for (var i = base; i < max; i++) { total += i } }) suite.on('complete', require('./print')) suite.run()
是的 ,只要debugger關鍵字出現,在全部測試的V8版本中,性能都會嚴重受到影響。
對於沒有debugger關鍵字的狀況,性能出現了連續的降低,咱們將在結論一節討論這個問題。
除了咱們的微基準測試外,咱們還能夠經過使用Mattero和我在建立Pino時放在一塊兒的最流行的Node.js的logger做爲基準測試來查看V8版本的總體效果。
下面的條形圖記錄了在Node.js 6.11(Crankshaft)中使用最流行的logger記錄一萬行日誌所花的時間(越少越好):
而下面是使用V8 6.1(Turbofan)的測試結果:
儘管全部的logger基準測試的速度都有所提升(大約是2倍),可是在新的Turbofan JIT編譯器中Winston logger的性能提高最明顯。這彷佛論證了在咱們的微基準測試中,從各類不一樣的方法所看到的速度趨同性:在Crankshaft中速度較慢的方法在Turbofan中明顯變快,而在Crankshaft中速度較快的方法在Turbofan中趨近於緩慢。Winston是最慢的,可能在Crankshaft中使用的方法要慢而在Turbofan中則要快一些,而在Crankshaft的方法中Pino被優化爲最快。另外咱們觀察到Pino的速度有提升,可是不明顯。
一些基準測試代表,V8 5.1, V8 5.8和5.9中緩慢的狀況隨着V8 6.0和V8 6.1中Turbofan的全面啓用而變得更快,而速度較快的方法其增加速度也會減慢,這一般與緩慢狀況的增加速度相匹配。
其中很大一部分是取決於Turbofan(V8 6.0及以上)中函數調用的成本。Turbofan的作法是優化那些常見的場景並消除「V8殺手」。這爲瀏覽器(Chrome)和服務器應用程序(Node)帶來了很大的好處。這種權衡(至少在一開始)是在性能最好的狀況下會下降速度。咱們的logger基準測試對比顯示出,Turbofan特性的整體淨效應即便在代碼基數明顯不一樣的狀況下(例如Winston與Pino)也能夠全面改善性能。
若是你已經關注JavaScript性能一段時間了,而且爲了適應底層引擎的怪異而對編碼行爲作了調整,那麼差很少是時候要去了解一些新的技術了。 若是你專一於最佳實踐,但願編寫出優秀的JavaScript代碼,則要感謝V8團隊的不懈努力,對於性能方面的改善即將到來。
本文由David Mark Clements和Matteo Collina撰寫,並由V8團隊的Franziska Hinkelmann和Benedikt Meurer進行了審閱。
本文的全部源代碼以及副本能夠查看https://github.com/davidmarkclements/v8-perf
本文的原始數據能夠在這裏找到:https://docs.google.com/spreadsheets/d/1mDt4jDpN_Am7uckBbnxltjROI9hSu6crf9tOa2YnSog/edit?usp=sharing
大部分微基準測試的運行環境爲Macbook Pro 2016,3.3 GHz Intel Core i7,16 GB 2133 MHz LPDDR3,其它如numbers,對象屬性移除,多態性,對象建立等部分的微基準測試的運行環境爲MacBook Pro 2014,在不一樣Node.js版本之間的測試是在同一臺機器上進行的。 咱們很謹慎以確保沒有其它程序的干擾。
原文地址:GET READY: A NEW V8 IS COMING, NODE.JS PERFORMANCE IS CHANGING.