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

17 浏览
0 Comments

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

很长一段时间以来,我一直认为C++比JavaScript更快。然而,今天我编写了一个基准测试脚本来比较这两种语言中浮点数计算的速度,结果令人惊讶!\nJavaScript的速度几乎是C++的4倍!\n我在我的i5-430M笔记本上让这两种语言执行相同的任务,即执行a = a + b 100000000次。C++大约需要410毫秒,而JavaScript只需要大约120毫秒。\n我真的不知道为什么JavaScript在这种情况下运行得如此快。有人能解释一下吗?\n我用的JavaScript代码(在Node.js下运行)是:\n

(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("时间消耗:" + (d2.getTime() - d1.getTime()) + "毫秒");
    }
    console.log("a = " + a);
})();

\n而C++的代码(通过g++编译)是:\n

#include 
#include 
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("时间消耗:%d毫秒\n", (end - start) * 1000 / CLOCKS_PER_SEC);
    }
    printf("a = %lf\n", a);
    return 0;
}

0
0 Comments

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

尽管这篇文章很旧,但我认为增加一些信息可能很有趣。简而言之,您的测试过于模糊,可能存在偏见。

关于速度测试方法的一点说明

在比较两种语言的速度时,首先必须明确定义要比较它们在哪个方面的性能。

- “天真”与“优化”的代码:测试的代码是由初学者还是专家编写的。这个参数根据参与项目的人员而有所不同。例如,在与科学家(非极客)合作时,您更希望获得“天真”代码的性能,因为科学家不一定是优秀的程序员。

- 允许的编译时间:您是否认为允许代码编译很长时间还是不允许。这个参数可能因您的项目管理方法而有所不同。如果需要进行自动化测试,也许为了减少编译时间而稍微牺牲一些速度可能是有意义的。另一方面,您可以认为发布版本允许较长的构建时间。

- 平台可移植性:您的速度是否应在一个或多个平台上进行比较(Windows、Linux、PS4等)。

- 编译器/解释器的可移植性:您的代码的速度是否与编译器/解释器无关。对于多平台和/或开源项目可能很有用。

- 其他专业参数,例如是否允许在代码中进行动态分配,是否要启用插件(在运行时动态加载库)等。

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

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

C ++参数的快速概述

由于我对JavaScript了解不够,所以我只会展示代码优化和编译类型如何在固定循环中改变C ++的速度,希望能回答“为什么JavaScript看起来比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”之间。简而言之,此处创建的代码根本没有进行优化:存在大量的内存访问(没有正确使用寄存器),因此存在大量的冗余操作来存储和重新加载结果。

编译器优化

相同的C ++编译为gcc 9.2,并使用-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

汇编代码很简短,因为func的返回值在编译时已经计算出来,call函数只是简单地返回它。

当然,a = b * 100000将始终编译为高效的汇编代码,因此只有在需要探索所有这些临时变量的FP舍入误差时才编写重复添加循环。

CPU具有缓存和存储转发。循环内的存储/重新加载仅增加约5或6个周期的延迟,而不是1000倍的速度。

这对于启用优化以及使编译器优化掉循环的有用点有一些帮助,但请参考关于这个问题的顶级答案:OP的clock()使用错误,并且他们在测试的JS和C ++实现上实际上C ++实际上比JS快250倍。

感谢编辑,我将RAM与速度性能进行了比较(因为真正的随机访问缓存很少有性能不佳的机会),但这引起了一个大误解。我认为在我的意见中更好地编辑它。编辑:我没有看到自己的拼写错误有多少(发表时没有时间重新阅读),所以非常感谢您进行更正。

是的,缓存未命中会产生影响,但是由于-O0引入的额外重新加载或溢出/重新加载将始终是对您已经刚刚访问的对象或堆栈的重新访问。通常可以认为堆栈在缓存中是热的,因为有调用/返回。在优化的代码中已经发生了对对象的初始访问,只是避免了进一步的访问。除了在缓存冲突缺失的罕见情况下,它错误地描述了使用-O0的成本,除非是关于访问DRAM的情况。

您绝对是对的!我今天才意识到我以前没有意识到的一些明显的事情!

对于编译时间与优化之间的权衡,在自动化测试中,您通常希望使用-O1-Og进行快速编译,不比低效的-O0构建慢多少,但可以进行基本的操作,如寄存器分配(但不包括内联)。集成/单元测试不一定需要使用与完整发布构建相同的构建选项。

