Why does JavaScript appear to be 4 times faster than C++?

前端 未结 5 1806
长发绾君心
长发绾君心 2021-01-29 22:42

For a long time, I had thought of C++ being faster than JavaScript. However, today I made a benchmark script to compare the speed of floating point calculations in the two langu

相关标签:
5条回答
  • 2021-01-29 23:22

    Doing a quick test with turning on optimization, I got results of about 150 ms for an ancient AMD 64 X2 processor, and about 90 ms for a reasonably recent Intel i7 processor.

    Then I did a little more to give some idea of one reason you might want to use C++. I unrolled four iterations of the loop, to get this:

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

    This let the C++ code run in about 44ms on the AMD (forgot to run this version on the Intel). Then I turned on the compiler's auto-vectorizer (-Qpar with VC++). This reduced the time a little further still, to about 40 ms on the AMD, and 30 ms on the Intel.

    Bottom line: if you want to use C++, you really need to learn how to use the compiler. If you want to get really good results, you probably also want to learn how to write better code.

    I should add: I didn't attempt to test a version under Javascript with the loop unrolled. Doing so might provide a similar (or at least some) speed improvement in JS as well. Personally, I think making the code fast is a lot more interesting than comparing Javascript to C++.

    If you want code like this to run fast, unroll the loop (at least in C++).

    Since the subject of parallel computing arose, I thought I'd add another version using OpenMP. While I was at it, I cleaned up the code a little bit, so I could keep track of what was going on. I also changed the timing code a bit, to display the overall time instead of the time for each execution of the inner loop. The resulting code looked like this:

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

    The primary addition here is the following (admittedly somewhat arcane) line:

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

    This tells the compiler to execute the outer loop in multiple threads, with a separate copy of inc for each thread, and adding together the individual values of total after the parallel section.

    The result is about what you'd probably expect. If we don't enable OpenMP with the compiler's -openmp flag, the reported time is about 10 times what we saw for individual executions previously (409 ms for the AMD, 323 MS for the Intel). With OpenMP turned on, the times drop to 217 ms for the AMD, and 100 ms for the Intel.

    So, on the Intel the original version took 90ms for one iteration of the outer loop. With this version we're getting just slightly longer (100 ms) for all 10 iterations of the outer loop -- an improvement in speed of about 9:1. On a machine with more cores, we could expect even more improvement (OpenMP will normally take advantage of all available cores automatically, though you can manually tune the number of threads if you want).

    0 讨论(0)
  • JS of any popular runtime is compiled in C++, so like you probably can't get it to run faster than equivalent native code ... you can prove it by induction by counting from 1 by 1 to google if you want

    0 讨论(0)
  • 2021-01-29 23:37

    I may have some bad news for you if you're on a Linux system (which complies with POSIX at least in this situation). The clock() call returns number of clock ticks consumed by the program and scaled by CLOCKS_PER_SEC, which is 1,000,000.

    That means, if you're on such a system, you're talking in microseconds for C and milliseconds for JavaScript (as per the JS online docs). So, rather than JS being four times faster, C++ is actually 250 times faster.

    Now it may be that you're on a system where CLOCKS_PER_SECOND is something other than a million, you can run the following program on your system to see if it's scaled by the same value:

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

    The output on my box is:

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

    showing that the scaling factor is a million. If you run that program, or investigate CLOCKS_PER_SEC and it's not a scaling factor of one million, you need to look at some other things.


    The first step is to ensure your code is actually being optimised by the compiler. That means, for example, setting -O2 or -O3 for gcc.

    On my system with unoptimised code, I see:

    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
    

    and it's three times faster with -O2, albeit with a slightly different answer, though only by about one millionth of a percent:

    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
    

    That would bring the two situations back on par with each other, something I'd expect since JavaScript is not some interpreted beast like in the old days, where each token is interpreted whenever it's seen.

    Modern JavaScript engines (V8, Rhino, etc) can compile the code to an intermediate form (or even to machine language) which may allow performance roughly equal with compiled languages like C.

    But, to be honest, you don't tend to choose JavaScript or C++ for its speed, you choose them for their areas of strength. There aren't many C compilers floating around inside browsers and I've not noticed many operating systems nor embedded apps written in JavaScript.

    0 讨论(0)
  • 2021-01-29 23:41

    Even if the post is old, I think it may be interesting to add some information. In summary, your test is too vague and may be biased.

    A bit about speed testing methodology

    When comparing speed of two languages, you first have to define precisely in which context you want to compare how they perform.

    • "naive" vs "optimized" code : whether or not code tested is made by a beginner or expert programmer. This parameter matter matter depending on who will participate in your project. For example, when working with scientists (non geeky ones), you will look more for "naive" code performance, because scientists aren't forcibly good programmers.

    • authorized compile time : whether you consider you allow the code to build for long or not. This parameter can matter depending on your project management methodology. If you need to do automated tests, maybe trading a bit of speed to increase compile time can be interesting. On the other hand, you can consider that distribution version is allowing a high amount of building time.

    • Platform portability : if your speed shall be compared on one platform or more (Windows, Linux, PS4...)

    • Compiler/interpreter portability : if your code's speed shall be compiler/interpreter independent or not. Can be useful for multiplatform and/or open source projects.

    • Other specialized parameters, as for example if you allow dynamic allocations in your code, if you want to enable plugins (dynamically loaded library at runtime) etc.

    Then, you have to make sure that your code is representative of what you want to test

    Here, (I assume you didn't compiled C++ with optimization flags), you are testing fast-compile speed of "naive" (not so naive actually) code. Because your loop is fixed size, with fixed data, you don't test dynamic allocations, and you -supposedly- allow code transformations (more on that in the next section). And effectively, JavaScript performs usually better than C++ in this case, because JavaScript optimizes at compile time by default, while C++ compilers needs to be told to optimize.

    A quick overview of C++ speed increase with parameters

    Because I am not knowledgeable enough about JavaScript, I'll only show how code optimization and compilation type can change c++ speed on a fixed for loop, hoping it will answer the question on "how JS can appear to be faster than C++ ?"

    For that let's use Matt Godbolt's C++ compiler explorer to see the assembly code generated by gcc9.2

    Non optimized code

    float func(){
        float a(0.0);
        float b(2.71);
        for (int i = 0;  i < 100000; ++i){
            a = a + b;
        }
        return a;
    }
    

    compiled with : gcc 9.2, flag -O0. Produces the following assembly code :

    func():
            pushq   %rbp
            movq    %rsp, %rbp
            pxor    %xmm0, %xmm0
            movss   %xmm0, -4(%rbp)
            movss   .LC1(%rip), %xmm0
            movss   %xmm0, -12(%rbp)
            movl    $0, -8(%rbp)
    .L3:
            cmpl    $99999, -8(%rbp)
            jg      .L2
            movss   -4(%rbp), %xmm0
            addss   -12(%rbp), %xmm0
            movss   %xmm0, -4(%rbp)
            addl    $1, -8(%rbp)
            jmp     .L3
    .L2:
            movss   -4(%rbp), %xmm0
            popq    %rbp
            ret
    .LC1:
            .long   1076719780
    
    

    The code for the loop is what is between ".L3" and ".L2". To be quick, we can see that the code created here is not optimized at all : a lot of memory access are made (no proper use of registers), and because of this there are a lot of wasted operations storing and reloading the result.

    This introduces an extra 5 or 6 cycles of store-forwarding latency into the critical path dependency chain of FP addition into a, on modern x86 CPUs. This is on top of the 4 or 5 cycle latency of addss, making the function more than twice as slow.

    compiler optimization

    The same C++ compiled with gcc 9.2, flag -O3. Produces the following assembly code:

    func():
            movss   .LC1(%rip), %xmm1
            movl    $100000, %eax
            pxor    %xmm0, %xmm0
    .L2:
            addss   %xmm1, %xmm0
            subl    $1, %eax
            jne     .L2
            ret
    .LC1:
            .long   1076719780
    

    The code is much more concise, and uses registers as much as possible.

    code optimization

    A compiler optimizes code very well usually, especially C++, given that the code is expressing clearly what the programmer wants to achieve. Here we want a fixed mathematical expression to be as fast a possible, so let's change the code a bit.

    constexpr float func(){
        float a(0.0);
        float b(2.71);
        for (int i = 0;  i < 100000; ++i){
            a = a + b;
        }
        return a;
    }
    
    float call() {
        return func();
    }
    

    We added a constexpr to the function to tell the compiler to try to compute it's result at compile time. And added a calling function to be sure that it will generate some code.

    Compiled with gcc 9.2, -O3, leads to following assembly code :

    call():
            movss   .LC0(%rip), %xmm0
            ret
    .LC0:
            .long   1216623031
    

    The asm code is short, since the value returned by func has been computed at compile time, and call simply returns it.


    Of course, a = b * 100000 would always compile to efficient asm, so only write the repeated-add loop if you need to explore FP rounding error over all those temporaries.

    0 讨论(0)
  • 2021-01-29 23:45

    This is a polarizing topic, so one may have a look at:

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

    Benchmarking all kinds of languages.

    Javascript V8 and such are surely doing a good job for simple loops as in the example, probably generating very similar machine code. For most "close to the user" applications Javscript surely is the better choice, but keep in mind the memory waste and the many times unavoidable performance hit (and lack of control) for more complicated algorithms/applications.

    0 讨论(0)
提交回复
热议问题