JavaScript 循环性能 - 为什么向 0 递减迭代器比递增更快
JavaScript 循环性能 - 为什么向 0 递减迭代器比递增更快
在他的书《Even Faster Web Sites》中,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
循环几乎完全相同。
我想知道,这是为什么?为什么递减迭代器更快?(我对此的技术背景感兴趣,而不是为了证明这一说法而进行基准测试。)
编辑:乍一看,这里使用的循环语法似乎是错误的。没有length-1
或i>=0
,所以让我们澄清一下(我也感到困惑)。
这是一般的for循环语法:
for ([initial-expression]; [condition]; [final-expression]) statement
- initial-expression -
var i=length
这个变量声明首先被评估。
- condition -
i--
这个表达式在每次循环迭代之前被评估。它会在第一次循环之前递减变量。如果这个表达式评估为
false
,循环就结束了。在JavaScript中,0 == false
,所以如果i
最终等于0
,它会被解释为false
,循环结束。 - final-expression
这个表达式在每次循环迭代结束时被评估(在下次评估条件之前)。在这里不需要,所以为空。在for循环中,这三个表达式都是可选的。
for循环语法不是问题的一部分,但因为它有点不常见,我认为澄清一下是有趣的。也许更快的原因之一是,它使用了更少的表达式(0 == false
的“技巧”)。
JavaScript循环性能-为什么将迭代器递减至0比递增更快?
在JavaScript中,使用循环是一种常见的操作。然而,有时我们会遇到性能问题,特别是在处理大量数据时。一个常见的问题是,为什么迭代器从大数递减至0比递增至大数更快?这篇文章将解释这个问题的原因,并提供解决方法。
首先,我们来比较两个例子。第一个例子是使用递增迭代器的循环:
for (var i=0; i<length; i++) { }
第二个例子是使用递减迭代器的循环:
for (var i=length; i--;) { }
如果我们将每个变量访问和每个运算符都视为一个指令,那么第一个循环将使用5个指令(读取i,读取length,评估i<length,测试(i<length)是否为真,递增i),而第二个循环只使用3个指令(读取i,测试i是否为真,递减i)。这是一个5:3的比率。
那么为什么递减迭代器的循环会更快呢?原因在于递减迭代器的循环少了一次变量访问的指令。在第一个循环中,我们需要在每次迭代中读取变量i的值,而在第二个循环中,我们只需要在第一次迭代之前读取一次变量i的值。这就是为什么递减迭代器的循环更快的原因。
解决这个问题的方法是使用递减迭代器的循环,即将迭代器初始化为length的值,并在循环中使用递减操作符。这样可以减少一次变量访问的指令,从而提高性能。
总结起来,递减迭代器的循环比递增迭代器的循环更快是因为它少了一次变量访问的指令。为了提高性能,我们应该使用递减迭代器的循环来处理大量数据。
JavaScript循环性能-为什么将迭代器递减至0比递增更快的问题的出现是因为与0进行比较的循环终点比与< length
(或另一个JS变量)进行比较更快。
这是因为序数运算符<, <=, >, >=
是多态的,所以这些运算符需要在操作符的左右两侧进行类型检查,以确定应该使用什么比较行为。
在这里有一些非常好的基准测试:
非常有趣的链接,可惜他没有添加使用前缀++i运算符的for循环测试,根据prototypejs.org/api/array应该比使用后缀i++递增运算符更快。
前缀(++i)和后缀(i++)之间的任何差异都取决于浏览器对JavaScript的实现(或者几乎没有差异)。逻辑上的差异更为重要(Douglas Crockford指出,使用这种结构很容易出错。)前缀(++i)版本在其余部分之前增加/减少,而后缀(i++)版本在增加/减少之后增加/减少。 i--正常工作,因为顺序是(1]如果为0则退出,2]递减,3]执行循环); --i将失败,因为顺序(1]递减,2]如果为0则退出,3]执行循环)永远不会处理第0个元素。
解决方法:
为了获得更好的性能,可以尝试将迭代器递减至0而不是递增。这是因为与0进行比较的速度比与其他变量进行比较的速度更快。此外,还可以考虑使用前缀++i运算符而不是后缀i++运算符。根据某些测试,前缀++i运算符可能比后缀i++运算符更快。
这些方法的效果可能因浏览器的JavaScript实现而异,因此建议进行基准测试以确定哪种方法在特定环境中具有更好的性能。
JavaScript循环性能 - 为什么迭代器向0递减比递增更快的原因以及解决方法
在早期的“古早时代”,以下代码:
for (i = 0; i < n; i++){ .. 循环体 .. }
会生成以下代码:
move register, 0
L1:
compare register, n
jump-if-greater-or-equal L2
-- 循环体 ..
increment register
jump L1
L2:
而逆向计数的代码:
for (i = n; --i>=0;){ .. 循环体 .. }
会生成以下代码:
move register, n
L1:
decrement-and-jump-if-negative register, L2
.. 循环体 ..
jump L1
L2:
因此,在循环体内部,逆向计数只执行了两个额外的指令,而不是四个。
值得注意的是,“古早时代”的JavaScript永远不会被转换成机器码,所以这有点无谓。
JavaScript仍然有一个解释器,即浏览器,必须决定如何执行代码。虽然它并没有被“编译”成机器码,但它必须以某种方式发送给处理器。应该说的是,应该说的是解释器而不是编译器,但说它不被转换成机器码不能完全准确。尽管这是由浏览器决定的。例如,Mozilla使用Spider Monkey。
我非常确定我们在说同一件事。JavaScript的原始实现包括一个解释器,而现代版本例如Spider Monkey和V8会有选择地即时编译代码。