JavaScript 循环性能 - 为什么将迭代器递减到 0 比递增更快

IT技术 javascript performance loops
2021-01-23 05:58:31

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-1or i>=0,所以让我们澄清一下(我也很困惑)。

这是一般的 for 循环语法:

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

    首先评估此变量声明。

  • 条件——i--

    该表达式在每次循环迭代之前计算。它将在第一次通过循环之前递减变量。如果此表达式的计算结果为false循环结束。在 JavaScript 中0 == false,如果ifinally 等于0它被解释为false循环结束。

  • 最终表达式

    该表达式在每次循环迭代结束时进行计算(在下一次计算condition 之前)。这里不需要它,它是空的。所有三个表达式在 for 循环中都是可选的。

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

6个回答

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

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

值得注意的是,“在过去”,javascript 永远不会被转换成机器代码,所以这有点有争议。
2021-04-05 05:58:31
@skeggse JavaScript 仍然有一个解释器,即浏览器,它必须决定如何执行代码。虽然它并没有完全“编译”,但它必须以某种方式发送到处理器。应该说的是interpreter而不是compiler,但是说它没有变成机器代码不能完全准确。尽管这取决于浏览器的判断力。例如,Mozilla 使用蜘蛛猴
2021-04-10 05:58:31
@haelmic 很确定我们在说同样的话。javascript 的原始实现由解释器组成,而现代版本如 Spider Monkey 和 V8 则有选择地 JIT 编译代码。
2021-04-10 05:58:31

我相信原因是因为您将循环终点与 0 进行比较,这比再次比较< length(或另一个 JS 变量)要快

正是因为序数运算符<, <=, >, >=是多态的,所以这些运算符需要在运算符的左右两边进行类型检查,以确定应该使用什么比较行为。

这里有一些非常好的基准测试:

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

pre- (++i) 和 post- (i++) 之间的任何差异都取决于浏览器对 Javascript 的实现(或者非常小)。逻辑上的差异重要得多(Douglas Crockford 指出这种结构很容易导致逻辑错误。) pre- (++i) 版本在语句的其余部分之前递增/递减,而 post- ( i++) 版本. i-- 工作顺序是 (1]exit if 0, 2]decrement, 3]perform loop); --i 会失败,因为顺序 (1]decrement, 2]exit if 0, 3]perform loop) 永远不会处理第 0 个元素。
2021-03-17 05:58:31
非常有趣的链接,很遗憾他没有使用前缀++i 运算符添加带有for 循环的测试,根据prototypejs.org/api/array应该比使用后缀i++ 增量运算符更快。
2021-04-10 05:58:31

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

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

for (var i=length; i--;) {
}

当您将每个变量访问和每个运算符算作一条指令时,前一个for循环使用 5 条指令(读取i、读取length、评估i<length、测试(i<length) == true、增量i),而后者仅使用 3 条指令(读取i、测试i == true、减量i)。那是5:3的比例。

您在计算后面的 for 循环中的指令数时错过了“Read i”,这使得比率为 5:3。
2021-03-18 05:58:31
您不想设置i = length,因为for循环也会对第一次迭代进行条件测试吗?
2021-03-19 05:58:31

那么使用反向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 这至少是一个比 for(i=length; i--;)

问题不是要找到更易读的语法,而是要找到为什么递减迭代器更快的解释。
2021-03-25 05:58:31
你有最好的答案,而(i--)是最快的: jsperf.com/while-vs-for
2021-03-31 05:58:31

for 2017 年增量与减量

在现代 JS 引擎中,for循环递增通常比递减更快(基于个人 Benchmark.js 测试),也更传统:

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在任何情况下都可以有效地省略。

2019 年增量仍然比减量快jsperf.com/ppi-vs-ipp-forloop/9
2021-03-23 05:58:31