Javascript в 4 раза быстрее, чем С++?

В течение долгого времени я думал о С++ быстрее, чем javascript. Однако сегодня я сделал сравнительный тест script, чтобы сравнить скорость вычислений с плавающей запятой на двух языках, и результат потрясающий!

Javascript почти в 4 раза быстрее, чем С++!

Я разрешил обеим языкам выполнять одну и ту же работу на моем ноутбуке i5-430M, выполняя a = a + b за 100000000 раз. С++ занимает около 410 мс, а javascript занимает всего около 120 мс.

Я действительно не знаю, почему javascript может работать так быстро в этом случае. Может кто-нибудь объяснить это?

Код, который я использовал для javascript (run with nodejs):

(function() {
    var a = 3.1415926, b = 2.718;
    var i, j, d1, d2;
    for(j=0; j<10; j++) {
        d1 = new Date();
        for(i=0; i<100000000; i++) {
            a = a + b;
        }
        d2 = new Date();
        console.log("Time Cost:" + (d2.getTime() - d1.getTime()) + "ms");
    }
    console.log("a = " + a);
})();

И код для С++ (скомпилированный g++):

#include <stdio.h>
#include <ctime>

int main() {
    double a = 3.1415926, b = 2.718;
    int i, j;
    clock_t start, end;
    for(j=0; j<10; j++) {
        start = clock();
        for(i=0; i<100000000; i++) {
            a = a + b;
        }
        end = clock();
        printf("Time Cost: %dms\n", (end - start) * 1000 / CLOCKS_PER_SEC);
    }
    printf("a = %lf\n", a);
    return 0;
}

Ответы

Ответ 1

У меня могут быть плохие новости для вас, если вы находитесь в системе Linux (которая, по крайней мере, в этой ситуации соответствует POSIX). Вызов clock() возвращает количество тактов, потребляемых программой, и масштабируется CLOCKS_PER_SEC, что составляет 1,000,000.

Это означает, что если вы находитесь в такой системе, вы говорите в микросекундах для C и миллисекунд для JavaScript (согласно JS online docs). Таким образом, вместо того, чтобы JS был в четыре раза быстрее, С++ на самом деле в 250 раз быстрее.

Теперь может случиться так, что вы находитесь в системе, где CLOCKS_PER_SECOND - это нечто большее, чем миллион, вы можете запустить следующую программу в своей системе, чтобы увидеть, масштабируется ли она по тому же значению:

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

#define MILLION * 1000000

static void commaOut (int n, char c) {
    if (n < 1000) {
        printf ("%d%c", n, c);
        return;
    }

    commaOut (n / 1000, ',');
    printf ("%03d%c", n % 1000, c);
}

int main (int argc, char *argv[]) {
    int i;

    system("date");
    clock_t start = clock();
    clock_t end = start;

    while (end - start < 30 MILLION) {
        for (i = 10 MILLION; i > 0; i--) {};
        end = clock();
    }

    system("date");
    commaOut (end - start, '\n');

    return 0;
}

Вывод на моем ящике:

Tuesday 17 November  11:53:01 AWST 2015
Tuesday 17 November  11:53:31 AWST 2015
30,001,946

показывающий, что масштабный коэффициент равен миллиону. Если вы запустите эту программу или исследуете CLOCKS_PER_SEC, и это не коэффициент масштабирования в один миллион, вам нужно посмотреть на некоторые другие вещи.


Первый шаг - обеспечить, чтобы ваш код фактически оптимизировался компилятором. Это означает, например, установку -O2 или -O3 для gcc.

В моей системе с неоптимизированным кодом я вижу:

Time Cost: 320ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
Time Cost: 300ms
a = 2717999973.760710

и он в три раза быстрее с -O2, хотя и с немного иным ответом, хотя только примерно на одну миллионную долю процента:

Time Cost: 140ms
Time Cost: 110ms
Time Cost: 100ms
Time Cost: 100ms
Time Cost: 100ms
Time Cost: 100ms
Time Cost: 100ms
Time Cost: 100ms
Time Cost: 100ms
Time Cost: 100ms
a = 2718000003.159864

Это приведет к тому, что две ситуации вернутся друг к другу, что бы я ожидал, поскольку JavaScript не является некоторым интерпретированным зверьком, как в старые времена, где каждый токен интерпретируется всякий раз, когда он видит.

