v8 / chrome / node.js函数内联

时间:2016-07-09 02:37:27

标签: javascript node.js performance optimization v8

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

  6. 我对以下事实感到惊讶:

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

    • 捕获变量的负面影响在迭代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秒。将这些片段放入函数中,声明其他变量或更改某些语句的顺序有时会提高性能,而其他时候则会进一步降低性能。

    • WTF?

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

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

1 个答案:

答案 0 :(得分:1)

  
      
  1. 调用循环中定义的(lambda)函数,该函数递增捕获的计数器
  2.   
  3. 调用循环中定义的(lambda)函数以递增计数器而不捕获
  4.   

为什么你认为,创造10亿!!!!!循环中的identic函数,可能有什么好主意?特别是如果你只调用它们一次(在这个循环中),然后将它们移开。

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

  

捕获变量的负面影响在迭代1e10次时会增长很多。那里发生了什么?如果我不得不采取疯狂的猜测,我会说超过1 ^ 31,变量会改变类型,并且函数没有针对此进行优化?

对,超过1 ^ 31它不再是int32,而是64位浮点数,你正在使用,并且突然之间,类型已经改变了=&gt;代码得到了优化。

  

在循环中声明一个函数会使代码变慢。 v8根本没有优化功能?我觉得它比那更聪明!我想我永远不应该在关键循环中使用lambdas

在大约100-150次呼叫之后,考虑优化功能。优化每次只调用一次或两次的最后一个函数是没有意义的。

  如果在循环中声明的函数捕获变量,那么它没什么区别。我猜捕获一个变量对于优化的代码是不好的,但对于没有优化的代码却不是那么糟糕?

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

结论:在循环之前创建一次函数,然后在循环中调用它。然后,当你传递或捕获变量时,它不应该有任何显着的性能影响。