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
),这些访问方法中的哪一种最快,为什么?
直接在全球范围内
x = x + 1;
直接在全球范围内,但前缀为window
window.x = window.x + 1;
在函数内部,不合格
function accessUnqualified() {
x = x + 1;
}
在函数内部,window
前缀
function accessWindowPrefix() {
window.x = window.x + 1;
}
在函数内部,缓存窗口为变量,前缀访问(模拟IIFE的局部参数)。
function accessCacheWindow() {
var global = window;
global.x = global.x + 1;
}
在IIFE(窗口作为参数)内部,带有前缀的访问权限。
(function(global){
global.x = global.x + 1;
})(window);
在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)。但为什么?据我所知,有些人认为此类测试对于性能调优毫无意义,这可能是真的。但是,为了知识,请为此幽默,帮助提高我对变量访问和范围链这些简单概念的理解。
如果您已经阅读了这篇文章,请感谢您的耐心等待。为长篇文章道歉,并可能将多个问题合并为一个 - 我认为它们都有些相关。
修改:根据要求共享我的基准代码。
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;
答案 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.x
比x
慢,甚至比其他任何东西慢。
这使我了解x = 1;
与window.x = 1;
相同的前提。我很遗憾地告诉你这是错的。 FWIW window
不是直接全局对象,它既是它的属性,也是对它的引用。试试window.window.window.window ...
每个变量都必须在environment record中“注册”,并且有两种主要类型:声明和对象。
功能范围使用声明性环境记录。
全局范围使用对象环境记录。这意味着此范围中的每个变量也是对象的属性,在本例中为全局对象。
种类反之亦然:该对象的每个属性都可以通过具有相同名称的标识符访问。但这并不意味着你正在处理一个变量。 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问题。或者访问环境记录对象通常很慢,而其他浏览器在这种特定情况下只是更好地进行优化。
答案 2 :(得分:7)
需要注意的一点是,测试微优化不再容易,因为JS引擎的JIT编译器将优化代码。一些极短时间的测试可能是由于编译器删除了“未使用”的代码并展开循环。
因此,有两件事需要担心“范围链查找”和阻碍JIT编译器编译或简化代码的能力的代码。 (后者非常复杂,所以你最好阅读一些技巧并留待它。)
范围链的问题是当JS引擎遇到类似x
的变量时,需要确定它是否在:
“范围链”本质上是这些范围的链接列表。查找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.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