11

I was looking for some micro optimizations of some JavaScript legacy code I was revisiting and noticed that in most frequently called for loops, counters were declared once in the global scope, outside the functions using them. I was curious whether that was indeed an optimization therefore I have created the following test case in JavaScript:

var tmp = 0;

function test(){

    let j = 0;

    function letItBe(){

        for(j = 0; j < 1000; j++){
            tmp = Math.pow(j, 2);
        }
    }

    function letItNotBe(){
        for(let l = 0; l < 1000; l++){
            tmp = Math.pow(l, 2);
        }
    }

    console.time("let it be");
    for(var i =0; i < 10000; i++){

        letItBe();
    }
    console.timeEnd("let it be");


    console.time("let it not be");
    for(var i =0; i < 10000; i++){

        letItNotBe();
    }
    console.timeEnd("let it not be");
}

test();

What happens is that letItNotBe() runs significantly faster than letItBe(), in Chrome, Firefox and also NodeJS

Chrome: enter image description here

NodeJS:

enter image description here

Changing let with var makes no difference.

Initally my logic was that declaring a new counter variable every time a function is called would be indeed slower than if a variable is initially declared and then simply reset to 0. However, it turns out to be quite the oposite and the difference in execution time is quite substential.

My simple explanation is that when the counter variable is declared ouside the function using it, in some way the JS transpiler needs to refer to this variable. And since it is in the parent scope it takes more executions to reference it when incrementing. However that's just blind guessing.

Can anybody give any meaningful explanation why this is happening, since I need to refactor the code and give a meaningful explanation mysefl besides the test that I already have :) Thanks.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Lys
  • 568
  • 1
  • 4
  • 18
  • 3
    Probably because it doesn't have to go look up the variable in a scope outside that of the for loop. – Dexygen Mar 14 '18 at 09:04
  • @GeorgeJempty Yeah right? That's my guess too... but is it? – Lys Mar 14 '18 at 09:05
  • OK I see your comment now but it's not just "blind guessing" -- `let` is block scoped. You might get different results if you use `var` instead. – Dexygen Mar 14 '18 at 09:07
  • I did try with var, nothing different, I will add this in the original post. – Lys Mar 14 '18 at 09:37
  • If you reverse the order, and/or run it longer (more iterations), does anything change? Maybe JIT kicks in for a 2nd more optimized pass later on. Have you ruled out CPU clock-speed effects? (slow to ramp up to turbo frequency?) – Peter Cordes Mar 14 '18 at 09:40
  • 1
    @PeterCordes Reversing the order changes nothing, same results. Giving more itterations also changes nothing. Tweaking CPU clock-speed, changes nothing. Runnin both functions 2 times in a row, changes nothing. :) – Lys Mar 14 '18 at 09:42
  • Declaring a variable that holds a primitive, should not have any cost. The lookup and scoping on the other hand might (depending on how the JIT will solve and optimize it) have overhead. – t.niese Mar 14 '18 at 09:43
  • *significantly faster* I'd hardly call 0.079 seconds significant. – Liam Mar 14 '18 at 09:47
  • @Liam: The OP already confirmed that increasing the iteration count didn't change the *relative* speeds. So you could run this for 1 second and have the slow version still take 5x longer. Besides, 0.079 seconds is 316 million clock cycles on a 4GHz CPU. It's time for ~8 random-access I/Os a rotating hard drive. It's time for light to go all the way around the Earth. It's over 2 frames of video at 30FPS. 80ms is plenty long! We aren't using abacii and carrier pigeons anymore, dude. – Peter Cordes Mar 14 '18 at 09:49
  • @Lys: What happens if you put `let j = 0;` inside the helper function, but outside the `for` loop? Is it maybe the function nesting that stopping JIT from doing a good job? – Peter Cordes Mar 14 '18 at 09:50
  • I think if you want to perform more valid tests possibly consider doing so on jsperf.com or another such site – Dexygen Mar 14 '18 at 10:59
  • @Liam It is still more than four times faster, makes significancy when the loop iterates million or more times. – Teemu Mar 14 '18 at 11:02
  • does it iterate a million or more times? If not this is premature optimisation which as we all know [*is the root of all evil*](https://softwareengineering.stackexchange.com/questions/80084/is-premature-optimization-really-the-root-of-all-evil) – Liam Mar 14 '18 at 11:08
  • 1
    @Liam yes it does, it is something that calculates physics and collision between particles, it is O(n^2) and it is called every FPS – Lys Mar 14 '18 at 11:15
  • So use the faster one...why does it matter? – Liam Mar 14 '18 at 11:18
  • 1
    @Liam It might be important to know why, so that you can avoid making slow iterations. Also, we shouldn't stick to the code examples literally, rather always think those as generic problems where the code in the question is only a single implementation of that generic issue. That way comments and possible answers are more useful to the future readers too. – Teemu Mar 14 '18 at 11:22
  • Ok, well I leave you to guessing about how the js engine is optimised while I do some work. Have fun guys ;) – Liam Mar 14 '18 at 11:23
  • @Liam Correct, sometimes we just have to move on and pick the "right" decision, time is precious :) However I can't help but be curious why things work the way they work... and as I said, it will be nicer if I commit changes with some meaningful describtion. Thanks for your time! – Lys Mar 14 '18 at 11:31

