倒序循环真的更快吗?

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个回答

4

曾经有人说,在C++中使用--i会更快,因为它只有一个结果,即递减后的值。i--需要将递减后的值存回i,并将原始值保留为结果(j=i--;)。在大多数编译器中,这将使用两个寄存器,而不是一个,可能会导致另一个变量必须写入内存,而不能保留为寄存器变量。

我同意那些认为现在没有区别的人的观点。


基准测试结果参差不齐:jsben.ch: --i 更快,https://jsben.ch/RpG0K。jsbench.me: i-- 更快,https://jsbench.me/i2kkzuk4kl/1。measurethat.net: --i 更快,https://www.measurethat.net/Benchmarks/ShowResult/162675。 - johny why

3
很少有时间被 i-- 或 i++ 消耗。如果您深入CPU架构,++ 操作比 -- 更快,因为 -- 操作将执行2的补码,但它发生在硬件内部,这将使其更快,并且 ++-- 之间没有什么重大区别,这些操作被认为是CPU中所消耗时间最少的。 for循环的运行方式如下:
  • 一开始初始化变量。
  • 检查循环的第二个操作数(例如<, >, <=等)中的限制条件。
  • 然后应用循环。
  • 增加循环并再次通过这些进程循环。
因此,
for (var i = Things.length - 1; i >= 0; i--) {
    Things[i]
}; 

将在开始时仅计算一次数组长度,这不需要很长时间,但

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

在每个循环中计算长度,会消耗大量时间。


var i = Things.length - 1; i >= 0; i-- 这段代码会多次计算长度。 - Dmitry
1
我不确定 "-- 操作会对二进制补码做什么,但我认为它意味着它会否定某些东西。不,它在任何架构上都不会否定任何东西。减去1和加1一样简单,你只需要制作一个借位电路而不是进位电路即可。 - Olathe

3

首先,包括JavaScript在内的所有编程语言中,i++i--所需的时间完全相同。

以下代码所需的时间差异很大。

快速:

for (var i = 0, len = Things.length - 1; i <= len; i++) { Things[i] };

缓慢:

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

因此,以下代码的执行时间也会不同。
快速执行:
for (var i = Things.length - 1; i >= 0; i--) { Things[i] };

慢速:

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

附注:由于编译器的优化,Slow 只对某些语言(JavaScript 引擎)而言是慢的。最好的方法是使用 '<' 而非 '<='(或 '=') 以及 '--i' 而非 'i--'


3
在许多情况下,这与处理器比其他比较更快与零无关。这是因为只有少数几个JavaScript引擎(JIT列表中的引擎)实际上生成机器语言代码。大多数JavaScript引擎构建源代码的内部表示,然后解释它们(想要了解这是什么感觉,请查看Firefox SpiderMonkey页面底部)。通常,如果一段代码执行的功能相同但导致更简单的内部表示,它将运行得更快。请记住,像将变量加/减一或将变量与某些内容进行比较等简单任务,解释器从一个内部“指令”移动到另一个内部“指令”的开销非常高,因此JS引擎内部使用的“指令”越少,效果越好。

3

嗯,我对JavaScript不太清楚,这可能只涉及重新评估数组长度,也可能与关联数组有关(如果只是递减,新条目不太可能需要分配 - 如果数组是稠密的话,某人可能会针对此进行优化)。

在低级汇编语言中,有一种循环指令称为DJNZ(递减并跳转如果非零)。因此,递减和跳转都在一个指令中进行,可能比INC和JL / JB(增加,如果小于/增加,如果下面)要稍微快一些。而且,与零进行比较比与其他数字进行比较更简单。但所有这些都非常微小,还取决于目标架构(例如,在智能手机上的Arm上可能会有所不同)。

我不会期望这些底层差异对解释性语言产生如此重大的影响,我只是没有在回复中看到DJNZ,所以想分享一个有趣的想法。


就记录而言,“DJNZ”是8051(z80)ISA中的一条指令。x86使用dec/jnz代替inc/cmp/jne,显然ARM也有类似的东西。我相信这不是JavaScript差异的原因;这是由于在循环条件中有更多要评估的内容。 - Peter Cordes

3

简单来说,i-- 和 i++ 所需的时间是相同的。

但在这种情况下,当你进行递增操作时,处理器会每次将变量加1时评估 .length,而在递减操作中,在这种情况下,它只会评估一次 .length 直到我们得到0。


1

帮助他人避免头疼 --- 投票支持这个问题!!!

这个页面上最受欢迎的答案在Firefox 14上不起作用,也无法通过jsLinter。 "while"循环需要一个比较运算符,而不是一个赋值运算符。它可以在Chrome、Safari甚至IE上工作。但在Firefox中却失败了。

这是有问题的!

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

这个会有用的!(在Firefox上能正常工作,通过了jsLinter)

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

2
我刚在Firefox 14上尝试了一下,它运行良好。我看过一些生产库的示例,它们使用while (i--)并且可以跨多个浏览器工作。你的测试可能有什么奇怪的地方吗?你是在使用Firefox 14的beta版本吗? - Matt Browne

1
回答这种问题的最佳方法是实际尝试一下。设置一个循环,计算一百万次迭代或其他次数,并分别进行测试。计时两个循环,并比较结果。
答案可能取决于您使用的浏览器。有些浏览器的结果可能与其他浏览器不同。

4
这仍然不能回答他为什么更快的问题。他只会得到一个基准测试结果,却不知道为什么会这样... - peirix
3
没错。但是,如果不知道每个浏览器中每个JavaScript引擎的确切实现方式,就几乎不可能回答“为什么”的部分。这里的很多答案都会提到一些轶事式的建议,比如“使用预先递减而不是后置递减”(C++优化技巧)和“与零比较”,尽管这些在本地代码编译语言中可能是正确的,但JavaScript与CPU的裸机操作相距甚远。 - Greg Hewgill
4
我完全不同意最好的方法是去尝试。少数几个例子无法代替集体知识,这也是像这样的论坛存在的全部意义。 - Christophe

1

很棒,有很多标记但没有答案:D

简单来说,与零进行比较始终是最快的比较方式。

因此,(a == 0)实际上比(a == 5)更快地返回True。

这很微不足道,但在集合中有1亿行时,它是可以测量的。

例如,在循环中你可能会说 i <= array.length 并增加 i

在向下循环中,你可能会说 i >= 0 并减少i。

比较速度更快。而不是循环的“方向”。


由于JavaScript引擎都不同,而且答案取决于您测量的确切浏览器,因此无法回答所述问题。 - Greg Hewgill
不,根据基本原则,与零进行比较是最快的。尽管您的说法也是正确的,但与零进行比较是绝对的黄金法则。 - Robert
4
只有编译器选择进行这种优化时才是正确的,但这并不保证。编译器可能会为(a==0)和(a==5)生成完全相同的代码,除了常量的值以外。如果比较两个变量,则CPU不会比将某个值与0进行比较更快。通常只有本地代码编译器才有机会在这个层面进行优化。 - Greg Hewgill

1
这只是一个猜测,但可能是因为处理器更容易将某物与0进行比较(i >= 0),而不是与另一个值进行比较(i < Things.length)。

3
+1 不要让别人投反对票。虽然重复评估 .length 比实际的增加/减少更为麻烦,但循环结束时的检查可能很重要。我的 C 编译器在像这样的循环中会给出一些警告:“备注 #1544-D:(ULP 13.1)检测到计数上升的循环。建议使用计数递减的循环,因为检测零值更容易”。 - harper

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