作者:韓子遲
網址:http://www.cnblogs.com/zichi/p/4668420.html
前言
上一篇《高效能JavaScript 達夫裝置》探討了達夫裝置對於程式碼效能的影響,本文主要探討並且測試各種常見的迴圈陳述句的效能以及流程控制中常見的最佳化。
迴圈陳述句
眾所周知,常用的迴圈陳述句有for、while、do-while以及for-in,forEach。除了for-in和forEach效能略低外,平時我們對前三者的選擇更多的是基於需求而非效能考慮,今天我們就對它們各自的效能做個測試,告訴我們最極端的情況下還能做哪些最佳化。
首先我們來談談為何for-in和forEach會比其他的慢。for-in一般是用在物件屬性名的遍歷上的,由於每次迭代操作會同時搜尋實體本身的屬性以及原型鏈上的屬性,所以效率肯定低下;而forEach是基於函式的迭代(需要特別註意的是所有版本的ie都不支援,如果需要可以用JQuery等庫),對每個陣列項呼叫外部方法所帶來的開銷是速度慢的主要原因。
接著我們看看每次迭代中for、while以及do-while都做了什麼。
var length = items.length;
for(var i = 0; i < length; i++)
process(items[i]);
var j = 0;
while(j < length)
process(items[j++]);
var k = 0;
do {
process(items[k++]);
} while(k < length);
上面的每個迴圈中,每次執行迴圈體時都會產生這樣的操作:
- 一次控制條件中的數值大小比較(i < length)
- 一次控制條件結果是否為true的比較(i < length === true)
- 一次自增操作(i++)
- 一次陣列查詢(items[i])
- 一次函式呼叫process(items[i])
我們可以透過顛倒陣列的順序來提高迴圈效能:
for(var i = items.length; i–; )
process(items[i]);
var j = items.length;
while(j–)
process(items[j]);
var k = items.length – 1;
do {
process(items[k]);
} while(k–);
本例中使用了倒序迴圈,並把減法操作整合在迴圈條件中。現在每個控制條件只是簡單地與0比較。控制條件與true值比較,任何非零數會自動轉換為true,而零值等同於false。實際上,控制條件從兩個比較(迭代數少於總數嗎?它是true嗎?)減少到一次比較(它是true嗎?)。每次迭代從兩次比較減少到一次,進一步提高了迴圈速度。
效能測試:
那麼事實真的如此嗎?真金不怕瀏覽器驗。測試程式碼很簡單,針對不同的8種情況封裝了8個函式(不加定時器firefox下無法列印profiles資訊,原因不明):
// init array
var a = [];
var length = 10;
for(var i = 0; i < length; i++)
a[i] = 1;
function for_in() {
var sum = 0;
for(var i in a)
sum += a[i];
}
function for_each() {
var sum = 0;
a.forEach(function(value, index, array) {
sum += value;
});
}
function for_normal() {
var sum = 0;
for(var i = 0; i < length; i++)
sum += a[i];
}
function for_reverse() {
var sum = 0;
for(var i = length; i–; )
sum += a[i];
}
function while_normal() {
var sum = 0;
var i = 0;
while(i < length)
sum += a[i++];
}
function while_reverse() {
var sum = 0;
var i = length;
while(i–)
sum += a[i];
}
function do_while_normal() {
var sum = 0;
var i = 0;
do {
sum += a[i++];
} while(i < length);
}
function do_while_reverse() {
var sum = 0;
var i = length – 1;
do {
sum += a[i];
} while(i–);
}
setTimeout(function() {
console.profile();
for_in();
for_each();
for_normal();
for_reverse();
while_normal();
while_reverse();
do_while_normal();
do_while_reverse();
console.profileEnd();
}, 1000);
當陣列長度為100時,我們發現firefox下的結果確實和預料的相似:for-each和for-in效率低下,倒序比正序效率略微提升。(chrome下的profiles由於時間太短不顯示)
當資料量達到100w時,firefox和chrome下的結果都如人所願,但是也略微有所不同。ff下的for-in表現地比for-each好,而chrome下for-in表現糟糕,直接提出了警告。而倒序迭代雖然效能略微有所提升,但是提升的不是很多,且降低了程式碼閱讀性。
小結:
- 倒序迭代確實能略微提升程式碼效能,但是犧牲了程式碼可讀性,除非追求極端效能最佳化情況下不然沒必要用
- 遍歷陣列能用普通的迴圈就不要用for-in和for-each
條件陳述句
常見的條件陳述句有if-else和switch-case,那麼什麼時候用if-else,什麼時候用switch-case陳述句呢?
我們先來看個簡單的if-else陳述句的程式碼:
if (value == 0){
return result0;
} else if (value == 1){
return result1;
} else if (value == 2){
return result2;
} else if (value == 3){
return result3;
} else if (value == 4){
return result4;
} else if (value == 5){
return result5;
} else if (value == 6){
return result6;
} else if (value == 7){
return result7;
} else if (value == 8){
return result8;
} else if (value == 9){
return result9;
} else {
return result10;
}
最壞的情況下(value=10)我們可能要做10次判斷才能傳回正確的結果,那麼我們怎麼最佳化這段程式碼呢?一個顯而易見的最佳化策略是將最可能的取值提前判斷,比如value最可能等於5或者10,那麼將這兩條判斷提前。但是通常情況下我們並不知道(最可能的選擇),這時我們可以採取二叉樹查詢策略進行效能最佳化。
if (value < 6){
if (value < 3){
if (value == 0){
return result0;
} else if (value == 1){
return result1;
} else {
return result2;
}
} else {
if (value == 3){
return result3;
} else if (value == 4){
return result4;
} else {
return result5;
}
}
} else {
if (value < 8){
if (value == 6){
return result6;
} else {
return result7;
}
} else {
if (value == 8){
return result8;
} else if (value == 9){
return result9;
} else {
return result10;
}
}
}
這樣最佳化後我們最多進行4次判斷即可,大大提高了程式碼的效能。這樣的最佳化思想有點類似二分查詢,和二分查詢相似的是,只有value值是連續的數字時才能進行這樣的最佳化。但是程式碼這樣寫的話不利於維護,如果要增加一個條件,或者多個條件,就要重寫很多程式碼,這時switch-case陳述句就有了用武之地。
將以上程式碼用switch-case陳述句重寫:
switch(value){
case 0:
return result0;
case 1:
return result1;
case 2:
return result2;
case 3:
return result3;
case 4:
return result4;
case 5:
return result5;
case 6:
return result6;
case 7:
return result7;
case 8:
return result8;
case 9:
return result9;
default:
return result10;
}
swtich-case陳述句讓程式碼顯得可讀性更強,而且swtich-case陳述句還有一個好處是如果多個value值傳回同一個結果,就不用重寫return那部分的程式碼。一般來說,當case數達到一定數量時,swtich-case陳述句的效率是比if-else高的,因為switch-case採用了branch table(分支表)索引來進行最佳化,當然各瀏覽器的最佳化程度也不一樣。
除了if-else和swtich-case外,我們還可以採用查詢表。
var results = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10];
//return the correct result
return results[value];
當資料量很大的時候,查詢表的效率通常要比if-else陳述句和swtich-case陳述句高,查詢表能用數字和字串作為索引,而如果是字串的情況下,最好用物件來代替陣列。當然查詢表的使用是有侷限性的,每個case對應的結果只能是一個取值而不能是一系列的操作。
小結:
- 當只有兩個case或者case的value取值是一段連續的數字的時候,我們可以選擇if-else陳述句
- 當有3~10個case數並且case的value取值非線性的時候,我們可以選擇switch-case陳述句
- 當case數達到10個以上並且每次的結果只是一個取值而不是額外的JavaScript陳述句的時候,我們可以選擇查詢表
參考
- Javascript各種迴圈測試,達夫裝置可用性分析
- Writing Efficient JavaScript: Chapter 7 – Even Faster Websites
- 《高效能JavaScript》