Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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;
  }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
107 views
Welcome To Ask or Share your Answers For Others

1 Answer

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share

548k questions

547k answers

4 comments

86.3k users

...