为什么在这个递归斐波那契代码中 GCC 生成的程序比 Clang 更快?

Why does GCC generate a faster program than Clang in this recursive Fibonacci code?

这是我测试过的代码:

#include <iostream>
#include <chrono>
using namespace std;

#define CHRONO_NOW                  chrono::high_resolution_clock::now()
#define CHRONO_DURATION(first,last) chrono::duration_cast<chrono::duration<double>>(last-first).count()

int fib(int n) {
    if (n<2) return n;
    return fib(n-1) + fib(n-2);
}

int main() {
    auto t0 = CHRONO_NOW;
    cout << fib(45) << endl;
    cout << CHRONO_DURATION(t0, CHRONO_NOW) << endl;
    return 0;
}

当然,计算斐波那契数列的方法要快得多,但这是一个很好的小压力测试,它侧重于递归函数调用。 除了使用 chrono 来测量时间之外,代码没有其他内容。

首先,我 运行 在 OS X 上 Xcode 测试了几次(这就是 clang),使用 -O3 优化。 运行.

用了大约9秒

然后,我在 Ubuntu 上用 gcc (g++) 编译了相同的代码(再次使用 -O3),那个版本只用了大约 6.3 秒到 运行!另外,我在 mac 上 运行ning Ubuntu inside VirtualBox,这只会对性能产生负面影响,如果有的话。

好了:

我知道这些是完全不同的编译器,所以它们做的事情也不同,但我看到的所有以 gcc 和 clang 为特色的测试只显示出很小的差异,在某些情况下,差异是相反的周围(叮当声更快)。

那么对于为什么 gcc 在这个特定的例子中远远击败 clang 有什么合乎逻辑的解释吗?

我不会说 gcc 比 clang 好很多。在我看来,性能差异(6.3 秒对 9 秒)相当小。在我的 FreeBSD 系统上,clang 需要 26.12 秒,gcc 需要 10.55 秒。

但是,调试它的方法是使用 g++ -Sclang++ -S 来获取程序集输出。

我在我的 FreeBSD 系统上测试了这个。汇编语言文件太长 post 这里,但 gcc 似乎在 Fibonacci 计算函数中执行多级内联(那里有 20 fib() 调用!)而 clang 只是调用 fib(n-1)fib(n-2) 没有内联级别。

顺便说一下,我的 gcc 版本是 4.2.1 20070831 patched [FreeBSD],clang 版本是 3.1 (branches/release_31 156863) 20120523。这些是 FreeBSD 9.1-RELEAESE 基本系统附带的版本. CPU 是 AMD Turion II Neo N40L 双核处理器 (1497.54-MHz)。

compiler explorer really does loop-unrolling and inlines a lot of function calls while Clang 3.5.1 calls fib twice each iteration without even tail call optimization 中的 GCC 4.9.2,如下所示

fib(int):                                # @fib(int)
        push    rbp
        push    rbx
        push    rax
        mov     ebx, edi
        cmp     ebx, 2
        jge     .LBB0_1
        mov     eax, ebx
        jmp     .LBB0_3
.LBB0_1:
        lea     edi, dword ptr [rbx - 1]
        call    fib(int)       # fib(ebx - 1)
        mov     ebp, eax
        add     ebx, -2
        mov     edi, ebx
        call    fib(int)       # fib(ebx - 2)
        add     eax, ebp
.LBB0_3:
        add     rsp, 8
        pop     rbx
        pop     rbp
        ret

GCC 版本长了 10 多倍,只有一个 fib 调用和 20 多个用于内联调用的标签,这也意味着最后一个调用已被尾优化为 jmp 或 GCC 已将一些递归转换为迭代(因为它分配了一个大数组来存储中间值)

我也考虑了 ICC,令人惊讶的是它在 fib 中有 10 个 call 指令,而且它还在 内联 fib 调用 9 次main,但它不会将递归代码转换为迭代

Here's the compiler outputs for comparison

请注意,您可以像这样修改代码以使输出更易于阅读

int fib(int n) {
    if (n<2) return n;
    int t = fib(n-1);
    return t + fib(n-2);
}

现在 compiler explorer 将用不同的颜色突出显示汇编输出中指令对应的源代码行,您将很容易看到这两个调用是如何进行的。 return t + fib(n-2) 行被 GCC 编译为

.L3:
        mov     eax, DWORD PTR [rsp+112]  # n, %sfp
        add     edx, DWORD PTR [rsp+64]   # D.35656, %sfp
        add     DWORD PTR [rsp+60], edx   # %sfp, D.35656
        sub     DWORD PTR [rsp+104], 2    # %sfp,