Современные движки JavaScript (V8, Rhino и т.д.) могут компилировать код в промежуточную форму (или даже на машинный язык), что может позволить производительности примерно равным с скомпилированными языками, такими как C.

Но, честно говоря, вы не склонны выбирать JavaScript или С++ для своей скорости, вы выбираете их для своих областей силы. Внутри браузеров не так много компиляторов C, и я не заметил много операционных систем и встроенных приложений, написанных на JavaScript.

Ответ 2

Выполняя быстрый тест с включением оптимизации, я получил результаты около 150 мс для древнего процессора AMD 64 X2 и около 90 мс для достаточно недавнего процессора Intel i7.

Затем я сделал немного больше, чтобы дать некоторое представление о одной причине, по которой вы, возможно, захотите использовать С++. Я развернул четыре итерации цикла, чтобы получить следующее:

#include <stdio.h>
#include <ctime>

int main() {
    double a = 3.1415926, b = 2.718;
    double c = 0.0, d=0.0, e=0.0;
    int i, j;
    clock_t start, end;
    for(j=0; j<10; j++) {
        start = clock();
        for(i=0; i<100000000; i+=4) {
            a += b;
            c += b;
            d += b;
            e += b;
        }
        a += c + d + e;
        end = clock();
        printf("Time Cost: %fms\n", (1000.0 * (end - start))/CLOCKS_PER_SEC);
    }
    printf("a = %lf\n", a);
    return 0;
}

Это позволяет коду С++ работать примерно на 44 мс на AMD (забыл запустить эту версию на Intel). Затем я включил автоинкрементатор компилятора (-Qpar с VС++). Это уменьшило время еще немного, до 40 мс на AMD и 30 мс на Intel.

Нижняя строка: если вы хотите использовать С++, вам действительно нужно научиться использовать компилятор. Если вы хотите получить действительно хорошие результаты, вы, вероятно, также захотите узнать, как лучше писать код.

Я должен добавить: я не пытался протестировать версию под Javascript с развернутым циклом. Это может обеспечить аналогичное (или, по крайней мере, некоторое) улучшение скорости в JS. Лично я считаю, что быстро сделать код намного интереснее, чем сравнивать Javascript с С++.

Если вы хотите, чтобы такой код работал быстро, разверните цикл (по крайней мере, на С++).

Поскольку возник вопрос о параллельных вычислениях, я подумал, что добавлю другую версию с помощью OpenMP. Пока я был на нем, я немного очистил код, поэтому я мог следить за тем, что происходит. Я также немного изменил временной код, чтобы отобразить общее время, а не время для каждого выполнения внутреннего цикла. Получившийся код выглядел так:

#include <stdio.h>
#include <ctime>

int main() {
    double total = 0.0;
    double inc = 2.718;
    int i, j;
    clock_t start, end;
    start = clock();

    #pragma omp parallel for reduction(+:total) firstprivate(inc)
    for(j=0; j<10; j++) {
        double a=0.0, b=0.0, c=0.0, d=0.0;
        for(i=0; i<100000000; i+=4) {
            a += inc;
            b += inc;
            c += inc;
            d += inc;
        }
        total += a + b + c + d;
    }
    end = clock();
    printf("Time Cost: %fms\n", (1000.0 * (end - start))/CLOCKS_PER_SEC);

    printf("a = %lf\n", total);
    return 0;
}

Основным дополнением здесь является следующая (правда, несколько тайная) линия:

#pragma omp parallel for reduction(+:total) firstprivate(inc)

Это говорит компилятору выполнить внешний цикл в нескольких потоках с отдельной копией inc для каждого потока и сложения отдельных значений total после параллельного раздела.

В результате получается то, что вы, вероятно, ожидаете. Если мы не включили OpenMP с флагом компилятора -openmp, сообщаемое время примерно в 10 раз больше, чем мы видели для отдельных исполнений ранее (409 мс для AMD, 323 MS для Intel). Когда OpenMP включен, время снижается до 217 мс для AMD и 100 мс для Intel.

Итак, на Intel первоначальная версия заняла 90 мс для одной итерации внешнего цикла. С этой версией мы получаем чуть больше (100 мс) для всех 10 итераций внешнего цикла - улучшение скорости около 9: 1. На машине с большим количеством ядер мы могли бы ожидать еще большего улучшения (OpenMP, как правило, автоматически использует все доступные ядра, хотя вы можете вручную настроить количество потоков, если хотите).