如何才能使node.js比c和java更快?基准测试比较node.js,c,java和python。

31

我编写了一个非常简单的基准测试程序,用4种不同的语言计算出10,000,000以内的所有质数。

  • (2.97秒) - node.js (javascript) (4.4.5)
  • (6.96秒) - c (c99)
  • (6.91秒) - java (1.7)
  • (45.5秒) - python (2.7)

以上是每种语言运行3次的平均值,用户时间

Node.js运行速度最快。这让我感到困惑,有两个原因:

  1. Javascript在这种情况下始终使用双精度浮点变量,而c和java使用(长)整数。使用整数进行数学计算应该更快。
  2. Javascript经常被称为解释性语言,但实际上它是一种即时编译语言。即使如此,JIT编译器怎么会比完全编译的语言更快呢? Python代码运行最慢,这并不奇怪,但为什么node.js代码的运行速度不与python大致相同呢?

我使用-O2优化编译了c代码,但我尝试了所有级别的优化,并没有明显的区别。

countPrime.js

"use strict";

var isPrime = function(n){
    //if (n !== parseInt(n,10)) {return false};
    if (n < 2) {return false};
    if (n === 2) {return true};
    if (n === 3) {return true};
    if (n % 2 === 0) {return false};
    if (n % 3 === 0) {return false};
    if (n % 1) {return false};
    var sqrtOfN = Math.sqrt(n);
    for (var i = 5; i <= sqrtOfN; i += 6){
        if (n % i === 0) {return false}
        if (n % (i + 2) === 0) {return false}
    }
    return true;
};

var countPrime = function(){
    var count = 0;
    for (let i = 1; i < 10000000;i++){
        if (isPrime(i)){
            count++;
        }
    }
    console.log('total',count);
};

countPrime();

Node.js 的结果

$ time node primeCalc.js
total 664579

real    0m2.965s
user    0m2.928s
sys     0m0.016s

$ node --version
v4.4.5

primeCalc.c

#include <stdio.h>
#include <math.h>

#define true 1
#define false 0

int isPrime (register long n){
    if (n < 2)      return false;
    if (n == 2)     return true;
    if (n == 3)     return true;
    if (n % 2 == 0) return false;
    if (n % 3 == 0) return false;
    if (n % 1)      return false;
    double sqrtOfN = sqrt(n);
    for (long i = 5; i <= sqrtOfN; i += 6){
        if (n % i == 0) return false;
        if (n % (i + 2) == 0) return false;
    }
    return true;
};

int main(int argc, const char * argv[]) {
    register long count = 0;
    for (register long i = 0; i < 10000000; i++){
        if (isPrime(i)){
            count++;
        }
    }

    printf("total %li\n",count);
    return 0;
}

C结果

$ gcc primeCalc.c -lm -g -O2 -std=c99 -Wall
$ time ./a.out
total 664579
real    0m6.718s
user    0m6.668s
sys     0m0.008s

PrimeCalc.java

公共类 PrimeCalc {

  public static void main(String[] args) {
     long count = 0;
     for (long i = 0; i < 10000000; i++){
        if (isPrime(i)){
           count++;
        }
     }
     System.out.println("total "+count);
  }


  public static boolean isPrime(long n) {
     if (n < 2)      return false;
     if (n == 2)     return true;
     if (n == 3)     return true;
     if (n % 2 == 0) return false;
     if (n % 3 == 0) return false;
     if (n % 1 > 0)  return false;
     double sqrtOfN = Math.sqrt(n);
     for (long i = 5; i <= sqrtOfN; i += 6){
        if (n % i == 0) return false;
        if (n % (i + 2) == 0) return false;
     }
     return true;
  };

}

Java 结果

 $ javac PrimeCalc.java 
 $ time java PrimeCalc
 total 664579
 real    0m7.197s
 user    0m7.036s
 sys     0m0.040s
 $ java -version
 java version "1.7.0_111"
 OpenJDK Runtime Environment (IcedTea 2.6.7) (7u111-2.6.7-0ubuntu0.14.04.3)
 OpenJDK 64-Bit Server VM (build 24.111-b01, mixed mode)

primeCalc.py

import math

def isPrime (n):
    if n < 2       : return False
    if n == 2      : return True
    if n == 3      : return True
    if n % 2 == 0  : return False
    if n % 3 == 0  : return False
    if n % 1 >0    : return False
    sqrtOfN = int(math.sqrt(n)) + 1
    for i in xrange (5, sqrtOfN, 6):
        if n % i == 0       : return False;
        if n % (i + 2) == 0 : return False;
    return True

count = 0;
for i in xrange(10000000) :
    if isPrime(i) :
        count+=1

print "total ",count

Python 结果

time python primeCalc.py
total  664579
real    0m46.588s
user    0m45.732s
sys     0m0.156s 
$ python --version
Python 2.7.6 

Linux

$ uname -a
Linux hoarfrost-node_6-3667558 4.2.0-c9 #1 SMP Wed Sep 30 16:14:37 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

