为什么 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版本var
和let
的比例是一样的
有人可以向我解释一下这种看似奇怪的行为背后的原因是什么吗?
来自未来的说明:这些历史性能差异不再准确或相关,因为现代引擎可以通过使用优化 let
语义var
行为上没有明显差异时的语义。当存在 可观察到的差异时,使用正确的语义对性能几乎没有影响,因为相关代码本质上已经是异步的。
基于 var
与 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
更新和复杂得多。
详细说明
这是 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源码里有图,会告诉你原理。
我写了一个非常简单的基准测试:
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版本var
和let
的比例是一样的
有人可以向我解释一下这种看似奇怪的行为背后的原因是什么吗?
来自未来的说明:这些历史性能差异不再准确或相关,因为现代引擎可以通过使用优化 let
语义var
行为上没有明显差异时的语义。当存在 可观察到的差异时,使用正确的语义对性能几乎没有影响,因为相关代码本质上已经是异步的。
基于 var
与 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
更新和复杂得多。
详细说明
这是 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 afor
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 thelet
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源码里有图,会告诉你原理。