在函数范围内声明函数与在函数范围外声明函数的性能
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" 中,run
和 mathAdd
都被内联,编译器随后发现结果未被使用,并消除了所有死代码,因此您正在进行基准测试一个空循环:它不创建函数,不调用函数,也不执行加法(i++
除外)。
随时检查生成的汇编代码以亲自查看:-)
事实上,如果您想创建更多的微基准测试,您应该习惯于检查汇编代码,以确保基准测试衡量的是您认为它衡量的内容。
[1] 我不确定为什么;如果我不得不猜测:可能有特殊的处理来检测这样一个事实,即 run
每次运行测试用例时都是一个新的闭包,它下面的代码总是相同的,但看起来特殊的外壳只适用于在调用的本地范围内运行,而不是像 run
→mathAdd
调用那样从上下文链加载。如果这个猜测是正确的,你可以称之为一个错误(Firefox 显然没有);另一方面,如果唯一的影响是微基准测试中的死代码消除不再起作用,那么它肯定不是一个需要修复的重要问题。
我在思考是否在函数作用域内或作用域外声明函数对性能的影响。
为此,我使用 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" 中,run
和 mathAdd
都被内联,编译器随后发现结果未被使用,并消除了所有死代码,因此您正在进行基准测试一个空循环:它不创建函数,不调用函数,也不执行加法(i++
除外)。
随时检查生成的汇编代码以亲自查看:-) 事实上,如果您想创建更多的微基准测试,您应该习惯于检查汇编代码,以确保基准测试衡量的是您认为它衡量的内容。
[1] 我不确定为什么;如果我不得不猜测:可能有特殊的处理来检测这样一个事实,即 run
每次运行测试用例时都是一个新的闭包,它下面的代码总是相同的,但看起来特殊的外壳只适用于在调用的本地范围内运行,而不是像 run
→mathAdd
调用那样从上下文链加载。如果这个猜测是正确的,你可以称之为一个错误(Firefox 显然没有);另一方面,如果唯一的影响是微基准测试中的死代码消除不再起作用,那么它肯定不是一个需要修复的重要问题。