Rust 和 C++ 的性能差异
Performance difference Rust and C++
我目前正在学习 Rust,作为第一个练习,我想实现一个计算第 n 个斐波那契数的函数:
fn main() {
for i in 0..48 {
println!("{}: {}", i, fibonacci(i));
}
}
fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
我运行它是:
$ time cargo run --release
real 0m15.380s
user 0m15.362s
sys 0m0.014s
作为练习,我也在 C++ 中实现了相同的算法。我期待类似的性能,但 C++ 代码 运行s 在 80% 的时间:
#include<iostream>
unsigned int fibonacci(unsigned int n);
int main (int argc, char* argv[]) {
for(unsigned int i = 0; i < 48; ++i) {
std::cout << i << ": " << fibonacci(i) << '\n';
}
return 0;
}
unsigned int fibonacci(unsigned int n) {
if(n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
编译为:
$ g++ test.cpp -o test.exe -O2
和运行宁:
$ time ./test.exe
real 0m12.127s
user 0m12.124s
sys 0m0.000s
为什么我看到这样的性能差异?我对在 Rust 中更快地计算斐波那契数(使用不同的算法)不感兴趣;我只对差异的来源感兴趣。这只是我学习Rust过程中的一个练习。
TL;DR:这不是 Rust vs C++,而是 LLVM (Clang) vs GCC。
不同的优化器优化代码的方式不同,在这种情况下,GCC 会生成更大但更快的代码。
这可以使用 godbolt 进行验证。
这里是 Rust,用两个 GCC 编译(通过 rustgcc-master):
example::fibonacci:
push r15
push r14
push r13
push r12
push rbp
xor ebp, ebp
push rbx
mov ebx, edi
sub rsp, 24
.L2:
test ebx, ebx
je .L1
cmp ebx, 1
je .L4
lea r12d, -1[rbx]
xor r13d, r13d
.L19:
cmp r12d, 1
je .L6
lea r14d, -1[r12]
xor r15d, r15d
.L16:
cmp r14d, 1
je .L8
lea edx, -1[r14]
xor ecx, ecx
.L13:
cmp edx, 1
je .L10
lea edi, -1[rdx]
mov DWORD PTR 12[rsp], ecx
mov DWORD PTR 8[rsp], edx
call example::fibonacci.localalias
mov ecx, DWORD PTR 12[rsp]
mov edx, DWORD PTR 8[rsp]
add ecx, eax
sub edx, 2
jne .L13
.L14:
add r15d, ecx
sub r14d, 2
je .L17
jmp .L16
.L4:
add ebp, 1
.L1:
add rsp, 24
mov eax, ebp
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
.L6:
add r13d, 1
.L20:
sub ebx, 2
add ebp, r13d
jmp .L2
.L8:
add r15d, 1
.L17:
add r13d, r15d
sub r12d, 2
je .L20
jmp .L19
.L10:
add ecx, 1
jmp .L14
并且使用 LLVM(通过 rustc):
example::fibonacci:
push rbp
push r14
push rbx
mov ebx, edi
xor ebp, ebp
mov r14, qword ptr [rip + example::fibonacci@GOTPCREL]
cmp ebx, 2
jb .LBB0_3
.LBB0_2:
lea edi, [rbx - 1]
call r14
add ebp, eax
add ebx, -2
cmp ebx, 2
jae .LBB0_2
.LBB0_3:
add ebx, ebp
mov eax, ebx
pop rbx
pop r14
pop rbp
ret
我们可以看到 LLVM 生成了一个原始版本——在循环的每次迭代中调用函数——而 GCC 通过内联一些调用来部分展开递归。这导致在 GCC 的情况下调用次数较少,并且每个函数调用的开销约为 5ns,这已经足够重要了。
我们可以通过 Clang 和 GCC 使用 LLVM the C++ version 做同样的练习,注意结果非常相似。
所以,正如所宣布的,这是 LLVM 与 GCC 的区别,而不是语言的区别。
顺便说一下,优化器可能会产生如此大相径庭的结果,这也是我对 rustc_codegen_gcc 计划(在 godbolt 上被称为 rustgcc-master 的进展感到非常兴奋的原因) 旨在将 GCC 后端插入 rustc 前端:一旦完成,任何人都可以根据自己的工作负载切换到更好的优化器。
我目前正在学习 Rust,作为第一个练习,我想实现一个计算第 n 个斐波那契数的函数:
fn main() {
for i in 0..48 {
println!("{}: {}", i, fibonacci(i));
}
}
fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
我运行它是:
$ time cargo run --release
real 0m15.380s
user 0m15.362s
sys 0m0.014s
作为练习,我也在 C++ 中实现了相同的算法。我期待类似的性能,但 C++ 代码 运行s 在 80% 的时间:
#include<iostream>
unsigned int fibonacci(unsigned int n);
int main (int argc, char* argv[]) {
for(unsigned int i = 0; i < 48; ++i) {
std::cout << i << ": " << fibonacci(i) << '\n';
}
return 0;
}
unsigned int fibonacci(unsigned int n) {
if(n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
编译为:
$ g++ test.cpp -o test.exe -O2
和运行宁:
$ time ./test.exe
real 0m12.127s
user 0m12.124s
sys 0m0.000s
为什么我看到这样的性能差异?我对在 Rust 中更快地计算斐波那契数(使用不同的算法)不感兴趣;我只对差异的来源感兴趣。这只是我学习Rust过程中的一个练习。
TL;DR:这不是 Rust vs C++,而是 LLVM (Clang) vs GCC。
不同的优化器优化代码的方式不同,在这种情况下,GCC 会生成更大但更快的代码。
这可以使用 godbolt 进行验证。
这里是 Rust,用两个 GCC 编译(通过 rustgcc-master):
example::fibonacci:
push r15
push r14
push r13
push r12
push rbp
xor ebp, ebp
push rbx
mov ebx, edi
sub rsp, 24
.L2:
test ebx, ebx
je .L1
cmp ebx, 1
je .L4
lea r12d, -1[rbx]
xor r13d, r13d
.L19:
cmp r12d, 1
je .L6
lea r14d, -1[r12]
xor r15d, r15d
.L16:
cmp r14d, 1
je .L8
lea edx, -1[r14]
xor ecx, ecx
.L13:
cmp edx, 1
je .L10
lea edi, -1[rdx]
mov DWORD PTR 12[rsp], ecx
mov DWORD PTR 8[rsp], edx
call example::fibonacci.localalias
mov ecx, DWORD PTR 12[rsp]
mov edx, DWORD PTR 8[rsp]
add ecx, eax
sub edx, 2
jne .L13
.L14:
add r15d, ecx
sub r14d, 2
je .L17
jmp .L16
.L4:
add ebp, 1
.L1:
add rsp, 24
mov eax, ebp
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
.L6:
add r13d, 1
.L20:
sub ebx, 2
add ebp, r13d
jmp .L2
.L8:
add r15d, 1
.L17:
add r13d, r15d
sub r12d, 2
je .L20
jmp .L19
.L10:
add ecx, 1
jmp .L14
并且使用 LLVM(通过 rustc):
example::fibonacci:
push rbp
push r14
push rbx
mov ebx, edi
xor ebp, ebp
mov r14, qword ptr [rip + example::fibonacci@GOTPCREL]
cmp ebx, 2
jb .LBB0_3
.LBB0_2:
lea edi, [rbx - 1]
call r14
add ebp, eax
add ebx, -2
cmp ebx, 2
jae .LBB0_2
.LBB0_3:
add ebx, ebp
mov eax, ebx
pop rbx
pop r14
pop rbp
ret
我们可以看到 LLVM 生成了一个原始版本——在循环的每次迭代中调用函数——而 GCC 通过内联一些调用来部分展开递归。这导致在 GCC 的情况下调用次数较少,并且每个函数调用的开销约为 5ns,这已经足够重要了。
我们可以通过 Clang 和 GCC 使用 LLVM the C++ version 做同样的练习,注意结果非常相似。
所以,正如所宣布的,这是 LLVM 与 GCC 的区别,而不是语言的区别。
顺便说一下,优化器可能会产生如此大相径庭的结果,这也是我对 rustc_codegen_gcc 计划(在 godbolt 上被称为 rustgcc-master 的进展感到非常兴奋的原因) 旨在将 GCC 后端插入 rustc 前端:一旦完成,任何人都可以根据自己的工作负载切换到更好的优化器。