50

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

JavaScript 似乎比 C++ 快 4 倍!

我让这两种语言在我的 i5-430M 笔记本电脑上做同样的工作,执行a = a + b了 100000000 次。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;
}
4

5 回答 5

278

如果您使用的是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;
}

我的盒子上的输出是:

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

显示比例因子为一百万。如果您运行该程序或进行调查CLOCKS_PER_SEC并且它不是一百万的比例因子,您需要查看其他一些东西。


第一步是确保您的代码实际上正在被编译器优化。这意味着,例如,设置-O2-O3for 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++,而是因为它们的优势领域而选择它们。浏览器中没有很多 C 编译器,我也没有注意到很多操作系统和用 JavaScript 编写的嵌入式应用程序。

于 2013-06-11T03:42:45.033 回答
9

在开启优化的情况下进行快速测试,我得到一个古老的 AMD 64 X2 处理器大约 150 毫秒的结果,而一个相当新的 Intel 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;
}

这让 C++ 代码在 AMD 上运行大约 44 毫秒(忘了在 Intel 上运行这个版本)。然后我打开了编译器的自动矢量化器(-Qpar 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;
}

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

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

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

结果与您可能期望的结果差不多。如果我们不使用编译器的-openmp标志启用 OpenMP,则报告的时间大约是我们之前看到的单个执行的 10 倍(AMD 为 409 毫秒,英特尔为 323 毫秒)。打开 OpenMP 后,AMD 的时间下降到 217 毫秒,而英特尔的时间下降到 100 毫秒。

因此,在 Intel 上,原始版本的外循环一次迭代需要 90 毫秒。在这个版本中,我们在外循环的所有 10 次迭代中都得到了稍微长一点的时间(100 毫秒)——速度提高了大约 9:1。在具有更多内核的机器上,我们可以期待更多改进(OpenMP 通常会自动利用所有可用内核,但您可以根据需要手动调整线程数)。

于 2013-06-11T04:14:24.857 回答
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;
}

编译: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

循环的代码是“.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

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

代码优化

编译器通常可以很好地优化代码,尤其是 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();
}

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

使用 gcc 9.2 编译,-O3,导致以下汇编代码:

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

asm 代码很短,因为 func 返回的值是在编译时计算的,而 call 只是简单地返回它。


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

于 2019-11-08T13:21:59.497 回答
5

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

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

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

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

于 2018-02-08T08:46:54.427 回答
-3

任何流行运行时的 JS 都是用 C++ 编译的,所以就像你可能无法让它比等效的本机代码运行得更快......如果你愿意,你可以通过从 1 到 google 的归纳来证明它

于 2020-04-03T15:40:01.667 回答