繁体   English   中英

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

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

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;之间没有区别x = 1; window.x = 1; 在全球范围内。 因此,期望他们表现出色是合乎逻辑的,对吗? 错误。 我进行了一些测试,发现访问时间存在显着差异。

好吧,也许如果我把window.x = 1; 在IIFE中,它应该运行得更快(即使只是一点点),对吗? 又错了。

好的,也许是Firefox; 让我们尝试使用Chrome(V8是JS速度的基准,是吗?),它应该在Firefox方面胜于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); 

请假定浏览器上下文,即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)。 为什么
  • 为什么在两个浏览器中#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; } } 

由于eval (可以访问本地框架!),因此Javascript难以优化。

但是,如果编译器足够聪明,可以检测到eval那么事情就会变得更快。

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

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

原因是,如果在查找时x导致局部或全局,则x始终是局部或全局,因此可以直接使用mov rax, [rbp+0x12] (当为局部)或mov rax, [rip+0x12345678]全局时为mov rax, [rip+0x12345678] 没有任何查找。

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

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对象访问全局对象是另一回事...而且我并不感到惊讶,因为它需要更长的时间( window也必须是常规对象)。

当我在Chrome中运行您的代码段时,除了直接访问window.x之外,其他所有方法都需要花费几毫秒的window.x 毫不奇怪,使用对象属性比使用变量要慢。 因此,唯一要回答的问题是为什么window.xx慢,甚至比其他任何东西都慢。

这使我想到了x = 1;前提x = 1; window.x = 1;相同window.x = 1; 很抱歉告诉您这是错误的。 FWIW window不直接是全局对象,它既是其属性,又是对其的引用。 尝试window.window.window.window ...

环境记录

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

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

全局范围使用对象环境记录。 这意味着该范围内的每个变量也是对象的属性,在这种情况下为全局对象。

此相反,它也可以工作:可以通过具有相同名称的标识符访问该对象的每个属性。 但这并不意味着您正在处理一个变量。 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的问题。 通常,访问环境记录对象的速度很慢,而在这种特定情况下,其他浏览器仅会进行更好的优化。

需要注意的一件事是,测试微优化不再容易,因为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);

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

我观察到,在一次通过该引用访问的循环中,一次又一次地存储对window引用的情况下,情况更快。 window参与左侧(LHS)查找的情况下,循环中的每次迭代都慢得多。

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

我对测试进行了一些修改,得到了以下结果。 window.x移至window.obj.x并获得相同的结果。 但是,当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