v8/chrome/node.js函数内联

4

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

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


背景

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

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


示例代码:迭代10亿次

  1. incrementing a counter:

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

    it takes about ~1 sec, on my system, both on Google Chrome/Chromium and v8. ~14 secs iterating 1e10 times.

  2. assigning to the counter the value of an incrementing function:

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

    it takes about ~1 sec. ~14 secs iterating 1e10 times.

  3. calling a function (declared only once) that increments the captured counter:

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

    it takes about ~3 sec. ~98 secs iterating 1e10 times.

  4. calling a (arrow) function defined in the loop that increments the captured counter:

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

    it takes about ~24 secs. (I noticed that a named function or an arrow one makes no difference)

  5. calling a (arrow) function defined in the loop to increment the counter without capturing:

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

    it takes about ~22 secs.

我对以下事实感到惊讶:

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

  • 捕获变量的负面影响在迭代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的指南。还是:什么鬼?!

  • 我试着玩了一下导致我开始进行这项研究的JS程序的瓶颈。 我看到了超过4个数量级之间的差异,这些实现我本不希望有任何不同。 我目前相信v8中的数值计算算法的性能是完全不可预测的,并将在C中重新编写瓶颈,并将其公开为v8的函数。


为什么?这是一个通用规则吗?--- 它应该在父作用域中查找。 - zerkms
@zerkms:好吧,如果没有优化。我想知道为什么代码片段3没有像代码片段2一样编译成完全相同的代码片段1。 - peoro
你似乎在使用“lambda函数”一词来表示“箭头函数”。它们并不是同一种东西。 - nnnnnn
@nnnnnn:没错,我倾向于将它们视为同义词。我正在修复这篇文章。 - peoro
1个回答

1
  1. 调用在循环中定义的(lambda)函数以增加捕获的计数器
  2. 调用在循环中定义的(lambda)函数以递增计数器而不进行捕获

你认为在循环中创建10亿个相同的函数可能是个好主意吗?特别是如果你只在这个循环内部调用它们一次,然后就扔掉它们。

实际上,我对v8引擎如何高效地处理这个疯狂的任务感到印象深刻。我本以为这至少需要几分钟的时间才能完成。再说一遍:我们正在谈论创建10亿个函数,然后调用它们一次。

当迭代1e10次时,捕获变量的负面影响会大大增加。发生了什么?如果我要猜测,我会说超过1^31时,变量会改变类型,而该函数没有针对此进行优化?

没错,在1^31之后,它不再是int32,而是64位浮点数,类型突然改变=>代码被取消优化。

在循环中声明函数会使代码变慢很多。v8根本不会优化函数吗?我还以为它比那聪明呢!我想我在关键循环中永远不应该使用lambda表达式。
大约在100-150次调用后,函数才被视为进行优化的候选项。对于仅被调用一次或两次的每个最后一个函数进行优化是没有意义的。
无论函数是否在循环中声明变量,都几乎没有区别。我想捕获变量对于优化的代码来说是不好的,但对于未优化的代码来说并不那么糟糕?
是的,访问捕获的变量需要比访问本地变量稍微长一点时间,但这不是重点;无论是对于优化的代码还是未优化的代码都是如此。重点在于你在循环中创建了10亿个函数。
结论:在循环之前创建函数,然后在循环中调用它。然后,无论您传递还是捕获变量,它都不应该有任何显着的性能影响。

你有任何来源吗?还是你从我的代码片段和结论中得出了结论? 我在问题中添加了一个新段落,展示了如何通过声明一个甚至没有在循环中使用的额外变量,将性能降低了12倍(如果不使用函数,则为2.5倍)。 我花了相当多的时间来调整我找到瓶颈的那段代码,并且我看到了很多奇怪的东西,这些都无法用你的答案解释。 - peoro
有时候在循环内部声明一个lambda函数是必要的,例如如果你需要做这样的事情:loop((x)=>{ f(()=>{ g(x); }); });(即在循环内部创建一个回调函数,该回调函数使用了循环作用域中声明的变量),这对我来说经常发生,特别是在异步或函数式库中。当然,你可以尝试努力找到一个hacky的解决方案(例如使用全局变量 - 只要没有任何异步操作),但这会使事情变得非常复杂,在任何情况下v8都应该能够进行优化。 - peoro
答案很到位,但有点情绪化,不太符合我的口味。 - shitpoet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接