附加的C运行时间(补遗)

  • (7.81秒) 无优化,gcc primeCalc.c -lm -std=c99 -Wall
  • (8.13秒) 优化0,gcc primeCalc.c -lm -O0 -std=c99 -Wall
  • (7.30秒) 优化1,gcc primeCalc.c -lm -O1 -std=c99 -Wall
  • (6.66秒) 优化2,gcc primeCalc.c -lm -O2 -std=c99 -Wall

    • 每个优化级别的用户时间的3次新运行平均值 *

我在这里阅读了这篇文章: 为什么这个NodeJS比本地C快两倍? 这段代码使用的例子实际上并没有做什么重要的事情。好像编译器可以在编译时就算出结果,甚至不需要执行100000000次循环来得出答案。 如果再添加另一个除法步骤,优化效果就会大大降低。

for (long i = 0; i < 100000000; i++) {
  d += i >> 1;    
  d = d / (i +1); // <-- New Term 
}
  • (1.88秒) 未进行优化
  • (1.53秒) 经过优化(-O2)

更新 2017年3月15日 阅读 @leon 的答案后我进行了一些验证测试。

测试1 - 32位 Beaglebone Black,计算10000000以内的664,579个质数

在具有32位处理器的Beaglebone black上运行未编辑的calcPrime.js和calcPrime.c代码。

  • C代码 = 62秒(gcc,long datatype)
  • JS代码 = 102秒(node v4)

测试2 - 64位 MacBook Pro,计算10000000以内的664,579个质数

将calcPrime.c代码中的长整型数据类型替换为uint32_t,并在我的MacBook pro(64位处理器)上运行。

  • C代码 = 5.73秒 (clang,long datatype)
  • C代码 = 2.43秒(clang,uint_32_t datatype)
  • JS代码 = 2.12秒(node v4)

测试3 - 64位 MacBook Pro,计算( i=1; i < 8,000,000,000; i+=10000 ),共91,836个质数

在C代码中使用unsigned long数据类型,强制javascript使用一些64位。 - C代码=20.4秒(clang,long datatype) - JS代码=17.8秒(node v4)

测试4 - 64位 MacBook Pro,计算(i = 8,000,00,001; i < 16,000,000,000; i+=10000),共86,277个质数

在C代码中使用unsigned long数据类型,强制javascript全部使用64位。 - C代码=35.8秒(clang,long datatype) - JS代码=34.1秒(node v4)

测试5 - Cloud9 64位Linux,(i = 0; i < 10000000; i++)

language    datatype    time    % to C
javascript  auto        3.22      31%
C           long        7.95     224%
C           int         2.46       0%
Java        long        8.08     229%
Java        int         2.15     -12%
Python      auto       48.43    1872%
Pypy        auto        9.51     287%

测试6 - Cloud9 64位Linux,(i = 8000000001; i < 16000000000; i+=10000)

javascript  auto       52.38      12%
C           long       46.80       0%
Java        long       49.70       6%
Python      auto      268.47     474%
Pypy        auto       56.55      21%

(所有结果是用户三次运行的平均秒数,运行之间的时间变化<10%)

不同的结果

当C和Java数据类型在整数范围内时转换为整数显著加快了执行速度。 在BBB和Cloud9计算机上,切换到ints使得C比node.js更快。 但是在我的Mac上,node.js程序仍然运行得更快。 可能是因为Mac使用clang,而BBB和Cloud 9计算机使用gcc。 有没有人知道gcc是否编译得比gcc更快?

在使用全部64位整数时,C比node.js在BBB和Cloud9 PC上稍微快一点,但在我的MAC上稍微慢一些。

从这些测试中可以看出,pypy比标准python快约四倍。

事实上,node.js甚至与C兼容,这让我感到惊奇。


4
v8是一款优化良好的编译器,实际上非常优化。然而,请注意微基准测试。 - Elliott Frisch
1
也许你对 longdouble 转换有所发现,这些转换可能会在长期运行中变得代价高昂。 - dvhh
1
可能是为什么这个NodeJS比本地C快2倍?的重复问题。 - enRaiser
1
-O2 -m64 将三次运行的平均时间从6.66秒降至6.41秒,但每次结果都会有几十分之一秒的差异,因此我无法确定它是否实际上产生了任何显著的结果。 - Timothy Vann
1
在我的 AMD A8-5500 上:gcc 5.4.0 - 4.5 秒;nodejs 4.2.6 - 4.7 秒。 - Muposat
显示剩余17条评论
2个回答

45

我花了几天时间研究JS / V8和C之间的性能差异,首先关注V8引擎生成的Hydrogen IR。然而,在确保没有任何特殊优化存在之后,我回到了分析汇编输出,并且想到答案是非常简单的,都可以归结为Jay Conrod的博客文章中的几句话:

根据规范,JavaScript中的所有数字都是64位浮点数。但我们经常使用整数,因此 V8在可能的情况下使用31位有符号整数表示数字

手头的这个例子允许将所有计算装入32位中,而node.js充分利用了这一点! C代码利用了long类型,而在OP(以及我的)平台上,它恰好是一个64位类型。因此,这是一个32位算术与64位算术问题,主要是由于昂贵的除法/余数操作。

