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 前端:一旦完成,任何人都可以根据自己的工作负载切换到更好的优化器。