c++ - 在进行基准测试时防止编译器优化

标签 c++ gcc clang performance-testing compiler-optimization

我最近看到了这个精彩的 cpp2015 演讲 CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"

提到的防止编译器优化代码的技术之一是使用以下函数。

static void escape(void *p) {
  asm volatile("" : : "g"(p) : "memory");
}

static void clobber() {
  asm volatile("" : : : "memory");
}

void benchmark()
{
  vector<int> v;
  v.reserve(1);
  escape(v.data());
  v.push_back(10);
  clobber()
}

我正在努力理解这一点。问题如下。

1) escape 相对于 clobber 的优势是什么?

2) 从上面的示例来看,clobber() 似乎阻止了前一条语句 (push_back) 的优化。如果是这样,为什么下面的代码片段不正确?

 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber()
 }

如果这还不够令人困惑,folly(FB 的线程库)有一个偶数 stranger implementation

相关片段:

template <class T>
void doNotOptimizeAway(T&& datum) {
  asm volatile("" : "+r" (datum));
}

我的理解是上面的代码片段通知编译器汇编 block 将写入数据。但是,如果编译器发现没有该数据的消费者,它仍然可以优化生产数据的实体,对吗?

我认为这不是常识,感谢您的帮助!

最佳答案

tl;dr doNotOptimizeAway 创建一个人为的“使用”。

这里有一点术语:“def”(“定义”)是一个语句,它为变量赋值; “use”是一个语句,它使用变量的值来执行一些操作。

如果紧跟在 def 之后,所有到程序导出的路径都没有遇到变量的使用,则 def 称为 dead 并且死代码消除 (DCE) 过程将删除它。这反过来可能会导致其他 def 失效(如果该 def 是由于具有可变操作数而使用)等。

想象一下聚合标量替换 (SRA) 通过后的程序,它将局部 std::vector 转换为两个变量 lenptr。在某些时候,程序会为 ptr 赋值;该语句是 def。

现在,原来的程序没有对 vector 做任何事情;换句话说,lenptr 都没有任何用途。因此,它们的所有定义都已失效,DCE 可以删除它们,从而有效地删除所有代码并使基准测试变得毫无值(value)。

添加 doNotOptimizeAway(ptr) 会创建人为使用,从而阻止 DCE 删除 defs。 (作为旁注,我认为“+”没有意义,“g”应该已经足够了)。

对于内存加载和存储可以遵循类似的推理线:存储(定义)是死的当且仅当没有到程序末尾的路径,其中包含从该存储位置加载(使用)。由于跟踪任意内存位置比跟踪单个伪寄存器变量要困难得多,因此编译器保守地推理 - 如果没有路径到程序末尾,存储就死了,这可能 可能 遇到使用该商店。

其中一个例子是内存区域的存储,它保证不会被别名 - 在内存被释放后,不可能使用该存储,这不会触发未定义的行为。 IOW,没有这样的用途。

因此编译器可以消除 v.push_back(42)。但是出现了 escape - 它导致 v.data() 被认为是任意别名,正如@Leon 上面描述的那样。

示例中 clobber() 的目的是创建对所有别名内存的人工使用。我们有一个商店(来自 push_back(42)),商店位于全局别名的位置(由于 escape(v.data())),因此 clobber() 可能包含对该存储的使用(IOW,可以观察到存储的副作用),因此不允许编译器删除该存储。

几个简单的例子:

例子一:

void f() {
  int v[1];
  v[0] = 42;
}

这不会生成任何代码。

例子二:

extern void g();

void f() {
  int v[1];
  v[0] = 42;
  g();
}

这只生成对 g() 的调用,没有内存存储。函数 g 不可能访问 v 因为 v 没有别名。

示例三:

void clobber() {
  __asm__ __volatile__ ("" : : : "memory");
}

void f() {
  int v[1];
  v[0] = 42;
  clobber();
}

与前面的示例一样,没有生成存储,因为 v 没有别名并且对 clobber 的调用没有内联。

例子四:

template<typename T>
void use(T &&t) {
  __asm__ __volatile__ ("" :: "g" (t));
}

void f() {
  int v[1];
  use(v);
  v[0] = 42;
}

这次 v 转义(即可以从其他激活帧访问)。然而,存储仍然被删除,因为在它之后没有潜在的内存使用(没有 UB)。

例子五:

template<typename T>
void use(T &&t) {
  __asm__ __volatile__ ("" :: "g" (t));
}

extern void g();

void f() {
  int v[1];
  use(v);
  v[0] = 42;
  g(); // same with clobber()
}

最后我们得到存储,因为 v 转义并且编译器必须保守地假设对 g 的调用可以访问存储的值。

(用于实验 https://godbolt.org/g/rFviMI )

关于c++ - 在进行基准测试时防止编译器优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40122141/

相关文章:

c++ - 函数定义上的全局命名空间作用域运算符

c - 如何正确为接收矩阵的 C 函数创建 header

c - 编译和链接dll时出错

c++ - 严格的别名,-ffast-math 和 SSE

c++ - 如何访问存储在列表中的对象

c++ - 不通过文件系统将 uint8_t* 缓冲区上传到 AWS S3

c++ - 长号。分配

c++ - 使用 unordered_map 移动构造函数

cuda - 用clang编译CUDA

linux - 安装安装