C++ 在基准测试时防止编译器优化

C++ 在基准测试时防止编译器优化,c++,gcc,clang,performance-testing,compiler-optimization,C++,Gcc,Clang,Performance Testing,Compiler Optimization,我最近遇到了这个精彩的演讲 提到的防止编译器优化代码的技术之一是使用以下函数 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(

我最近遇到了这个精彩的演讲

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

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()
}
静态无效转义(void*p){
asm volatile(“::”g“(p:”内存”);
}
静态空隙冲击器(){
asm易失性(“:”内存”);
}
无效基准()
{
向量v;
v、 储备(1);
逃避(v.data());
v、 推回(10);
clobber()
}
我正在努力理解这一点。问题如下

1) 逃跑比撞人有什么好处

2) 从上面的例子来看,clobber()似乎阻止了前面的语句(push_back)以这种方式进行优化。如果是这样,为什么下面的代码片段不正确

 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber()
 }
 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber();
 }
void benchmark()
{
向量v;
v、 储备(1);
v、 推回(10);
clobber()
}
如果这还不够混乱的话,folly(FB的线程库)有一个偶数

相关片段:

template <class T>
void doNotOptimizeAway(T&& datum) {
  asm volatile("" : "+r" (datum));
}
模板
void dono优化中途(T&基准){
asm volatile(“:”+r(基准));
}
我的理解是,上面的代码段通知编译器程序集块将写入数据。但是,如果编译器发现该数据没有使用者,它仍然可以优化生成该数据的实体,对吗

我想这不是常识,任何帮助都是非常感谢的

1) 逃跑比撞人有什么好处

escape()
escape()
以以下重要方式补充了
clobber()

clobber()
的作用仅限于可能通过假想的全局根指针访问的内存。换句话说,编译器的分配内存模型是通过指针相互引用的块的连接图,并且所述假想的全局根指针用作该图的入口点。(在此模型中不考虑内存泄漏,即编译器忽略了一旦可访问的块由于指针值丢失而变得不可访问的可能性)。新分配的块不是此类图的一部分,并且不会受到
clobber()
的任何副作用的影响
escape()
确保传入的地址属于全局可访问的内存块集。当应用于新分配的内存块时,
escape()
具有将其添加到所述图形的效果

2) 从上面的示例来看,clobber()似乎可以防止 前面的语句(push_back)将以优化方式进行优化。如果是这样的话 为什么下面的代码段不正确

 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber()
 }
 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber();
 }
void benchmark()
{
向量v;
v、 储备(1);
v、 推回(10);
clobber();
}

隐藏在
v.reserve(1)
中的分配对于
clobber()
不可见,直到它通过
escape()
tl注册;dr
DonoOptimizeAway
创建了一个人工“使用”

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

如果从def之后的一点开始,程序出口的所有路径都没有遇到使用变量的情况,则该def称为
dead
,而dead code Elimination(DCE)pass将删除它。这反过来可能会导致其他def失效(如果该def由于具有可变操作数而被使用),等等

想象一下在聚合的标量替换(SRA)过程之后的程序,它将本地
std::vector
转换为两个变量
len
ptr
。在某个点上,程序将一个值分配给
ptr
;那份声明是一份声明

现在,原来的程序没有对向量做任何处理;换句话说,
len
ptr
都没有任何用途。因此,它们的所有def都是死的,DCE可以删除它们,从而有效地删除所有代码并使基准变得毫无价值

添加
doNotOptimizeAway(ptr)
会创建一种人为使用,从而防止DCE删除DEF。(作为旁注,我认为“+”中没有任何一点,“g”应该足够了)

内存加载和存储也可以遵循类似的推理路线:如果没有指向程序结尾的路径,则存储(def)已失效,其中包含来自该存储位置的加载(使用)。由于跟踪任意内存位置比跟踪单个伪寄存器变量要困难得多,编译器会保守地进行推理——如果没有指向程序结尾的路径,则存储区将死亡,这可能会遇到对该存储区的使用

其中一种情况是存储到内存区域,保证不会出现别名-在释放内存后,不可能使用该存储,因为它不会触发未定义的行为。瞧,没有这样的用途

因此,编译器可以消除
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
没有别名。
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()
}