倒序循环真的更快吗?

295

我听过这个问题很多次。JavaScript 循环倒序计数真的更快吗?如果是这样,为什么呢?我看到一些测试套件示例显示反向循环更快,但我找不到任何解释为什么!

我猜这是因为循环不再需要每次评估一个属性来检查它是否完成,而只需针对最终数字值进行检查。

即:

for (var i = count - 1; i >= 0; i--)
{
  // count is only evaluated once and then the comparison is always on 0.
}

6
嘿嘿,那需要无限时间。试试i--。 - StampedeXV
6
倒序的 for 循环更快,因为上界(嘿嘿,下界)循环控制变量不需要被定义或从对象中获取;它是一个常数零。 - Josh Stodola
92
没有真正的区别。本地循环结构始终非常快。不必担心它们的性能。请放心使用。 - James Allardice
25
针对这类问题,请_展示_你所参考的文章。 - Lightness Races in Orbit
23
非常低端的和使用电池供电的设备存在重要的区别。这个区别在于,使用 i-- 时,将i与0进行比较以确定循环结束,而使用 i++ 时,将i与大于0的数字进行比较。我相信性能差异大约为20纳秒(类似于 cmp ax,0 vs. cmp ax,bx)- 如果你每秒循环数千次,那为什么不让每次循环多获得20纳秒的收益呢 :) - luben
显示剩余22条评论
34个回答

946

并不是i--i++更快。 实际上,它们都一样快。

在升序循环中需要时间的是为每个i计算数组大小。 在这个循环中:

for(var i = array.length; i--;)

在声明 i 时,您只评估了 .length 一次,而对于此循环:

您仅评估了 .length 一次,这发生在声明 i 时,而不是在每次循环迭代中重新评估。

for(var i = 1; i <= array.length; i++)

每次增加i并检查是否i <= array.length时,你都要评估.length

在大多数情况下,你甚至不需要担心这种优化。


32
在for循环的头部引入一个变量来表示数组长度并在循环中使用它,这样做是否值得? - ragklaat
45
不,除非你正在建立一个基准并想要表明观点。 - Jon
34
这要看情况:重复评估 .length 多次是否比声明和定义一个新变量更快。在大多数情况下,这样的优化是不必要的(并且错误的),除非你的 length 很耗时。 - alestanis
13
@Dr.Dredel说:这不是比较,而是评估。0array.length 更快地进行评估。好吧,理论上是这样的。 - Lightness Races in Orbit
24
值得一提的是,我们正在谈论解释型语言,例如Ruby、Python。而编译型语言,如Java,在编译器级别上进行了优化,这将使这些差异“平滑”,以至于在for loop的声明中是否有.length并不重要。 - Haris Krajina
显示剩余24条评论

208

这个人比较了许多JavaScript中的循环,使用了许多不同的浏览器。他还准备了一个测试套件,你可以自己运行。

在所有情况下(除非我漏掉了某个情况),最快的循环方式是:

var i = arr.length; //or 10
while(i--)
{
  //...
}

