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

lig*_*ght 36 javascript performance firefox google-chrome

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

我们今天的期望:

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

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

var x = 0;
(function(window) {
    // accessing window.x here should be faster
})(window);
Run Code Online (Sandbox Code Playgroud)

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

好吧,也许如果我将window.x = 1;内部放置在IIFE中,它应该运行得更快(即使只是略微),对吧?又错了.

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

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

  1. 直接在全球范围内

    x = x + 1;
    
    Run Code Online (Sandbox Code Playgroud)
  2. 直接在全球范围内,但前缀为 window

    window.x = window.x + 1;
    
    Run Code Online (Sandbox Code Playgroud)
  3. 功能内部,不合格

    function accessUnqualified() {
        x = x + 1;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  4. 在函数内部,带window前缀

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

    function accessCacheWindow() {
        var global = window;
        global.x = global.x + 1;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  6. 在IIFE(窗口作为参数)内部,带有前缀访问.

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

     (function(global){
         x = x + 1;
     })(window);
    
    Run Code Online (Sandbox Code Playgroud)

请假设浏览器上下文,即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
Run Code Online (Sandbox Code Playgroud)

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

  • 加前缀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;
  }
}
Run Code Online (Sandbox Code Playgroud)

650*_*502 13

Javascript因为eval(可以访问本地帧!)而非常糟糕.

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

如果您只有局部变量,捕获的变量和全局变量,并且如果您可以假设没有搞乱,eval那么理论上:

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

原因在于,如果x在本地或全局中查找结果,那么它将始终是本地或全局,因此可以直接访问mov rax, [rbp+0x12](当为本地时)或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
Run Code Online (Sandbox Code Playgroud)

再次在运行时不需要"查找".

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

当然,使用该window对象访问全局是另一回事......我不会感到意外需要更长的时间(window需要也是常规对象).


a b*_*ver 8

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

这导致我的前提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问题.或者访问环境记录对象通常很慢,而其他浏览器在这种特定情况下只是更好地优化.


Ed *_*lot 7

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

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

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

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

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

在下面的代码示例中,console.log(a);首先尝试a在innerFunc()中的本地范围内进行解析.它没有找到局部变量,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);
Run Code Online (Sandbox Code Playgroud)


Kir*_*tin 6

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

我已经观察到window,在通过此引用访问的循环中,将引用存储一次并进一步存储的情况更快.并且window参与左侧(LHS)查找循环中每次迭代的情况要慢得多.

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

我通过一些修改运行测试并得到以下结果.移动window.xwindow.obj.x并得到相同的结果.然而,当x进入window.location.x(location也是一个大本机对象)时,时间发生了巨大变化:

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
Run Code Online (Sandbox Code Playgroud)