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)之后,这些访问方法中哪一个会最快,为什么?
直接在全球范围内
x = x + 1;
Run Code Online (Sandbox Code Playgroud)直接在全球范围内,但前缀为 window
window.x = window.x + 1;
Run Code Online (Sandbox Code Playgroud)功能内部,不合格
function accessUnqualified() {
x = x + 1;
}
Run Code Online (Sandbox Code Playgroud)在函数内部,带window前缀
function accessWindowPrefix() {
window.x = window.x + 1;
}
Run Code Online (Sandbox Code Playgroud)在函数内部,缓存窗口作为变量,前缀访问(模拟IIFE的局部参数).
function accessCacheWindow() {
var global = window;
global.x = global.x + 1;
}
Run Code Online (Sandbox Code Playgroud)在IIFE(窗口作为参数)内部,带有前缀访问.
(function(global){
global.x = global.x + 1;
})(window);
Run Code Online (Sandbox Code Playgroud)在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).但为什么呢?我知道有些人认为这样的测试对于性能调优毫无意义,这可能是真的.但是,为了知识,请请幽默我,并帮助提高我对变量访问和范围链等这些简单概念的理解.
如果您已经阅读了这篇文章,请感谢您的耐心等待.为长篇文章道歉,并可能将多个问题合并为一个 - 我认为它们都有些相关.
编辑:按要求共享我的基准代码.
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需要也是常规对象).
当我在Chrome中运行您的代码段时,除了直接访问外,每个选项都需要几毫秒window.x.毫无疑问,使用对象属性比使用变量要慢.所以唯一要回答的问题是为什么比其他任何东西window.x慢x,甚至更慢.
这导致我的前提x = 1;是相同的window.x = 1;.我很遗憾地告诉你这是错的.FWIW window不是直接全局对象,它既是它的属性,也是对它的引用.尝试window.window.window.window ...
每个变量都必须在环境记录中 "注册",并且有两种主要类型:声明式和对象式.
函数范围使用声明性环境记录.
全局范围使用对象环境记录.这意味着此范围中的每个变量也是对象的属性,在本例中为全局对象.
它还样的一轮工作的另一种方式:该对象的每个属性都通过相同名称的标识进行访问.但这并不意味着你正在处理一个变量.该with语句是使用对象环境记录的另一个示例.
创建变量与向对象添加属性不同,即使该对象是环境记录也是如此.尝试 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,需要确定它是否在:
"范围链"本质上是这些范围的链接列表.查找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)
恕我直言(遗憾的是我无法找到证明任何关于它的理论真或假的方法)这与window不仅是全局范围而且具有大量属性的本机对象有关.
我已经观察到window,在通过此引用访问的循环中,将引用存储一次并进一步存储的情况更快.并且window参与左侧(LHS)查找循环中每次迭代的情况要慢得多.
所有案例都有不同时间的问题仍然存在,但显然这是由于js引擎优化.对此的一个论点是不同的浏览器显示不同的时间比例.最奇怪的赢家#3可以通过以下假设来解释:由于流行的使用,这种情况得到了很好的优化.
我通过一些修改运行测试并得到以下结果.移动window.x到window.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)
| 归档时间: |
|
| 查看次数: |
2433 次 |
| 最近记录: |