JavaScript循环的性能 – 为什么迭代器的递减速度比递增快

在他的“ 甚至更快的网站”一书中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]); } 

这对于for循环, do-while循环和while循环几乎是一样的。

我想知道,这是什么原因? 为什么要更快地减less迭代器? (我对这个技术背景感兴趣,而不是基准certificate这个说法。)


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

这里是一般的循环语法:

 for ([initial-expression]; [condition]; [final-expression]) statement 
  • 初始expression式var i=length

    首先评估这个variables声明。

  • 条件 – 我 –

    这个expression式在每个循环迭代之前被评估。 它会在第一次通过循环之前递减variables。 如果此expression式计算结果为false则循环结束。 在JavaScript中是0 == false所以如果i终于等于0它被解释为false ,循环结束。

  • 最终expression

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

for循环的语法不是问题的一部分,但是因为它有点不寻常,我认为澄清它是有趣的。 也许有一个更快的原因是,因为它使用较less的expression式( 0 == false “技巧”)。

我不确定使用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: 

所以在循环内部只做两个额外的指令而不是四个。

我相信这是因为你比较了循环终点和0,这比再次比较< length (或另一个JSvariables)更快。

这是因为有序运算符<, <=, >, >=是多态的,所以这些运算符需要在运算符的左右两边进行types检查,以确定应该使用哪种比较行为。

这里有一些很好的基准:

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

很容易说迭代可以有更less的指令。 我们来比较一下这两个:

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

当你把每个variables访问和每个操作符都作为一条指令进行计数时,前一个for循环使用5条指令(读取i ,读取length ,评估i<length ,testing(i<length) == true ,递增i ) 3条指令(读i ,testingi == true ,递减i )。 那是5:3的比例。

那么使用一个反向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至less是一个更可读的代码比for(i=length; i--;)

我也一直在探索循环速度,并且有兴趣find有关递减速度快于递增的消息。 但是,我还没有find一个testing来certificate这一点。 jsperf上有很多循环基准。 这是一个testing递减:

http://jsperf.com/array-length-vs-cached/6

caching你的数组长度,但是(也推荐Steve Souders的书)似乎是一个成功的优化。

还有一个更“高性能”的版本。 由于每个参数在for循环中都是可选的,所以可以跳过第一个参数。

 var array = [...]; var i = array.length; for(;i--;) { do_teh_magic(); } 

有了这个,你甚至可以跳过对[initial-expression]的检查。 所以你最终只剩下一个操作。

2017年for增加与减less

在现代的JS引擎中, for循环的递增速度通常比递减(基于个人Benchmark.jstesting)要快,也是比较传统的:

 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

我已经在C#和C ++(类似的语法)上进行了基准testing。 在那里,实际上,性能在循环中与在do whilewhile相比本质上for不同的。 在C ++中,增加时性能更好。 它也可能取决于编译器。

在Javascript中,我认为,这一切都取决于浏览器(JavaScript引擎),但这种行为是可以预料的。 Javascript是针对使用DOM进行优化的。 所以想象一下,在每一次迭代中,你都会遍历DOM元素的集合,而当你必须删除它们时,你需要增加一个计数器。 你删除了0元素,然后删除了1元素,但是你跳过了那个取0的地方。 当向后循环时,该问题消失。 我知道给出的例子不是正确的,但是我确实遇到了必须从不断变化的对象集合中删除项目的情况。

因为后向循环比前向循环更经常是不可避免的,所以我猜测JS引擎已经为此而优化了。

你有时间吗? Sounders先生在现代翻译方面可能是错误的。 这恰恰是一个优秀的编译器作者可以做出很大的改变。

我不知道是否更快,但我看到的一个原因是,当你使用增量迭代大数组元素时,你往往写:

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

你本质上是访问数组N(元素数)次的长度属性。 而当你减less,你只能访问一次。 这可能是一个原因。

但是你也可以写如下的递增循环:

 for(var i = 0, len = array.length; i < len; i++) { ... } 

在现代的JS引擎中,正向和反向循环之间的区别几乎是不存在的。 但性能差异归结为两件事情:

a)每个周期对每个长度属性进行额外的查找

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