Javascript中for-in效率分析和优化

栏目: JavaScript · 发布时间: 5年前

内容简介:Javascript程序中,我们经常使用Object来模拟dictionary/map/hashmap的行为,也会使用for-in语法来遍历dictionary的元素。但你是否遇到过由于使用for-in而导致程序产生性能问题呢?Javascript里的数据结构比较简单,除了数组,就是使用对象模拟的字典/Hash表。比如:或者:

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循环里调用

欢迎关注微信公众号:

Javascript中for-in效率分析和优化


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript快速开发工具箱

JavaScript快速开发工具箱

Robin Nixon / 陈武、姚飞 / 清华大学出版社 / 2011-11 / 59.00元

《JavaScript快速开发工具箱:轻松解决JavaScript日常编程问题的100个插件工具》通透讲解100个现成的JavaScript插件,引导您使用这些利器得心应手地创建动态Web内容。《JavaScript快速开发工具箱:轻松解决JavaScript日常编程问题的100个插件工具》开篇讲解JavaScript、CSS和DOM,此后每章都列举一个完整示例,指导您将特定效果快速应用于网页。使......一起来看看 《JavaScript快速开发工具箱》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试