很长一段时间以来,我一直认为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);
})();
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;
}
答案 0 :(得分:196)
如果您使用 Linux 系统(至少在这种情况下符合POSIX),我可能会有一些坏消息。 clock()
调用返回程序消耗的时钟周期数,并按CLOCKS_PER_SEC
进行缩放,即1,000,000
。
这意味着,如果您 on 这样的系统,那么您在微秒中为C和毫秒谈论JavaScript(如根据{{3}})。因此,而不是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
并且不缩放因子为100万,则需要查看其他一些内容。
第一步是确保编译器实际优化您的代码。这意味着,例如,为-O2
设置-O3
或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编写的嵌入式应用程序。
答案 1 :(得分: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;
}
这让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;
}
这里的主要补充是以下(当然有些神秘):
#pragma omp parallel for reduction(+:total) firstprivate(inc)
这告诉编译器在多个线程中执行外部循环,每个线程使用inc
的单独副本,并在并行部分之后将total
的各个值加在一起。
结果是关于你可能期望的。如果我们不使用编译器的-openmp
标志启用OpenMP,则报告的时间大约是我们之前单独执行的时间的10倍(AMD为409毫秒,英特尔为323 MS)。打开OpenMP后,AMD的时间下降到217毫秒,英特尔下降到100毫秒。
因此,在Intel上,原始版本在外循环的一次迭代中花费了90ms。对于这个版本,我们对外循环的所有10次迭代只需稍微长一点(100 ms) - 速度提高约9:1。在具有更多内核的计算机上,我们可以期待更多的改进(OpenMP通常会自动利用所有可用的内核,但您可以根据需要手动调整线程数。)
答案 2 :(得分:2)
这是一个极端主题,所以可以看看:
https://benchmarksgame-team.pages.debian.net/benchmarksgame/
对各种语言进行基准测试。
Javascript V8等在示例中肯定能够很好地完成简单的循环,可能会产生非常相似的机器代码。 对于大多数&#34;靠近用户&#34;应用程序Javscript肯定是更好的选择,但要记住更复杂的算法/应用程序的内存浪费和不可避免的性能损失(以及缺乏控制)。
答案 3 :(得分:2)
即使帖子过旧,我认为添加一些信息也可能很有趣。总而言之,您的测试太含糊,可能会有偏差。
在比较两种语言的速度时,首先必须精确定义要在哪种上下文中比较它们的性能。
“原始”代码与“优化”代码:测试的代码是否由初学者或专家程序员编写。此参数很重要,具体取决于谁将参与您的项目。例如,与科学家(非怪胎的人)一起工作时,您会更多地考虑“天真的”代码性能,因为科学家不是强行成为优秀程序员的人。
授权的编译时间:是否考虑允许长时间构建代码。该参数可能很重要,具体取决于您的项目管理方法。如果您需要进行自动化测试,也许会花点时间来增加编译时间可能会很有趣。另一方面,您可以考虑使用发行版允许大量构建时间。
平台可移植性:如果要在一个或多个平台(Windows,Linux,PS4 ...)上比较您的速度
编译器/解释器的可移植性:无论代码的速度是否独立于编译器/解释器。对于多平台和/或开源项目很有用。
其他专用参数,例如,如果您允许在代码中进行动态分配,是否要启用插件(运行时动态加载的库)等。
然后,您必须确保您的代码代表您要测试的内容
在这里,(我假设您没有使用优化标志编译C ++),您正在测试“天真”(实际上不是那么天真)代码的快速编译速度。因为循环的大小是固定的,数据是固定的,所以您不测试动态分配,并且您应该允许代码转换(在下一节中有更多介绍)。实际上,在这种情况下,JavaScript的性能通常比C ++好,这是因为JavaScript默认情况下会在编译时进行优化,而C ++编译器则需要进行优化。
由于我对JavaScript不够了解,所以我只会说明代码优化和编译类型如何在固定的for循环上改变c ++的速度,希望它能回答“ JS看起来比C ++更快”的问题?”
为此,让我们使用Matt Godbolt的C ++ compiler explorer来查看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上将FP附加到a
的关键路径依赖链中引入了额外的5 or 6 cycles of store-forwarding latency。这是addss
的4或5个周期延迟的基础,使该函数的运行速度慢了两倍。
编译器优化
使用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舍入误差,请仅编写重复添加循环。
答案 4 :(得分:-1)
任何流行的运行时的JS都是用C ++编译的,因此就像您可能无法使其运行得比同等的本机代码快...如果需要,您可以通过从1到1的谷歌计数来归纳证明它< / p>