我的观点可能不够清楚,是关于速度要求的上下文:我们只测试关于发布构建的速度,编译时间几乎是无限的,还是在构建过程中仍然需要快速编译,甚至是调试?我并不一定是在谈论单元/集成测试,而是更多关于系统测试。不过,有些语言允许构造(例如C ++模板,它是图灵完备的)最终导致更长的编译时间,而不管编译优化如何。有些语言比其他语言更难解析/编译等等。

是的,当然,大多数情况下,“一次构建,多次运行”的总CPU时间比-O3 -flto花费的时间少。可能的例外包括进行大量数值计算的小程序。如果将CPU时间花费在节省相同质量的位数上,这些位数将在许多下载或在存储上长期保留文件时摊销。

0
0 Comments

为什么JavaScript的运行速度看起来比C++快4倍?

在进行了一次简单的测试后,我得到了以下结果:对于一台古老的AMD 64 X2处理器,大约需要150毫秒,而对于一台相对较新的Intel i7处理器,大约需要90毫秒。

然后,我做了一些改进,以展示为什么你可能想要使用C++。我展开了循环的四次迭代,得到以下结果:

#include 
#include 
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++代码的运行速度约为44毫秒(忘记在Intel上运行这个版本)。然后,我打开了编译器的自动向量化功能(使用VC++的-Qpar选项)。这进一步减少了时间,使得在AMD上约为40毫秒,在Intel上约为30毫秒。

要点是:如果你想使用C++,你确实需要学习如何使用编译器。如果你想得到非常好的结果,你可能还想学习如何编写更好的代码。

我应该补充一点:我没有尝试在JavaScript中测试展开了循环的版本。这样做可能会在JS中提供类似的(或至少一些)速度提升。就个人而言,我认为使代码运行更快比比较JavaScript和C++更有趣。

如果你想让这样的代码运行得更快,展开循环(至少在C++中)。

由于涉及到并行计算,我想再添加一个使用OpenMP的版本。在这个过程中,我还稍微整理了代码,以便更好地跟踪情况。我还稍微修改了计时代码,以显示整体时间,而不是每个内部循环执行的时间。最终的代码如下所示:

#include 
#include 
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毫秒,Intel为323毫秒)。打开了OpenMP后,时间分别降至217毫秒(AMD)和100毫秒(Intel)。

因此,在Intel上,原始版本的外部循环迭代一次需要90毫秒。而在这个版本中,对于外部循环的10次迭代,我们只需要略微更长的时间(100毫秒)--速度提高了约9倍。在具有更多核心的机器上,我们可以期望得到更多的改进(OpenMP通常会自动利用所有可用的核心,但如果需要,您也可以手动调整线程数)。

: 是和不是 - 它仍然在单个核心上执行。通过一些额外的工作(例如一些OpenMP指令),我们可以让它在多个核心上执行,从而再次有效地提高速度。然而,到目前为止,我只是让它更好地利用了单个核心的资源(暴露出指令级并行性,而不是线程级并行性)。

gcc6及更高版本会注意到它们可以将de从循环中优化掉,并将c+d+e计算为c+c+cgodbolt.org/g/1BLDfX(或使用FMA,将fma(c, 2.0, c) = c*2.0 + c。如果这是合法的话,c*3.0也将是合法的...)无论如何,只有两个addsd在循环中(对于ac),这在具有延迟:吞吐量比大于2的CPU上(例如Sandybridge的3:1,Skylake的4:0.5)更适合超线程。顺便说一下,clang使用128位向量进行自动向量化。我认为它可能也在做同样的事情,将a与以0.0开头的3个向量分离开来。

顺便说一下,Athlon X2是一个K10核心,我认为。或者可能是K8,无论哪种方式,addsd的延迟为4,吞吐量为每个时钟1个,所以4个累加器刚好可以隐藏FP加法的延迟。

: 是的,我相信如果你想在现代Intel上获得更好的性能,你需要展开更多的内部循环迭代(大约8个左右,如果我没记错的话)。稍微麻烦一些,但应该可以将速度大致提高一倍。

事实证明,即使是刚好够用的FP累加器,在数据来自内存时仍然比更多累加器略慢一些。所以我猜测,当有这么多并行依赖链时,微操作调度并没有做得很完美。可能在这种情况下,当没有任何缓存缺失,只有寄存器时,调度会做得更好。无论如何,是的,这样的东西是AVX512将体系结构向量寄存器数量增加一倍的主要原因之一。

