为啥在 Chrome 上的 for 循环中使用 let 这么慢?
Posted
技术标签:
【中文标题】为啥在 Chrome 上的 for 循环中使用 let 这么慢?【英文标题】:Why is using `let` inside a `for` loop so slow on Chrome?为什么在 Chrome 上的 for 循环中使用 let 这么慢? 【发布时间】:2017-03-19 20:53:47 【问题描述】:重大更新。
Chrome Canary 59 的新 Ignition+Turbofan engines 尚未在 Chrome 主要版本中解决该问题。测试显示 let
和 var
声明的循环变量的时间相同。
原来的(现在没有实际意义的)问题。
当在 Chrome 上的 for
循环中使用 let
时,与将变量移动到循环范围之外相比,它的运行速度非常慢。
for(let i = 0; i < 1e6; i ++);
需要两倍的时间
let i; for(i = 0; i < 1e6; i ++);
发生了什么事?
片段展示了差异,并且只影响 Chrome,而且只要我记得 Chrome 支持let
,它就一直如此。
var times = [0,0]; // hold total times
var count = 0; // number of tests
function test()
var start = performance.now();
for(let i = 0; i < 1e6; i += 1);
times[0] += performance.now()-start;
setTimeout(test1,10)
function test1()
// this function is twice as quick as test on chrome
var start = performance.now();
let i ; for(i = 0; i < 1e6; i += 1);
times[1] += performance.now()-start;
setTimeout(test2,10)
// display results
function test2()
var tot =times[0]+times[1];
time.textContent = tot.toFixed(3) + "ms";
time1.textContent = ((times[0]/tot)*100).toFixed(2) + "% " + times[0].toFixed(3) + "ms";
time2.textContent = ((times[1]/tot)*100).toFixed(2) + "% " + times[1].toFixed(3) + "ms";
if(count++ < 1000);
setTimeout(test,10);
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
var time = document.createElement("span");
var time1 = document.createElement("span");
var time2 = document.createElement("span");
div.textContent = "Total execution time : "
div1.textContent = "Test 1 : "
div2.textContent = "Test 2 : "
div.appendChild(time);
div1.appendChild(time1);
div2.appendChild(time2);
document.body.appendChild(div);
document.body.appendChild(div1);
document.body.appendChild(div2);
test2()
当我第一次遇到这种情况时,我以为是因为新创建的 i 实例,但以下显示并非如此。
请参阅代码 sn-p,因为我已经消除了附加的 let 声明被 ini 随机优化然后添加到 k 的不确定值的任何可能性。
我还添加了第二个循环计数器p
var times = [0,0]; // hold total times
var count = 0; // number of tests
var soak = 0; // to stop optimizations
function test()
var j;
var k = time[1];
var start = performance.now();
for(let p =0, i = 0; i+p < 1e3; p++,i ++)j=Math.random(); j += i; k += j;;
times[0] += performance.now()-start;
soak += k;
setTimeout(test1,10)
function test1()
// this function is twice as quick as test on chrome
var k = time[1];
var start = performance.now();
let p,i ; for(p = 0,i = 0; i+p < 1e3; p++, i ++)let j = Math.random(); j += i; k += j
times[1] += performance.now()-start;
soak += k;
setTimeout(test2,10)
// display results
function test2()
var tot =times[0]+times[1];
time.textContent = tot.toFixed(3) + "ms";
time1.textContent = ((times[0]/tot)*100).toFixed(2) + "% " + times[0].toFixed(3) + "ms";
time2.textContent = ((times[1]/tot)*100).toFixed(2) + "% " + times[1].toFixed(3) + "ms";
if(count++ < 1000);
setTimeout(test,10);
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
var time = document.createElement("span");
var time1 = document.createElement("span");
var time2 = document.createElement("span");
div.textContent = "Total execution time : "
div1.textContent = "Test 1 : "
div2.textContent = "Test 2 : "
div.appendChild(time);
div1.appendChild(time1);
div2.appendChild(time2);
document.body.appendChild(div);
document.body.appendChild(div1);
document.body.appendChild(div2);
test2()
【问题讨论】:
第一个必须在每次迭代中创建一个新级别的范围,如果它没有被优化以避免它。 更多信息:***.com/questions/37792934/… 我认为在早期,let
的语义将与您的第二个示例相匹配,其中头部中的 let
声明的范围为围绕整个循环的块。每种方法各有利弊。
不确定为什么最后一个示例保持其性能特征。也许他们看到j
在该范围内的单个突变之外从未使用过,因此他们优化了添加/分配。甚至可能是声明,当出于某种原因在该位置声明时。谁知道。需要推测,除非熟悉实现的代码库。
@squint 我再次修改以消除第二个让优化的可能性。 (见sn-p)让我明白的是,额外的时间比我所看到的任何解释所造成的要多得多。认为是时候咬紧牙关看看代码库了。
【参考方案1】:
更新: 2018 年 6 月:Chrome 现在对此问题的优化比首次发布此问题和答案时要好得多;如果您不在循环中创建函数,那么在 for
中使用 let
将不再有任何明显的损失(如果您这样做了,那么收益是值得的)。
因为循环的每次迭代都会创建一个新的i
,因此在循环中创建的闭包会覆盖i
该迭代。 evaluation of a for
loop body 的算法规范中涵盖了这一点,该规范描述了每次循环迭代创建一个新的变量环境。
例子:
for (let i = 0; i < 5; ++i)
setTimeout(function()
console.log("i = " + i);
, i * 50);
// vs.
setTimeout(function()
let j;
for (j = 0; j < 5; ++j)
setTimeout(function()
console.log("j = " + j);
, j * 50);
, 400);
这是更多的工作。 如果您不需要为每个循环使用新的 请参阅上面的更新,除了极端情况外无需避免它。i
,请在循环外使用let
。
我们可以预期,现在除了模块之外的所有东西都已实现,V8 可能会改进新东西的优化,但功能最初应该优先于优化也就不足为奇了。
很高兴其他引擎已经完成了优化,但 V8 团队显然还没有做到这一点。请参阅上面的更新。
【讨论】:
我已经用另一个 sn-p 更新了我的答案。在更快的函数中添加第二个声明。如果创建变量需要时间,那么它应该放慢速度。如果您的答案是正确的,那么在每次迭代结束时,如果要重新创建i
的值,它会存储在哪里?
@Blindman67:没有“如果”,see the specification。您的新 sn-p 只是练习了优化器的不同部分(可能是死代码消除,可能是他们在优化主体中的声明比在头部中做得更好)。
@Blindman67:我没有回答您的“如果正在重新创建,在每次迭代结束时存储的 i 的值在哪里” 问题:它是从以前的每次迭代执行环境。它是这样工作的:创建一个每次迭代的环境,运行初始化程序,然后运行主体;然后创建一个新的每次迭代环境,将先前环境中的i
的值复制到新环境中,成为当前环境,完成增量,完成主体;冲洗,重复。
...这就是为什么 for
初始化程序中的 let i
可能与循环体内的 let j
不同的部分原因,从优化的角度来看,即使它们都被重新创建每次循环迭代。【参考方案2】:
重大更新。
Chrome Canary 60.0.3087 的新 Ignition+Turbofan engines 尚未在 Chrome 主要版本中解决该问题。测试显示 let
和 var
声明的循环变量的时间相同。
旁注。我的测试代码使用 Function.toString()
并在 Canary 上失败,因为它返回 "function() "
而不是 "function () "
作为过去的版本(使用正则表达式轻松修复),但对于那些潜在的问题使用Function.toSting()
更新感谢用户 Dan. M 提供链接 https://bugs.chromium.org/p/v8/issues/detail?id=4762(并提醒),该链接提供了更多关于该问题的信息。
上一个答案
优化器选择退出。
这个问题让我困惑了一段时间,两个答案都是显而易见的答案,但它没有任何意义,因为时间差太大而无法创建新的作用域变量和执行上下文。
为了证明这一点,我找到了答案。
简答
优化器不支持声明中带有 let 语句的 for 循环。
Chrome 版本 55.0.2883.35 测试版,Windows 10。
一张价值千言万语的图片,应该是第一个看的地方。
上述配置文件的相关功能
var time = [0,0]; // hold total times
function letInside()
var start = performance.now();
for(let i = 0; i < 1e5; i += 1); // <- if you try this at home don't forget the ;
time[0] += performance.now()-start;
setTimeout(letOutside,10);
function letOutside() // this function is twice as quick as test on chrome
var start = performance.now();
let i; for(i = 0; i < 1e5; i += 1)
time[1] += performance.now()-start;
setTimeout(displayResults,10);
由于 Chrome 是主要参与者,循环计数器的阻塞范围变量无处不在,那些需要高性能代码并认为块范围变量很重要的人function(for(let i; i<2;i++...)//?WHY?
应该暂时考虑替代语法并声明循环计数器在循环之外。
我想说时间差是微不足道的,但鉴于函数内的所有代码都没有使用 for(let i...
进行优化,因此应谨慎使用。
【讨论】:
您可以跟踪 V8 的任何相关错误/问题吗?令人遗憾的是,在最流行的 js 引擎之一中仍然潜伏着这样一个主要的性能杀手,特别是因为let
变得越来越普遍。
@DanM。谢谢你的链接。我很少使用 let,事实上它让我敏锐地意识到块作用域何时会提供对数/语法优势,这在像 javascript 这样的高度精细的语言中是非常罕见的。我还没有在 Chrome 中尝试 for(let i ...
,如果有变化,新的 Ignition+Turbofan 引擎(仅限测试版)会更新我的答案。【参考方案3】:
@T.J.Crowder 已经回答了标题问题,但我会回答你的疑问。
当我第一次遇到这种情况时,我以为是因为新创建的 i 实例,但以下显示并非如此。
其实是因为i
变量新创建的作用域。哪个(尚未)优化,因为它是 more complicated than a simple block scope。
请参阅第二个代码 sn-p,因为我已经消除了使用带有随机数的 ini 优化附加 let 声明的任何可能性,然后添加到 k 的不确定值。
您在
中的附加let j
声明
let i; for (i = 0; i < 1e3; i ++) let j = Math.random(); j += i; k += j;
// I'll ignore the `p` variable you had in your code
已优化。对于优化器来说,这是一件非常简单的事情,它可以通过将循环体简化为来完全避免该变量
k += Math.random() + i;
除非您在其中创建闭包或使用eval
或类似的可憎之物,否则实际上并不需要范围。
如果我们引入这样的闭包(作为死代码,希望优化器没有意识到)和坑
let i; for (i=0; i < 1e3; i++) let j=Math.random(); k += j+i; function f() j;
反对
for (let i=0; i < 1e3; i++) let j=Math.random(); k += j+i; function f() j;
然后我们会看到它们以大致相同的速度运行。
var times = [0,0]; // hold total times
var count = 0; // number of tests
var soak = 0; // to stop optimizations
function test1()
var k = time[1];
var start = performance.now();
let i; for(i=0; i < 1e3; i++) let j=Math.random(); k += j+i; function f() j;
times[0] += performance.now()-start;
soak += k;
setTimeout(test2,10)
function test2()
var k = time[1];
var start = performance.now();
for(let i=0; i < 1e3; i++) let j=Math.random(); k += j+i; function f() j;
times[1] += performance.now()-start;
soak += k;
setTimeout(display,10)
// display results
function display()
var tot =times[0]+times[1];
time.textContent = tot.toFixed(3) + "ms";
time1.textContent = ((times[0]/tot)*100).toFixed(2) + "% " + times[0].toFixed(3) + "ms";
time2.textContent = ((times[1]/tot)*100).toFixed(2) + "% " + times[1].toFixed(3) + "ms";
if(count++ < 1000)
setTimeout(test1,10);
var div = document.createElement("div");
var div1 = document.createElement("div");
var div2 = document.createElement("div");
var time = document.createElement("span");
var time1 = document.createElement("span");
var time2 = document.createElement("span");
div.textContent = "Total execution time : "
div1.textContent = "Test 1 : "
div2.textContent = "Test 2 : "
div.appendChild(time);
div1.appendChild(time1);
div2.appendChild(time2);
document.body.appendChild(div);
document.body.appendChild(div1);
document.body.appendChild(div2);
display();
【讨论】:
以上是关于为啥在 Chrome 上的 for 循环中使用 let 这么慢?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我的代码在执行时的初始嵌套 for 循环中进入无限循环?
为啥 for..of / for..in 循环可以使用 const 而普通的 for 循环在 JS 中只能使用 let 或 var 作为其变量? [复制]