简体   繁体   中英

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, ie 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; } } 

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 (a captured variables wrapped up in cells and cells copied when creating closures) this will require two extra indirection steps... ie 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 (eg 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).

When I run your code snippet in Chrome every alternative takes a few milliseconds, except for directly accessing window.x . And unsurprisingly using object properties is slower than using variables. So the only question to answer is why is window.x is slower than x and even slower than anything else.

Which leads me to your premise that x = 1; is the same as window.x = 1; . And I'm sorry to tell you that's wrong. FWIW window is not directly the global object, it's both a property of it and a reference to it. Try window.window.window.window ...

Environment records

Every variable has to be "registered" in an environment record and there are two primary kinds: Declarative and object.

Function scope uses a declarative environment record.

Global scope uses an object environment record. And that means that every variable in this scope is also a property of an object, in this case the global object.

It also kind of works the other way round: Every property of that object can be accessed through an identifier with the same name. But that doesn't mean you are dealing with a variable. The with statement is another example of using an object environment record.

The difference between x=1 and window.x = 1

Creating a variable is not the same as adding a property to an object, even if that object is an environment record. Try Object.getOwnPropertyDescriptor(window, 'x') in both cases. When x is a variable then the property x is not configurable . One consequence is that you cannot delete it.

When we only see window.x we don't know if it's a variable or a property. So without further knowledge, we simply cannot treat it as a variable. Variables live in the scope, on a stack, you name it. A compiler could check if there's also a variable x but that check would probably cost more than simply doing window.x = window.x + 1 . And don't forget that window only exists in Browsers. JavaScript engines also work in other environments which may have a differently named property or even none at all.

Now why is window.x so much slower on Chrome? Interestingly in Firefox it's not. In my test runs FF is much faster and the performance of window.x is on par with every other object access. The same is true for Safari. So it may be a Chrome issue. Or accessing an environment record object is slow in general and the other browsers simply optimize better in this specific case.

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

IMHO (unfortunately I can't find a way to prove any theory about it true or false) this is connected with the fact that window is not only the global scope but also a native object with huge amount of properties.

I've made an observation that cases are faster where reference to window is stored once and further in the loop accessed via this reference. And cases where window is taking part in Left-hand Side (LHS) lookups each iteration in the loop are much slower.

The question why all cases have different timings is still open, but obviously it is due to js engine optimizations. One argument for this is different browsers show different time proportions. The weirdest winner #3 can be explained by an assumption that due to popular usage this scenario was well optimized.

I've run tests with some modifications and got following results. Moved window.x to window.obj.x and got same results. However when x was in window.location.x ( location being also a big native object) the timings changed dramatically:

1. access x directly    - Completed in 4278ms
2. access window.x     - Completed in 6792ms
3. accessUnqualified() - Completed in 4109ms
4. accessWindowPrefix()- Completed in 6563ms
5. accessCacheWindow() - Completed in 4489ms
6. access IIFE window  - Completed in 4326ms
7. access IIFE x      - Completed in 4137ms

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM