web-dev-qa-db-ja.com

JavaScriptがC ++より4倍速いように見えるのはなぜですか?

長い間、私はC++がJavaScriptよりも高速であると考えていました。しかし、今日、2つの言語の浮動小数点計算の速度を比較するベンチマークスクリプトを作成しました。結果は驚くべきものです。

JavaScriptはC++のほぼ4倍の速さです!

I5-430Mラップトップで両方の言語に同じ仕事をさせて、a = a + b 100,000,000回。 C++は約410ミリ秒かかりますが、JavaScriptは約120ミリ秒しかかかりません。

この場合、なぜJavaScriptが非常に高速に実行されるのか、私にはまったく分かりません。誰もそれを説明できますか?

JavaScriptに使用したコードは次のとおりです(Node.jsで実行)。

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

C++(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;
}
28
streaver91

Linuxシステム(少なくともこの状況ではPOSIXに準拠しています)を使用している場合、悪いニュースがあるかもしれません。 clock() 呼び出しは、プログラムによって消費され、CLOCKS_PER_SEC1,000,000)でスケーリングされたクロックティック数を返します。

つまり、このようなシステムで)であれば、マイクロ秒Cの場合、ミリ秒JavaScriptの場合( JSオンラインドキュメント に従って)。したがって、JSは4倍速くなるのではなく、C++は実際には250倍速くなります。

CLOCKS_PER_SECONDが100万以外のものであるシステムにいる場合は、システムで次のプログラムを実行して、同じ値でスケーリングされているかどうかを確認できます。

#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

スケーリング係数が100万であることを示しています。そのプログラムを実行するか、CLOCKS_PER_SECを調査し、それがnot100万のスケーリング係数である場合、他のことを調べる必要があります。


最初のステップは、コンパイラーによってコードが実際に最適化されていることを確認することです。これは、たとえば、gcc-O2または-O3を設定することを意味します。

最適化されていないコードを使用するシステムでは、次のように表示されます。

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では3倍速くなりますが、答えはわずかに異なりますが、百万分の1程度しかありません。

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は昔のように解釈される獣ではなく、各トークンが表示されるたびに解釈されるので、2つの状況は互いに同等に戻ります。

最新のJavaScriptエンジン(V8、Rhinoなど)は、コードを中間形式(またはマシン言語)にコンパイルできるため、Cなどのコンパイル言語とほぼ同等のパフォーマンスが得られる場合があります。

しかし、正直に言うと、JavaScriptまたはC++を選択する傾向はありません。その強みの領域に合わせて選択します。ブラウザ内には多くのCコンパイラが浮かんでおらず、JavaScriptで書かれた多くのオペレーティングシステムや組み込みアプリに気付いていません。

202
paxdiablo

最適化を有効にして簡単なテストを行ったところ、古代のAMD 64 X2プロセッサーで約150ミリ秒、かなり最近のIntel i7プロセッサーで約90ミリ秒の結果が得られました。

次に、C++を使用する理由の1つを少し説明しました。これを得るために、ループの4つの反復を展開しました。

#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;
}

これにより、AMD上でC++コードを約44msで実行できます(Intel上でこのバージョンを実行するのを忘れていました)。次に、コンパイラの自動ベクトライザー(VC++での-Qpar)をオンにしました。これにより、時間はさらに少し短縮され、AMDでは約40ミリ秒、Intelでは30ミリ秒になりました。

結論:C++を使用する場合、コンパイラーの使用方法を学ぶ必要があります。本当に良い結果を得たいなら、おそらくより良いコードを書く方法を学びたいと思うでしょう。

追加する必要があります:Javascriptでループを展開した状態でバージョンをテストしようとしませんでした。そうすることで、JSでも同様の(または少なくともいくつかの)速度の改善が得られる可能性があります。個人的には、JavascriptをC++と比較するよりも、コードを高速にする方がはるかに興味深いと思います。

このようなコードを高速に実行するには、ループを展開します(少なくともC++では)。

並列計算の主題が生じたので、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倍(AMDで409 ms、Intelで323 MS)です。 OpenMPをオンにすると、AMDでは217ミリ秒、Intelでは100ミリ秒になります。

そのため、Intelでは、元のバージョンは外部ループの1回の反復に90ミリ秒かかりました。このバージョンでは、外側のループの10回の繰り返しすべてでわずかに長くなります(100ミリ秒)-速度が約9:1向上します。より多くのコアを搭載したマシンでは、さらに改善が期待できます(OpenMPは通常、使用可能なすべてのコアを自動的に利用しますが、必要に応じてスレッド数を手動で調整できます)。

8
Jerry Coffin

これは二極化されたトピックなので、以下をご覧ください。

https://benchmarksgame-team.pages.debian.net/benchmarksgame/

あらゆる種類の言語のベンチマーク。

Javascript V8などは、例のように単純なループに対して確かに良い仕事をしており、おそらく非常によく似たマシンコードを生成しています。ほとんどの「ユーザーに近い」アプリケーションでは、確実にJavscriptの方が適していますが、より複雑なアルゴリズム/アプリケーションでは、メモリの浪費と避けられないパフォーマンスヒット(および制御の欠如)が何度もあります。

2
Raymund Hofmann