c++ - 基准测试、代码重新排序、 volatile

标签 c++ benchmarking compiler-optimization volatile

我决定要对特定函数进行基准测试,所以我天真地编写了这样的代码:

#include <ctime>
#include <iostream>

int SlowCalculation(int input) { ... }

int main() {
    std::cout << "Benchmark running..." << std::endl;
    std::clock_t start = std::clock();
    int answer = SlowCalculation(42);
    std::clock_t stop = std::clock();
    double delta = (stop - start) * 1.0 / CLOCKS_PER_SEC;
    std::cout << "Benchmark took " << delta << " seconds, and the answer was "
              << answer << '.' << std::endl;
    return 0;
}

一位同事指出,我应该将 startstop 变量声明为 volatile 以避免代码重新排序。例如,他建议优化器可以像这样有效地重新排序代码:

    std::clock_t start = std::clock();
    std::clock_t stop = std::clock();
    int answer = SlowCalculation(42);

起初我怀疑这种极端的重新排序是否被允许,但经过一些研究和实验后,我了解到确实如此。

但 volatile 感觉不是正确的解决方案; volatile 不只是用于内存映射 I/O 吗?

尽管如此,我添加了 volatile 并发现基准测试不仅耗时明显更长,而且每次运行都非常不一致。如果没有 volatile(并且幸运地确保代码没有被重新排序),基准测试始终需要 600-700 毫秒。使用 volatile,通常需要 1200 毫秒,有时甚至超过 5000 毫秒。两个版本的反汇编列表显示除了寄存器选择不同之外几乎没有区别。这让我想知道是否有另一种方法可以避免代码重新排序而不会产生如此巨大的副作用。

我的问题是:

What is the best way to prevent code reordering in benchmarking code like this?

我的问题类似于 this one (这是关于使用 volatile 来避免省略而不是重新排序),this one (没有回答如何防止重新排序)和this one (争论问题是代码重新排序还是死代码消除)。虽然这三个人都在这个确切的主题上,但没有人真正回答我的问题。

更新:答案似乎是我的同事弄错了,这样的重新排序不符合标准。我支持所有这么说的人,并将赏金授予马克西姆。

我见过一种情况(基于 this question 中的代码),其中 Visual Studio 2010 按照我的说明重新排序了时钟调用(仅在 64 位版本中)。我正在尝试做一个最小的案例来说明这一点,以便我可以在 Microsoft Connect 上提交错误。

对于那些说 volatile 应该慢得多的人,因为它强制读取和写入内存,这与发出的代码不太一致。在我对 this question 的回答中,我展示了带有和不带有 volatile 的代码的反汇编。在循环内部,所有内容都保存在寄存器中。唯一的显着差异似乎是寄存器选择。我不太了解 x86 程序集,无法知道为什么非易失版本的性能始终快,而易失版本的性能不一致(有时甚至非常慢)。

最佳答案

A colleague pointed out that I should declare the start and stop variables as volatile to avoid code reordering.

对不起,你的同事错了。

编译器不会对定义在编译时不可用的函数的调用重新排序。简单地想象一下,如果编译器对诸如 forkexec 之类的调用重新排序或围绕这些调用移动代码,将会产生怎样的欢笑。

换句话说,任何没有定义的函数都是编译时内存屏障,即编译器不会移动调用之前的后续语句或调用之后的先前语句。

在您的代码中,对 std::clock 的调用最终会调用一个定义不可用的函数。

我不能推荐足够的观看atomic Weapons: The C++ Memory Model and Modern Hardware因为它讨论了关于(编译时)内存屏障和 volatile 以及许多其他有用的东西的误解。

Nevertheless, I added volatile and found that not only did the benchmark take significantly longer, it also was wildly inconsistent from run to run. Without volatile (and getting lucky to ensure the code wasn't reordered), the benchmark consistently took 600-700 ms. With volatile, it often took 1200 ms and sometimes more than 5000 ms

不确定是否应该归咎于 volatile

报告的运行时间取决于基准测试的运行方式。确保禁用 CPU 频率缩放,以便它不会打开涡轮模式或在运行过程中切换频率。此外,微基准测试应作为实时优先级进程运行,以避免调度噪音。可能是在另一次运行期间,某些后台文件索引器开始与您的基准测试竞争 CPU 时间。见 this了解更多详情。

一个好的做法是测量执行函数多次所需的时间并报告 min/avg/median/max/stdev/total 时间数。高标准偏差可能表明没有进行上述准备工作。第一次运行通常是最长的,因为 CPU 缓存可能是冷的,它可能需要许多缓存未命中和页面错误,并且还会在第一次调用时解析来自共享库的动态符号(惰性符号解析是 Linux 上的默认运行时链接模式,例如),而后续调用将以更少的开销执行。

关于c++ - 基准测试、代码重新排序、 volatile ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15041519/

相关文章:

C++ 获取运行时间和内存使用情况

C++ : In how many ways compiler optimizes away our code?

c++ - 现代编译器能否优化表达式派生自函数的常量表达式?

ruby-on-rails - Ruby 方法的测量和基准测试时间

java - 是否有免费工具可以分析 Java 方法的执行时间?

c++ - 编写代码帮助编译器进行优化

c++ - 静态成员函数和线程安全

java - 将 java 转移到 c++

c# - 为什么 DwmGetWindowAttribute 与 DWMWA_EXTENDED_FRAME_BOUNDS 在切换监视器时表现异常?

c++ - 为什么我的 struct stat 有一个 st_mtim 而不是 st_mtime 字段?