文章出處

原文:Bulletproof JavaScript benchmarks

做JavaScript的基準測試并沒有想的那么簡單。即使不考慮瀏覽器差異所帶來的影響,也有很多難點-或者說陷阱需要面對。

這是為何我創建了jsPerf的一個原因,一個你可以輕松創建并分享各種代碼片段對比結果的簡單工具。用起來非常省事,只需把想要測試的代碼錄入然后jsPerf會為你創建好可以跨平臺跑起來的測試用例。

內部實現上,最開始jsPerf用的是一個基于JSLitmus的基準測試庫,我將它稱作Benchmark.js。后續又往里面添加了更多的特性,最近,John-David Dalton干脆將這個庫徹底重寫了一遍。所以現在Benchmark.js已經比之前好很多了。

本文將對JavaScript基準測試的編寫和運行有一定的參考意義。

基準測試的類型

有很多方法可以測試一段JavaScript代碼的性能。最常見的做法是類似下面這樣的:

方案A

var totalTime,
    start = new Date,
    iterations = 6;
while (iterations--) {
  // 被測試的代碼
}
// totalTime → 運行該測試代碼6次需要的時間(單位:毫秒)
totalTime = new Date - start;

這種方案將被測試的代碼循環執行多次直到預設值(本例為6次)。最后用結束時的時間減去開始的時間,得到運行的總時間。

方案A被用于SlickSpeed, Taskspeed, SunSpider, 和 Kraken這些流行的基準測試庫中。

缺撼

鑒于現在的設備和瀏覽器運行得越來越快,這種將代碼運行固定次數的測試方法有很大概念會得到一個0ms的時間差結果,顯然0是毫無意義的。

方案B

另一種方案是計算固定時間內進行了多少運算量。較之前的做法,這回你不用指定一個固定的循環次數了。

var hz,
    period,
    startTime = new Date,
    runs = 0;
do {
  // 被測試的代碼
  runs++;
  totalTime = new Date - startTime;
} while (totalTime < 1000);

// 將毫秒轉為秒
totalTime /= 1000;

// period → 單位運算的耗時
period = totalTime / runs;

// hz → 單位時間(1秒)內進行的運算量
hz = 1 / period;

// 上面兩步可以簡寫如下:
// hz = (runs * 1000) / totalTime;

將測試代碼一直循環運行直到總耗時totalTime 大于等于1000毫秒,也就是1秒種。

方案B 用于DromaeoV8 Benchmark Suite這兩個庫。

不足

由于有垃圾回收,(運行時的)引擎對代碼的動態優化以及其他進程等的影響,此方案在重復進行測試時得到的結果不盡相同。為了得到更精確的測試結果,需要多次測試取均值。而上面提到的V8 庫只會對測試運行一次,Dromaeo 則會運行5次,但其實還可以做得更徹底以獲取更加精準的結果。一個可行的途徑就是想辦法將目前的測試時間由1000毫秒壓縮到50毫秒,當然前提是系統提供給我們一個沒有誤差且絕對精確的時鐘,這能保證時間盡可能多地用于運行測試代碼(而不會過多地被操作系統的中間停頓浪費掉)。

方案 C

JSLitmus 這個庫結合了前面兩種方案的優點。采用方案A 來將測試代碼運行n次,同時動態調整這個n值以保證測試能夠進行到一個最小的時長,也就是方案B所描述的那樣。

癥結

JSLitmus 規避了方案A的缺點但同時引入了方案B的不足之處。為了進一步提高測試的準確率,JSLitmus 將結果進行了量化,取出3次空測試(譯注:不太理解這里的空測試為何物,不掛測試代碼空跑??)中運行最快的一次,再將每次基準測試的結果減去這個最快值。不幸的是這種做法為了規避B方案的毛病(譯注:B方案需要運行多次以得到更多采樣集合以取均值,換句話說要得到越準確的結果就要耗費越多的時間)反而使結果更不可靠了,因為取3次中最快的一次本身就不符合統計規律(譯注:按統計學的做法,為了得到3次中最快的一次結果,這里又需要運行另外的測試來拿到一個所謂的最快的結果的集合,然后從中求均值)。盡管JSLitmus可以多次運行這樣的基準測試,將量化后的均值與每次測試結果的均值進行差額運算,但這樣得到的最終結果其身上的誤差已經足夠掩蓋之前我們為了提高準確率而做的任何努力了。

方案 D

前面三種方案的短肋可以通過方法轉編(function compilation 編譯轉化之意)和循環展開(loop unrolling)。

function test() {
  x == y;
}

while (iterations--) {
  test();
}

// …將會編譯轉化為 →
var hz,
    startTime = new Date;

x == y;
x == y;
x == y;
x == y;
x == y;
// …

hz = (runs * 1000) / (new Date - startTime);

這種做法將測試代碼變成了展開的形式,避免了循環和量化工作(譯注:沒有了循環也就無需統計單位時間內的運算量了)。

問題

然而,縱然如此它還是有不足之處的。將函數轉編會消耗大量內存同時也把CPU拖慢。當你把一個測試跑上幾百萬次時,可以想象到會創建大量的字符串和轉編無數的函數。

這還不算,因為一個函數完全有可能在遇到return后提前結束執行。所以如果測試中函數在第3行就返回了,將循環展開成上百萬的代碼就顯得毫無意義。看來檢測這些可能的提前退出還是很有必要的,然后回歸到使用while語句(也就是方案A的做法)加上對循環結果的量化。

