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

Sou*_*ink 61 javascript performance loops

在他的书" 更快的网站"中, Steve Sounders写道,提高循环性能的一种简单方法是将迭代器递减到0而不是递增到总长度(实际上这一章是由Nicholas C. Zakas编写的).根据每次迭代的复杂性,此更改可以比原始执行时间节省高达50%的成本.例如:

var values = [1,2,3,4,5];
var length = values.length;

for (var i=length; i--;) {
   process(values[i]);
}
Run Code Online (Sandbox Code Playgroud)

这对于for循环,do-while循环和while循环几乎相同.

我想知道,这是什么原因?为什么要这么快地递减迭代器呢?(我对此技术背景感兴趣,而不是在证明此声明的基准测试中感兴趣.)


编辑:乍一看这里使用的循环语法看起来不对.没有length-1或者i>=0,让我们澄清(我很困惑太).

这是循环语法的一般:

for ([initial-expression]; [condition]; [final-expression])
   statement
Run Code Online (Sandbox Code Playgroud)
  • 初始表达 -var i=length

    首先评估此变量声明.

  • 条件 -i--

    在每次循环迭代之前计算此表达式.它将在第一次通过循环之前递减变量.如果此表达式求值为false循环结束.在JavaScript中是0 == false这样的,如果i最终等于0它被解释为false并且循环结束.

  • 最终表达

    在每次循环迭代结束时(在下一次条件评估之前)评估该表达式.这里不需要它是空的.所有三个表达式在for循环中都是可选的.

for循环语法不是问题的一部分,但因为它有点不常见,我认为澄清它是有趣的.也许有一个原因是它更快,因为它使用较少的表达式(0 == false"技巧").

Mik*_*vey 65

我不确定Javascript,在现代编译器下它可能没关系,但在"过去的日子"这段代码:

for (i = 0; i < n; i++){
  .. body..
}
Run Code Online (Sandbox Code Playgroud)

会产生

move register, 0
L1:
compare register, n
jump-if-greater-or-equal L2
-- body ..
increment register
jump L1
L2:
Run Code Online (Sandbox Code Playgroud)

而后向计数代码

for (i = n; --i>=0;){
  .. body ..
}
Run Code Online (Sandbox Code Playgroud)

会产生

move register, n
L1:
decrement-and-jump-if-negative register, L2
.. body ..
jump L1
L2:
Run Code Online (Sandbox Code Playgroud)

所以在循环中它只做两个额外的指令而不是四个.

  • 值得一提的是,“在过去” JavaScript永远都不会被转换成机器代码,因此这有一点争议。 (3认同)

djd*_*d87 27

我相信原因是因为你将循环结束点与0进行比较,这比再次比较< length(或另一个JS变量)要快 .

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

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

在JavaScript中编写循环的最快方法是什么?


Gum*_*mbo 15

很容易说迭代可以有更少的指令.我们来比较这两个:

for (var i=0; i<length; i++) {
}

for (var i=length; i--;) {
}
Run Code Online (Sandbox Code Playgroud)

当您将每个变量访问和每个运算符计为一条指令时,前一个for循环使用5条指令(读取i,读取length,评估i<length,测试(i<length) == true,增量i),而后者仅使用3条指令(读取i,测试i == true,递减i).这是5:3的比例.


Mar*_*aio 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]);
}
Run Code Online (Sandbox Code Playgroud)

IMO这个至少比一个更易读的代码 for(i=length; i--;)


Ale*_*rMP 1

我对 C# 和 C++(类似语法)进行了基准测试。实际上,与或for相比,循环的性能本质上有所不同。在 C++ 中,递增时性能会更好。它还可能取决于编译器。do whilewhile

我认为在 Javascript 中,这一切都取决于浏览器(Javascript 引擎),但这种行为是可以预料的。Javascript 针对 DOM 进行了优化。因此,想象一下,您循环遍历每次迭代时获得的 DOM 元素集合,并在必须删除它们时递增计数器。您删除了该0元素,然后删除1了该元素,但随后跳过了取代 的元素0。当向后循环时,这个问题就消失了。我知道给出的示例不仅仅是正确的,但我确实遇到过必须从不断变化的对象集合中删除项目的情况。

因为向后循环比前向循环更常见,所以我猜测 JS 引擎就是为此而优化的。