我想将计算平方根的实现与 Node.js 中的 native Math.sqrt()
的执行时间进行比较。
所以我做了一些函数来测试它:
function getDuration(func, argumentGenerator, multiplier = 1, argumentOffset = 0) {
let args = []
for (let i = 0; i < multiplier; i++) {
args.push(argumentGenerator(i + argumentOffset))
}
let result = []
const start = performance.now()
for (let i = 0; i < multiplier; i++) {
result.push(func(args[i]))
}
const end = performance.now()
return {
time: end - start,
result: result,
}
}
function measureTime(func, repeat = 1, argumentGenerator, multiplier = 1) {
let result = []
for (let i = 0; i < repeat; i++) {
result.push(
getDuration(func, argumentGenerator, multiplier, i * multiplier)
);
}
return result
}
和测试对象:
const indexArg = (i) => i * i + 1;
let functionsToTest = [
[
function BOTH(x) {
return Math.sqrt(x) - NewtonSquareRoot(x)
},
1e3, indexArg, 1e4
],
[
NewtonSquareRoot,
1e3, indexArg, 1e4
],
[
Math.sqrt,
1e3, indexArg, 1e4
],
];
let results = {}
for (const fArg of functionsToTest) {
let result = measureTime(...fArg)
results[fArg[0].name] = MMMM(result.map(x => x.time))
}
console.table(results);
结果是:
┌──────────────────┬────────┬────────┬────────┬────────┐
│ (index) │ min │ max │ mean │ median │
├──────────────────┼────────┼────────┼────────┼────────┤
│ BOTH │ 0.9142 │ 3.8853 │ 1.3225 │ 1.2812 │
│ NewtonSquareRoot │ 0.9435 │ 2.515 │ 1.6164 │ 1.6612 │
│ sqrt │ 0.1026 │ 0.9474 │ 0.1225 │ 0.107 │
└──────────────────┴────────┴────────┴────────┴────────┘
我希望“BOTH”函数的平均执行时间更接近其他两个函数的总和,但它甚至比单独执行我的实现还要低。
我注意到函数的重新排列会导致第一个函数的时间缩短:
┌──────────────────┬────────┬────────┬────────┬────────┐
│ (index) │ min │ max │ mean │ median │
├──────────────────┼────────┼────────┼────────┼────────┤
│ NewtonSquareRoot │ 0.8823 │ 3.3975 │ 1.2753 │ 1.2644 │
│ sqrt │ 0.1025 │ 0.8317 │ 0.1325 │ 0.1067 │
│ BOTH │ 1.1295 │ 2.443 │ 1.55 │ 1.541 │
└──────────────────┴────────┴────────┴────────┴────────┘
┌──────────────────┬────────┬────────┬────────┬────────┐
│ (index) │ min │ max │ mean │ median │
├──────────────────┼────────┼────────┼────────┼────────┤
│ sqrt │ 0.0294 │ 1.7835 │ 0.062 │ 0.0329 │
│ NewtonSquareRoot │ 0.9475 │ 3.354 │ 1.6375 │ 1.6593 │
│ BOTH │ 1.1293 │ 2.4975 │ 1.5394 │ 1.5409 │
└──────────────────┴────────┴────────┴────────┴────────┘
(由于某种原因,“BOTH”在最后的结果中仍然较低) 但在我看来,如果函数不是第一个,结果是一致的。
为什么会发生这种情况? 我怎样才能使其与第一个功能保持一致?
我正在考虑添加虚拟来跳过第一次执行
measureTime((x) => x, 1, (i) => i, 32);
结果是一致的,但它看起来并不是一个优雅的解决方案。
仅当虚拟重复次数为 32 或更高并且我迷路时,此方法才有效。
最佳答案
(这里是 V8 开发人员。)
Why does that happen?
因为只要 func(args[i])
调用始终调用相同的函数,它就会被内联。内联避免了调用开销,这对于微小函数来说是相当可衡量的。
当然,真实的代码只会有一个 sqrt
函数,因此内联这些调用对于引擎来说绝对是正确的策略。它只会在通常由简单的基准测试框架创建的人工场景中成为一个“问题”。
这是微基准测试最常遇到的陷阱之一。
How can I make it consistent for 1st function also? I was thinking about adding dummy to skip the first execution
是的,这是显而易见的解决方案。这就是我们在自己的一些微基准测试中所做的事情 ( example )。请记住,这会故意禁用特定的优化;以这种方式生成的结果是否有意义取决于您感兴趣的内容。
(注意:郑重声明,微基准测试陷阱同样适用于我们自己的测试,也适用于其他人的测试。从结果中得出结论时必须小心。我们总是检查生成的机器代码,以确保这些微基准测试测量了我们想要的结果衡量!它们不是我们制定决策的主要工具,它们只是我们用来关注性能和发现意外回归的几种技术之一。)
作为替代方案,您可以为要测量的每个函数复制 getDuration
函数(也可能是 measureTime
)。这可能感觉更不优雅,但它实际上更接近现实世界的代码,因为它不需要规避引擎启发式。
最好的解决方案是根本不依赖微基准。相反,采用执行大量 sqrt 运算的真实应用程序,使用 Math.sqrt 测量其性能,然后插入自定义替换并再次测量性能。这样,您就可以在实践中看到实际影响,并且不会存在微基准测试特定工件污染您结果的风险(并可能导致您得出两个选项中哪一个更快的错误结论)。
This works only if the dummy repetition count is 32 or higher
另一个启发式。 V8 仅开始收集已运行一段时间的代码的类型/调用反馈。这可以提高性能并减少运行频率较低的函数的内存消耗。不用说,我不会依赖始终为 32 的阈值,它可能会随着版本和环境而变化。
关于JavaScript - 为什么使用初始参数调用函数更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74581976/