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

5
当使用-O3编译下面的基准代码时,我对它在延迟方面所产生的差异感到印象深刻,因此我开始想知道编译器是否会通过某种方式删除代码来作弊。有没有办法检查这一点?使用-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;
}

1
想一想,如果编译器删除了任何重要的代码,你的程序就不会按预期工作。 - Captain Obvlious
1
你需要对 x 进行一些不可逆的操作,例如输出它。 - David Schwartz
1
15倍并不罕见(当然,这取决于你在做什么!)由于您的优化代码得到了相同的结果(并且您通过打印输出强制计算实际结果,因此编译器不允许完全省略计算),因此似乎这是真正的收益。顺便说一句,我见过比这更高的增益(也有更低的)。 - Cameron
1
在这种情况下,我怀疑大部分收益是由于从/到O0堆栈加载的变量现在具有适当的寄存器分配,从而大大减少了内部循环的开销。此外,内部if体中的两个表达式在内部循环中是常量,并且可能被提升到其外部。实际上,整个内部循环可能已经被重写,将if放在其外部而不是内部。有趣的是检查生成的汇编并查看。 - Cameron
趁着这个机会,我之前为了好玩做了这个,也许对你有用。当然,在技术上也可能遭受重新排序的惨败。 - Baum mit Augen
显示剩余7条评论
3个回答

2
编译器在启用优化时,肯定会“作弊”,删除不必要的代码。它实际上会尽最大努力加快您的代码,这几乎总会导致令人印象深刻的速度提升。如果它能够推导出一个计算结果的公式,以恒定时间计算结果,而不是使用这个循环,那么它就会这样做。一个常数因子15并不罕见。
但这并不意味着您应该分析未经优化的构建!事实上,在使用C和C++等语言时,未经优化的构建性能几乎完全没有意义。您根本不需要担心这个问题。
当然,这可能会干扰您所展示的微基准测试。以下两点需要注意:
1. 这种微小的优化通常并不重要。更好的方法是对实际程序进行分析,然后消除瓶颈。
2. 如果您真的需要这样的微基准测试,请使其依赖于某些运行时输入,并显示结果。这样,编译器就无法删除功能本身,只能使其合理快速。
由于您似乎正在这样做,您展示的代码有很大可能是一个合理的微基准测试。您应该注意的一件事是,您的编译器是否将两个对get_nano_ts();的调用移动到循环的同一侧。它可以这样做,因为“运行时间”不算作可观察的副作用。(标准甚至没有规定您的机器以有限的速度运行。)在这里,有人认为通常这不是一个问题,尽管我无法判断给出的答案是否有效。
如果您的程序除了要进行基准测试的事情之外不做任何昂贵的操作(如果可能,它本来就不应该这样做),您也可以将时间测量“移到外面”,例如使用time

哇!我该如何确保它不会移动我的 get_nano_ts() 调用?那将是完全的损失! - LatencyGuy
@LatencyGuy 打印它。通常情况下,编译器不能改变程序的可见输出(有少数例外)。这意味着产生副作用的操作顺序不能被改变。 - luk32
@LatencyGuy 在这种情况下,我建议使用我在回答末尾提到的time方法。通常情况下,volatile可以帮助您明确地禁用某些东西的优化。 - Baum mit Augen
@luk32 但只要保持打印的相对顺序,它可以重新排列在打印之间发生的事情。再次强调,执行时间不算作可见的副作用。因此,仅仅打印是不足以保证任何事情的。 - Baum mit Augen
@luk32 我猜我只能在计算中使用“开始”时间,尽管那样每次都会给我一个不同的答案。至于“停止”时间,我不确定该怎么做。哎呀,如果编译器移动我的 get_nano_ts(),那么基准测试就会变得十分困难,是吧? - LatencyGuy
@LatencyGuy 正如我所说,你可以使用 volatile 来防止一些移动。如果你关心这方面的细节,请搜索或发布一个新问题。请记住,仅对单个孤立的函数或循环进行基准测试通常并不太有帮助。要对整个大程序进行基准测试,你需要使用 分析器 - Baum mit Augen

1

在衡量你认为正在测量的内容时,很难进行基准测试。在内部循环的情况下:

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 编译器选项,并查看它生成的汇编代码。

0

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

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

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)
}

当比较几种不同算法的基准时,这也可以作为每个算法产生相同结果的检查。


如果编译器可以消除代码,为什么还要对其进行性能分析,因为它不会出现在生产构建中。启用优化以进行基准测试绝对是可取的,而不是使用计时器函数,使用性能分析可以获得您整个代码的准确图片。 - Captain Obvlious
@LatencyGuy 如果这样的话,你不会因为优化而失去任何你无法承受损失的代码,因为优化不会改变功能。只是要记住,如果你不使用结果,优化器可能会删除整个计算过程。 - Galik
1
@CaptainObvlious 基准测试程序是非常人为的代码环境,你经常想要测试算法,在真实代码中,这些算法不会被优化掉,但在你的人工基准测试中,如果编译器意识到你从未使用结果,它们可能会被优化掉。 - Galik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接