当前位置: 首页 > 知识库问答 >
问题:

基准测试、代码重新排序、易失性

范侯林
2023-03-14

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

#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变量声明易失性,以避免代码重新排序。例如,他建议优化器可以像这样有效地重新排序代码:

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

起初,我怀疑这种极端的重新排序是允许的,但经过一些研究和实验,我知道是允许的。

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

然而,我添加了< code>volatile,发现不仅基准测试花费的时间明显更长,而且每次运行之间也非常不一致。在没有volatile的情况下(幸运的是确保代码没有被重新排序),基准测试持续花费600-700 ms。在使用volatile的情况下,通常花费1200 ms,有时超过5000 ms。两个版本的反汇编清单除了寄存器的不同选择之外,几乎没有任何区别。这让我想知道是否有另一种方法来避免代码重新排序,而不会产生如此巨大的副作用。

我的问题是:

在这样的基准测试代码中,防止代码重新排序的最好方法是什么?

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

更新:答案似乎是我的同事错了,像这样的重新排序与标准不一致。我已经投票支持了所有这么说的人,并将赏金授予马克西姆。

我见过一个案例(基于这个问题中的代码),Visual Studio 2010按照我所展示的方式对时钟调用进行了重新排序(仅在64位版本中)。我试图做一个最小的案例来说明这一点,这样我就可以在Microsoft Connect上提交一个bug。

对于那些认为volatile应该慢得多,因为它会强制对内存进行读写的人来说,这与发出的代码不太一致。在我对这个问题的回答中,我展示了使用和不使用volatile的代码的反汇编。在循环中,所有内容都保存在寄存器中。唯一显著的差异似乎是寄存器选择。我不太了解x86汇编,不知道为什么非易失性版本的性能一直很快,而易失性版的性能却一直很慢(有时甚至非常慢)。

共有3个答案

赫连琦
2023-03-14

Volatile确保一件事,而且只有一件事:每次从内存中读取易失性变量的读数 - 编译器不会假设该值可以缓存在寄存器中。同样,写入将被写入内存。编译器不会将其保存在寄存器中“一段时间,然后将其写入内存”。

为了防止编译器重新排序,您可以使用所谓的编译器Geofence。MSVC包括3个编译器Geofence:

_ReadWriteBarrier()-全Geofence

_ReadBarrier()-用于装载的双面Geofence

_WriteBarrier()-商店的双面Geofence

国际刑事法院包括__memory_barrier()全Geofence。

全栅栏通常是最好的选择,因为在这个层次上不需要更精细的粒度(编译器栅栏在运行时基本上是无成本的)。

状态重新排序(大多数编译器在启用优化时都会这样做),这也是某些程序在使用编译器优化编译时无法操作的主要原因。

将建议阅读 http://preshing.com/20120625/memory-ordering-at-compile-time,以了解编译器重构等我们可能面临的潜在问题。

黄弘盛
2023-03-14

防止重新排序的通常方法是一个编译障碍,即asm易失性(":::"内存");(使用gcc)。这是一条什么都不做的asm指令,但是我们告诉编译器它会破坏内存,所以不允许跨它重新排序代码。这样做的成本只是删除重新排序的实际成本,这显然不是像其他地方建议的那样改变优化级别等的情况。

我相信_ReadWriteBarrier相当于微软的东西。

根据Maxim Yegorushkin的回答,重新排序不太可能是你的问题的原因。

谢昊乾
2023-03-14

一位同事指出,我应该将start和stop变量声明为volatile,以避免代码重新排序。

很抱歉,但您的同事错了。

编译器不会对定义在编译时不可用的函数调用进行重新排序。想象一下,如果编译器将这些调用重新排序为< code>fork和< code>exec,或者在这些调用周围移动代码,会有多么有趣。

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

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

我不能推荐足够的观看原子武器:C内存模型和现代硬件,因为它讨论了关于(编译时)内存障碍和< code>volatile以及许多其他有用的东西的误解。

然而,我添加了volatile,发现不仅基准测试花费的时间明显更长,而且每次运行之间也非常不一致。没有volatile(幸运的是确保代码没有被重新排序),基准测试持续花费600-700 ms。使用volatile,通常花费1200 ms,有时超过5000 ms

不确定易失性是否是罪魁祸首。

报告的运行时间取决于基准是如何运行的。确保禁用CPU频率缩放,以便它不会在运行过程中打开turbo模式或切换频率。此外,微基准应该作为实时优先级进程运行,以避免调度噪声。可能在另一次运行中,一些后台文件索引器开始与您的基准竞争CPU时间。更多细节见此。

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

 类似资料:
  • a)为什么除了“00”之外还有其他输出? b)如何修改代码以便始终打印“00”。 对于a)我的回答是:在没有任何volatile/同步构造的情况下,编译器可以重新排序一些指令。特别是“this.initialint=val;”和“this.flag=true;”可以切换,这样就可以发生这种情况:线程都启动了,t1充电在前面。给定重新排序的指令,它首先设置flag=true。在它到达“this.in

  • 问题内容: 据我所知,在Java中,volatile变量使线程直接对主CPU进行读/写操作(而不是在每个线程的缓存中),因此使其更改对其他线程可见。 我不知道的是:因此,为什么这项工作(易失性)可以阻止编译器/ CPU对代码的重新排序语句。 谢谢 :) 问题答案: 这是一个很好的示例,说明了禁止重新排序的目的是要解决的问题(从此处获取): 在此示例中,为易失性,但不是。如果作者和阅读者同时执行并且

  • 问题内容: 我最近一直在研究基准测试,我一直对记录程序数据等感兴趣。我想知道我们是否可以在程序内部实现我们自己的内存使用代码并有效地实现自己的时间消耗代码。我知道如何检查代码运行所需的时间: 我还研究了健壮的Java基准测试,第1部分:问题,本教程非常全面。显示的负面影响。然后,本教程建议我们使用(使其更准确吗?)。 我还查看了确定Java中的内存使用情况以了解内存使用情况。该网站显示了如何实施。

  • 我对下面的代码段有一个问题。结果可能有一个结果[0,1,0](这是用JCStress执行的测试)。那么这是怎么发生的呢?我认为数据写入(data=1)应该在Actor2(guard2=1)中写入到guard2之前执行。我说得对吗?我问,因为很多时候我读到挥发物周围的说明没有重新排序。此外,根据这一点:http://tutorials.jenkov.com/java-concurrency/vola

  • 11.4. 基准测试 基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。 下面是IsPalindrome函数的基准测试,其中循

  • GoCPPLua (JIT) 策略执行的负载在model_b_test.go中进行基准测试。 测试是: 英特尔 酷睿 i7-6700HQ CPU @ 2.60GHz, 2601 Mhz, 4 核, 8 处理器 go test -bench= -benchmem 的测试结果如下 (op = 一次 Enforce() 调用, ms = 毫秒, KB = 千字节): 测试用例 规则大小 时间开销 (m