25

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()
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • 4
    The first has to create a new level of scope on each iteration if it's not optimized to avoid it. –  Nov 06 '16 at 12:48
  • 1
    More info: http://stackoverflow.com/questions/37792934/why-is-let-slower-than-var-in-a-for-loop-in-nodejs –  Nov 06 '16 at 12:53
  • 1
    I think that early on, the semantics of `let` were going to match your second example, where the `let` declaration in the head was scoped to a block surrounding the entire loop. There are pros and cons to each approach. –  Nov 06 '16 at 13:01
  • Not sure why the last example maintains its performance characteristics. Maybe they see that `j` is never used beyond its single mutation in that scope, so they optimize away the addition/assignment. Maybe even the declaration, when declared in that position for some reason. Who knows. Requires conjecture, unless one is familiar with the implementation's codebase. –  Nov 06 '16 at 13:14
  • @squint I have modified again to eliminate possibility of the second let being optimised out. (see snippet) What gets me is that the extra time is so so much greater than what would be caused by any of the explanations I have seen. Think it's time I bite that bullet and have a look in the code base. – Blindman67 Nov 06 '16 at 13:24
  • Yeah, probably worth doing. I suspect you'll find that the specific part copying the previous value to the new context is somehow poorly optimized (or the need to do it blocks some optimization). No question at all that the hit is larger than one would expect (although of course miniscule in the overall context of things). – T.J. Crowder Nov 06 '16 at 13:40
  • Note that your bottom (as of this writing) test ends up at 55%/45% if you let it run long enough that crankshaft will kick in (say, `< 2e4` instead of `< 1e3`). – T.J. Crowder Nov 06 '16 at 17:55
  • @T.J.Crowder Even with 1e3 i get 55%/45%. Chrome 54.0.2840.90 on Ubuntu. – Scimonster Nov 06 '16 at 19:09

3 Answers3

24

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I have updated my answer with another snippet. Adding a second declaration in the quicker function. If it took time to create the variable then it should have slowed down. And if your answer is correct where is the value of `i` stored at the end of each iteration if it is being recreated? – Blindman67 Nov 06 '16 at 13:05
  • 1
    @Blindman67: There's no "if" about it, [see the specification](http://www.ecma-international.org/ecma-262/7.0/index.html#sec-forbodyevaluation). Your new snippet just exercises a different part of the optimizer (could be dead code elimination, could be that they've done a better job optimizing declarations in teh body than in the head). – T.J. Crowder Nov 06 '16 at 13:18
  • 1
    @Blindman67: I didn't answer your *"where is the value of i stored at the end of each iteration if it is being recreated"* question: It's copied from the previous per-iteration execution environment. It works like this: A per-iteration environment is created, the initializer is run, and the body is run; then a new per-iteration environment is created, the value of `i` from the previous environment is copied to the new environment, that becomes the current environment, the increment is done, and the body is done; rinse, repeat. – T.J. Crowder Nov 06 '16 at 13:23
  • 1
    ...which is part of why the `let i` in the `for` initializer is probably different from the `let j` inside the loop body, from an optimization perspective, even though they both get recreated for each loop iteration. – T.J. Crowder Nov 06 '16 at 13:27
6

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.


Community
  • 1
  • 1
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Is there any related bug/issue for V8 you can track? It such a shame that there is still sucha major perf killer lurking in one of the most popular js engines, especially since `let` is becoming more and more common. – Dan M. May 03 '17 at 14:18
  • @DanM. Thank you for the link. I very rarely use let, in fact it has made me acutely aware when block scope will provide a logarithmic / syntactic advantage which is very rare in a highly granular language like JavaScript. I have yet to try `for(let i ...` in Chromes new Ignition+Turbofan engines (betas only) will update my answer if there is a change. – Blindman67 May 03 '17 at 18:39
2

@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();
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375