为什么JavaScript看起来比C++快4倍?

str*_*r91 29 javascript c++ floating-point performance benchmarking

很长一段时间以来,我一直认为C++比JavaScript更快.然而,今天我制作了一个基准脚本来比较两种语言中浮点计算的速度,结果令人惊叹!

JavaScript似乎比C++快4倍!

我让这两种语言在我的i5-430M笔记本电脑上做同样的工作,执行a = a + b了1亿次.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);
})();
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

pax*_*blo 209

如果您使用的是Linux系统(至少在这种情况下符合POSIX),我可能会有一些坏消息.该clock()调用返回程序消耗的时钟周期数并按比例缩放CLOCKS_PER_SEC,即1,000,000.

这意味着,如果你这样一个系统,你在谈论微秒为C和毫秒为JavaScript(按照JS在线文档).因此,不是JS快四倍,而C++实际上要快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;
}
Run Code Online (Sandbox Code Playgroud)

我的盒子上的输出是:

Tuesday 17 November  11:53:01 AWST 2015
Tuesday 17 November  11:53:31 AWST 2015
30,001,946
Run Code Online (Sandbox Code Playgroud)

显示缩放因子是一百万.如果您运行该程序,或者调查CLOCKS_PER_SEC并且它不是一百万的缩放因子,您需要查看其他一些内容.


第一步是确保编译器实际优化代码.这意味着,例如,设置-O2-O3gcc.

在我的系统中,未经优化的代码,我看到:

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
Run Code Online (Sandbox Code Playgroud)

它的速度提高了三倍-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
Run Code Online (Sandbox Code Playgroud)

这将使两种情况相互恢复,这是我所期待的,因为JavaScript不像过去那样被解释为野兽,每当它被看到时,每个标记都被解释.

现代JavaScript引擎(V8,Rhino等)可以将代码编译为中间形式(甚至是机器语言),这可能使性能与C等编译语言大致相同.

但是,说实话,你不倾向于选择JavaScript或C++来提高速度,你可以根据它们的强度来选择它们.浏览器内部没有很多C编译器,我没有注意到许多操作系统或用JavaScript编写的嵌入式应用程序.

  • 人.你救了我们! (44认同)
  • @ user2189264,感觉不到,措施!感觉可能很好_start_一个假设,但它在评估它是不好的:-)在任何情况下,程序之外的打印时间被称为_includes_超出你测量的东西(例如上述过程启动/关闭) ). (14认同)
  • 有人怎么样“__REKT__”?问题是为什么 JS 看起来比 C++ 快(显然暗示它不应该)。这个答案解释了最可能的原因。社区已经因嘲笑人们提问而闻名,这对于社区驱动的问答网站来说非常尴尬。 (6认同)
  • @ user2189264:叹息......如果你有权访问C++ 11,只需使用`<chrono>` - http://solarianprogrammer.com/2012/10/14/cpp-11-timing-code-performance/ - 没有理由使用"CLOCKS_PER_SEC"相关的测量(特别是如果在比较时不考虑这种依赖性......). (4认同)
  • 我认为事实并非如此,400ms 是很容易感觉到的东西。输出看起来真的比 javascript 慢。 (2认同)
  • 我的意思是我的原始脚本的时间成本将在每个大循环(总共 10 个大循环)之后打印到屏幕上。并且每个循环的时间对于 c++ 来说是 400 毫秒,对于 javascript 来说是 100 毫秒,这些时间已经足够让我感受到差异了。 (2认同)
  • @paxdiablo,我不是要怀疑你。但这一次,我打印出常量值CLOCKS_PER_SEC,它是1000。可能我们使用了不同的平台。 (2认同)

Jer*_*fin 8

通过开启优化进行快速测试,我得到了一个古老的AMD 64 X2处理器大约150毫秒的结果,以及一个合理的最新英特尔i7处理器大约90毫秒的结果.

然后我做了一些更多的事情来了解你可能想要使用C++的一个原因.我展开了循环的四次迭代,得到这个:

#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;
}
Run Code Online (Sandbox Code Playgroud)

这让C++代码在AMD上运行大约44ms(忘了在Intel上运行这个版本).然后我打开了编译器的自动矢量化器(-Vpar with VC++).这样可以将时间进一步缩短,在AMD上大约40毫秒,在英特尔上大约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;
}
Run Code Online (Sandbox Code Playgroud)

这里的主要补充是以下(当然有些神秘)线:

#pragma omp parallel for reduction(+:total) firstprivate(inc)
Run Code Online (Sandbox Code Playgroud)

这告诉编译器在多个线程中执行外部循环,inc每个线程都有一个单独的副本,并将total并行部分之后的各个值相加.

结果是关于你可能期望的.如果我们不使用编译器的-openmp标志启用OpenMP ,则报告的时间大约是我们之前针对单个执行所看到的时间的10倍(AMD为409毫秒,英特尔为323 MS).打开OpenMP后,AMD的时间降至217毫秒,英特尔的时间降至100毫秒.

