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-1
或i>=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 while
或while
相比本质上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++)