JavaScript循环性能 - 为什么将迭代器减少到比递增更快的速度

时间:2010-08-19 10:09:32

标签: javascript performance loops

在他的书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-1i>=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“技巧”)。

12 个答案:

答案 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变量)要快。

这是因为序数运算符<, <=, >, >=是多态的,因此这些运算符需要对运算符的左侧和右侧进行类型检查,以确定应该使用哪种比较行为。

这里有一些非常好的基准:

<强> What's the Fastest Way to Code a Loop in JavaScript

答案 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)每个周期额外查找每个长度属性

&#13;
&#13;
//example:
    for(var i = 0; src.length > i; i++)
//vs
    for(var i = 0, len = src.length; len > i; i++)
&#13;
&#13;
&#13;

这是反向环路的最大性能增益,显然可以应用于正向环路。

b)额外的变量赋值。

反向循环的较小增益是它只需要一个变量赋值而不是2

&#13;
&#13;
//example:
    var i = src.length; while(i--)
&#13;
&#13;
&#13;

答案 8 :(得分:1)

我已经对C#和C ++(类似语法)进行了基准测试。实际上,与fordo 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)

你有时间自己定时吗? Sounders先生在现代口译方面可能是错的。这恰恰是一种优化,在这种优化中,优秀的编译器编写者可以发挥重大作用。

答案 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