v8/chrome/node.js内联函数

v8/chrome/node.js function inline

本文关键字:函数 node chrome v8 js      更新时间:2023-09-26

如何编写 v8 将内联的函数?

是否有任何工具可以预编译我的代码以静态内联某些函数?静态转换函数和函数调用以避免捕获值?


背景

注意到我编写的JS程序的瓶颈是一个非常简单的函数调用:我在循环中调用函数数百万次迭代,手动内联函数(即用其代码替换函数)将代码加速了几个数量级。

之后,我尝试对这个问题进行了一些研究,但无法推断出 v8 如何优化函数调用以及如何编写高效函数的规则。


示例代码:迭代 10 亿次

  1. 递增计数器:

    let counter = 0;
    while(counter < 1e9) ++counter;
    

    在我的系统上,在Google Chrome/Chromium和v8上都需要~1秒。 ~14迭代1e10次。

  2. 为计数器分配递增函数的值:

    function incr(c) { return c+1; }
    let counter = 0;
    while(counter < 1e9) counter = incr(counter);
    

    迭代 1e10 次大约需要 ~14 秒。

  3. 调用一个函数(仅声明一次)来递增捕获的计数器:

    let counter = 0;
    function incr() { ++counter; }
    while(counter < 1e9) incr();
    

    迭代 1e10 次大约需要 ~3 秒~98 秒。

  4. 调用循环中定义的 (箭头) 函数,该函数递增捕获的计数器:

    let counter = 0;
    while(counter < 1e9) (()=>{ ++counter; })();
    

    大约需要 ~24 秒。(我注意到命名函数或箭头没有区别)

  5. 调用循环中定义的 (arrow) 函数以在不捕获的情况下递增计数器:

    let counter = 0;
    while(counter < 1e9) {
        const incr = (c)=>c+1;
        counter = incr(counter);
    }
    

    大约需要 ~22 秒

我对以下事实感到惊讶:

  • 捕获变量会减慢代码速度。为什么?这是一般规则吗?我是否应始终避免在性能关键函数中捕获变量?

  • 捕获变量的负面影响在迭代 1e10 次时会增长很多。这是怎么回事?如果我不得不胡乱猜测,我会说超过 1^31 变量会改变类型,并且函数没有为此进行优化?

  • 在循环中声明一个函数会大大减慢代码的速度。 V8 根本不优化功能?我以为它比这更聪明!我想我永远不应该在关键循环中声明函数......

  • 循环中声明的函数是否捕获变量几乎没有区别。我想捕获变量对优化代码不利,但对未优化的代码则不那么糟糕?

  • 考虑到所有这些,我真的很惊讶 v8 可以完美地内联持久的非捕获功能。我想这些是性能方面唯一可靠的吗?


编辑1:添加一些额外的片段来暴露额外的怪异。

我创建了一个新文件,其中包含以下代码:

const start = new Date();
function incr(c) { return c+1; }
let counter = 0;
while(counter < 1e9) counter = incr(counter);
console.log( new Date().getTime() - start.getTime() );

它打印一个接近 ~1 秒的值。

然后我在文件末尾声明了一个新变量。任何变量都可以正常工作:只需将let x;附加到截图即可。代码现在需要 ~12 秒才能完成。

如果您没有使用该incr函数,而只是像第一个代码段中那样使用++counter,则额外的变量会使性能从 ~1 秒下降到 ~2.5 秒。 将这些代码片段放入函数中,声明其他变量或更改某些语句的顺序有时会提高性能,而有时则会进一步降低性能。

  • 跆拳道?

  • 我知道像这样的奇怪效果,并且我已经阅读了一堆有关如何为 v8 优化 JS 的指南。仍然:WTF?!

  • 玩了一段时间JS程序的瓶颈,这使我开始了这项研究。我看到实现之间的差异超过 4 个数量级,我不会想到会有任何不同。我目前确信 v8 中数字处理算法的性能是完全不可预测的,我将重写 C 中的瓶颈并将其作为函数公开给 v8。

  1. 调用循环中定义的 (lambda) 函数,该函数递增捕获的计数器
  2. 调用循环中定义的 (lambda) 函数以在不捕获的情况下递增计数器

为什么你认为,在一个循环中创建10亿!!!!相同的函数,可能是个好主意?特别是如果你只调用它们一次(在这个循环内),然后把它们带走。

实际上,我对 v8 引擎处理这项疯狂任务的效率印象深刻。我会想,至少需要几分钟才能做到这一点。再说一遍:我们谈论的是创建 10 亿个函数,然后调用它们一次。

捕获变量的负面影响在迭代 1e10 次时会增长很多。这是怎么回事?如果我不得不胡乱猜测,我会说超过 1^31 变量会改变类型,并且函数没有为此进行优化?

是的,超过 1^31 它不再是 int32,而是一个 64 位浮点数,你正在使用它,突然之间,类型发生了变化 => 代码得到优化。

在循环中声明一个函数会大大减慢代码的速度。 V8 根本不优化功能?我以为它比这更聪明!我想我永远不应该在关键循环中使用 lambda

在大约 100-150 次调用后考虑对函数进行优化。优化每个只调用一次或两次的最后一个函数是没有意义的。

循环中声明的函数是否捕获变量几乎没有区别。我想捕获变量对优化代码不利,但对未优化的代码则不那么糟糕?

是的,访问捕获的变量比访问局部变量花费的时间稍长,但这不是这里的重点;无论是优化代码还是非优化代码。这里的重点仍然是你在循环中创建 10 亿个函数。

结论:在循环之前创建一次函数,然后在循环中调用它。然后,无论您传递还是捕获变量,它都不应该对性能产生任何重大影响。