JavaScript - 为什么使用初始参数调用函数更快?

标签 javascript node.js function v8 execution-time

我想将计算平方根的实现与 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/

相关文章:

javascript - 如何在启动时删除所有 tinymce 实例?

javascript - 使用多个变量重复 javascript 函数

javascript - R 与 Node JS 的集成

jquery - 使用 "click"函数循环数组

javascript - 如何迭代这个 JSON 对象并将每个项目输出为表头?

javascript - Selenium 处理超时 driver.wait

node.js - ../this在父级和子级具有相同值时在内部循环内返回 View 对象

function - Emacs 定义一个将参数设置为 nil 的函数

javascript - 单击另一个元素时单击元素

javascript - 使用 HTML 表单提前输入 - 根据选择自动填充下一个单元格