即使帖子很旧,我认为添加一些信息可能会很有趣。总之,你的测试太模糊了,可能有偏见。
一点关于速度测试方法
在比较两种语言的速度时,您首先必须准确定义要比较它们的性能的上下文。
“幼稚”与“优化”代码:测试代码是否由初学者或专家程序员编写。此参数的重要性取决于谁将参与您的项目。例如,当与科学家(非极客)一起工作时,您会更多地寻找“幼稚”的代码性能,因为科学家并不是强制的优秀程序员。
授权编译时间:您是否考虑允许代码长时间构建。此参数可能很重要,具体取决于您的项目管理方法。如果您需要进行自动化测试,也许用一点速度来减少编译时间可能会很有趣。另一方面,您可以考虑分发版本允许大量的构建时间。
平台可移植性:如果要在一个或多个平台(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
这是在 4 或 5 个周期延迟的基础上addss
,使函数慢两倍以上。
编译器优化
使用 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 舍入误差,请仅编写重复添加循环。