Javascript中for-in效率分析和優化
Javascript程式中,我們經常使用Object來模擬dictionary/map/hashmap的行為,也會使用for-in語法來遍歷dictionary的元素。但你是否遇到過由於使用for-in而導致程式產生效能問題呢?
問題
Javascript裡的資料結構比較簡單,除了陣列,就是使用物件模擬的字典/Hash表。比如:
var dict = { key1: "value1", key2: "value2" };
或者:
var dict = {}; dict.key1 = "value1"; dict.key2 = "value2";
使用起來非常方便。
如果有一個200000個元素的字典,我們一樣可以用物件輕鬆搞定:
const dataLength = 200000; let objInt = {}; for(var ii=0;ii<dataLength;++ii){ objInt[ii] = {}; }
挨個遍歷字典的元素可以這樣使用for-in這樣寫:
for(var key in objInt){ // do something var val = obj[key]; }
但for-in這樣的寫法有很大的效能問題,你是否遇到呢?
實驗
測試一
為分析for-in的效能問題,我們構建一個簡單的測試程式:
const dataLength = 200000; let objInt = {}; for(var ii=0;ii<dataLength;++ii){ objInt[ii] = {}; } function runTestCase(obj){ console.time( 'for-in' ); for(var key in obj){ // do nothing } console.timeEnd( 'for-in' ); console.log("---------------------------") } // 取樣10次 for(var ii=0; ii<10;++ii){ runTestCase(objInt); }
某次的執行結果:
for-in: 33.838134765625ms --------------------------- for-in: 22.391845703125ms --------------------------- for-in: 25.2509765625ms --------------------------- for-in: 26.045166015625ms --------------------------- for-in: 25.205078125ms --------------------------- for-in: 25.132080078125ms --------------------------- for-in: 25.23193359375ms --------------------------- for-in: 26.073974609375ms --------------------------- for-in: 26.580078125ms --------------------------- for-in: 27.85595703125ms ---------------------------
從資料上可以發現,只是遍歷物件屬性就花費了近30ms。如果重新整理率是30fps,30ms意味著耗費了近一幀的時間。從這個角度看,for-in確實優點慢!
分析一
上面的資料是for-in整個迴圈的時間,從執行順序看,第一次執行花費的時間明顯多於其餘9次;如果單步除錯每一個for-in語句,我們會發現剛執行到for-in,第一步執行的時候會卡頓,後續的迴圈並不卡。
由此我們先做兩個假設:
-
在第一次執行for-in的時候,JS引擎會做快取,後續的for-in會重用某些資料
-
在每一次for-in開始執行的時候,JS引擎會針對for迴圈做“初始化”,準備迴圈所需要的key。
假如我們顯式地位for迴圈準備key列表,可以這樣來實現:
let keys = Object.keys(obj); for(var ii=0;ii<keys.length;++ii){ var val = obj[keys[ii]]; }
基於此,我們做如下的測試。
測試二
function runTestCase(obj){ console.time( 'for-in' ); for(var key in obj){ // do something //var val = obj[key]; } console.timeEnd( 'for-in' ); console.time( 'keys' ); let keys = Object.keys(obj); console.timeEnd( 'keys' ); console.time( 'for' ); for(var ii=0;ii<keys.length;++ii){ var val = obj[keys[ii]]; } console.timeEnd( 'for' ); console.log("---------------------------") }
某次執行的資料如下:
------------integer as the key--------------- for-in: 35.127197265625ms keys: 25.034912109375ms for: 8.205078125ms --------------------------- for-in: 26.971923828125ms keys: 23.35986328125ms for: 4.507080078125ms --------------------------- for-in: 26.576904296875ms keys: 22.1259765625ms for: 4.619140625ms --------------------------- for-in: 27.137939453125ms keys: 24.44189453125ms for: 4.93896484375ms --------------------------- for-in: 25.48193359375ms keys: 22.7060546875ms for: 4.5869140625ms --------------------------- for-in: 25.342041015625ms keys: 24.0732421875ms for: 4.565185546875ms --------------------------- for-in: 26.733642578125ms keys: 20.468994140625ms for: 5.10205078125ms --------------------------- for-in: 24.662109375ms keys: 21.843017578125ms for: 5.114013671875ms --------------------------- for-in: 25.3349609375ms keys: 21.201171875ms for: 4.283935546875ms --------------------------- for-in: 25.453857421875ms keys: 22.219970703125ms for: 4.64697265625ms ---------------------------
分析二
從測試結果我們可以得到以下的結論:
Time(keys) < Time(for-in) (Time(keys) + Time(for)) ≈ Time(for-in)
基於測試二,我們可以斷定:for-in內部呼叫了類似Object.keys(...)這樣方法進行了初始化。
測試三
以上兩個實驗,我們都是針對整數型別的key做了測試。應用中string型別的key更為常見,我們再對string的key做一下對比:
let objStr = {}; for(var ii=0;ii<dataLength;++ii){ objStr["key."+ii] = {}; } console.log("------------string as the key---------------") for(var ii=0; ii<10;++ii){ runTestCase(objStr); }
某次的執行結果:
------------string as the key--------------- for-in: 98.906005859375ms keys: 84.493896484375ms for: 18.296142578125ms --------------------------- for-in: 107.96484375ms keys: 112.447021484375ms for: 15.771728515625ms --------------------------- for-in: 98.1650390625ms keys: 87.136962890625ms for: 17.81494140625ms --------------------------- for-in: 91.898681640625ms keys: 92.19287109375ms for: 16.776123046875ms --------------------------- for-in: 102.074951171875ms keys: 82.09814453125ms for: 17.291259765625ms --------------------------- for-in: 96.35302734375ms keys: 82.85400390625ms for: 17.014892578125ms --------------------------- for-in: 94.77001953125ms keys: 83.259033203125ms for: 18.739990234375ms --------------------------- for-in: 92.93798828125ms keys: 83.68115234375ms for: 18.369873046875ms --------------------------- for-in: 95.656005859375ms keys: 79.9150390625ms for: 16.451904296875ms --------------------------- for-in: 96.9970703125ms keys: 81.939208984375ms for: 15.505859375ms ---------------------------
分析三
對比整數的key,可以發現string做為key,效能明顯下降,耗時是integer型別key的3~4倍:Hash計算是有代價的。
所以,如果能用integer型別的資料做為字典的key,就不要用string型別的key。
方案
只讀字典
如果應用的場景中,字典是隻讀的,我們只需要把keys快取起來,每次迴圈直接就能得到keys的列表,省去中間商賺差價的環節:
class ReadonlyDict{ constructor(obj){ this.items = obj; this.keys = Object.keys(obj); } forEach(callback){ var keys = this.keys; var items = this.items; for(var ii=0;ii<keys.length;++ii){ var key = keys[ii]; var value = items[key]; callback(key,value); } } }
測試程式:
console.log("------------Integer Dict---------------") console.time( 'dict' ); let dictInt = new ReadonlyDict(objInt); console.timeEnd( 'dict' ); for(var ii=0; ii<10;++ii){ console.time( 'dict-forEach' ); dictInt.forEach(function(key, val){ }); console.timeEnd( 'dict-forEach' ); } console.log("------------String Dict---------------") console.time( 'dict' ); let dictStr = new ReadonlyDict(objStr); console.timeEnd( 'dict' ); for(var ii=0; ii<10;++ii){ console.time( 'dict-forEach' ); dictStr.forEach(function(key, val){ }); console.timeEnd( 'dict-forEach' ); }
執行結果:
------------Integer Dict--------------- dict: 21.92919921875ms dict-forEach: 8.44775390625ms dict-forEach: 12.47265625ms dict-forEach: 9.833984375ms dict-forEach: 8.887939453125ms dict-forEach: 8.472900390625ms dict-forEach: 8.531982421875ms dict-forEach: 7.826171875ms dict-forEach: 7.029052734375ms dict-forEach: 7.34814453125ms dict-forEach: 7.2958984375ms ------------String Dict--------------- dict: 85.69921875ms dict-forEach: 28.767822265625ms dict-forEach: 28.82080078125ms dict-forEach: 27.6630859375ms dict-forEach: 34.80517578125ms dict-forEach: 21.677001953125ms dict-forEach: 25.086181640625ms dict-forEach: 22.539794921875ms dict-forEach: 23.110107421875ms dict-forEach: 24.0830078125ms dict-forEach: 22.655029296875ms
對比測試二、三,可以看出回撥函式呼叫也是有代價的。可以直接呼叫ReadonlyDict的資料成員進行遍歷。
可變字典
可以在字典內容變化後,重新呼叫一下Object.keys(),更新keys裡的內容。 或者封裝set(key,value)方法。
具體實現略。
總結
-
在遍歷屬性多的物件時候,for-in效率低下:for-in內部偷偷呼叫了Object.keys()。
-
對不可變字典,可以快取keys在後續的迴圈中重用。
-
Hash計算是有代價的:遍歷物件,integer型別的key比string型別的key更高效
-
callback呼叫是有代價的:儘量避免for迴圈裡呼叫
歡迎關注微信公眾號: