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

Why is let slower than var in a for loop in nodejs?

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

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 引擎,尽管通常版本略有不同) :

// Since Node runs code in a function wrapper with a different
// `this` than global code, do that:
(function() {
  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')
}).call({});

结果让我吃惊:

var: 89.162ms
let: 320.473ms

我在Node 4.0.0 && 5.0.0 && 6.0.0测试过,每个node版本varlet的比例是一样的

有人可以向我解释一下这种看似奇怪的行为背后的原因是什么吗?

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

基于 varlet 之间的机制差异,运行时的这种差异是由于 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 更新和复杂得多。

详细说明

这是 for 循环中 let 的一些外行解释,适用于那些不关心公认的密集规格,但好奇 let 是如何重新编写的人-为每次迭代声明,同时仍保持连续性。

But let can't possibly be re-declared for each iteration, because if you change it inside the loop, it propagates to the next iteration!

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

(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 所示:

"use strict";

(function () {
  var _loop = function _loop(_j) {
    _j++; // here's the change inside the new scope
    setTimeout(function () {
      console.log("j: " + _j + " seconds");
    }, _j * 1000);
    j = _j; // here's the change being propagated back to maintain continuity
  };

  for (var j = 0; j < 5; j++) {
    _loop(j);
  }
})();


Derek Ziemba brings up :

Internet Explorer 14.14393 doesn't seem to have these [performance] issues.

不幸的是,Internet Explorer incorrectly implemented let syntax by essentially using the simpler var semantics,因此比较其性能是一个有实际意义的问题:

In Internet Explorer, let within a for loop initializer does not create a separate variable for each loop iteration as defined by ES2015. Instead, it behaves as though the loop were wrapped in a scoping block with the let immediately before the loop.


* This transpiled version on Babel's REPL demonstrates what happens when you declare a let variable in a for loop. A new declarative environment is created to hold that variable (details here), and then for each loop iteration another declarative environment is created to hold a per-iteration copy of the variable; each iteration's copy is initialized from the previous one's value (details here),但它们是独立的变量,正如每个闭包中输出的值所证明的那样。

对于这个问题。我试图从 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源码里有图,会告诉你原理。