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()
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.
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.
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.
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.
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 See update above, no need to avoid it other than edge cases.i
for each loop, use let
outside the loop.
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.
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 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.
@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();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With