【问题标题】:v8/chrome/node.js function inlinev8/chrome/node.js 函数内联
【发布时间】:2016-07-13 23:17:16
【问题描述】:

如何编写 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);
    

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

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

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

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

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

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

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

  5. 调用循环中定义的(箭头)函数来增加计数器而不捕获:

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

    大约需要 ~22 秒

我对以下事实感到惊讶:

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

  • 当迭代 1e10 次时,捕获变量的负面影响会增加很多。那里发生了什么事?如果我不得不大胆猜测,我会说超过 1^31 变量会改变类型,并且函数没有为此优化?

  • 在循环中声明函数会大大降低代码速度。 v8根本没有优化功能?我认为它比那更聪明!我想我永远不应该在关键循环中声明函数......

  • 在循环中声明的函数是否捕获变量几乎没有什么区别。我猜捕获一个变量对于优化的代码是不利的,但对于未优化的代码来说并不是那么糟糕?

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


编辑 1:添加一些额外的 sn-ps 以暴露额外的怪异之处。

我创建了一个新文件,里面有以下代码:

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 函数,而只是在第一个 sn-p 中使用 ++counter,则额外的变量会使性能从 ~1 秒降低到 ~2.5 秒。将这些 sn-ps 放入函数中,声明其他变量或更改某些语句的顺序有时会提高性能,而有时会进一步降低性能。

  • WTF?

  • 我知道像this one 这样的奇怪效果,并且我已经阅读了很多关于如何为 v8 优化 JS 的指南。还是:WTF?!

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

【问题讨论】:

  • “为什么?这是一般规则吗?” --- 它应该在父作用域中查找
  • @zerkms:好吧,如果没有优化的话。我想知道为什么 sn-p 3 没有像 sn-p 2 那样完全编译成 sn-p 1。
  • 您似乎在使用术语“lambda 函数”来表示“箭头函数”。它们不是一回事。
  • @nnnnnn:对,我倾向于将它们用作同义词。我正在修改帖子。

标签: javascript node.js performance optimization v8


【解决方案1】:
  1. 调用循环中定义的 (lambda) 函数以递增捕获的计数器
  2. 调用循环中定义的 (lambda) 函数来递增计数器而不进行捕获

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

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

当迭代 1e10 次时,捕获变量的负面影响会增加很多。那里发生了什么事?如果我不得不大胆猜测,我会说超过 1^31 变量会改变类型,并且函数没有为此优化?

对,超过 1^31 后,它不再是 int32,而是一个 64 位浮点数,而且突然之间,类型发生了变化 => 代码被取消优化。

在循环中声明一个函数会大大降低代码的速度。 v8根本没有优化功能?我认为它比那更聪明!我想我永远不应该在关键循环中使用 lambdas

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

在循环中声明的函数是否捕获变量几乎没有什么区别。我猜捕获一个变量对优化代码不利,但对于未优化的代码来说不是那么糟糕?

是的,访问一个捕获的变量比访问一个局部变量要花一点时间,但这不是重点;既不是优化代码也不是非优化代码。这里的重点仍然是你在一个循环中创建了 10 亿个函数。

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

【讨论】:

  • 你有任何来源,或者你是从我的sn-ps和我的结论中得出的结论吗?我在问题中添加了一个新段落,展示了如何通过声明一个甚至没有在循环中使用的额外变量将性能降低 12 倍(如果不使用函数,则降低 2.5 倍)。我花了相当长的时间来修补一段代码,在那里我发现了让我开始这一切的瓶颈,而且我看到了很多我无法用你的答案来解释的奇怪之处。
  • 除此之外,在循环中声明 lambda 函数有时是必要的,例如,如果您需要执行以下操作:loop((x)=&gt;{ f(()=&gt;{ g(x); }); });(即在循环中,您需要创建一个回调使用在循环范围内声明的变量之一)——这对我来说经常发生,尤其是在异步或函数式库中。当然,您可以努力尝试并找到一个 hacky 解决方案(例如,使用全局变量 - 只要没有什么是异步的),但这会使事情复杂化很多,无论如何 v8 应该能够优化。
  • 答案很中肯,但对我来说有点情绪化
猜你喜欢
  • 2011-11-03
  • 2016-07-24
  • 2012-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多