在他的书Even Faster Web Sites中,Steve Sounders写道,提高循环性能的一种简单方法是将迭代器递减到0而不是递增到总长度(实际上这一章是由尼古拉斯写的C. Zakas )。根据每次迭代的复杂程度,此更改可以比原始执行时间节省高达50%的成本。例如:
var values = [1,2,3,4,5];
var length = values.length;
for (var i=length; i--;) {
process(values[i]);
}
for
循环,do-while
循环和while
循环几乎相同。
我想知道,这是什么原因? 为什么要以更快的速度递减迭代器呢?(我对此技术背景感兴趣,而不是在证明此声明的基准测试中感兴趣。)
编辑:乍一看,这里使用的循环语法看起来不对。没有length-1
或i>=0
,所以让我们澄清一下(我也很困惑)。
以下是循环语法的一般原则:
for ([initial-expression]; [condition]; [final-expression])
statement
initial-expression - var i=length
首先评估此变量声明。
条件 - i--
在每次循环迭代之前计算此表达式。它将在第一次通过循环之前递减变量。如果此表达式求值为false
,则循环结束。在JavaScript中0 == false
因此,如果i
最终等于0
,则会将其解释为false
并且循环结束。
最终表达
在每次循环迭代结束时(在下一次评估 condition 之前)计算此表达式。这里不需要它是空的。在for循环中,所有三个表达式都是可选的。
for循环语法不是问题的一部分,但因为它有点不常见,我认为澄清它是有趣的。也许有一个原因是它更快,因为它使用较少的表达式(0 == false
“技巧”)。
答案 0 :(得分:65)
我不确定Javascript,在现代编译器下它可能没关系,但在“过去的日子”这段代码:
for (i = 0; i < n; i++){
.. body..
}
会生成
move register, 0
L1:
compare register, n
jump-if-greater-or-equal L2
-- body ..
increment register
jump L1
L2:
而后向计数代码
for (i = n; --i>=0;){
.. body ..
}
会生成
move register, n
L1:
decrement-and-jump-if-negative register, L2
.. body ..
jump L1
L2:
所以在循环中它只执行两个额外的指令而不是四个。
答案 1 :(得分:26)
我相信原因是因为你将循环结束点与0进行比较,这比再次比较< length
(或另一个JS变量)要快。
这是因为序数运算符<, <=, >, >=
是多态的,因此这些运算符需要对运算符的左侧和右侧进行类型检查,以确定应该使用哪种比较行为。
这里有一些非常好的基准:
答案 2 :(得分:15)
很容易说迭代可以有更少的指令。我们只是比较这两个:
for (var i=0; i<length; i++) {
}
for (var i=length; i--;) {
}
当您将每个变量访问权限和每个运算符计为一条指令时,前for
循环使用5条指令(读取i
,读取length
,评估i<length
,测试(i<length) == true
,增加i
),而后者仅使用3条指令(阅读i
,测试i == true
,减少i
)。这是5:3的比例。
答案 3 :(得分:6)
然后使用反向while循环怎么样:
var values = [1,2,3,4,5];
var i = values.length;
/* i is 1st evaluated and then decremented, when i is 1 the code inside the loop
is then processed for the last time with i = 0. */
while(i--)
{
//1st time in here i is (length - 1) so it's ok!
process(values[i]);
}
IMO至少是一个比for(i=length; i--;)
答案 4 :(得分:3)
我一直在探索循环速度,并有兴趣找到关于递减比递增更快的这一点。但是,我还没有找到一个证明这一点的测试。 jsperf上有很多循环基准测试。这是测试递减的一个:
http://jsperf.com/array-length-vs-cached/6
缓存你的数组长度(也推荐Steve Souders&#39;书)似乎确实是一个成功的优化。
答案 5 :(得分:2)
还有一个更“高性能”的版本。 由于for循环中的每个参数都是可选的,因此您甚至可以跳过第一个参数。
var array = [...];
var i = array.length;
for(;i--;) {
do_teh_magic();
}
有了这个,你甚至可以跳过对[initial-expression]
的检查。所以你最终只剩下一个操作了。
答案 6 :(得分:2)
for
增量与2017年的减少量在现代JS引擎中,for
循环递增通常比递减(基于个人Benchmark.js测试)更快,也更常规:
for (let i = 0; i < array.length; i++) { ... }
如果length = array.length
有任何相当大的积极影响,这取决于平台和数组长度,但通常不会:
for (let i = 0, length = array.length; i < length; i++) { ... }
最近的V8版本(Chrome,Node)对array.length
进行了优化,因此无论如何都可以有效地省略length = array.length
。
答案 7 :(得分:2)
在现代JS引擎中,正向和反向循环之间的差异几乎不存在。但性能差异归结为两件事:
a)每个周期额外查找每个长度属性
//example:
for(var i = 0; src.length > i; i++)
//vs
for(var i = 0, len = src.length; len > i; i++)
&#13;
这是反向环路的最大性能增益,显然可以应用于正向环路。
b)额外的变量赋值。
反向循环的较小增益是它只需要一个变量赋值而不是2
//example:
var i = src.length; while(i--)
&#13;
答案 8 :(得分:1)
我已经对C#和C ++(类似语法)进行了基准测试。实际上,与for
或do while
相比,while
循环中的性能基本不同。在C ++中,递增时性能更高。它也可能取决于编译器。
在Javascript中,我认为,这一切都取决于浏览器(Javascript引擎),但这种行为是可以预期的。 Javascript针对使用DOM进行了优化。因此,假设您遍历每次迭代时获得的DOM元素集合,并在必须删除它们时递增计数器。您删除了0
元素,然后删除了1
元素,但是您跳过了0
所占的位置。向后循环时,问题消失了。我知道给出的例子不仅仅是正确的例子,但我确实遇到过必须从不断变化的对象集合中删除项目的情况。
因为后向循环比前向循环更经常是不可避免的,所以我猜测JS引擎是为此而优化的。
答案 9 :(得分:1)
我不确定它是否更快但我看到的一个原因是,当你使用增量迭代一个大元素数组时,你倾向于写:
for(var i = 0; i < array.length; i++) {
...
}
您实际上是访问数组N的长度属性(元素数)次。 而当你减少时,你只能访问它一次。这可能是一个原因。
但您也可以按如下方式编写递增循环:
for(var i = 0, len = array.length; i < len; i++) {
...
}
答案 10 :(得分:1)
答案 11 :(得分:0)
不更快(至少在现代浏览器中):
// Double loops to check the initialization performance too
const repeats = 1e3;
const length = 1e5;
console.time('Forward');
for (let j = 0; j < repeats; j++) {
for (let i = 0; i < length; i++) {}
}
console.timeEnd('Forward'); // 58ms
console.time('Backward');
for (let j = repeats; j--;) {
for (let i = length; i--;) {}
}
console.timeEnd('Backward'); // 64ms
在数组迭代的情况下,差异甚至更大:
const repeats = 1e3;
const array = [...Array(1e5)];
console.time('Forward');
for (let j = 0; j < repeats; j++) {
for (let i = 0; i < array.length; i++) {}
}
console.timeEnd('Forward'); // 34ms
console.time('Backward');
for (let j = 0; j < repeats; j++) {
for (let i = array.length; i--;) {}
}
console.timeEnd('Backward'); // 64ms