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

时间:2015-06-26 08:03:59

标签: javascript performance google-chrome firefox

JS性能提示的多个来源鼓励开发人员减少"范围链查找"。例如,IIFE被吹捧为具有奖励的好处"减少范围链查找"当您访问全局变量时。这听起来很合乎逻辑,甚至可能被视为理所当然,所以我并没有质疑智慧。像许多其他人一样,我一直很高兴地使用IIFE认为除了避免全局命名空间污染之外,还会比任何全球代码都提升性能。

我们今天的期望:

(function($, window, undefined) {
    // apparently, variable access here is faster than outside the IIFE
})(jQuery, window);

简化/扩展到一般情况,人们会期望:

var x = 0;
(function(window) {
    // accessing window.x here should be faster
})(window);

根据我对JS的理解,全局范围内x = 1;window.x = 1;之间没有区别。因此,期望它们具有同等性能是合乎逻辑的,对吧? 错误。我进行了一些测试,发现访问时间存在显着差异。

好吧,也许如果我将window.x = 1;放在IIFE中,它应该运行得更快(即使只是略微),对吧? 再次出现错误。

好吧,也许是它的Firefox;让我们尝试使用Chrome(V8是JS速度的基准,是吗?)它应该击败Firefox以获取直接访问全局变量等简单的东西,对吗? 又错了

因此,我开始在两个浏览器的每个浏览器中确切地找出哪种访问方法最快。因此,我们假设我们从一行代码开始:var x = 0;。在声明x之后(并愉快地附加到window),这些访问方法中的哪一种最快,为什么?

  1. 直接在全球范围内

    x = x + 1;
    
  2. 直接在全球范围内,但前缀为window

    window.x = window.x + 1;
    
  3. 在函数内部,不合格

    function accessUnqualified() {
        x = x + 1;
    }
    
  4. 在函数内部,window前缀

    function accessWindowPrefix() {
        window.x = window.x + 1;
    }
    
  5. 在函数内部,缓存窗口为变量,前缀访问(模拟IIFE的局部参数)。

    function accessCacheWindow() {
        var global = window;
        global.x = global.x + 1;
    }
    
  6. 在IIFE(窗口作为参数)内部,带有前缀的访问权限。

     (function(global){
         global.x = global.x + 1;
     })(window);
    
  7. 在IIFE内部(窗口作为参数),不合格的访问。

     (function(global){
         x = x + 1;
     })(window);
    
  8. 请假设浏览器上下文,即window是全局变量。

    我写了一个快速时间测试来循环增量操作一百万次,并对结果感到惊讶。我找到了什么:

                                 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
    

    我重复了几次测试,数字似乎是指示性的。但它们让我感到困惑,因为他们似乎暗示:

    • 前缀window要慢得多(#2 vs#1,#4 vs#3)。但为什么
    • 访问函数中的全局(假设额外的范围查找)更快(#3 vs#1)。的 WHY ...
    • 为什么#5,#6,#7在两个浏览器中的结果如此不同?

    据我所知,有些人认为此类测试对于性能调优毫无意义,这可能是真的。但是,为了知识,请为此幽默,帮助提高我对变量访问和范围链这些简单概念的理解。

    如果您已经阅读了这篇文章,请感谢您的耐心等待。为长篇文章道歉,并可能将多个问题合并为一个 - 我认为它们都有些相关。

    修改:根据要求共享我的基准代码。

    
    
    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;
      }
    }
    &#13;
    &#13;
    &#13;

4 个答案:

答案 0 :(得分:12)

由于$ mvn install:install-file -Dfile=/path/to/myutils-1.0-test.jar -DgroupId=my.group -DartifactId=myutils -Dversion=1.0 -Dclassifier=test (可以访问本地框架!),Javascript很难进行优化。

但是,如果编译器足够聪明,可以检测到eval没有任何作用,那么事情就会快得多。

如果您只有局部变量,捕获的变量和全局变量,并且如果您认为没有弄乱eval,那么理论上:

  • 本地变量访问只是内存中的直接访问,具有与本地帧的偏移
  • 全局变量访问只是内存中的直接访问
  • 捕获的变量访问需要双重间接

原因是,如果eval查找结果是本地或全局,那么它将始终是本地或全局,因此可以直接访问x(当一个本地的)或全球的mov rax, [rbp+0x12]。没有任何查询。

对于捕获的变量,由于生命周期问题,事情稍微复杂一些。在一个非常常见的实现(捕获的变量包含在单元格和创建闭包时复制的单元格)中,这将需要两个额外的间接步骤...即例如

