GCC optimize-sibling-calls 的反向优化

#include <benchmark/benchmark.h>
static inline constexpr int FIBONACCI_N = 10;

int fib(int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
static void BM_fib(benchmark::State &state) {
    for (auto _ : state) {
        int res = fib(FIBONACCI_N);
        benchmark::DoNotOptimize(res);
    }
}
BENCHMARK(BM_fib);

int x;  // Magic line.
        // clang does not use DP anymore: much slower.
        // gcc chooses a different optimizing approach: much faster.

int fib_x(int n) { return n < 2 ? n : fib_x(n - 1) + fib_x(n - 2) + x; }
static void BM_fib_x(benchmark::State &state) {
    for (auto _ : state) {
        int res = fib_x(FIBONACCI_N);
        benchmark::DoNotOptimize(res);
    }
}
BENCHMARK(BM_fib_x);

BENCHMARK_MAIN();

编译条件:GCC 13.2 -O2 -std=c++17

结果:因为 x 改变了 fib_x 的优化流程,fib_x 虽然看起来更复杂或者和 fib 同样复杂,但实际上比 fib 快。在编译时打开 -fno-optimize-sibling-calls 禁用这项优化,结果就正常了。

Open in Quick C++ BenchmarkOpen in Compiler Explorer

随后我又补充了对单个函数关闭 sibling 优化的补充实验,结果是 sibling 优化使得 fib 严重减速,fib_x 速度变化不大(有时候略微提速)。从 Compiler Explorer 的结果来看,这个行为是从 GCC 10.1 开始产生的。在之前 sibling 优化结果则符合预期,fibfib_x 两个函数都得到了不同程度的加速,其中 fib 因为更简单所以获得的加速比更大。

WHY?

Note

Compiler Explorer 上每次运行分配到的机器配置不同、负载不同,因此只有时间的相对变化有意义,不同次运行结果之间的时间比较没有意义。

Tip

clang 的表现比较正常,而且 -O2 下斐波那契递归求解被改造成了动态规划。带有 x 的版本阻碍了优化,因而速度较慢一些。