因此,在Intel上,原始版本在外循环的一次迭代中花费了90ms.对于这个版本,我们对外循环的所有10次迭代只需稍微长一点(100 ms) - 速度提高约9:1.在具有更多内核的计算机上,我们可以期待更多的改进(OpenMP通常会自动利用所有可用的内核,但您可以根据需要手动调整线程数).

  • @ user2189264:是和否 - 它仍在单核中执行.通过更多的工作(例如一些openMP指令),我们可以让它在多个内核上执行,从而有效地再次提高速度.到目前为止,我所做的只是让它更好地利用单核上的资源(暴露的指令级并行,而不是线程级并行). (2认同)

Ray*_*ann 6

这是一个两极分化的话题,所以你可以看看:

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

对各种语言进行基准测试。

Javascript V8 等对于简单循环肯定做得很好,如示例中所示,可能生成非常相似的机器代码。对于大多数“接近用户”的应用程序,Javscript 肯定是更好的选择,但请记住,对于更复杂的算法/应用程序,内存浪费和多次不可避免的性能下降(和缺乏控制)。


Fel*_*oni 6

即使帖子很旧,我认为添加一些信息可能会很有趣。总而言之,您的测试过于模糊,可能存在偏见。

关于速度测试方法的一些知识

在比较两种语言的速度时,您首先必须准确定义要在哪个上下文中比较它们的性能。

  • “朴素”与“优化”代码:无论测试的代码是由初学者还是专家程序员编写的。此参数的重要性取决于谁将参与您的项目。例如,当与科学家(非极客)一起工作时,您会更多地寻找“天真的”代码性能,因为科学家并不是强行成为优秀的程序员。

  • 授权编译时间:您是否认为允许代码长时间构建。该参数可能很重要,具体取决于您的项目管理方法。如果您需要进行自动化测试,也许牺牲一点速度来减少编译时间可能会很有趣。另一方面,您可以认为分发版本允许大量的构建时间。

  • 平台可移植性:是否要在一个或多个平台(Windows、Linux、PS4...)上比较您的速度

  • 编译器/解释器可移植性:代码的速度是否独立于编译器/解释器。对于多平台和/或开源项目很有用。

  • 其他专用参数,例如,如果您允许在代码中动态分配,如果您想启用插件(在运行时动态加载库)等。

然后,您必须确保您的代码能够代表您想要测试的内容

在这里,(我假设您没有使用优化标志编译 C++),您正在测试“天真的”(实际上并不那么天真)代码的快速编译速度。因为您的循环是固定大小的,具有固定数据,所以您不测试动态分配,并且您应该允许代码转换(下一节将详细介绍)。实际上,在这种情况下,JavaScript 的性能通常比 C++ 更好,因为 JavaScript 默认在编译时进行优化,而 C++ 编译器需要被告知进行优化。

使用参数提高 C++ 速度的快速概述

因为我对 JavaScript 的了解不够,所以我只会展示代码优化和编译类型如何在固定的 for 循环上改变 C++ 的速度,希望它能回答“JS 如何看起来比 C++ 更快?”的问题。

为此,让我们使用 Matt Godbolt 的 C++编译器资源管理器来查看 gcc9.2 生成的汇编代码

未优化的代码

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

编译:gcc 9.2,标志 -O0。产生以下汇编代码:

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

Run Code Online (Sandbox Code Playgroud)

循环的代码位于“.L3”和“.L2”之间。简而言之,我们可以看到这里创建的代码根本没有优化:进行了大量内存访问(没有正确使用寄存器),因此存在大量浪费的存储和重新加载结果的操作。

在现代 x86 CPU 上,这将额外5 或 6 个周期的存储转发延迟引入到 FP 添加的关键路径依赖链中a。这是在 4 或 5 个周期延迟的基础上addss,使函数速度慢了一倍多。

编译器优化

使用 gcc 9.2 编译的相同 C++,标志 -O3。生成以下汇编代码:

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

代码更加简洁,并且尽可能使用寄存器。

代码优化

编译器通常可以很好地优化代码,尤其是 C++,因为代码清楚地表达了程序员想要实现的目标。这里我们希望固定的数学表达式尽可能快,所以让我们稍微改变一下代码。

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();
}
Run Code Online (Sandbox Code Playgroud)

我们在函数中添加了一个 constexpr 来告诉编译器在编译时尝试计算它的结果。并添加了一个调用函数以确保它会生成一些代码。

使用 gcc 9.2, -O3 编译,生成以下汇编代码:

call():
        movss   .LC0(%rip), %xmm0
        ret
.LC0:
        .long   1216623031
Run Code Online (Sandbox Code Playgroud)

asm 代码很短,因为 func 返回的值已经在编译时计算出来,而 call 只是返回它。


当然,a = b * 100000总是会编译为高效的 asm,因此如果您需要探索所有这些临时变量的 FP 舍入错误,则仅编写重复添加循环。