Comment peut-nœud.js être plus rapide que c et java? Référence comparant noeud.js, c, java et python


J'ai fait un programme d'analyse comparative très simple qui calcule tous les nombres premiers jusqu'à 10 000 000 dans 4 langues différentes.

  • (2.97 secondes) - nœud.js (javascript) (4.4.5)
  • (6.96 secondes) - c (c99)
  • (6.91 secondes) - java (1.7)
  • (45.5 secondes) - python (2.7)

ci-dessus est la moyenne de 3 exécutions chacune, le temps de l'utilisateur

Noeud.js fonctionne de loin le plus rapide. C'est déroutant pour moi pour deux raisons:

  1. javascript utilise toujours des flottants de double précision pour les variables alors que c et java utilisent des entiers (longs) dans ce cas. Les mathématiques avec des entiers devraient être plus rapides.
  2. javascript est souvent appelé interprété alors qu'il s'agit en fait d'un langage compilé juste à temps. Mais même ainsi, comment le compilateur JIT peut-il être plus rapide qu'un langage entièrement compilé? Le code python s'exécute le plus lentement, ce qui n'est pas une surprise, mais pourquoi le nœud ne l'est-il pas.code js fonctionnant à une vitesse similaire à la python?

J'ai compilé le code c avec l'optimisation-O2, mais je l'ai essayé avec tous les niveaux d'optimisation et cela n'a pas fait de différence notable.

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();

Noeud.résultats 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;
}

Résultats 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

Public class 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;
  };

}

Résultats 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 résultats

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

Temps d'exécution c supplémentaires (addendum)

  • (7.81 s) aucune optimisation, gcc primeCalc.c-lm-std=c99-Mur
  • (8.13 s) optimisation 0, gcc primeCalc.c-lm-O0-std=c99-Mur
  • (7.30 s) optimisation 1, gcc primeCalc.c-lm-O1-std=c99-Mur
  • (6.66 s) optimisation 2, gcc primeCalc.c-lm-O2-std=c99-Mur

    • moyenne de 3 nouvelles exécutions par temps utilisateur de niveau d'optimisation *

J'ai lu le post ici: Pourquoi ce NodeJS est-il 2 fois plus rapide que le C natif? Ce code utilise un exemple qui ne fait rien de significatif. C'est comme si le compilateur peut déterminer le résultat au moment de la compilation et il n'a même pas besoin d'exécuter la boucle 100000000 fois pour trouver la réponse. Si l'on ajoute une autre étape de division au calcul, l'optimisation est beaucoup moins significative.

for (long i = 0; i < 100000000; i++) {
  d += i >> 1;    
  d = d / (i +1); // <-- New Term 
}
  • (1.88 secondes) sans optimisation
  • (1.53 secondes) avec optimisation (-O2)

Mise à Jour 03/15/2017 Après avoir lu la réponse de @ leon, j'ai effectué quelques tests de vérification.

Test 1-32 bits Beaglebone Noir, 664,579 nombres premiers jusqu'à 10,000,000

CalcPrime non édité.js et calcPrime.c fonctionnant sur le Beaglebone black qui dispose d'un processeur 32 bits.

  • Code C = 62 secondes (gcc, type de données long)
  • Code JS = 102 secondes (nœud v4)

Test 2-64 Bits Macbook Pro, 664,579 nombres premiers jusqu'à 10,000,000

Remplacer les types de données longs dans calcPrime.code c avec uint32_t et fonctionnant sur mon MacBook pro qui a un processeur 64 bits.

  • Code C = 5,73 secondes (clang, type de données long)
  • code C = 2.43 secondes (clang, uint_32_t datatype)
  • Code JS = 2,12 secondes (nœud v4)

Test 3-64 Bits Macbook Pro, 91,836 nombres premiers (i=1; i

Utilisez des types de données longs non signés dans le code C, forcez javascript à utiliser certains bits 64. - Code C = 20,4 secondes (clang, type de données long) - Code JS = 17,8 secondes (nœud v4)

Test 4-64 Bits Macbook Pro, 86,277 nombres premiers (i = 8,000,00,001; i

Utilisez des types de données longs non signés dans le code C, forcez javascript à utiliser tous les 64 bits. - Code C = 35,8 secondes (clang, type de données long) - Code JS = 34,1 secondes (nœud v4)

Test 5-Cloud9 Linux 64 bits, (i = 0; 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%

Test 6-Cloud9 Linux 64 bits, (i = 8000000001; i

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%

(Tous les résultats sont la moyenne des secondes de l'utilisateur pour trois essais, variation de temps entre les essais

Des Résultats Mitigés

Changer le type de données C et Java en entier lorsque dans la plage d'entiers accélérait considérablement l'exécution. Sur le BBB et Cloud9 les ordinateurs passant à ints ont rendu C plus rapide que node.js. Mais sur mon Mac le nœud.le programme js a toujours fonctionné plus rapidement. Peut-être parce que le Mac utilise clang et que les ordinateurs BBB et Cloud 9 utilisent gcc. Est-ce que quelqu'un sait si gcc compile des programmes plus rapides que gcc?

Lors de l'utilisation de tous les entiers 64 bits, C était un peu plus rapide que node.js sur le PC BBB et Cloud9 mais un peu plus lent sur mon MAC.

Vous pouvez également voir que pypy est environ quatre fois plus rapide que python standard dans ces test.

Le fait que le nœud.js est même compatible avec C m'étonne.

Author: Community, 2016-09-07

1 answers

J'ai passé quelques jours à étudier la différence de performance entre JS/V8 et C, en me concentrant tout d'abord sur l'hydrogène IR généré par le moteur V8. Cependant, après m'être assuré qu'aucune optimisation extraordinaire n'y est présente, je suis revenu à l'analyse de la sortie de l'assemblage et il m'a frappé que la réponse était très simple, se résumant aux quelques phrases dans Le blog de Jay Conrod sur les internes de V8:

Selon la spécification, tous les nombres en JavaScript sont des doubles à virgule flottante 64 bits. Nous travaillons souvent avec des entiers, donc V8 représente les nombres avec des entiers signés 31 bits chaque fois que possible.

L'exemple actuel permet d'ajuster tous les calculs en 32 bits et nœud.js tire pleinement parti de ce! Le code C utilise le type long, qui sur la plate-forme OP (ainsi que ma) se trouve être un type 64 bits. Ainsi, il s'agit d'un problème arithmétique 32 bits vs arithmétique 64 bits, principalement dû à l'opération coûteuse de division / reste.

Si long dans le code C est remplacé par int, alors le binaire produit par gcc beats nœud.js.

De plus, si la boucle est faite pour rechercher des nombres premiers sur une plage qui est en dehors du domaine des nombres 32 bits, les performances du nœud.la version js diminue considérablement.


Preuve

Le code source utilisé se trouve plus loin dans la réponse, sous les résultats.

Compter les nombres premiers inférieurs à 10 millions avec C et noeud.js

$ 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

Chiffres de performance au voisinage de la limite des entiers 32 bits signés

Compter les nombres premiers dans la plage de longueur 100 000 à partir du nombre contenu dans la première colonne:

              | 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);
}
 15
Author: Leon, 2017-03-15 15:57:26