在函数范围内声明函数与在函数范围外声明函数的性能

Performance of declaring function withing a scope of a function vs outside of it

我在思考是否在函数作用域内或作用域外声明函数对性能的影响。

为此,我使用 jsperf 创建了一个测试,结果对我来说很有趣,我希望有人能解释这里发生了什么。

测试:https://jsperf.com/saarman-fn-scope/1

Google Chrome 结果:

Microsoft Edge 结果:

Firefox 结果:

我相信 Chrome 和 Firefox 案例中发生的事情是内联 mathAdd 函数。 因为它是一个在函数内创建和调用的简单函数,没有副作用,所以编译器将调用站点替换为函数的内部代码。

生成的代码如下所示:

const run = count => {
  return 10 + count
}

for (let count = 0; count < 1000; count++) {
  run(count)
}

这会在运行时保存一个函数声明,并在调用该函数时保存一个新的堆栈帧。

我怀疑在函数分离的情况下,编译器无法保证内联是安全的,最终你每次都要付出新的栈帧和函数调用的代价run 被调用。


我又做了一些测试:https://jsperf.com/saarman-fn-scope/5

将 Test2 的代码移动到循环(All in loop)中,我希望编译器内联函数调用,因为它是循环的块范围并且包含很少的代码。 这个预期是错误的,但它仍然比 Test1

也许功能深度是问题所在?将Test1的代码移到for循环中(All in loop 2),结果是最慢的...

总而言之,我完全无法预测 JS 引擎何时应用这些调用优化。

值得注意的是,浏览器引擎一直致力于优化常见的 JS 模式。所以为了他们的优化而去优化你的代码通常是没有意义的。


原则上,当你需要更好的性能时,避免函数调用,避免函数声明。但要时刻提防过早的优化。 想象一下没有函数阅读代码会有多难!

要点:

如果引擎应用巧妙的优化,代码会更快。

Edge 太慢了。

Chrome 由于某种原因在第一种情况下没有达到快速路径。也许优化只会随着更多的迭代而开始。

一个函数是否在另一个函数内部并不重要²,Firefox 在这种情况下证明了这一点。

顺便说一句,最好的优化是:

是的,没什么,因为您的代码没有任何可观察到的效果。什么都不做真的可以很快

²:从性能角度来看不是,但从设计角度来看确实重要

这里是 V8 开发人员。简而言之:您已成为微基准测试陷阱的牺牲品。实际上,"Test 1" 的效率稍微高一些,但根据您的整体程序,差异可能太小而不重要。

"Test 1" 效率更高的原因是它创建的闭包更少。将其视为:

let mathAdd = new Function(...);
for (let i = 0; i < 1000; i++) {
  mathAdd();
}

对比

for (let i = 0; i < 1000; i++) {
  let mathAdd = new Function(...);
  mathAdd();
}

就像调用 new Object()new MyFunkyConstructor() 一样,在循环外只调用一次比每次迭代都调用效率更高。

"Test 1"显得变慢的原因是测试设置的一个产物。在这种情况下 jsperf.com 恰好将代码包装到函数中的具体方式碰巧破坏了 V8 的内联机制 [1]。所以在 "Test 1" 中,run 是内联的,但 mathAdd 不是,所以执行了实际的调用,并进行了实际的添加。另一方面,在 "Test 2" 中,runmathAdd 都被内联,编译器随后发现结果未被使用,并消除了所有死代码,因此您正在进行基准测试一个空循环:它不创建函数,不调用函数,也不执行加法(i++ 除外)。

随时检查生成的汇编代码以亲自查看:-) 事实上,如果您想创建更多的微基准测试,您应该习惯于检查汇编代码,以确保基准测试衡量的是您认为它衡量的内容。

[1] 我不确定为什么;如果我不得不猜测:可能有特殊的处理来检测这样一个事实,即 run 每次运行测试用例时都是一个新的闭包,它下面的代码总是相同的,但看起来特殊的外壳只适用于在调用的本地范围内运行,而不是像 runmathAdd 调用那样从上下文链加载。如果这个猜测是正确的,你可以称之为一个错误(Firefox 显然没有);另一方面,如果唯一的影响是微基准测试中的死代码消除不再起作用,那么它肯定不是一个需要修复的重要问题。