我最近看到了这个精彩的 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
转换为两个变量 len
和 ptr
。在某些时候,程序会为 ptr
赋值;该语句是 def。
现在,原来的程序没有对 vector 做任何事情;换句话说,len
或 ptr
都没有任何用途。因此,它们的所有定义都已失效,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/