[英]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 中测试过,每个节点版本var
和let
的比例是一样的。
有人可以向我解释隐藏这种看似奇怪的行为的原因是什么?
来自未来的注意事项:这些历史性能差异不再准确或相关,因为现代引擎可以在没有可观察到的行为差异时使用var
语义来优化let
语义。 当存在可观察到的差异时,使用正确的语义对性能几乎没有影响,因为相关代码本质上已经是异步的。
基于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
更新和复杂得多。
这里有一些关于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); } })();
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.