以前咱們討論過宏觀層面上的JavaScript
性能問題,討論了asm.js
、WebAssembly
和WebWorker
技術,接下來咱們探究一下JavaScript
在微觀層面上的性能問題,並逐步瞭解這些性能問題是否真實存在,以及是否須要花大量時間去優化。node
若是咱們要測試一段代碼的運行速度(執行時間),咱們一般第一時間會想到編寫如下代碼進行測試:git
var start = Date.now()
// do something
console.log('用時:' + (Date.now() - start))
複製代碼
這在很長一段時間裏,我都認爲這段代碼能測試出絕大數多正確的結果,而事實上這段代碼的結果很是不許確程序員
基於以上自寫測試用例的弊端,咱們首先須要作的是重複,簡單的說,就是用循環把測試代碼包起來,但這並非一個簡單的循環屢次求平均值的過程,相關的考慮因素還有定時器精度,結果分佈狀況等。可靠的測試應該結合統計學的合理實踐,因此在本身沒有更好的解決方法以前,選用成熟的測試工具是一個正確的決定,Benchmark.js
就是一個這樣的js庫。es6
npm
方式安裝benchmark
github
npm install benchmark --save
複製代碼
編寫一個測試文件web
// index.js
var Benchmark = require('benchmark');
function foo () {
var arr = new Array(10000)
for(var i = 0;i < arr.length;i++) {
arr[i] = 0
}
}
var bench = new Benchmark(
'foo test', // 測試名
foo, // 測試內容
{
setup: `console.log('start')`, // 每一個測試循環開始時調用
teardown: `console.log('over')` // 每一個測試循環結束時調用
}
)
bench.run() // 開始測試
console.log(bench.hz) // 每秒運行數
console.log(bench.stats.moe) // 出錯邊界
console.log(bench.stats.variance) // 樣本方差
複製代碼
第三個參數中的setup
和teardown
是咱們尤爲要注意的,第三個參數指定測試用例的一些額外信息,其中的setup
表示每一個測試周期開始時執行的方法,能夠只是方法體,也能夠是指定方法,teardown
表示每一個測試周期結束時執行的方法,類型同上。也就是運行上面的代碼setup
不止執行一次,具體執行次數由Benchmark.prototype.circle
決定。chrome
好比在一次測試環境中,測試運算A每秒可運行10 000 000
次,運算B每秒可運行8 000 000
,這隻能在數學意義上來說B比A慢了20%
。 咱們換個比較方法,從上面的結果不難推出A單次運行須要100ns
,聽說人眼一般能分辨100ms
如下的事件,人腦能夠處理的最快速度是13ms
。也就是運算A要運行650 000
次纔能有但願被人類感知到,而在web
應用中,幾乎不多會進行相似的操做。 比較這麼微小的差別和比較++a
a++
在性能上的差別同樣,意義不大。npm
因爲引擎優化的存在,因此你不能肯定一個運算A是否始終比運算B快,下面的代碼瀏覽器
var a = '12'
// 測試1
var A = Number(a)
// 測試2
var B = parseInt(a)
複製代碼
這段代碼想比較Number
和parseInt
在類型轉換上的性能差別,可是因爲引擎優化的存在,這種測試會變得沒有參考性,因爲引擎優化沒有被歸入es的規範內容,可能有些引擎在運行測試代碼的時候進行了啓發式優化,它發現A和B都沒有在後續被使用,因此在整個測試中實際上什麼事情都沒有發生,而在真實環境中,可能又並不是如此。因此咱們必須讓測試環境更可能的接近真實環境。性能優化
不少狀況下須要測試不一樣環境下的代碼運行狀況,好比在chrome
和在手機版chrome
中的結果對比,在滿電手機和電量2%
如下手機的運行結果對比。jsPerf.com
是一個共享測試用例和測試結果的平臺。
程序員們浪費了大量的時間用於思考,或擔憂他們的程序中非關鍵部分的速度,這些針對效率的努力在調試和維護方面帶來了強烈的負面效果。咱們應該在,好比說97%的時間裏,忘掉小處的效率:過早優化是萬惡之源。但咱們不該該錯過關鍵的3%的機會。 《計算訪談6》
不該該在非關鍵部分花太多時間,好比你的應用是一個動畫表現的應用,就應該重點優化動畫循環相關的代碼。
// 測試1
var x = [1,2,3,4,5]
x.sort()
// 測試2
var x = [1,2,3,4,5]
x.sort(function (a,b) {
return a - b
})
複製代碼
這兩個測試對比sort(..)
內建方法和自定義方法的性能,可是這建立的了一個不公平的對比:
18
排在2
前面。// 測試1
var x = false;
var y = x ? 1 : 2;
// 測試2
var x;
var y = x ? 1 : 2;
複製代碼
上面這個測試若是是想比較Boolean
值強制類型轉換對性能的影響,那麼就建立了一個不公平的對比,由於測試2少作了x
的賦值操做。要消除這個影響,應該這樣作:
// 測試2
var x = undefined;
var y = x ? 1 : 2;
複製代碼
最後咱們來實際測試一下,在for
循環中是否須要預先將arr.length
設定好
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite; // Benchmark.Suite是用來比較多個測試用例的類
var arr = new Array(1000)
suite.add('len', function () { // 添加一個測試用例
for (var i = 0; i < arr.length; i++) {
arr[i] = 1
}
}, {
setup: function () {
arr = new Array(1000)
}
})
.add('preLen', function () {
for (var i = 0, len = arr.length; i < len; i++) {
arr[i] = 1
}
}, {
setup: function () {
arr = new Array(1000)
}
})
.run()
console.log(suite[0].hz)
console.log(suite[1].hz)
// 1160748.8603394227 // 1188525.8945115102 // 1182959.0564495493
// 1167161.734632912 // 1196721.6273367293 // 1195146.3296931305
複製代碼
以上代碼的測試環境爲nodejs@v8.11.4
,測試結果能夠看出將arr.length
提早保存反而會形成反優化,其實背後的緣由就是在v8
等現代JavaScript
引擎中對這種循環已經作過優化,不會在每次循環都會去訪問arr.length
,因此開發者再也不須要考慮這方面的問題,不要想在這方面能比引擎更聰明,結果只會拔苗助長。
es規範一般不會涉及性能方面的要求,但es6
中有一個例外,那就是尾調用優化(Tail Call Optimization
, TCO
),簡單的說,尾調用就是在一個函數的末尾進行的函數調用。
在遞歸中,尾調用優化可能起到很是重要的做用
// 非尾調用
function foo () {
foo()
}
// 非尾調用
function foo () {
return 1 + foo()
}
// 尾調用
function foo () {
return foo()
}
複製代碼
調用一個新的函數須要額外預留一塊內存來管理調用幀,稱爲棧幀
,在沒有TCO
的遞歸調用中,遞歸層級太多會致使棧溢出,遞歸沒法運行。而在支持TCO
的環境並正確書寫TCO
規範的遞歸函數,第二層的遞歸函數中直接使用上層函數的棧幀,依次類推。這樣不只速度快,也更節省內存。
感謝評論區大佬的指正,TCO雖然是es6的一部分,但實質是個很是有爭議的提案,主流瀏覽器幾乎沒有實現它,chrome實現過一段時間,chrome已經棄用 ,這是一份支持尾調用優化的引擎列表compat-table,能夠看到Safari@12
支持尾調用優化,有興趣的小夥伴能夠去驗證一下。 遞歸一般是堆棧溢出的「高發區」,咱們能夠將遞歸改成循環的方式避免使用遞歸
"use strict"
var a = 0,
b = 0;
function demo () {
a++
if (a < 100000) {
return demo()
}
return a
}
setTimeout(() => {
console.log('遞歸: ' + demo())
},1000)
function demo2 () {
b++
if (b < 100000) {
return function () {
return demo2()
}
}
return b
}
function runner (fn) {
let val = fn()
while (typeof val == 'function') {
val = val()
}
return val
}
setTimeout(() => {
console.log('循環:' + runner(demo2))
})
複製代碼
咱們可使用nvm
安裝node@6.2.0
使用--harmony_tailcalls
參數體驗尾調用優化 上面的代碼運行結果