: 如果我有足够的雄心,去尝试使用半打的迭代次数,但是当我已经将速度提高了约9倍时,可能不值得花费更多的时间和精力来进一步改进。

: 是的,这个具体案例不需要更多的测试;一般的思路已经足够了:展开归约,直到你的循环在FP吞吐量上达到瓶颈,而不是延迟。对于任何实际热点循环,可以使用agner.org/optimize来查看其他CPU的延迟:吞吐量比,如果你使用的是FP添加吞吐量较低的处理器(例如Intel Skylake)。

: 另一方面,展开4次以上的迭代并不需要太多的工作。快速测试展开8次内部循环迭代显示,它们以大约49毫秒的速度运行(在Haswell上)。无论如何,Athlon X2已经过时了(我最接近的是Steamroller,似乎至少从内部循环展开8倍的改进中受益)。:-)

: Steamroller的ADDSD吞吐量为每个时钟1个,延迟为5个周期,K10为4个周期。(或者如果输入不来自FP加/减/乘(FMA单元),则为6个周期。大多数CPU只在整数/FP之间有旁路延迟,但是Bulldozer系列在FMA域内有一个特殊的快速路径来进行转发。)

使用Intel Q6600,这个测试显示C++需要120毫秒,JavaScript需要1300毫秒。原始版本的测试在C++和JavaScript中都显示约为380毫秒。

0
0 Comments

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

如果你在一个符合POSIX标准的Linux系统上,你要接受一个坏消息。clock()函数返回程序消耗的时钟滴答数,并且按照CLOCKS_PER_SEC进行缩放,而CLOCKS_PER_SEC的值是1000000。

这意味着,如果你在这样的系统上,C++的计时单位是微秒,而JavaScript的计时单位是毫秒(根据JS在线文档)。所以,实际上C++比JavaScript快250倍。

也许你的系统的CLOCKS_PER_SECOND不是一百万,你可以在你的系统上运行下面的程序,看看它是否以相同的值进行缩放:

#include 
#include 
#include 
#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并且它不是一百万的缩放因子,你需要看看其他一些东西。

第一步是确保你的代码实际上已经被编译器进行了优化。这意味着,例如,对于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时,速度会快三倍,尽管答案略有不同,但只有百万分之一的差距:

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编写的。

我认为这不是事实,400毫秒是很容易感觉到的。输出看起来比JavaScript真的慢。

可能是启动和关闭进程需要那么长的时间。我会更新以显示“证据”。

我的原始脚本的时间成本将在每个大循环(总共10个大循环)之后打印到屏幕上。每个循环的时间对于C++来说是400毫秒,对于JavaScript来说是100毫秒,这对我来说已经足够感受到差异了。

不是感受,是测量!感受可能对于提出假设来说是好的,但对于评估假设来说是没有用的 🙂 无论如何,在程序外打印时间包含了你正在测量的内容之外的东西(比如前面提到的进程启动/关闭)。

如果你真的想使用你的那种方法,将循环增加100倍,这样“感受”和“测量”就开始接近了。除非你的C程序然后需要运行40秒,否则你可以将其归结为测量非常小的时间的困难。

我敢打赌,时钟在我的计算机上返回的是毫秒。我将内部循环更改为1000000000,是原始值的十倍。每个内部循环的时间是4秒,每个内部循环的输出大约是4100毫秒。所以要么打印方法需要4秒,循环需要4.1毫秒,要么循环确实需要4100毫秒。打印方法不可能需要这么长的时间,因为在之前的情况下,每个循环,包括打印,只需要大约半秒钟。

我建议你去检查一下CLOCKS_PER_SEC的值。这将是最终的答案。并告诉我们你使用的操作系统是哪个。

我不是怀疑你。但是这一次,我打印出了常量值CLOCKS_PER_SEC,它是1000。也许我们使用的是不同的平台。

没关系,这意味着你的平台不是POSIX。我会更新答案。

如果你有C++11,就使用库,没有理由使用依赖于CLOCKS_PER_SEC的测量方法(特别是如果在比较时没有考虑到这一点)。

为什么有人会"受伤"呢?问题是关于为什么JavaScript看起来比C++快(显然这不应该是这样)。这个答案解释了最有可能的原因。这个社区已经因为嘲笑人们提问而蒙羞,这对于一个由社区驱动的问答网站来说是相当尴尬的。

0