Multiple sources for JS performance tips encourage developers to reduce "scope chain lookup". For example, IIFEs are touted as having a bonus benefit of "reducing scope chain lookup" when you access global variables. This sounds quite logical, perhaps even taken for granted, so I didn't question the wisdom. Like many others, I have been happily using IIFEs thinking that on top of avoiding global namespace pollution, there's gonna be a performance boost over any global code.
What we expect today:
(function($, window, undefined) { // apparently, variable access here is faster than outside the IIFE })(jQuery, window);
Simplifying / extending this to a generalized case, one would expect:
var x = 0; (function(window) { // accessing window.x here should be faster })(window);
Based on my understanding of JS, there is no difference between x = 1;
and window.x = 1;
in the global scope. Therefore, it is logical to expect them to be equally performant, right? WRONG. I ran some tests and discovered that there's a significant difference in access times.
Ok, maybe if I place the window.x = 1;
inside an IIFE, it should run even faster (even if just slightly), right? WRONG again.
Ok, maybe it's Firefox; let's try Chrome instead (V8 is the benchmark for JS speed, yea?) It should beat Firefox for simple stuff like accessing a global variable directly, right? WRONG yet again.
So I set out to find out exactly which method of access is fastest, in each of the two browsers. So let's say we start with one line of code: var x = 0;
. After x
has been declared (and happily attached to window
), which of these methods of access would be fastest, and why?
Directly in global scope
x = x + 1;
Directly in global scope, but prefixed with window
window.x = window.x + 1;
Inside a function, unqualified
function accessUnqualified() { x = x + 1; }
Inside a function, with window
prefix
function accessWindowPrefix() { window.x = window.x + 1; }
Inside a function, cache window as variable, prefixed access (simulate local param of an IIFE).
function accessCacheWindow() { var global = window; global.x = global.x + 1; }
Inside an IIFE (window as param), prefixed access.
(function(global){ global.x = global.x + 1; })(window);
Inside an IIFE (window as param), unqualified access.
(function(global){ x = x + 1; })(window);
Please assume browser context, i.e. window
is the global variable.
I wrote a quick time test to loop the increment operation a million times, and was surprised by the results. What I found:
Firefox Chrome ------- ------ 1. Direct access 848ms 1757ms 2. Direct window.x 2352ms 2377ms 3. in function, x 338ms 3ms 4. in function, window.x 1752ms 835ms 5. simulate IIFE global.x 786ms 10ms 6. IIFE, global.x 791ms 11ms 7. IIFE, x 331ms 655ms
I repeated the test a few times, and the numbers appear to be indicative. But they are confusing to me, as they seem to suggest:
window
is much slower (#2 vs #1, #4 vs #3). But WHY?I understand there are some who think such tests are pointless for performance tuning, and that may well be true. But please, for the sake of knowledge, just humor me and help improve my understanding of these simple concepts like variable access and scope chain.
If you have read this far, thank you for your patience. Apologies for the long post, and for possibly lumping multiple questions into one - I think they are all somewhat related.
Edit: Sharing my benchmark code, as requested.
var x, startTime, endTime, time; // Test #1: x x = 0; startTime = Date.now(); for (var i=0; i<1000000; i++) { x = x + 1; } endTime = Date.now(); time = endTime - startTime; console.log('access x directly - Completed in ' + time + 'ms'); // Test #2: window.x x = 0; startTime = Date.now(); for (var i=0; i<1000000; i++) { window.x = window.x + 1; } endTime = Date.now(); time = endTime - startTime; console.log('access window.x - Completed in ' + time + 'ms'); // Test #3: inside function, x x =0; startTime = Date.now(); accessUnqualified(); endTime = Date.now(); time = endTime - startTime; console.log('accessUnqualified() - Completed in ' + time + 'ms'); // Test #4: inside function, window.x x =0; startTime = Date.now(); accessWindowPrefix(); endTime = Date.now(); time = endTime - startTime; console.log('accessWindowPrefix()- Completed in ' + time + 'ms'); // Test #5: function cache window (simulte IIFE), global.x x =0; startTime = Date.now(); accessCacheWindow(); endTime = Date.now(); time = endTime - startTime; console.log('accessCacheWindow() - Completed in ' + time + 'ms'); // Test #6: IIFE, window.x x = 0; startTime = Date.now(); (function(window){ for (var i=0; i<1000000; i++) { window.x = window.x+1; } })(window); endTime = Date.now(); time = endTime - startTime; console.log('access IIFE window - Completed in ' + time + 'ms'); // Test #7: IIFE x x = 0; startTime = Date.now(); (function(global){ for (var i=0; i<1000000; i++) { x = x+1; } })(window); endTime = Date.now(); time = endTime - startTime; console.log('access IIFE x - Completed in ' + time + 'ms'); function accessUnqualified() { for (var i=0; i<1000000; i++) { x = x+1; } } function accessWindowPrefix() { for (var i=0; i<1000000; i++) { window.x = window.x+1; } } function accessCacheWindow() { var global = window; for (var i=0; i<1000000; i++) { global.x = global.x+1; } }
Global variables are really slow, in addition to all the other reasons not to use them.
i think this may be a reason: Since Global variables are stored in heap memory,your code needs to access heap memory each time. May be because of above reason code runs slow. global variables are stored in DATA segment of STACK and not in heap.
Yes, it is almost certainly slightly slower. Most of the time it will however not matter and the cost will be outweighted by the "logic and style" benefit. Technically, a function-local static variable is the same as a global variable.
Short answer - No, good programmers make code go faster by knowing and using the appropriate tools for the job, and then optimizing in a methodical way where their code does not meet their requirements.
Javascript is terrible for optimization because of eval
(that can access the local frame!).
If however the compilers are smart enough to detect that eval
plays no role then things can get a lot faster.
If you only have local variables, captured variables and global variables and if you can assume no messing up with eval
is done then, in theory:
The reason is that if x
when looked up results in a local or in a global then it will always be a local or a global and thus it could be accessed directly say with mov rax, [rbp+0x12]
(when a local) or mov rax, [rip+0x12345678]
when a global. No lookup whatsoever.
For captured variables things are slightly more complex because of lifetime issues. On a very common implementation (with captured variables wrapped up in cells and cell address copied when creating closures) this will require two extra indirection steps... i.e. for example
mov rax, [rbp] ; Load closure data address in rax mov rax, [rax+0x12] ; Load cell address in rax mov rax, [rax] ; Load actual value of captured var in rax
Once again no "lookup" needed at runtime.
All this means that the timing you are observing is a consequence of other factors. For the mere variable access the difference between a local, a global and a captured variable are very tiny compared to other issues like caching or implementation details (e.g. how the garbage collector is implemented; a moving one for example would require an extra indirection for globals).
Of course accessing a global using the window
object is another matter... and I'm not very surprised it takes longer (window
is required to be also a regular object).
One thing to note is that testing micro-optimizations is no longer easy to do because the JS engine's JIT compiler will optimize code. Some of your tests that have extremely small times are probably due to the compiler removing "unused" code and unrolling loops.
So there's really two things to worry about "scope chain lookup" and code that impedes the JIT compiler's ability to compile or simplify the code. (The latter is very complex so you'd be best to read up on a few tips and leave it at that.)
The issue with scope chain is that when the JS engine encounters a variable like x
, it needs to determine whether that is in:
The "scope chain" is essentially a linked list of these scopes. Looking up x
requires first determining if it is a local variable. If not, walk up any closures and look for it in each. If not in any closure, then look in the global context.
In the following code example, console.log(a);
first tries to resolve a
in the local scope within innerFunc(). It doesn't find a local variable a
so it looks in its enclosing closure and also doesn't find a variable a
. (If there were additional nested callbacks causing more closures, it would have to inspect each of them) After not finding a
in any closure, it finally looks in the global scope and does find it there.
var a = 1; // global scope (function myIife(window) { var b = 2; // scope in myIife and closure due to reference within innerFunc function innerFunc() { var c = 3; console.log(a); console.log(b); console.log(c); } // invoke innerFunc innerFunc(); })(window);
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