如果将C代码中的long替换为int,则gcc生成的二进制文件会击败node.js。

此外,如果将循环设置为查找超出32位数字范围的质数,则node.js版本的性能显著下降。


证明

使用的源代码可以在答案中进一步找到结果。

使用C和node.js计算小于10百万的质数数量

$ gcc count_primes.c -std=c99 -O3 -lm -o count_primes_long
$ sed 's/long/int/g; s/%li/%i/g' count_primes.c > count_primes_int.c
$ gcc count_primes_int.c -std=c99 -O3 -lm -o count_primes_int

# Count primes <10M using C code with (64-bit) long type
$ time ./count_primes_long 0 10000000
The range [0, 10000000) contains 664579 primes

real    0m4.394s
user    0m4.392s
sys 0m0.000s

# Count primes <10M using C code with (32-bit) int type
$ time ./count_primes_int 0 10000000
The range [0, 10000000) contains 664579 primes

real    0m1.386s
user    0m1.384s
sys 0m0.000s

# Count primes <10M using node.js/V8 which transparently does the
# job utilizing 32-bit types
$ time nodejs ./count_primes.js 0 10000000
The range [ 0 , 10000000 ) contains 664579 primes

real    0m1.828s
user    0m1.820s
sys 0m0.004s

接近有符号32位整数极限的性能数据

统计从第一列数字开始、长度为100,000的范围内质数的数量:

              | node.js | C (long) 
-----------------------------------
2,000,000,000 | 0.293s  | 0.639s    # fully within the 32-bit range
-----------------------------------
2,147,383,647 | 0.296s  | 0.655s    # fully within the 32-bit range
-----------------------------------
2,147,453,647 | 2.498s  | 0.646s    # 50% within the 32-bit range
-----------------------------------
2,147,483,647 | 4.717s  | 0.652s    # fully outside the 32-bit range
-----------------------------------
3,000,000,000 | 5.449s  | 0.755s    # fully outside the 32-bit range
-----------------------------------

count_primes.js

"use strict";

var isPrime = function(n){
    if (n < 2) {return false};
    if (n === 2) {return true};
    if (n === 3) {return true};
    if (n % 2 === 0) {return false};
    if (n % 3 === 0) {return false};
    var sqrtOfN = Math.sqrt(n);
    for (var i = 5; i <= sqrtOfN; i += 6){
        if (n % i === 0) {return false}
        if (n % (i + 2) === 0) {return false}
    }
    return true;
};

var countPrime = function(S, E){
    var count = 0;
    for (let i = S; i < E;i++){
        if ( isPrime(i) ) { ++count; }
    }
    return count;
};

if( process.argv.length != 4) {
    console.log('Usage: nodejs count_prime.js <range_start> <range_length>');
    process.exit();
}

var S = parseInt(process.argv[2]);
var N = parseInt(process.argv[3]);
var E = S+N;
var P = countPrime(S, E);
console.log('The range [', S, ',', E, ') contains', P, 'primes');

count_primes.c

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define true 1
#define false 0

int isPrime (register long n){
    if (n < 2)      return false;
    if (n == 2)     return true;
    if (n == 3)     return true;
    if (n % 2 == 0) return false;
    if (n % 3 == 0) return false;
    double sqrtOfN = sqrt(n);
    for (long i = 5; i <= sqrtOfN; i += 6){
        if (n % i == 0) return false;
        if (n % (i + 2) == 0) return false;
    }
    return true;
};

int main(int argc, const char * argv[]) {
    if ( argc != 3 ) {
        fprintf(stderr, "Usage: count_primes <range_start> <range_length>\n");
        exit(1);
    }
    const long S = atol(argv[1]);
    const long N = atol(argv[2]);
    register long count = 0;
    for (register long i = S; i < S + N; i++){
        if ( isPrime(i) ) ++count;
    }
    printf("The range [%li, %li) contains %li primes\n", S, S+N, count);
}

JS/V8比C语言更快的原因是什么? - mavavilj

-1

当我在Python中运行了您的代码并使用了新算法:

real    0m3.583s
user    0m3.297s
sys     0m0.094s

比您上面的C基准测试更快。我认为,一种更简单的语言可以帮助您设计更好的算法,但这只是我的观点。(还可以使用多进程使速度更快)
def allPrimes(N):
    is_prime = [1]*N
    # We know 0 and 1 are composites
    is_prime[0] = 0
    is_prime[1] = 0

    i = 2
    # This will loop from 2 to int(sqrt(x))
    while i*i <= N:
        # If we already crossed out this number, then continue
        if is_prime[i] == 0:
            i += 1
            continue

        j = 2*i
        while j < N:
            # Cross out this as it is composite
            is_prime[j] = 0
            # j is incremented by i, because we want to cover all multiples of i
            j += i

        i += 1
    return is_prime




print("total", sum(allPrimes(10000000)))

1
你的C语言算法基准是什么?我认为在这个问题中我们比较的是语言速度,而不是算法速度。当然,一个人可以编写更好的算法,但你需要使用相同的算法在不同的语言中进行基准测试。你不能用你的时间来与op的算法时间进行比较。 - theprogrammer

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