1 Answers1

9

I've read a book High Performance JavaScript, the author explained this at Chapter 2 "Data Access" - Section "Managing Scope" - Part "Identifier Resolution Performance".

Identifier resolution isn’t free, as in fact no computer operation really is without some sort of performance overhead. The deeper into the execution context’s scope chain an identifier exists, the slower it is to access for both reads and writes. Consequently, local variables are always the fastest to access inside of a function, whereas global variables will generally be the slowest (optimizing JavaScript engines are capable of tuning this in certain situations).

...

The general trend across all browsers is that the deeper into the scope chain an identifier exists, the slower it will be read from or written to.

...

Given this information, it’s advisable to use local variables whenever possible to improve performance in browsers without optimizing JavaScript engines. A good rule of thumb is to always store out-of-scope values in local variables if they are used more than once within a function.

In your case, letItBe and letItNotBe work in the same way, using the same out-of-scope tmp variable, and both of them are closures.The only difference is the counter variables of for loops:

  • variable j is defined for function test(), it's 'out-of-scope' for function letItBe(), so executing letItBe() will cause the engine to do more works on identifier resolution
  • variable l is defined in scope of for loop (see let keyword in the for loop), so resolution is faster
kite.js.org
  • 1,599
  • 10
  • 11
  • This suggests that a local variable within `letItBe()` would be fast, whether or not it's declared within the for loop itself. Have you checked that? i.e. that the question title is missing the point that it's not just outside the `for` loop, it's outside the nested function the `for` loop is in? – Peter Cordes Mar 14 '18 at 11:45
  • 1
    @PeterCordes You are right, it's just when I wrote the title I found it hard to come up with a short descriptive title, that accuratly reflects the case. And yes, declaring the variable within the function outside the for loop is just as fast as declaring it within the for loop. – Lys Mar 14 '18 at 12:07
  • I will actually accept this answer, since it is basically referenced to trustworthy source (a JS book) and it confirms my and most of people's guesses of why this is happening. – Lys Mar 14 '18 at 12:11
  • @Lys: titles are hard. I usually end up making them long, but that's better than generic or inaccurate, IMO. Much easier for people to find interesting / relevant questions when the question title says what's *really* going on. (Sometimes writing a good title for future readers requires knowing the answer, but that's fine, and what edits are for :) And BTW, this question went from 2 to 4 upvotes in the 10 minutes after I retitled it, so I think describing the nesting is more interesting / sensible. – Peter Cordes Mar 14 '18 at 12:33
  • @PeterCordes Thanks for the edit, and for your time :) I am generally trying to do my best, but as you mentioned sometimes it's hard and was just considering editing it, when I saw you did it for me! – Lys Mar 14 '18 at 12:47