奇怪的 JavaScript 性能取决于变量范围

Curious JavaScript performance dependent on variable scope

本文关键字:取决于 变量 范围 性能 JavaScript      更新时间:2023-09-26

在测试一个 JavaScript 项目的性能时,我注意到一个非常奇特的行为 - JavaScript 成员访问性能似乎受到它们所在范围的严重影响。我写了一些性能测试,结果相差好几个数量级

我在 Windows 10 64 位上测试了以下浏览器:

  • 谷歌浏览器,版本49.0.2623.75 m - 使用V8 JavaScript引擎
  • Mozilla Firefox 版本 44.0.2 - 使用 SpiderMonkey JavaScript 引擎
  • Microsoft Edge,版本25.10586 - 使用Chakra JavaScript引擎

以下是我运行的最相关的测试及其各自的结果:

// Code running on global scope, accessing a variable on global scope
// Google Chrome:   63000 ms.
// Mozilla Firefox: 57000 ms.
// Microsoft Edge:  21000 ms.
var begin = performance.now();
var i;
for(i = 0; i < 100000000; i++) { }
var end = performance.now();
console.log(end - begin + " ms.");

// Code running on local scope, accessing a variable on global scope
// Google Chrome:   61500 ms.
// Mozilla Firefox: 47500 ms.
// Microsoft Edge:  22000 ms.
var begin = performance.now();
var i;
(function() {
    for(i = 0; i < 100000000; i++) { }
})();
var end = performance.now();
console.log(end - begin + " ms.");
// Code running on local scope, accessing a variable on local scope
// Google Chrome:   50 ms.
// Mozilla Firefox: 28 ms.
// Microsoft Edge:  245 ms.
var begin = performance.now();
(function() {
    var i;
    for(i = 0; i < 100000000; i++) { }
})();
var end = performance.now();
console.log(end - begin + " ms.");

在本地和全局范围内运行的代码之间的差异在误差范围内,尽管Firefox似乎确实在本地范围内获得了相当一致的20%的性能提升

最大的惊喜是在本地范围内访问变量,在Chrome和Firefox上快1200到1600倍,在Edge上快90倍。

为什么在三个不同的浏览器/JavaScript引擎上会这样?

通过在 .js Node 下运行代码并在node命令行上传递 --print_opt_code 开关,您可以查看 V8 JavaScript 引擎生成的实际机器代码(与 Chrome 中使用的相同)。例如,如果将代码放在名为 test.js 的文件中,则可以运行:

node --print_opt_code test.js

在上一个示例中,V8 能够将 i 变量放入RAX寄存器中,而不是将其保存在内存中。这是上述命令打印的代码的内部循环,带有一些额外的注释。(之前和之后都有额外的代码;这只是内部循环本身。

 84  33c0           xorl rax,rax                 ; i = 0
 86  3d00e1f505     cmp rax, 0x5f5e100           ; compare i with 100000000
 91  0f8d12000000   jge 115                      ; exit loop if i >= 100000000
 97  493ba548080000 REX.W cmpq rsp, [r13+0x848]  ; check for bailout?
104  0f8246000000   jc 180                       ; bailout if necessary
110  83c001         addl rax, 0x1                ; i++
113  ebe3           jmp 86                       ; back to top of loop
115  ...

请注意,0x5f5e100 100000000以十六进制表示。

如您所见,这是一个相当紧密的循环,只有几条指令。大多数代码是 JavaScript 代码的直接翻译;我唯一有点不确定的是地址 97 和 104 上的两条指令,如果满足某个条件,它们就会退出循环。

如果你用其他版本的JavaScript代码运行类似的测试,你会看到更长的指令序列。请注意,Node 将所有代码包装在它提供的包装函数中。因此,如果你想做一些类似于你的第一个示例的事情,你可能需要像这样编写循环以获得类似的效果:

for(global.i = 0; global.i < 100000000; global.i++) { }

也许有一种方法可以告诉 Node 不要使用其外部包装器函数;我对 Node 不够熟悉,无法就此提供建议。

全局命名空间中的变量的性能要差得多,但不完全是因为@Freddie提到的原因。全局命名空间中的变量可能会被外部内容更改,从而强制解释器每次通过循环重新加载值。使用局部变量,JIT 引擎可以将循环优化为每次迭代几个机器周期,这似乎是这里正在发生的事情。

在技术 1 - http://www.webreference.com/programming/javascript/jkm3/index.html 下查看此内容

全局变量的性能很慢,因为它们位于高度填充的命名空间中。它们不仅与许多其他用户定义的量和 JavaScript 变量一起存储,浏览器还必须区分全局变量和当前上下文中对象的属性。当前上下文中的许多对象都可以由变量名称引用,而不是作为对象属性引用,例如 alert() 与 window.alert() 同义。缺点是这种便利性会减慢使用全局变量的代码。