函數體的提取

在Benchmark.js的實現中,使用了一個稍微不同的做法。你可以認為它結合了方案A,B,C還有D的長處。考慮到內存因素,我們沒有將循環展開。為了控制住增大結果誤差的因素,同時又讓測試代碼可以使用較為自然的實現和變量,我們將每個測試代碼的函數體提取出來。譬如,當用下面的代碼進行測試時:

var x = 1,
    y = '1';

function test() {
  x == y;
}

while (iterations--) {
  test();
}

// …轉會轉編為 →

var x = 1,
    y = '1';
while (iterations--) {
  x == y;
}

如此一來,Benchmark.js 使用一個與 JSLitmus近似的技術:將提取出來的函數體放到一個循環中(這是方案A的做法),重復執行直到達到一個最小的時限(這是方案B),最后重復整個流程取一個嚴格意義上的統計均值作為結果。

注意事項

有偏差的毫秒時鐘

某些瀏覽器與操作系統的組合中,由于種種因素存在時鐘不準的情況。

例如:

Windows XP開機后,程序執行的時鐘周期為 10毫秒,這在其他操作系統中一般為15毫秒。意思就是每隔10毫秒操作系統會接收到來自硬件(譯注:也就是CPU的時鐘系統)的一次中斷。

一些很老的瀏覽器(IE或者火狐2)嚴重依賴操作系統的時鐘,也就是說每次你調用new Date().getTime()它其實直接從系統那里去拿這個時間。很顯然,如果內部系統的時間都只間隔10毫秒或者15毫秒才更新一次,那測試結果會受很大影響,準確性大大降低。這個問題是需要解決的。

值得慶幸的是,JavaScript是可以拿到最小的時間度量單位的。這之后,我們可以通過數學方式將測試結果的不確定性降低到只有1%。為此,我們將這個最小時間度量單位除以2以得到這個不確定性的值。假設我們在XP上用IE6,此種情況下最小的度量單位是15毫秒。這個不確定性的值就為15ms/2=7.5ms。然后我們想控制結果的誤差到1%,于是乎我們將剛才得到的不確定性值除以0.01,就得到了達到測試要求需要的最小測試時限為:7.5/0.01=750ms

備選時鐘

當啟用--enable-benchmarking 標志后,Chrome和Chromium會暴露出一個叫做chrome.Interval的方法,可以用它作為一個高精度的時鐘。

在編寫Benchmark.js庫時, John-David Dalton 經過一番折騰后將Java里這個納秒級的時鐘通過一個小的Java applet插件暴露到了JavaScript中。

使用更高精度的時鐘可以縮短測試周期,相應地可以跑更多測試樣本,從而得到一個誤差更小的測試結果。

Firebug 會禁用火狐的 JIT

啟用Firebug后會禁用火狐高性能的實時(just-in-time JIT)本地代碼編譯,然后你的代碼會跑在普通的JavaScript解釋器里面。這將會比原先慢很多。所以在跑基準測試時千萬記得關掉Firebug。

其他瀏覽器的元素審查工具比如WebKit的Web Inspector或者歐朋瀏覽器的Dragonfly在開啟時也有類似問題,盡管相比于上面的情況會小很多。所以在跑測試時最好還是關掉這些,或多或少還是會影響測試結果的。

瀏覽器缺陷和特性

基準測試內部實現中的一些循環機制容易受到一些瀏覽器本身缺陷的影響,比如像最近IE9的dead-code-removal展示的那樣。火狐TraceMonkey 引擎的一個bug,還有歐朋11 querySelectorAll結果的緩存都會影響到測試結果。這些都是在進行測試是需要注意的。

統計學的重要性

大多數的基準測試/測試代碼給出的結果并且沒有嚴格符合統計學要求。John Resig(譯注:jQuery原始作者)在他早前的一篇文章「JavaScript 基準測試的質量」中有提到。簡單來說,就是應該盡量考慮到每個測試結果的誤差并去減小它。擴大測試的樣本值,健全的測試執行,都能夠起到減少誤差的作用。

跨瀏覽器的測試

如果你想在不同瀏覽器中進行測試且想得到較可靠的結果,一定要在真實的瀏覽器中測試。不要依賴于IE自帶的兼容模式,此模式跟他所模擬的版本是存在實質性差異的。

還有就是除了跟大多其他瀏覽器一樣會限制腳本的時間,IE(8及以下)還限制了代碼的指令數不能超過5百萬。事實上以現在CPU的吞吐能力,這樣的數量級處理起來只是半秒鐘的事情。如果你配置確實過硬,跑起來倒也沒什么只是IE會給出一個Script Warning的警告,這種情況下你可以通過修改注冊表來增大這個數量限制。幸運的是微軟還提供了一個修復助手的程序,你只需要運行即可,比修改注冊表方便多了。更可喜的是,IE9以上,這個逗逼的限制被移除了。

總結

無論你只是跑了一些測試,或者寫一些用例,抑或正在自己寫一個基準測試庫,關于JavaScript基準測試的奧義遠比你看到得要多(譯注:就是水很深,并不是跑個分那么簡單)。Benchmark.js和jsPerf每周都有更新,包含bug修復,新功能添加和一些提升準確率的技巧。但愿主流瀏覽器也能夠為此做些努力吧...


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()