以来,我一直认为C++比JavaScript更快。但是,今天我做了一个基准脚本来比较两种语言的浮点计算速度,结果是惊人的!
JavaScript 似乎比 C++ 快了近 4 倍!
我让两种语言在我的 i5-430M 笔记本电脑上完成相同的工作,执行 100000000 次a = a + b
。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: %dmsn", (end - start) * 1000 / CLOCKS_PER_SEC);
}
printf("a = %lfn", a);
return 0;
}
使用的是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
并且它不是一百万的比例因子,你需要看看其他一些东西。
第一步是确保您的代码实际上由编译器优化。这意味着,例如,为 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编写的嵌入式应用程序。
启用优化进行快速测试,我得到了大约 150 毫秒的旧 AMD 64 X2 处理器和大约 90 毫秒的合理最新英特尔 i7 处理器的结果。
然后我做了更多的事情来说明你可能想要使用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: %fmsn", (1000.0 * (end - start))/CLOCKS_PER_SEC);
}
printf("a = %lfn", a);
return 0;
}
这使得C++代码在AMD上运行大约44毫秒(忘记在英特尔上运行此版本(。然后我打开了编译器的自动矢量化器(-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: %fmsn", (1000.0 * (end - start))/CLOCKS_PER_SEC);
printf("a = %lfn", total);
return 0;
}
这里的主要补充是以下(诚然有些晦涩难懂(行:
#pragma omp parallel for reduction(+:total) firstprivate(inc)
这告诉编译器在多个线程中执行外部循环,每个线程都有一个单独的inc
副本,并将并行部分之后的各个total
值相加。
结果与您可能期望的差不多。如果我们不使用编译器的 -openmp
标志启用 OpenMP,则报告的时间大约是我们之前看到的单个执行的 10 倍(AMD 为 409 毫秒,英特尔为 323 毫秒(。打开 OpenMP 后,AMD 的时间降至 217 毫秒,英特尔的时间降至 100 毫秒。
因此,在英特尔上,原始版本需要 90 毫秒才能进行一次外循环迭代。在这个版本中,对于外循环的所有 10 次迭代,我们只得到了稍微长一点(100 毫秒(——速度提高了大约 9:1。在具有更多内核的计算机上,我们可以期待更多的改进(OpenMP 通常会自动利用所有可用内核,但您可以根据需要手动调整线程数(。
即使帖子很旧,我认为添加一些信息可能会很有趣。总之,您的测试过于模糊,可能会有偏见。
关于速度测试方法的一些信息
在比较两种语言的速度时,您首先必须精确定义要比较它们的性能。
-
"幼稚"与"优化"代码:无论测试的代码是否由初学者或专家程序员编写。此参数很重要,具体取决于谁将参与您的项目。例如,当与科学家(非极客(合作时,你会更多地寻找"幼稚"的代码性能,因为科学家不是强行的好程序员。
-
授权编译时间:您是否考虑允许代码长时间构建。此参数可能很重要,具体取决于您的项目管理方法。如果你需要做自动化测试,也许交换一点速度来减少编译时间会很有趣。另一方面,您可以认为分发版本允许大量的构建时间。
-
平台可移植性:如果您的速度应在一个或多个平台上进行比较(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
的关键路径依赖链中。 这是在 addss
的 4 或 5 个周期延迟之上,使函数慢两倍以上。
编译器优化
同样的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
asm 代码很短,因为 func 返回的值是在编译时计算的,调用只是返回它。
<小时 />当然,a = b * 100000
总是编译为高效的asm,所以只有在需要探索所有这些临时的FP舍入误差时,才编写重复加法循环。
这是一个两极分化的话题,所以可以看看:
https://benchmarksgame-team.pages.debian.net/benchmarksgame/
对各种语言进行基准测试。
Javascript V8 等肯定在简单的循环方面做得很好,如示例中所示,可能会生成非常相似的机器代码。对于大多数"贴近用户"的应用程序,Javscript 肯定是更好的选择,但请记住,对于更复杂的算法/应用程序,内存浪费和多次不可避免的性能影响(以及缺乏控制(。
有很多优点,优化标志的影响。然而,我只是想指出,写得不好的代码在任何语言中都会表现不佳,无论它多么"接近金属"。
你的代码是这样编写的,它会产生一个很长的依赖链,任何优化编译器都无法摆脱,除非你明确告诉它忽略严格的算术合规性。
请注意,过去 10 年的每个中高端台式机 CPU 的时钟频率为 3-4 Ghz,每个内核每个周期可以计算 2-8 个双精度 FP 指令,从而产生 6-30 GFLOPS 之间的任何时间。这意味着您的 JS 实现达到 1 GFLOPS 的效率仅为 3-15%。适当优化的代码在达到>90% 峰值 FP 时几乎没有问题,这还不包括多核并行性。
简而言之,人们不妨比较一下bubblesort或其他一些没有人实际使用的极低效算法的效率。低效代码不必要地触发太多缓存未命中,或者由于依赖链或复杂的不可预测逻辑而在管道中创建太多执行停滞,在任何语言中都将执行得同样糟糕。
无论如何,使用 --fast-math 进行编译可能会优化链的一部分。
流行运行时的 JS 都是用 C++ 编译的,所以你可能无法让它比等效的本机代码运行得更快......如果你愿意,你可以通过从 1 乘 1 数到 Google 的归纳来证明这一点