12
有趣,但我想知道预减量是否会更快。因为它不需要存储 i 的中间值。 - tvanfosson
4
如果您使用前缀减法 --i,则在循环中必须使用 var current = arr[i-1];,否则会导致偏移一个位置。 - DaveAlger
2
我听说 i-- 比 --i 更快,因为在第二种情况下,处理器需要递减并针对新值进行测试(指令之间存在数据依赖性),而在第一种情况下,处理器可以测试现有值并稍后递减该值。我不确定这是否适用于 JavaScript 或仅适用于非常低级的 C 代码。 - marcus
很不幸,由于Oracle重组其博客内容,您帖子中的两个链接已经失效了 :( - RBT
@RBT 我已经编辑了链接。由于我找不到当前内容的链接(如果它仍然存在),我用网络档案链接替换了它们。测试页面仍然可以正常工作。 - Baldrickk
显示剩余10条评论

132

我试图通过这个答案给出一个广泛的概述。

以下括号中的想法曾经是我的信仰,直到最近我测试了这个问题:

[[对于低级语言(如C /C++),代码被编译成处理器有一个特殊的条件跳转命令当一个变量为零(或非零)。 此外,如果您非常关心优化,可以使用 ++i 而不是 i++,因为 ++i 是一个单一的处理器命令,而 i++ 意味着 j=i+1, i=j。]]

可以通过展开循环来实现真正快速的循环:

for(i=800000;i>0;--i)
    do_it(i);

它可能比其他方法慢得多

for(i=800000;i>0;i-=8)
{
    do_it(i); do_it(i-1); do_it(i-2); ... do_it(i-7);
}

但造成这种情况的原因可能会相当复杂(仅举几个例子,游戏中存在处理器命令预处理和缓存处理等问题)。

就像你所问的那样,在高级语言(比如JavaScript)方面,如果你依赖库和内置函数进行循环,你可以优化代码。让它们决定最佳实践。

因此,在 JavaScript 中,我建议使用类似以下的方法:

array.forEach(function(i) {
    do_it(i);
});

此外,使用array.forEach等复杂库函数是低级语言的最佳实践。这些库可以将操作(多线程)放在背后,并且专业程序员会使它们保持最新状态。同时,在C/C++中,即使进行50亿(50,000 x 100,000)次操作,如果针对常量进行测试,则前缀和后缀没有区别。而对于循环展开,根据具体情况可能会有所帮助。总之,保持i--/i++++i/i++的差异集中在面试中,如果有可用的array.forEach或其他复杂库函数,请使用它们。


18
关键词是“可以”。您展开的循环可能比原来的循环更慢。优化时,始终进行测量,以便确切了解您所做更改的影响。 - jalf
1
@jalf 确实如此,+1。不同的解循环长度(>=1)在效率上是不同的。这就是为什么如果可能的话,将此工作留给库更方便,更不用说浏览器运行在不同的架构上,所以让它们决定如何执行 array.each(...) 可能会更好。我不认为他们会尝试使用解循环普通的 for 循环进行实验。 - Barney Szabolcs
1
@BarnabasSzabolcs 这个问题是特指 JavaScript,而不是 C 或其他语言。在 JS 中确实有区别,请见我下面的答案。虽然这并不适用于此问题,但是 +1 优秀回答! - A.M.K
1
然后,你获得了+1,并获得了一个“优秀回答”徽章。真的是个非常棒的回答。 :) - Rohit Jain
1
APL/A++也不是一种语言。人们使用它们来表达他们对语言细节不感兴趣,而是关注这些语言共享的东西。 - Barney Szabolcs
显示剩余13条评论

35

i--i++的速度是一样快的。

下面的代码与您的代码一样快,但使用了额外的变量:

var up = Things.length;
for (var i = 0; i < up; i++) {
    Things[i]
};

建议不要每次计算数组大小,对于大型数组会导致性能下降。


7
你在这里显然是错的。现在循环控制需要额外的内部变量(即i和up),并且这可能会限制JavaScript引擎优化代码的潜力。JIT将简单的循环转换为直接的机器指令,但如果循环有太多变量,则JIT无法进行优化。一般来说,它受到JIT使用的体系结构或CPU寄存器的限制。一次初始化并向下运行是最干净的解决方案,这就是为什么建议到处都这样做的原因。 - Pavel P
额外的变量将被普通解释器/编译器的优化器消除。关键在于“与0比较”-请参阅我的答案以获取更多说明。 - H.-Dirk Schmitt
2
最好将 'up' 放在循环内:for (var i=0, up=Things.length; i<up; i++) {} - thdoan
为什么内部变量会引起问题?这是错误的,因为循环控制不会有“额外的变量”,因为upThings.length不是通过引用(如对象或数组)传递的,而是直接按值传递的。换句话说,它被插入到循环控制中,就像数字10或100000一样。你只是在循环外面重新命名了它,所以没有任何区别。 因此,答案本身是完全有效的。当看到i < up时,循环控制看到的是i < 10而不是i <(对数字10的引用)。实际上,up内部存储了一个原始值,不要忘记这一点。 - Eve

25

既然您对此感兴趣,可以看一下Greg Reimer关于JavaScript循环基准测试的博客文章:JavaScript中最快的编码循环方式是什么?

我为不同的JavaScript循环编码方式构建了循环基准测试套件。已经有一些这样的测试了,但我没有找到任何一个承认本机数组和HTML集合之间的差异。

您还可以通过打开https://blogs.oracle.com/greimer/resource/loop-test.html(如果浏览器被例如NoScript阻止JavaScript,则无法使用)在循环上进行性能测试

编辑:

