简体   繁体   English

为什么使用window.variable访问变量的速度较慢?

[英]Why is accessing a variable using window.variable slower?

Multiple sources for JS performance tips encourage developers to reduce "scope chain lookup". JS性能提示的多种来源鼓励开发人员减少“范围链查找”。 For example, IIFEs are touted as having a bonus benefit of "reducing scope chain lookup" when you access global variables. 例如,当您访问全局变量时,IIFE被吹捧为具有“减少范围链查找”的额外好处。 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. 像许多其他人一样,我一直很高兴地使用IIFE,认为除了避免全局名称空间污染之外,与任何全局代码相比,性能都将得到提高。

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; 根据我对JS的了解, x = 1;之间没有区别x = 1; and window.x = 1; 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; 好吧,也许如果我把window.x = 1; inside an IIFE, it should run even faster (even if just slightly), right? 在IIFE中,它应该运行得更快(即使只是一点点),对吗? WRONG again. 又错了。

Ok, maybe it's Firefox; 好的,也许是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? 让我们尝试使用Chrome(V8是JS速度的基准,是吗?),它应该在Firefox方面胜于Firefox,例如直接访问全局变量,对吗? 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; 假设我们从一行代码开始: var x = 0; . After x has been declared (and happily attached to window ), which of these methods of access would be fastest, and why? 在声明了x (并愉快地将其附加到window )之后,这些访问方法中哪一种访问速度最快,为什么?

  1. Directly in global scope 直接在全球范围内

     x = x + 1; 
  2. Directly in global scope, but prefixed with window 直接在全局范围内,但以window为前缀

     window.x = window.x + 1; 
  3. Inside a function, unqualified 函数内部,不合格

     function accessUnqualified() { x = x + 1; } 
  4. Inside a function, with window prefix 在函数内部,带有window前缀

     function accessWindowPrefix() { window.x = window.x + 1; } 
  5. Inside a function, cache window as variable, prefixed access (simulate local param of an IIFE). 在函数内部,将缓存窗口作为变量进行前缀访问(模拟IIFE的本地参数)。

     function accessCacheWindow() { var global = window; global.x = global.x + 1; } 
  6. Inside an IIFE (window as param), prefixed access. 在IIFE(作为参数的窗口)内,有前缀访问。

      (function(global){ global.x = global.x + 1; })(window); 
  7. Inside an IIFE (window as param), unqualified access. 在IIFE(作为参数的窗口)中,没有资格的访问。

      (function(global){ x = x + 1; })(window); 

Please assume browser context, ie window is the global variable. 请假定浏览器上下文,即window是全局变量。

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). window前缀要慢得多(#2 vs#1,#4 vs#3)。 But WHY ? 但是为什么呢?
  • accessing a global in a function (supposedly extra scope lookup) is faster (#3 vs #1). 在函数中访问全局变量(可能是额外的作用域查找)更快(#3 vs#1)。 WHY ?? 为什么
  • Why are the #5,#6,#7 results so different across the two browsers? 为什么在两个浏览器中#5,#6,#7结果如此不同?

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!). 由于eval (可以访问本地框架!),因此Javascript难以优化。

If however the compilers are smart enough to detect that eval plays no role then things can get a lot faster. 但是,如果编译器足够聪明,可以检测到eval那么事情就会变得更快。

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: 如果只有局部变量,捕获的变量和全局变量,并且可以假设不对eval进行任何改动,那么理论上:

  • 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. 原因是,如果在查找时x导致局部或全局,则x始终是局部或全局,因此可以直接使用mov rax, [rbp+0x12] (当为局部)或mov rax, [rip+0x12345678]全局时为mov rax, [rip+0x12345678] 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). 当然,使用window对象访问全局对象是另一回事...而且我并不感到惊讶,因为它需要更长的时间( window也必须是常规对象)。

When I run your code snippet in Chrome every alternative takes a few milliseconds, except for directly accessing window.x . 当我在Chrome中运行您的代码段时,除了直接访问window.x之外,其他所有方法都需要花费几毫秒的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. 因此,唯一要回答的问题是为什么window.xx慢,甚至比其他任何东西都慢。

Which leads me to your premise that x = 1; 这使我想到了x = 1;前提x = 1; is the same as window.x = 1; window.x = 1;相同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. FWIW window不直接是全局对象,它既是其属性,又是对其的引用。 Try window.window.window.window ... 尝试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. with语句是使用对象环境记录的另一个示例。

The difference between x=1 and window.x = 1 x = 1和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. 在两种情况下均尝试使用Object.getOwnPropertyDescriptor(window, 'x') When x is a variable then the property x is not configurable . x是变量时,则属性x是不可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. 当我们仅看到window.x我们不知道它是变量还是属性。 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 . 编译器可以检查是否还有变量x但是检查可能比仅仅执行window.x = window.x + 1要花费更多。 And don't forget that window only exists in Browsers. 并且不要忘记该window仅存在于浏览器中。 JavaScript engines also work in other environments which may have a differently named property or even none at all. JavaScript引擎也可以在其他环境中工作,这些环境可能具有不同的名称属性,甚至根本没有属性。

Now why is window.x so much slower on Chrome? 现在,为什么window.x在Chrome上这么慢? Interestingly in Firefox it's not. 有趣的是,Firefox并非如此。 In my test runs FF is much faster and the performance of window.x is on par with every other object access. 在我的测试运行中,FF的速度要快得多,并且window.x的性能与其他所有对象访问都相当。 The same is true for Safari. Safari也是如此。 So it may be a Chrome issue. 因此,这可能是Chrome的问题。 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. 需要注意的一件事是,测试微优化不再容易,因为JS引擎的JIT编译器将优化代码。 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. 因此,实际上有两件事情需要担心“作用域链查找”和妨碍JIT编译器编译或简化代码的代码。 (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: 范围链的问题是,当JS引擎遇到类似x的变量时,它需要确定该变量是否在:

  • local scope 当地范围
  • closure scopes (such as that created by IIFE) 关闭范围(例如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. 查找x需要首先确定它是否是局部变量。 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); 在以下代码示例中, console.log(a); first tries to resolve a in the local scope within innerFunc(). 首先尝试在innerFunc()的本地范围内解析a It doesn't find a local variable a so it looks in its enclosing closure and also doesn't find a variable a . 它找不到局部变量a因此它在其封闭的闭包中查找,也找不到变量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. (如果还有其他嵌套的回调导致更多的关闭,则必须检查它们中的每个)在任何关闭中均未找到a之后,它最终会在全局范围内查找并在此处找到它。

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. 恕我直言(不幸的是,我找不到任何方法来证明它的正确与否),这与以下事实有关: window不仅是全局范围,而且还是具有大量属性的本机对象。

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. 我观察到,在一次通过该引用访问的循环中,一次又一次地存储对window引用的情况下,情况更快。 And cases where window is taking part in Left-hand Side (LHS) lookups each iteration in the loop are much slower. window参与左侧(LHS)查找的情况下,循环中的每次迭代都慢得多。

The question why all cases have different timings is still open, but obviously it is due to js engine optimizations. 为什么所有情况的时机都不一样的问题仍然存在,但这显然是由于js引擎优化所致。 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. 可以通过以下假设来解释最奇怪的赢家#3:由于这种用法得到了很好的优化。

I've run tests with some modifications and got following results. 我对测试进行了一些修改,得到了以下结果。 Moved window.x to window.obj.x and got same results. window.x移至window.obj.x并获得相同的结果。 However when x was in window.location.x ( location being also a big native object) the timings changed dramatically: 但是,当xwindow.location.xlocation也是一个大型本机对象)时,时间发生了巨大变化:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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