为啥在 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 主要版本中解决该问题。测试显示 letvar 声明的循环变量的时间相同。


原来的(现在没有实际意义的)问题。

当在 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 主要版本中解决该问题。测试显示 letvar 声明的循环变量的时间相同。

旁注。我的测试代码使用 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&lt;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 作为其变量? [复制]

为啥我使用多处理/多线程的函数在 for 循环中使用时如此缓慢,但在循环之外却没有?

为啥不总是在 vue.js for 循环中使用索引作为键?

为啥这个for循环不执行?

为啥打印和回声在“for”循环中表现不同[重复]