米兰·亚当诺夫斯基(Milan Adamovsky)创建的一项较新的基准测试可以在运行时进行,点击此处选择不同的浏览器进行测试。

在Firefox 17.0上的Mac OS X 10.6测验结果如下:

基准测试示例。


@dreamcash 在 Chrome 88.x 上,所有的反向循环都比所有正向循环更快。https://jsben.ch/eng8b,https://www.measurethat.net/Benchmarks/ShowResult/162677,https://jsbench.me/5ykkzsysa9。有时是反向 for 循环,有时是反向优化 for 循环,有时是反向 while 循环。 - johny why
@johnywhy 好的,我误解了。那还是有一点区别的。你能否发一篇回答并包含这些结果呢? - dreamcrash

22

问题不在于--++,而在于比较操作。使用--可以与0进行比较,而使用++需要将其与长度进行比较。在处理器上,通常可以与零进行比较,而与有限整数进行比较则需要减法。

a++ < length

实际上编译为:

a++
test (a-length)

编译时在处理器上需要更长的时间。


15

我认为在JavaScript中说i--i++快是没有意义的。

首先,这完全取决于JavaScript引擎的实现。

其次,假设最简单的结构被即时编译并转换成本机指令,则i++i--将完全取决于执行它的CPU。也就是说,在ARM(手机)上,向0下降更快,因为递减和与零比较是在单个指令中执行的。

也许你认为其中一个比另一个更快,因为建议使用的方式是

for(var i = array.length; i--; )

但建议的方式并不是因为其中一个更快,而只是因为如果您编写

for(var i = 0; i < array.length; i++)

在每次迭代时,array.length 都必须被计算 (更聪明的JavaScript引擎可能能够推断出循环不会改变数组的长度)。虽然它看起来像一个简单的语句,但实际上是由JavaScript引擎在幕后调用的一些函数。

另一个原因,为什么 i-- 被认为是“更快”的是因为JavaScript引擎只需要分配一个内部变量来控制循环(即 var i 变量)。如果与 array.length 或其他变量进行比较,则需要使用多个内部变量来控制循环,而内部变量的数量是JavaScript引擎的有限财产。循环中使用的变量越少,JIT就越有机会进行优化。这就是为什么 i-- 可以被认为是更快的原因...


3
关于如何评估array.length,需要谨慎措辞。引用它时,并不会进行计算(它只是一个属性,在创建或更改数组索引时设置)。如果有额外的开销,那是因为JS引擎没有针对该名称优化查找链。 - kojiro
不确定Ecma规范说了什么,但是了解不同JavaScript引擎的一些内部情况,这并不是简单的getLength(){return m_length;},因为其中涉及到一些维护工作。但是如果你试着往回思考:编写数组实现,其中长度实际上需要计算,那将是相当巧妙的 :) - Pavel P
ECMA规范要求必须预先计算length属性。每当添加或更改一个作为数组索引的属性时,length必须立即更新。 - kojiro
我想说的是,如果你试着去思考,违反规范是相当困难的。 - Pavel P
1
x86 在这方面类似于 ARM。dec/jnz 相当于 inc eax / cmp eax, edx / jne - Peter Cordes

15

简短回答

对于正常的代码,特别是在高级语言如JavaScript中,i++i--没有性能差异。

性能的标准是在for循环中和比较语句中的使用。

这个规则适用于所有高级语言,并且大多数独立于JavaScript的使用。解释在于最终生成的汇编代码。

详细解释

循环中可能会出现性能差异。背景是,在汇编代码层面上,您可以看到与0比较只是一个语句,不需要额外的寄存器。

每次循环都会发出此比较,并可能导致可衡量的性能提升。

for(var i = array.length; i--; )

将被评估为类似于伪代码的形式:

 i=array.length
 :LOOP_START
 decrement i
 if [ i = 0 ] goto :LOOP_END
 ... BODY_CODE
 :LOOP_END

请注意,0是一个字面常量,也就是说它是一个不变的值。

for(var i = 0 ; i < array.length; i++ )

将被评估为类似于这样的伪代码(假定正常解释器优化):

 end=array.length
 i=0
 :LOOP_START
 if [ i < end ] goto :LOOP_END
 increment i
 ... BODY_CODE
 :LOOP_END

请注意,end是一个需要CPU寄存器的变量。这可能会在代码中引发额外的寄存器交换,并需要在if语句中使用更昂贵的比较语句

个人意见

