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, как правило, автоматически использует все доступные ядра, хотя вы можете вручную настроить количество потоков, если хотите).