使用 -O3 或 -Ofast 来编译基准代码是否现实,或者它会删除代码吗?

Is it realistic to use -O3 or -Ofast to compile your benchmark code or will it remove code?

当使用 -O3 编译下面的基准代码时,它在延迟方面的差异给我留下了深刻的印象,所以我开始怀疑编译器是否不 "cheating" 以某种方式删除代码。有办法检查吗?我可以安全地使用 -O3 进行基准测试吗?期望速度提高 15 倍是否现实?

没有 -O3 的结果:平均:239 纳米最小值:230 纳米(900 万次迭代)
-O3 的结果:平均:14 纳米,最小值:12 纳米(900 万次迭代)

int iterations = stoi(argv[1]);
int load = stoi(argv[2]);

long long x = 0;

for(int i = 0; i < iterations; i++) {

    long start = get_nano_ts(); // START clock

    for(int j = 0; j < load; j++) {
        if (i % 4 == 0) {
            x += (i % 4) * (i % 8);
        } else {
            x -= (i % 16) * (i % 32);
        }
    }

    long end = get_nano_ts(); // STOP clock

    // (omitted for clarity)
}

cout << "My result: " << x << endl;

注:我用clock_gettime来测量:

long get_nano_ts() {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000 + ts.tv_nsec;
}

您应该始终在启用优化的情况下进行基准测试。然而,重要的是要确保你想要计时的东西不会被编译器优化掉。

一种方法是在计时器停止后打印出计算结果:

long long x = 0;

for(int i = 0; i < iterations; i++) {

    long start = get_nano_ts(); // START clock

    for(int j = 0; j < load; j++) {
        if (i % 4 == 0) {
            x += (i % 4) * (i % 8);
        } else {
            x -= (i % 16) * (i % 32);
        }
    }

    long end = get_nano_ts(); // STOP clock

    // now print out x so the compiler doesn't just ignore it:
    std::cout << "check: " << x << '\n',

    // (omitted for clarity)
}

比较几种不同算法的基准时,也可以检查每种算法是否产生相同的结果。

编译器肯定会"cheating"并在启用优化编译时删除不必要的代码。它实际上花了很长时间来加速你的代码,这几乎总是会带来令人印象深刻的加速。如果它能够以某种方式推导出一个公式来计算常数时间的结果而不是使用这个循环,它就会。一个常数因子 15 没什么特别的。

但这并不意味着您应该分析未优化的构建!事实上,当使用像 C 和 C++ 这样的语言时,未优化构建的性能几乎完全没有意义。你完全不用担心。

当然,这可能会干扰您上面显示的微基准测试。有两点:

  1. 通常情况下,这种微优化也无关紧要。更喜欢分析您的 实际 程序,然后消除瓶颈。
  2. 如果您真的想要这样的微基准测试,让它依赖于一些运行时输入并显示结果。这样,编译器就无法删除功能本身,只是让它变得相当快。

由于您似乎正在这样做,所以您展示的代码很有可能成为一个合理的微观基准。您应该注意的一件事是您的编译器是否将对 get_nano_ts(); 的两个调用都移动到循环的同一侧。允许这样做,因为 "run time" 不算作可观察到的副作用。 (该标准甚至不要求您的机器以有限速度运行。)有人争论说 here 这通常不是问题,但我无法真正判断给出的答案是否有效。

如果您的程序除了要进行基准测试的事情之外没有做任何昂贵的事情(如果可能的话,无论如何都不应该做),您还可以移动时间测量 "outside",例如time.

很难对您认为正在衡量的内容进行基准测试。以内循环为例:

for (int j = 0;  j < load;  ++j)
        if (i % 4 == 0)
                x += (i % 4) * (i % 8);
        else    x -= (i % 16) * (i % 32);

精明的编译器可能能够看穿并​​将代码更改为类似以下内容:

 x = load * 174;   // example only

我知道这不等价,但有一些相当简单的表达式可以替代该循环。

确定的方法是使用gcc -S编译器选项并查看它生成的汇编代码。