《High Performance JavaScript》一书对于JavaScript中的如何提高循环的性能介绍的十分详尽,自己写了些简单的代码进行了测试,测试结果竟然出乎意料。
首先需要说明的是,本次测试的浏览器版本为Mozilla Firefox V7.01、基于chrome内核的傲游V3.1.8.1000,至于IE8那个废材我实在无语(系统是XP,IE9装不了无法测试)。
书中的主要观点主要有下面几个:
1、除 for-in循环外,其他循环类型性能相当,难以确定哪种循环更快。选择循环类型应基于需求而不是性能。
2、减少迭代的工作量,包括减少对象成员和数组项查找的次数、如果数组元素的处理顺序与任务无关使用倒序循环可以略微提高循环性能。
3、减少迭代次数,介绍了最广为人知的限制循环迭代次数的模式称作“达夫设备”。
倒序循环
首先出问题的是倒序循环,一般在正常的循环中,每次运行循环体都要发生如下几个操作:
1、在控制条件中读一次属性(items.length),如果缓存了数组的长度,这一条可忽略
2、在控制条件中执行一次比较(i < items.length)
3、比较操作,察看条件控制体的运算结果是不是 true(i < items.length == true)
4、一次自加操作(i++)
5、一次数组查找(items[i])
6、一次函数调用(process(items[i]))
在倒序循环中,每次迭代中只进行如下操作:
1、在控制条件中进行一次比较(i == true)
2、一次减法操作(i--)
3、一次数组查询(items[i])
4、一次函数调用(process(items[i]))
倒序循环减少了操作,那么性能的提高虽说很微弱,但应该是百分百确定的对不?但是我测试的结果却不尽然,简单说一下测试代码,首先创建一个长度为10000的数组,每一项为长度4-10的字符,每个循环体要进行的操作是判断字符串是否为全数字,如果是,值累加1,最后每种循环都循环100次,做三次测试。先上代码:
var chars = '0123456789abcdef';
function getRandomString() {
var len = Math.ceil(Math.random() * 7) + 3; // 4-10
var result = "";
while (len--) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
var size = 10000;
var mult = 100;
var ary = [];
var lsize = size;
while (lsize--) { ary.push('' + getRandomString() + ''); }
function for_in() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
for (var i in ary) {
var item = i;
if (isInt.test(item)) { item += 1; }
}
}
function for_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
for (var i = 0, len = ary.length; i < len; i++) {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
}
}
function for_reverse() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
for (var i = ary.length; i--; ) {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
}
}
function while_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
i = 0,
l = ary.length;
while (i < l) {
var item = ary[i++];
if (isInt.test(item)) { item += 1; }
}
}
function while_reverse() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
i = ary.length;
while (i--) {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
}
}
function do_while_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
i = 0,
l = ary.length;
do {
var item = ary[i++];
if (isInt.test(item)) { item += 1; }
} while (i < l)
}
function do_while_reverse() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
i = ary.length - 1;
do {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
} while (i--)
}
console.log('数组长度:' + ary.length);
console.log('Profiling will begin in 2 seconds...');
setTimeout(function () {
var currTime;
for (var k = 0; k < 3; k++) {
console.log('第' + (k + 1) + '次循环测试:')
currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
for_in();
for_normal();
for_reverse();
while_normal();
while_reverse();
do_while_normal();
do_while_reverse();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
}
}, 2000);
测试n次后发现,while、do-while都要比for循环要快些,for寻找只是偶尔比前两个快,在火狐下do-while要被while快些,在chrome下不明显。
fox循环的倒序循环性能略微提升比较确定,但是不管在火狐还是在chrome下while、do-while的正循环都要快于倒序循环,这是咋回事?别问我,我也不知......
再上测试的截屏,这里不得不赞Firebug,它的profile可不chrome强多了,不过性能还是比不上chrome,尤其后面达夫设备的测试跟是明显。
在达夫设备测试之前先进行另外一项测试,一上面的for循环为例,如果把循环体内对数组项的操作单独另写一个函数,然后在循环内调用,性能会有什么样的影响呢?
var chars = '0123456789abcdef';
function getRandomString() {
var len = Math.ceil(Math.random() * 7) + 3; // 4-10
var result = "";
while (len--) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
var size = 10000;
var mult = 100;
var ary = [];
var lsize = size;
while (lsize--) { ary.push('' + getRandomString() + ''); }
function process(item) {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
if (isInt.test(item)) { item += 1; }
}
function for_normal_fn() {
for (var i = 0, len = ary.length; i < len; i++) { process(ary[i]); }
}
function for_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
for (var i = 0, len = ary.length; i < len; i++) {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
}
}
console.log('数组长度:' + ary.length);
console.log('Profiling will begin in 2 seconds...');
setTimeout(function () {
console.log('普通for循环测试:')
var currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
for_normal();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
console.log('for循环内调用外部函数测试:')
currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
for_normal_fn();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
}, 2000);
测试的结果:调用外部函数的for循环比之前的for循环所用的时间,火狐下多了二十倍,chrome没那么夸张,也多用的50ms左右,同是浏览器,差距咋就这么大呢(火狐你也要坚强些,因为IE在这测试里连说话的资格也没有,哈哈)......
达夫设备
这里摘抄一下书中关于达夫设备的基本理念:每次循环中最多可 8 次调用 process()函数。循环迭代次数为元素总数除以8。 因为总数不一定是 8的整数倍, 所以 startAt 变量存放余数, 指出第一次循环中应当执行多少次 process()。比方说现在有 12 个元素,那么第一次循环将调用 process()4次,第二次循环调用 process()8 次,用 2 次循环代替了 12次循环。
下面我们比较一下for、while、Duff's Device三种循环:
var chars = '0123456789abcdef';
function getRandomString() {
var len = Math.ceil(Math.random() * 7) + 3; // 4-10
var result = "";
while (len--) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
var size = 10000;
var mult = 100;
var ary = [];
var lsize = size;
while (lsize--) { ary.push('' + getRandomString() + ''); }
function for_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
for (var i = 0, len = ary.length; i < len; i++) {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
}
}
function while_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
i = 0,
l = ary.length;
while (i < l) {
var item = ary[i++];
if (isInt.test(item)) { item += 1; }
}
}
function duff_fast() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
item, i = 0,
l = ary.length,
n = l % 8;
while (n--) {
item = ary[i++];
if (isInt.test(item)) { item += 1; }
}
n = parseInt(l / 8);
while (n--) {
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
item = ary[i++];
if (isInt.test(item)) { item += 1; }
}
}
console.log('数组长度:' + ary.length);
console.log('Profiling will begin in 2 seconds...');
setTimeout(function () {
console.log('普通for循环测试:')
var currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
for_normal();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
console.log('普通while循环测试:')
var currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
while_normal();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
console.log('达夫设备循环测试:')
currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
duff_fast();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
}, 2000);
经过n次测试,发现while、Duff's Device要比for要快,而while与Duff's Device竟然也不分伯仲啊,这、这、这.....while咋就这么强呢......
还有一点达夫设备实际上把八次迭代合在一次迭代中,如果每次对数组项的操作代码量较少的情况下你可以像上面的代码一样简单的复制八次,但是如果操作量大一些的话为了便于阅读和调试源代码,你就必须把操作代码包装到另外一个process函数中,然后在达夫设备调用此函数,还记得我们前面的关于for循环调用外部函数的测试吗?性能就会差太多了,可以看下面的测试:
var chars = '0123456789abcdef';
function getRandomString() {
var len = Math.ceil(Math.random() * 7) + 3; // 4-10
var result = "";
while (len--) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
var size = 10000;
var mult = 100;
var ary = [];
var lsize = size;
while (lsize--) { ary.push('' + getRandomString() + ''); }
function process(item) {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
if (isInt.test(item)) { item += 1; }
}
function for_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/;
for (var i = 0, len = ary.length; i < len; i++) {
var item = ary[i];
if (isInt.test(item)) { item += 1; }
}
}
function while_normal() {
var isInt = /(^[0-9]$)|(^[1-9][0-9]+$)/,
i = 0,
l = ary.length;
while (i < l) {
var item = ary[i++];
if (isInt.test(item)) { item += 1; }
}
}
function duff_normal() {
var i = 0,
l = ary.length,
n = l % 8;
while (n--) {
process(ary[i++]);
}
n = parseInt(l / 8);
while (n--) {
process(ary[i++]);
process(ary[i++]);
process(ary[i++]);
process(ary[i++]);
process(ary[i++]);
process(ary[i++]);
process(ary[i++]);
process(ary[i++]);
}
}
console.log('数组长度:' + ary.length);
console.log('Profiling will begin in 2 seconds...');
setTimeout(function () {
console.log('普通for循环测试:')
var currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
for_normal();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
console.log('普通while循环测试:')
var currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
while_normal();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
console.log('达夫设备调用外部函数循环测试:')
currTime = new Date();
console.profile();
for (var i = 0; i < mult; i++) {
duff_normal();
}
console.profileEnd();
console.log('用时:' + (new Date() - currTime) + 'ms');
}, 2000);
看到测试结果了吧,惨不忍睹啊......还是用while或do-while循环吧......
苦苦的苦瓜 2011-10-10