即使这篇文章已经过时,我认为添加一些信息可能会有趣。总的来说,你的测试过于模糊,可能存在偏见。
关于速度测试方法的一点说明
当比较两种语言的速度时,首先必须明确定义要在哪个上下文中比较它们的性能。
"naive" vs "optimized" code:代码是由初学者还是专家程序员编写的,这个参数取决于谁参与你的项目。例如,在与科学家(非极客)合作时,你更关注“天真”的代码性能,因为科学家不一定是优秀的程序员。
授权编译时间:是否考虑允许代码长时间构建。这个参数可能会影响你的项目管理方法论。如果你需要进行自动化测试,也许可以在降低编译时间的同时稍微牺牲一些速度。另一方面,你可以认为分发版本允许高度的构建时间。
平台可移植性:如果你的速度应该在一个或多个平台上进行比较(Windows、Linux、PS4...)
编译器/解释器可移植性:如果你的代码速度应该与编译器/解释器无关。对于多平台和/或开源项目可能会有用。
其他专业参数,例如,如果你允许在你的代码中进行动态分配,如果你想启用插件(在运行时动态加载的库)等。
然后,您需要确保您的代码代表了您想要测试的内容。在这里,我假设您没有使用优化标志编译C ++,因为您正在测试“天真”的(实际上并不是那么天真)代码的快速编译速度。因为您的循环具有固定的大小和固定的数据,所以您不会测试动态分配,并且您-可能-允许代码转换(下一节将更详细地介绍)。在这种情况下,JavaScript通常比C ++表现更好,因为JavaScript默认情况下在编译时进行优化,而C ++编译器需要被告知进行优化。
C ++参数快速增加的简要概述
由于我对JavaScript的了解不够深入,我只展示了如何通过代码优化和编译类型来改变固定for循环的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”之间。可以看出,这里创建的代码并没有进行任何优化:存在大量的内存访问(没有正确使用寄存器),因此有很多浪费操作来存储和重新加载结果。
这会在现代x86 CPU中将FP加法进入“a”的关键路径依赖链中引入额外的
存储转发延迟5或6个周期。这还要加上
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
由于func返回值已在编译时计算,因此asm代码很短,call指令只需返回该值。
当然,
a = b * 100000
总是编译成高效的汇编代码,所以只有在需要探索所有这些临时变量的FP舍入误差时才编写重复加法循环。
-O3 -ffast-math
,并观察 C++ 时间的变化。 - Jesse Good