为什么在 nodejs 的 for 循环中 let 比 var 慢?

IT技术 javascript node.js ecmascript-6 v8
2021-01-20 11:39:21

我写了一个非常简单的基准测试:

console.time('var');
for (var i = 0; i < 100000000; i++) {}
console.timeEnd('var')


console.time('let');
for (let i = 0; i < 100000000; i++) {}
console.timeEnd('let')

如果您正在运行 Chrome,您可以在这里尝试(因为 NodeJS 和 Chrome 使用相同的 JavaScript 引擎,尽管通常版本略有不同):

结果令我惊讶:

var: 89.162ms
let: 320.473ms

我已经在 Node 4.0.0 && 5.0.0 && 6.0.0 中测试过,var并且let每个节点版本之间的比例都相同。

有人可以向我解释隐藏这种看似奇怪的行为的原因是什么?

2个回答

来自未来的注意事项:这些历史性能差异不再准确或相关,因为现代引擎可以在没有可观察到的行为差异时let使用var语义来优化语义当存在观察到的差异时,使用正确的语义对性能几乎没有影响,因为相关代码本质上已经是异步的。

基于varvs.的机制let差异,运行时的这种差异是由于var存在于匿名函数的整个块范围内,而let仅存在于循环中,并且每次迭代都必须重新声明。* 见下文这是一个演示这一点的示例:

(function() {
  for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(`i: ${i} seconds`);
    }, i * 1000);
  }
  // 5, 5, 5, 5, 5


  for (let j = 0; j < 5; j++) {
    setTimeout(function() {
      console.log(`j: ${j} seconds`);
    }, 5000 + j * 1000);
  }
  // 0, 1, 2, 3, 4
}());

请注意,在i循环的所有迭代中共享,而let不是。根据您的基准,node.js 似乎只是没有优化范围规则,let因为它比var现在更新和复杂得多

细化

这里有一些对letinfor循环的外行解释,对于那些不想查看公认的密集规范但又好奇如何let在每次迭代中重新声明同时仍保持连续性的人。

但是let不可能为每次迭代重新声明,因为如果您在循环内更改它,它会传播到下一次迭代!

首先,这是一个几乎可以验证这种潜在反驳的例子:

(function() {
  for (let j = 0; j < 5; j++) {
    j++; // see how it skips 0, 2, and 4!?!?
    setTimeout(function() {
      console.log(`j: ${j} seconds`);
    }, j * 1000);
  }
}());

你是部分正确的,因为这些变化尊重 的连续性j但是,它仍然在每次迭代时重新声明,如 Babel 所示:


Derek Ziemba提出了一个有趣的观点

Internet Explorer 14.14393 似乎没有这些 [性能] 问题。

不幸的是,Internet Explorer通过使用更简单的语义错误地实现了let语法var,因此比较其性能是一个有争议的问题:

在Internet Explorer中,let一个内for环路初始化不会为每次循环迭代创建由ES2015限定一个单独的变量。相反,它的行为就像循环被包装在一个作用域块中,let紧邻循环之前。


* Babel 的 REPL 上的这个转译版本演示了letfor循环中声明变量时会发生什么创建一个新的声明性环境来保存该变量(详情请点击此处),然后对于每次循环迭代, 创建另一个声明性环境来保存每次迭代的变量副本;每次迭代的副本都是从前一个的值(详见此处初始化的,但它们是单独的变量,正如每个闭包中输出的值所证明的那样。

Internet Explorer 14.14393 似乎没有这些问题。使用 'let' 循环时,它比 Chrome 54 快 3 倍,一般循环快 175%。 jsbench.github.io/#e86c06909d7eedd18f8b9bac101e9d6d
2021-03-25 11:39:21
@squint 措辞有所改进。
2021-03-28 11:39:21
@JanOsch:不,请仔细查看他的j示例及其输出的内容。这很迷人。这是 Babel 对简化版本的转译。变量有两个方面,一个是跨循环维护的,另一个(我们在正文中看到的)每次都创建。
2021-04-09 11:39:21
@JanOsch:如果您指的是“...var存在于整个块范围内”,我认为“块”是指块的“封闭”范围。措辞可以改进。
2021-04-09 11:39:21
@PatrickRoberts 你的回答证明了另一件事:声明的变量var仍然在全局范围内:见https://developer.mozilla.org/pl/docs/Web/JavaScript/Reference/Statements/let#let-scoped_variables_in_for_loops
2021-04-14 11:39:21

对于这个问题。我试图从 chrome V8 源代码中找到一些线索。这是 V8 循环剥离代码:

https://github.com/v8/v8/blob/5.4.156/src/compiler/loop-peeling.cc

我试着理解它,我认为 for 循环在实现中有一个中间层。for 循环将在中间层保存增量值。

如果循环使用let来声明“i”,V8会为每一次循环迭代声明一个新变量i,将中间层增量变量的值复制到新声明的“i”中,然后将其放入循环体作用域;

如果循环使用var声明“i”,V8只会把中间层的增量值引用到循环体作用域。它将减少循环迭代的性能开销。

对不起,我的泳池英语。v8 源代码中有一个图表,它将向您展示机制。