mov rax, [rip+0x12345678]

再次没有&#34;查找&#34;在运行时需要。

所有这些意味着您观察的时间是其他因素的结果。对于纯粹的变量访问,与其他问题(如缓存或实现细节)相比,本地,全局和捕获变量之间的差异非常小(例如,如何实现垃圾收集器;例如,移动的变量需要额外的全局变量间接)。

当然,使用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 对象访问全局是另一回事......我不会感到意外需要更长的时间(window也需要成为常规对象)。

答案 1 :(得分:8)

当我在Chrome中运行您的代码段时,除了直接访问window.x之外,每个选项都需要几毫秒。毫无疑问,使用对象属性比使用变量要慢。所以唯一要回答的问题是为什么window.xx慢,甚至比其他任何东西慢。

这使我了解x = 1;window.x = 1;相同的前提。我很遗憾地告诉你这是错的。 FWIW window不是直接全局对象,它既是它的属性,也是对它的引用。试试window.window.window.window ...

环境记录

每个变量都必须在environment record中“注册”,并且有两种主要类型:声明和对象。

功能范围使用声明性环境记录。

全局范围使用对象环境记录。这意味着此范围中的每个变量也是对象的属性,在本例中为全局对象。

种类反之亦然:该对象的每个属性都可以通过具有相同名称的标识符访问。但这并不意味着你正在处理一个变量。 with语句是使用对象环境记录的另一个示例。

x = 1和window.x = 1

之间的差异

创建变量与向对象添加属性不同,即使该对象是环境记录也是如此。在这两种情况下都请尝试Object.getOwnPropertyDescriptor(window, 'x')。如果x是变量,则属性x不是configurable。一个结果是你无法删除它。

当我们只看到window.x时,我们不知道它是变量还是属性。因此,如果没有进一步的了解,我们根本无法将其视为变量。变量存在于作用域中,在堆栈上,您可以命名。编译器可以检查是否还有变量x,但该检查可能比仅仅window.x = window.x + 1花费更多。不要忘记window仅存在于浏览器中。 JavaScript引擎也可以在其他环境中工作,这些环境可能具有不同的命名属性,甚至根本没有。

现在为什么window.x在Chrome上这么慢?有趣的是在Firefox中并非如此。在我的测试运行中,FF速度更快,window.x的性能与其他所有对象访问相同。 Safari也是如此。所以它可能是Chrome问题。或者访问环境记录对象通常很慢,而其他浏览器在这种特定情况下只是更好地进行优化。

答案 2 :(得分:7)

需要注意的一点是,测试微优化不再容易,因为JS引擎的JIT编译器将优化代码。一些极短时间的测试可能是由于编译器删除了“未使用”的代码并展开循环。

因此,有两件事需要担心“范围链查找”和阻碍JIT编译器编译或简化代码的能力的代码。 (后者非常复杂,所以你最好阅读一些技巧并留待它。)

范围链的问题是当JS引擎遇到类似x的变量时,需要确定它是否在:

  • 本地范围
  • 关闭范围(例如由IIFE创建的)
  • 全球范围

“范围链”本质上是这些范围的链接列表。查找x需要首先确定它是否是局部变量。如果没有,走向任何封闭,并在每个封闭中寻找它。如果不在任何闭包中,那么请查看全局上下文。

在下面的代码示例中,console.log(a);首先尝试在innerFunc()中的本地范围内解析a。它没有找到局部变量a,因此它在封闭的闭包中查找,也找不到变量a。 (如果有额外的嵌套回调导致更多的闭包,则必须检查每个闭包)在没有在任何闭包中找到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);

答案 3 :(得分:6)

恕我直言(遗憾的是我无法找到证明任何关于它的理论的方法是真是假)这与window不仅是全局范围而且是具有大量的本地对象的事实有关。属性。

我已经观察到案例更快,其中对window的引用存储一次,并且在通过此引用访问的循环中进一步存储。并且window在循环中每次迭代中参与Left-hand Side (LHS) lookups的情况要慢得多。

为什么所有案例都有不同的时间的问题仍然存在,但显然这是由于js引擎优化。对此的一个论点是不同的浏览器显示不同的时间比例。最奇怪的赢家#3可以通过以下假设来解释:由于流行的使用,这种情况得到了很好的优化。

我通过一些修改运行测试并得到以下结果。已将window.x移至window.obj.x并获得相同的结果。但是,如果x位于window.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