作者:韓子遲
網址:http://www.cnblogs.com/zichi/p/4661253.html
前言
在《高效能JavaScript》一書的第四章演演算法和流程控制中,提到了減少迭代次數加速程式的策略—達夫裝置(Duff’s device)。達夫裝置本身很好理解,但是其效果是否真的像書中所說“如果迭代次數超過1000,那麼達夫裝置的執行效率將明顯提升”?還是隨著瀏覽器效能的逐漸增強,這種以犧牲程式碼閱讀性而獲取的效能提升已經微不足道?
達夫裝置
達夫裝置真的很簡單,說白了就是“迴圈體展開”。看如下的程式碼:
var a = [0, 1, 2, 3, 4];
var sum = 0;
for(var i = 0; i < 5; i++)
sum += a[i];
console.log(sum);
我們將迴圈體展開來寫:
var a = [0, 1, 2, 3, 4];
var sum = 0;
sum += a[0];
sum += a[1];
sum += a[2];
sum += a[3];
sum += a[4];
console.log(sum);
因為少作了多次的for迴圈,很顯然這段程式碼比前者效率略高,而且隨著陣列長度的增加,少作的for迴圈將在時間上體現更多的優勢。
達夫裝置這種思想或者說是策略,原來是運用在C語言上的,Jeff Greenberg將它從C語言移植到了JavaScript上,我們可以來看看他寫的模板程式碼:
var iterations = Math.floor(items.length / 8),
startAt = items.length % 8,
i = 0;
do {
switch(startAt) {
case 0: process(items[i++]);
case 7: process(items[i++]);
case 6: process(items[i++]);
case 5: process(items[i++]);
case 4: process(items[i++]);
case 3: process(items[i++]);
case 2: process(items[i++]);
case 1: process(items[i++]);
}
startAt = 0;
} while(–iterations);
註意看switch/case陳述句,因為沒有寫break,所以除了第一次外,之後的每次迭代實際上會執行8次!Duff’s Device背後的基本理念是:每次迴圈中最多可呼叫8次process()。迴圈的迭代次數為總數除以8。由於不是所有數字都能被8整除,變數startAt用來存放餘數,便是第一次迴圈中應呼叫多少次process()。
此演演算法一個稍快的版本取消了switch陳述句,將餘數處理和主迴圈分開:
var i = items.length % 8;
while(i) {
process(items[i–]);
}
i = Math.floor(items.length / 8);
while(i) {
process(items[i–]);
process(items[i–]);
process(items[i–]);
process(items[i–]);
process(items[i–]);
process(items[i–]);
process(items[i–]);
process(items[i–]);
}
儘管這種方式用兩次迴圈代替了之前的一次迴圈,但它移除了迴圈體中的switch陳述句,速度比原始迴圈更快。
效能測試
接著我們來進行達夫裝置的效能測試。如果迭代中的操作複雜的話,會減小達夫裝置最佳化對於時間的影響,所以迴圈內部我只選取了簡單的操作;而且為了方便操作,選取了8的倍數作為陣列長度。
var a = [];
var times = 1000;
// init array
for(var i = 1; i <= times; i++)
a[i] = i;
// ordinary way
console.time(‘1’);
var sum = 0;
for(var i = 1; i <= times; i++)
sum += 1 / a[i];
console.log(sum);
console.timeEnd(‘1’);
// Duff’s device
console.time(‘2’);
var sum = 0;
while(times) {
sum += 1 / a[times–];
sum += 1 / a[times–];
sum += 1 / a[times–];
sum += 1 / a[times–];
sum += 1 / a[times–];
sum += 1 / a[times–];
sum += 1 / a[times–];
sum += 1 / a[times–];
}
console.log(sum);
console.timeEnd(‘2’);
當times取值較小時,得到了這樣的結果(chrome 版本 43.0.2357.134 m):
此時心中一萬頭草泥馬奔騰而過,默默問自己為什麼會出現這樣有悖常理的結果。直到times取值越來越大:
而在firefox(39.0)下則出現了更詭異的一幕,似乎達夫裝置對其不起任何效果:
那麼達夫裝置真的達不到想象當中的最佳化程度了嗎?為了驗證自己的猜想,同時在網上找到了一個外國朋友寫的測試程式碼JavaScript Loop Optimization,大多數的測試結果還是和預料一致,但是也能捕獲到這樣的截圖:
總結
經過測試,我覺得在迭代次數少的情況下,完全沒有必要用達夫裝置進行最佳化,且不說程式碼可讀性差,有時甚至會適得其反,而多大的迭代次數算多,多大算少,也不好說,特定的瀏覽器特定的版本都有其一定的取值。老版本的瀏覽器運用達夫裝置最佳化效能能得到大幅度的提升,而新版的瀏覽器引擎肯定對迴圈迭代陳述句進行了更強的最佳化,所以達夫裝置能實現的最佳化效果日趨減弱甚至於沒有。而在查閱資料的過程中,有人提出while迴圈的效果和達夫裝置不相上下,接下去也會針對不同的迴圈方式作進一步的探索。