对于高级语言,可读性(有助于可维护性)比轻微的性能改进更重要。

通常从数组开始到结束的经典迭代更好。

更快的从数组末尾到开头的迭代会导致可能不需要的反向序列。

附言

正如评论所问: --ii--的区别在于对i进行递减之前还是之后进行评估。

最好的解释是试一下;-) 这里是一个Bash示例。

 % i=10; echo "$((--i)) --> $i"
 9 --> 9
 % i=10; echo "$((i--)) --> $i"
 10 --> 9

1
很好的解释。只是一个有点超出范围的问题,您能否请解释一下--ii--之间的区别呢? - Afshin Mehrabani

15

我在Sublime Text 2中看到了同样的建议。

正如之前所说,主要的改进并不是在for循环的每次迭代中评估数组的长度。这是一种众所周知的优化技术,在JavaScript中特别有效,当数组是HTML文档的一部分时(为所有li元素执行for循环)。

例如,

for (var i = 0; i < document.getElementsByTagName('li').length; i++)

for (var i = 0, len = document.getElementsByTagName('li').length; i < len; i++)

慢得多。

从我的角度来看,在你的问题中,主要的改进在于它没有声明额外的变量(例如我的示例中的len)

但是如果问我,整个重点不在于i++与i--的优化,而在于不必在每次迭代中评估数组的长度(可以在jsperf上看到基准测试结果)。


1
我对这里的“计算”一词有异议。请参阅我对Pavel的回答的评论。ECMA规范指出,当您引用数组长度时,并不会进行计算。 - kojiro
“评估”会是更好的选择吗?顺便说一下,我不知道这个有趣的事实。 - BBog
额外的变量将被常见解释器/编译器的优化器消除。数组长度评估也是如此。重点在于“与0比较”-请参阅我的答案以获得进一步的解释。 - H.-Dirk Schmitt
@H.-DirkSchmitt,撇开你的常识回答不谈,在JavaScript的历史长河中,编译器并没有优化查找链的性能成本。据我所知,V8是第一个尝试优化的。 - kojiro
@H.-DirkSchmitt 就像 kojiro 所说的那样,这是一个众所周知和成熟的技巧。即使在现代浏览器中不再相关,这仍然不会使其过时。此外,使用引入新变量来表示长度或使用 OP 问题中的技巧,这仍然是我认为最好的方法。这只是一个聪明的做法和良好的实践,我认为这与编译器被优化以处理 JS 中经常以错误方式完成的某些事情无关。 - BBog
在大多数情况下,我认为应该使用这种风格。有时候需要在循环内引用长度,那么这种结构就非常方便。 - Sebastian

13

由于其他答案似乎没有回答你的具体问题(其中超过一半显示C示例并讨论较低级别的语言,你的问题是关于JavaScript的),所以我决定自己写一个。

因此,在这里给你:

简单回答:i--通常更快,因为它不必每次运行时都运行与0的比较,各种方法的测试结果如下:

测试结果:这个 jsPerf“证明”的那样,arr.pop()实际上是远远最快的循环。但是,专注于--ii--i++++i,如你在问题中提到的,以下是jsPerf (它们来自多个jsPerf,请参见下面的来源)的结果摘要:

在Firefox中,--ii--相同,而在Chrome中i--更快。

在Chrome中,基本的for循环(for (var i = 0; i < arr.length; i++))比i----i更快,而在Firefox中则更慢。

在Chrome和Firefox中,缓存的arr.length明显更快,其中Chrome领先约170,000 ops/sec。

没有显着差异,++i在大多数浏览器中比i++更快,据我所知,在任何浏览器中都不会反过来。

简要总结:arr.pop()是远远最快的循环;对于特别提到的循环,i--是最快的循环。

来源:http://jsperf.com/fastest-array-loops-in-javascript/15http://jsperf.com/ipp-vs-ppi-2

希望这回答了你的问题。


1
似乎你的 pop 测试之所以看起来很快,是因为在大多数循环中它将数组大小减少到了0。但是为了确保公平,我设计了这个 jsperf 来以相同的方式创建每个测试的数组 - 这似乎显示 .shift() 在我的几个浏览器中是胜出者 - 这不是我预期的结果 :) http://jsperf.com/compare-different-types-of-looping - Pebbl
因为你是唯一一个提到 ++i 的人,所以我点了个赞 :D - jaggedsoft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接