【问题标题】:Why is this NodeJS 2x faster than native C?为什么这个 NodeJS 比原生 C 快 2 倍?
【发布时间】:2015-02-10 12:49:30
【问题描述】:

为了在工作中进行演示,我想将 NodeJS 的性能与 C 进行比较。这是我写的:

Node.js (for.js):

var d = 0.0,
    start = new Date().getTime();

for (var i = 0; i < 100000000; i++)
{
  d += i >> 1;
}

var end = new Date().getTime();

console.log(d);
console.log(end - start);

C (for.c)

#include <stdio.h>
#include <time.h>

int main () {
  clock_t start = clock();

  long d = 0.0;

  for (long i = 0; i < 100000000; i++)
  {
    d += i >> 1;    
  }

  clock_t end = clock();
  clock_t elapsed = (end - start) / (CLOCKS_PER_SEC / 1000); 

  printf("%ld\n", d);
  printf("%lu\n", elapsed);
}

我使用 GCC 编译了我的 for.c 并运行它:

gcc for.c
./a.out

结果:

2499999950000000
198

然后我在 NodeJS 中试了一下:

node for.js

结果:

2499999950000000
116

经过多次运行,我发现无论如何都是如此。如果我将 for.c 切换为在循环中使用 double 而不是 long,那么 C 花费的时间会更长!

不是想发起一场激烈的战争,但为什么 Node.JS(116 毫秒)在执行相同操作时比原生 C(198 毫秒)快这么多? Node.JS 是否应用了 GCC 没有开箱即用的优化?

编辑:

根据 cmets 的建议,我运行了 gcc -Wall -O2 for.c。结果提高到 C 需要 29 毫秒。这就引出了一个问题,为什么原生 C 设置没有像 Javascript 编译器那样优化?另外,-Wall 和 -02 在做什么。我真的很好奇这里发生了什么。

【问题讨论】:

  • 尝试使用gcc -Wall -O2 for.c 编译并再次进行基准测试。使用gcc(或clang)而不通过任何显式优化标志进行基准测试是没有用的!
  • 您在 Node.js 中测量 wall 时间,在 C 中测量 CPU 时间。
  • 再次测量编译:gcc -march=native -O2 for.c(我的编译器标志为 260,我的编译器标志为 98!即使只是 -O1 也将运行时间减少到一半以下。)
  • 嗯,v8 引擎也不是解释器,它会编译代码(并在可能的情况下动态优化它)。
  • 简短的回答是,当您不指定任何优化时,gcc 不会进行 any 优化。它只是尽可能快地生成代码,不管它运行得有多慢。编译 C(和 C++)往往很慢,以至于对于大多数开发来说,大多数人只想让一些东西尽快运行,不管运行速度有多慢。只有当代码至少接近完成时,它们才会打开优化器并忍受较慢的编译时间。

标签: c node.js performance optimization


【解决方案1】:

这就引出了一个问题,为什么原生 C 设置没有像 Javascript 编译器那样优化?

由于 C 是静态编译和链接的,因此可能需要对整个代码库进行冗长的构建步骤(我曾经在一个完全优化的构建过程中花费了将近一个小时,但否则只需 10 分钟),并且非常危险, 硬件级语言,如果你不小心对待它,会冒很多未定义行为的风险,编译器的默认设置通常不会优化到碎片,因为这是一个开发人员/调试版本,旨在帮助更快地进行调试和生产力周转。

因此,在 C 语言中,您可以在未优化但构建速度更快、更易于调试的开发人员/调试构建和非常优化、构建速度较慢、更难的构建之间获得明显的区别。调试运行非常快的生产/发布构建,编译器的默认设置通常偏爱前者。

使用 v8/NodeJS 之类的东西,您正在处理即时编译器(动态编译),它仅在运行时动态构建和优化必要的代码。最重要的是,JS 是一种更安全的语言,并且通常也为安全性而设计,不允许您使用硬件的原始位和字节。

因此,它不需要像 C/C++ 这样的原生静态编译语言的那种强大的发布/调试构建区别。但是如果你真的想的话,它也不能让你像在 C 中那样把踏板放在金属上。

许多试图对来自其他语言的 C/C++ 进行基准测试的人经常无法理解这种构建区别以及编译器/链接器优化设置的重要性,并感到困惑。如您所见,通过适当的设置,很难超越这些允许您编写真正低级代码的本机编译器和语言的性能。

【讨论】:

    【解决方案2】:

    添加注册关键字会有所帮助

    #include <stdio.h>
    #include <time.h>
    
    int main () {
      register long i, d;
      clock_t start = clock();
      i = d = 0L;
    
      for (i = 0; i < 100000000L; i++) {
        d += i >> 1;
      }
    
      clock_t end = clock();
      clock_t elapsed = (end - start) / (CLOCKS_PER_SEC / 1000);
    
      printf("%ld\n", d);
      printf("%lu\n", elapsed);
    }
    

    并使用 C 编译器进行编译

     cc     for.c   -o for
    
    ./for ; node for.js
    

    返回

    2499999950000000
    97
    2499999950000000
    222
    

    【讨论】:

    • 有趣的是,我最近读到register 不再比任何-OX 选项好多少。但这是有道理的。不错的帖子。
    【解决方案3】:

    我还进行了一些计算素数的测试,我发现 Node.js 的速度大约是 C see here 的两倍。

    当您有一个非常简单的计数类型的循环时,-O2 优化有时可以将输出转换为简单的公式,甚至无需迭代循环。有关说明,请参阅Karl's Blog。如果你在例程中添加一些更复杂的东西,node.js 很可能会再次变得更快。例如,我在您的示例程序中添加了一个除数项,而 c -O2 优化不再能够将其转换为简单的公式,并且 node.js 再次变得更快。

    我仍然对 node.js 如何在简单的整数计算中比 c 施法感到困惑,但到目前为止,在我所做的每一个测试中,它都更快。我还使用尚未发布的按位计算进行了一些测试,但 node.js 仍然更快。

    【讨论】:

      【解决方案4】:

      node.js 和 C 的不同之处在于 node.js 解释 JavaScript,而 C 将代码编译为机器语言。因此,两者的处理方式不同。对于 node.js,您只需运行 .js 文件。 C与此有很大不同。当您使用 GCC 将代码编译为机器语言时,您必须提供编译器优化设置,即“标志”。如果您为 GCC 指定 -O3 标志,您的 node.js 程序实际上比 C 程序慢。事实上,node.js 程序花费的时间是 C 程序的两倍。您说您想了解更多关于 C 编译器如何优化代码的信息。这是一个复杂的主题/领域,我强烈建议阅读 this Wikipedia article 关于编译器优化以了解更多信息。

      简而言之,您进行了不公平的比较,因为您没有优化您的 C 代码。

      【讨论】:

      猜你喜欢
      • 2016-07-07
      • 2014-07-13
      • 2011-08-14
      • 2011-08-22
      • 2013-06-06
      • 2018-08-12
      • 2013-09-23
      • 1970-01-01
      • 2014-02-19
      相关资源
      最近更新 更多