你不知道的JavaScript——性能測試和調優

性能測試和調優

你不知道的JavaScript讀書筆記

以前咱們討論過宏觀層面上的JavaScript性能問題,討論了asm.jsWebAssemblyWebWorker技術,接下來咱們探究一下JavaScript在微觀層面上的性能問題,並逐步瞭解這些性能問題是否真實存在,以及是否須要花大量時間去優化。node

性能測試問題

若是咱們要測試一段代碼的運行速度(執行時間),咱們一般第一時間會想到編寫如下代碼進行測試:git

var start = Date.now()

// do something

console.log('用時:' + (Date.now() - start))

複製代碼

這在很長一段時間裏,我都認爲這段代碼能測試出絕大數多正確的結果,而事實上這段代碼的結果很是不許確程序員

  1. 它頗有可能報告的時間是0,由於他的運行時間可能小於1ms。或者在一些早期引擎中,定時器的精度只有15ms,也就是這個運算至少要運行15ms纔會有結果輸出。
  2. 對於一個單次的運行幾乎沒有任何參考價值,咱們不能保證引擎或系統在此刻沒有受到其餘因素干擾。
  3. 在得到時間戳時可能存在延遲。
  4. 不能肯定引擎是否對這段測試代碼進行了優化。在真實程序中引擎是否會一樣優化這段代碼,若是不能,這就會致使真實環境中代碼運行變慢。

Benchmark.js

基於以上自寫測試用例的弊端,咱們首先須要作的是重複,簡單的說,就是用循環把測試代碼包起來,但這並非一個簡單的循環屢次求平均值的過程,相關的考慮因素還有定時器精度,結果分佈狀況等。可靠的測試應該結合統計學的合理實踐,因此在本身沒有更好的解決方法以前,選用成熟的測試工具是一個正確的決定,Benchmark.js就是一個這樣的js庫。es6

npm方式安裝benchmarkgithub

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) // 樣本方差
複製代碼

第三個參數中的setupteardown是咱們尤爲要注意的,第三個參數指定測試用例的一些額外信息,其中的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)
複製代碼

這段代碼想比較NumberparseInt在類型轉換上的性能差別,可是因爲引擎優化的存在,這種測試會變得沒有參考性,因爲引擎優化沒有被歸入es的規範內容,可能有些引擎在運行測試代碼的時候進行了啓發式優化,它發現A和B都沒有在後續被使用,因此在整個測試中實際上什麼事情都沒有發生,而在真實環境中,可能又並不是如此。因此咱們必須讓測試環境更可能的接近真實環境。性能優化

jsPerf.com

不少狀況下須要測試不一樣環境下的代碼運行狀況,好比在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(..)內建方法和自定義方法的性能,可是這建立的了一個不公平的對比:

  1. 在循序測試中,自定義方法會不斷被建立,這顯然會增長額外的開銷。
  2. 忽略了內建方法的額外工做:內建方法是將比較值強制裝換成字符串進行比較,好比內建排序會把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參數體驗尾調用優化 上面的代碼運行結果

enter description here
相關文章
相關標籤/搜索