Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is accessing a variable using window.variable slower?

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?

  1. Directly in global scope

    x = x + 1; 
  2. Directly in global scope, but prefixed with window

    window.x = window.x + 1; 
  3. Inside a function, unqualified

    function accessUnqualified() {     x = x + 1; } 
  4. Inside a function, with window prefix

    function accessWindowPrefix() {     window.x = window.x + 1; } 
  5. 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; } 
  6. Inside an IIFE (window as param), prefixed access.

     (function(global){      global.x = global.x + 1;  })(window); 
  7. 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:

  • prefixing with window is much slower (#2 vs #1, #4 vs #3). But WHY?
  • accessing a global in a function (supposedly extra scope lookup) is faster (#3 vs #1). WHY??
  • Why are the #5,#6,#7 results so different across the two browsers?

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;    }  }
like image 457
light Avatar asked Jun 26 '15 08:06

light


People also ask

Do global variables slow down program?

Global variables are really slow, in addition to all the other reasons not to use them.

Why are global variables slow?

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.

Are static variables slow C++?

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.

Do global variables make programs run faster?

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.


2 Answers

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:

  • A local variable access is just a direct access in memory with an offset from the local frame
  • A global variable access is just a direct access in memory
  • A captured variable access requires a double indirection

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

like image 159
6502 Avatar answered Oct 05 '22 16:10

6502


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:

  • local scope
  • closure scopes (such as that created by IIFE)
  • global scope

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); 
like image 44
Ed Ballot Avatar answered Oct 05 '22 17:10

Ed Ballot