JS:在闭包的父级作用域中分配变量会影响性能。为什么?

3
在研究JavaScript中的闭包时,我想到了下面的小例子,但我并不真正理解发生了什么。
我希望通过在紧密循环的函数内使用var声明变量来玩弄垃圾收集器,以导致大量分配和释放。我试图通过将var声明放在闭包的父级作用域中来避免这种情况,并期望闭包函数会更快。无论这个想法有多糟糕,我都遇到了这个小问题。
var withClosure = function() {
    var a, b, c, d, e, f, g;
    return function () {
        a = 1;
        b = 2;
        c = 3;
        d = 4;
        e = 5;
        f = 6;
        g = 7;
    };
}();

var withoutClosure = function () {
    var a = 1;
    var b = 2;
    var c = 3;
    var d = 4;
    var e = 5;
    var f = 6;
    var g = 7;
};

console.time("without");
for (var i = 0; i < 1000000000; i++) {
    withoutClosure();
}
console.timeEnd("without");


console.time("withcsr");
for (var i = 0; i < 1000000000; i++) {
    withClosure();
}
console.timeEnd("withcsr");


/*
Output on my machine:
    without: 1098.329ms
    withcsr: 8878.812ms

Tested with node v.6.0.0 and Chrome 50.0.2661.102 (64-bit)
*/

在父级作用域中分配变量使闭包在我的机器上运行速度比正常版本慢8倍。使用更多变量会使情况变得更糟。如果我只是读取变量而不是分配给它们,问题就不存在了。

这是什么原因导致的?有人能解释一下吗?


1
也许JS引擎正在以不同的方式优化这些函数 - 非闭包函数可能会被优化为一个空函数,因为没有任何变量被读取。 - nnnnnn
在每个函数结束时返回变量的总和(因此读取值)不会改变它。 - unR
如果是汇编语言,可能更有意义,因为你可以使用CPU提供的许多寄存器来执行特定的功能。如果你在外部使用,比如另一个函数,推入、弹出堆栈变量可能会使程序绑定到内存速度。 - YOU
如果你担心内存分配成本和垃圾回收,可以使用一个巨大的类型化数组作为你的内存,并且进行所有的内存管理,因为这是https://en.wikipedia.org/wiki/Asm.js的标准。 - le_m
var不会在紧密循环内分配和释放变量,因为这些声明是被提升的。 - user663031
显示剩余2条评论
2个回答

3

没有闭包的例子中,任何一个良好的Javascript引擎都会意识到函数内的变量被初始化但在超出范围之前从未被读取,因此可以将其删除而不影响函数的输出。

有闭包的例子中,变量仍然处于作用域中,因此无法进行优化。

这个演讲详细解释了JIT Javascript编译器进行的一些优化:https://www.youtube.com/watch?v=65-RbBwZQdU


这已经在评论中提出过了。我还尝试了另一个例子,在其中读取、求和并返回值。https://jsfiddle.net/r1hh1tzc/,问题仍然存在。 - unR
返回值并没有什么区别,因为它将被优化为“返回28”。 - Nick Long
实际上,因为您没有对返回值执行任何操作,所以函数可能根本不会被执行。 - Nick Long
我想到了另一个例子来避免未使用和可预测的变量 https://jsfiddle.net/2830emgk/ ,但问题仍然存在。我仍然不确定根本原因。然而,您的答案和视频清楚地表明,我的基准测试尝试非常幼稚且相当无用,因为我可能无法隔离问题,JIT会干扰我的结果以及我进行的测试来形成我的前提。 - unR
确实,编写一个能够产生有效结果的微基准测试非常困难。 - Nick Long

1
这与符号表的工作方式有关。符号表将符号(例如变量名)映射到它们的值,就像一个关联数组。符号表与其他类型的关联数组(如哈希表)的区别在于,为了简单和效率,它们是分层的(每个符号表可能有一个父级)。符号解析从当前作用域的符号表开始,并沿着其路径向上到根作用域,使用第一个匹配符号的值或未定义的值(如果没有符号与根匹配)。
当变量在当前作用域中声明并引用时,只会命中一个符号表:当前作用域的符号表。然而,当变量在父作用域中声明并在当前作用域中引用时,则需要两次命中符号表:一次是当前作用域的符号表未命中,另一次是父作用域的符号表命中。因此,从父作用域引用变量大约比从当前作用域引用变量慢两倍。
这篇文章对符号表的解释做得不错:http://www.tutorialspoint.com/compiler_design/compiler_design_symbol_table.htm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接