当前位置: 首页 > 知识库问答 >
问题:

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

朱鸿畅
2023-03-14

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

JavaScript似乎比C快近4倍!

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

共有3个答案

郭兴文
2023-03-14

即使帖子很旧,我想补充一些信息可能会很有意思。综上所述,你的测试太模糊,可能会有偏差。

在比较两种语言的速度时,首先必须精确地定义要比较它们的执行情况的上下文。

>

  • “幼稚”与“优化”代码:无论测试的代码是否由初学者或专家程序员编写。此参数很重要,具体取决于谁将参与您的项目。例如,在与科学家(非极客科学家)合作时,你会更多地寻找“天真”的代码性能,因为科学家不是强行的优秀程序员。

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

    平台可移植性:如果您的速度需要在一个或多个平台上进行比较(Windows、Linux、PS4…)

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

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

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

    在这里,(我假设您没有用优化标志编译C),您正在测试“天真”(实际上并不那么天真)代码的快速编译速度。因为循环的大小是固定的,数据也是固定的,所以您不需要测试动态分配,而且您应该允许代码转换(下一节将对此进行详细介绍)。实际上,在这种情况下,JavaScript的性能通常比C好,因为JavaScript默认在编译时进行优化,而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 CPUs上的< code>a中的FP加法的关键路径依赖链中引入了额外的5或6个周期的存储转发延迟。除了< code>addss的4或5个周期延迟之外,该函数的速度慢了两倍多。

    编译器优化

    同样用gcc 9.2编译的C,flag -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
    

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

    当然,< code>a = b * 100000将始终编译为高效的asm,因此,如果您需要研究所有这些临时变量的FP舍入误差,只需编写重复加法循环。

  • 公子昂
    2023-03-14

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

    然后,我做了更多的工作,给出了一个您可能想要使用C的原因。我展开了四次循环html" target="_blank">迭代,得到了以下结果:

    #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上运行大约44ms(忘记在Intel上运行这个版本)。然后我打开编译器的自动矢量化器(-Qpar with VC)。这将时间进一步缩短了一点,在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 副本,并将并行部分之后的单个总值相加。

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

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

    景修杰
    2023-03-14

    如果您使用的是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,它不是一百万的缩放因子,你需要看一些其他的东西。

    第一步是确保你的代码确实被编译器优化了。这意味着,例如,为< code>gcc设置< code>-O2或< code>-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编写的嵌入式应用程序。

     类似资料:
    • 为什么比快?我使用的是CPython 3.5.2。 我试着改变我提升的幂,看看它是怎么做的,例如,如果我提升x的10或16的幂,它会从30跳到35,但如果我提升10.0作为浮动,它只是在24.1~4左右移动。 我想这和浮点转换和2次方有关,但我真的不知道。

    • 问题内容: 示例代码在这里 问题答案: 我认为速度更快,因为使用矢量化方式和熊猫构建在此数组上。 慢,因为它使用。 操作是最快的,然后是。 请参阅此答案,并更好地解释pandas开发人员。

    • 问题内容: 我不知道为什么numba在这里击败numpy(超过3倍)。我在这里进行基准测试时是否犯了一些根本性的错误?对于numpy来说似乎是完美的情况,不是吗?请注意,作为检查,我还运行了一个结合了numba和numpy的变体(未显示),正如预期的那样,它与不带numba的numpy运行相同。 (顺便说一下,这是一个后续问题:数字处理二维数组的最快方法:dataframe vs series v

    • 问题内容: 如果我声明并看看,它不会给我。 因此,我必须使用以下重复(因而很糟糕)的样式构造: 例如,如果我想获得利润,是否真的需要使用它? 是我对Android或Java的误解,还是两者兼而有之? 问题答案: 我认为您对“ LayoutParams”的理解不正确。视图(或布局)必须是“父视图的LayoutParams”的实例。 例如,这是RelativeLayout中的LinearLayout。

    • 问题内容: 为了在工作中进行演示,我想比较NodeJS和C的性能。这是我写的: Node.js(for.js): 我使用GCC编译for.c并运行它: 结果: 然后我在NodeJS中尝试了它: 结果: 在运行了无数次之后,我发现无论如何它都是成立的。如果我将for.c切换double为long在循环中使用a而不是a ,则C花费的时间甚至更长! 不是试图发动火焰战争,但是为什么执行相同操作的Node

    • 我看了几个教程,他们所有的列表首选对话框都是这样的。 列表偏好对话框 但我的对话看起来像这样 我的对话 知道为什么我的对话框看起来不一样吗?我查看了教程,我的xml代码看起来和他们的一样。 下面是我的pref_general.xml代码。 和部分活动代码