Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is using `let` inside a `for` loop so slow on Chrome?

MAJOR UPDATE.

Thought as yet not on the Chrome major release the new Ignition+Turbofan engines for Chrome Canary 59 has solved the problem. Test show identical times for let and var declared loop variables.


Original (now moot) question.

When using let in a for loop on Chrome it runs very slowly, compared to moving the variable just outside the loop's scope.

for(let i = 0; i < 1e6; i ++); 

takes twice as long as

{ let i; for(i = 0; i < 1e6; i ++);}

What is going on?

Snippet demonstrates the difference and only affects Chrome and has been so for as long as I can remember Chrome supporting 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()

When I first encountered this I thought it was because of the newly created instance of i but the following shows this is not so.

See code snippet as I have eliminated any possibility of the additional let declaration being optimised out with ini with random and then adding to indeterminate value of k.

I also added a second loop counter 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()
like image 394
Blindman67 Avatar asked Nov 06 '16 12:11

Blindman67


People also ask

Can we use let in for loop?

In the second example, using let , the variable declared in the loop does not redeclare the variable outside the loop. When let is used to declare the i variable in a loop, the i variable will only be visible within the loop.

Is Let slower than VAR?

In terms of performance comparison, var is faster and let is slower inside the loops while running or executing the code. Re-declaring var declared a variable in the same function or scope gives rise to Syntax Error whereas let declared variable cannot be redeclared.

How can I improve my loop performance?

The best ways to improve loop performance are to decrease the amount of work done per iteration and decrease the number of loop iterations. Generally speaking, switch is always faster than if-else , but isn't always the best solution.

Why We Use let in for loop?

According to MDN using let in the for loop like that should bind the variable in the scope of the loop's body. Things work as I'd expect them when I use a temporary variable inside the block.


3 Answers

Update: June 2018: Chrome now optimizes this much better than it did when this question and answer were first posted; there's no longer any appreciable penalty for using let in the for if you aren't creating functions in the loop (and if you are, the benefits are worth the cost).


Because a new i is created for each iteration of the loop, so that closures created within the loop close over the i for that iteration. This is covered by the specification in the algorithm for the evaluation of a for loop body, which describes creating a new variable environment per loop iteration.

Example:

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);

That's more work. If you don't need a new i for each loop, use let outside the loop. See update above, no need to avoid it other than edge cases.

We can expect that now that everything but modules has been implemented, V8 will probably improve optimization of the new stuff, but it's not surprising that functionality should be initially prioritized over optimization.

It's great that other engines have already done the optimization, but the V8 team apparently just haven't got there yet. See update above.

like image 83
T.J. Crowder Avatar answered Oct 07 '22 05:10

T.J. Crowder


MAJOR UPDATE.

Thought as yet not on the Chrome major release the new Ignition+Turbofan engines for Chrome Canary 60.0.3087 has solved the problem. Test show identical times for let and var declared loop variables.

Side note. My testing code uses Function.toString() and failed on Canary because it returns "function() {" not "function () {" as past versions (easy fix using regexp) but a potencial problem for those that use Function.toSting()

Update Thanks to the user Dan. M who provide the link https://bugs.chromium.org/p/v8/issues/detail?id=4762 (and heads up) which has more on the issue.


Previous answer

Optimiser opted out.

This question has puzzled me for some time and the two answers are the obvious answers, but it made no sense as the time difference was too great to be the creation of a new scoped variable and execution context.

In an effort to prove this I found the answer.

Short answer

A for loop with a let statement in the declaration is not supported by the optimiser.

Chrome profile of the two functions shown the slow function is not optimized Chrome Version 55.0.2883.35 beta, Windows 10.

A picture worth a thousand words, and should have been the first place to look.

The relevant functions for the above profile

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);
}

As Chrome is the major player and the blocked scoped variables for loop counters are everywhere, those who need performant code and feel that block scoped variables are important function{}(for(let i; i<2;i++}{...})//?WHY?should consider for the time being the alternative syntax and declare the loop counter outside the loop.

I would like to say that the time difference is trivial, but in light of the fact that all code within the function is not optimized using for(let i... should be used with care.


like image 6
Blindman67 Avatar answered Oct 07 '22 04:10

Blindman67


@T.J.Crowder already answered the title question, but I'll answer your doubts.

When I first encountered this I thought it was because of the newly created instance of i but the following shows this is not so.

Actually, it is because of the newly created scope for the i variable. Which is not (yet) optimised away as it is more complicated than a simple block scope.

See the second code snippet as I have eliminated any possibility of the additional let declaration being optimised out with ini with random and then adding to indeterminate value of k.

Your additional let j declaration in

{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

was optimised out. That's a pretty trivial thing to do for an optimiser, it can avoid that variable altogether by simplifying your loop body to

k += Math.random() + i;

The scope isn't really needed unless you create closures in there or use eval or similar abominations.

If we introduce such a closure (as dead code, hopefully the optimiser doesn't realise that) and pit

{let i; for (i=0; i < 1e3; i++) { let j=Math.random(); k += j+i; function f() { j; }}}

against

for (let i=0; i < 1e3; i++) { let j=Math.random(); k += j+i; function f() { j; }}

then we'll see that they run at about the same speed.

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();
like image 2
Bergi Avatar answered Oct 07 '22 04:10

Bergi