繁体   English   中英

为什么在 nodejs 的 for 循环中 let 比 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 中测试过,每个节点版本varlet的比例是一样的。

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

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

基于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更新和复杂得多。

细化

这里有一些关于let in for循环的外行解释,适用于那些不想查看公认的密集规范,但很好奇如何在每次迭代中重新声明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 所示:

 "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提出了一个有趣的观点

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

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

在 Internet Explorer 中, for循环初始化器中的let不会为 ES2015 定义的每个循环迭代创建单独的变量。 相反,它的行为就像循环被包裹在同一个范围块let立即循环之前。


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

对于这个问题。 我试图从 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 源代码中有一个图表,它将向您展示机制。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM