文章出處

「注釋」作者在本文里沒有說明這么一個事實:
目前的版本Lo-Dash v2.4.1并沒有引入延遲求值的特性,Lo-Dash 3.0.0-pre中部分方法進行了引入,比如filter(),map(),reverse()
原文How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation.

我時常覺得像Lo-Dash這樣優秀的庫已經無法再優化了。它整合了各種奇技淫巧已經將JavaScript的性能開發到了極限。它使用了最快速的語句,優化的算法,甚至還會在發版前做性能測試以保證回歸沒問題。

延遲求值

但似乎我錯了-還可以讓Lo-Dash有明顯的提升。只需將關注點從細微的優化轉移到算法上來。譬如,在一次循環中我們往往會去優化循環體:

var len = getLength();
for(var i = 0; i < len; i++) {
    operation(); // <- 10ms - 如何做到 9ms?!
}

但針對循環體的優化往往很難,很多時候已經到極限了。相反,優化getLength() 函數盡量減少循環次數變得更有意義了。你想啊,這個數值越小,需要循環的10ms就越少。

這便是Lo-Dash實現延遲求值的大致思路。重要的是減少循環次數,而不是每次循環的時間。讓我們考察下面的例子:

function priceLt(x) {
   return function(item) { return item.price < x; };
}
var gems = [
   { name: 'Sunstone', price: 4 }, { name: 'Amethyst', price: 15 },
   { name: 'Prehnite', price: 20}, { name: 'Sugilite', price: 7  },
   { name: 'Diopside', price: 3 }, { name: 'Feldspar', price: 13 },
   { name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 }
];

var chosen = _(gems).filter(priceLt(10)).take(3).value();

我們只想取出3個價格低于10元的小球。通常情況下我們先過濾整個數據源,最后從所有小于10的元素里返回前面三個即可。

但這種做法并不優雅。它處理了全部8個數據,但其實只需要處理前面5個我們就能拿到結果了。同樣為了得到正確的結果,延遲求值則只處理最少的元素。優化后如下圖所示:

一下子就獲得了37.5%的性能提升。很容易找出提升X1000+的例子。比如:

var phoneNumbers = [5554445555, 1424445656, 5554443333, … ×99,999];

// 取出100個含 `55` 的號碼
function contains55(str) {
    return str.contains("55"); 
};

var r = _(phoneNumbers).map(String).filter(contains55).take(100);

這個例子中mapfilter 將遍歷99999 個元素,但很有可能我們只需處理到1000個元素的時候就已經拿到想要的結果了。這回性能的提升就太明顯了(benchmark):

流水線

延遲求值同時帶來了另一個好處,我稱之為“流水線”。要旨就是避免產生中間數組,而是對一個元素一次性進行完所有操作。下面用代碼說話:

var result = _(source).map(func1).map(func2).map(func3).value();

上面看似優雅的寫法在原始的Lo-Dash里會轉換成下面的樣子(直接求值):

var result = [], temp1 = [], temp2 = [], temp3 = [];

for(var i = 0; i < source.length; i++) {
   temp1[i] = func1(source[i]);
}

for(i = 0; i < source.length; i++) {
   temp2[i] = func2(temp1[i]);
}

for(i = 0; i < source.length; i++) {
   temp3[i] = func3(temp2[i]);
}
result = temp3;

當引入了延遲求值后,代碼大致就成這樣的了:

var result = [];
for(var i = 0; i < source.length; i++) {
   result[i] = func3(func2(func1(source[i])));
}

減少不必要的中間變量多少會帶來性能上的提升,特別是在數據源特別巨大,內存又吃緊的情況下。

延遲執行

延遲求值帶來的另一個好處是延遲執行。無論何時你寫了段鏈式代碼,只有在顯式地調用了.value()后才會真正執行。這樣一來,在數據源需要異步去拉取的情況下,可以保證我們處理的是最新的數據。

var wallet = _(assets).filter(ownedBy('me'))
                      .pluck('value')
                      .reduce(sum);

$json.get("/new/assets").success(function(data) {
    assets.push.apply(assets, data); // 更新數據源
    wallet.value(); // 返回的結果是最新的
});

而且這種機制在某些情況下也會提高執行效果。我們可以老早發送一個請求獲取數據,然后指定一個精確的時間來執行。

后記

延遲求值并且不算什么新技術。在一些庫中已經在使用了,比如LINQ,Lazy.js還有其他等等。那么問題來了,Lo-Dash存在的意義是啥?我想就是你仍然可以使用你熟悉的Underscore 接口但享受一個更高效的底層實現,不需要額外的學習成本,代碼上面也不會有大的變動,只需稍加修改。


文章列表


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

    IT工程師數位筆記本

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