Kod*_*ior 10 c++ gcc clang performance-testing compiler-optimization
我最近遇到了这个辉煌的cpp2015演讲CppCon 2015:Chandler Carruth"调优C++:基准测试,CPU和编译器!哦,我的!"
提到的阻止编译器优化代码的技术之一是使用以下函数.
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()
}
Run Code Online (Sandbox Code Playgroud)
我试图理解这一点.问题如下.
1)越过clobber逃脱的好处是什么?
2)从上面的例子看起来,clobber()可以防止先前的语句(push_back)被优化.如果是这样的话,为什么下面的代码片段不正确?
void benchmark()
{
vector<int> v;
v.reserve(1);
v.push_back(10);
clobber()
}
Run Code Online (Sandbox Code Playgroud)
如果这不够混乱,那么愚蠢(FB的线程化lib)就会有一个更奇怪的实现
相关片段:
template <class T>
void doNotOptimizeAway(T&& datum) {
asm volatile("" : "+r" (datum));
}
Run Code Online (Sandbox Code Playgroud)
我的理解是上面的片段通知编译器程序集块将写入数据.但是如果编译器发现没有这个数据的消费者,它仍然可以优化生成数据的实体吗?
我认为这不是常识,任何帮助表示赞赏!
1) 与 clobber 相比,escape 的优势是什么?
escape()没有优势clobber()。以以下重要方式escape() 补充 clobber():
的影响clobber()仅限于可通过虚拟全局根指针访问的内存。换句话说,编译器的内存分配模型是一个通过指针相互引用的块的连通图,并且所述假想的全局根指针作为该图的入口点。(此模型中不考虑内存泄漏,即编译器忽略了曾经可访问的块可能由于丢失的指针值而变得不可访问的可能性)。新分配的块不是此类图的一部分,并且不受 的任何副作用的影响clobber()。escape()确保传入的地址属于全局可访问的内存块集。当应用于新分配的内存块时,escape()具有将其添加到所述图形的效果。
2) 从上面的例子来看,看起来 clobber() 阻止了之前的语句 ( push_back ) 被优化的方式。如果是这种情况,为什么下面的代码片段不正确?
Run Code Online (Sandbox Code Playgroud)void benchmark() { vector<int> v; v.reserve(1); v.push_back(10); clobber(); }
隐藏在里面的分配v.reserve(1)是不可见的,clobber()直到它通过escape().
tl; dr doNotOptimizeAway创造了一种人为的"使用".
这里有一点术语:"def"("定义")是一个语句,它为变量赋值; "use"是一个语句,它使用变量的值来执行某些操作.
如果从def之后的那一点开始,程序出口的所有路径都没有遇到变量的使用,则调用def dead并且死代码消除(DCE)传递将删除它.这反过来可能导致其他defs变为死亡(如果def因为具有可变操作数而被使用),等等.
想象一下在Scalar Replacement of Aggregates(SRA)传递之后的程序,它将局部变为std::vector两个变量len和ptr.在某些时候,程序会为...分配一个值ptr; 那个陈述是一个def.
现在,原始程序没有对矢量做任何事情; 换句话说,没有任何使用len或ptr.因此,他们所有的def都已经死了,DCE可以删除它们,有效地删除所有代码并使基准毫无价值.
添加doNotOptimizeAway(ptr)会创建一个人工使用,这会阻止DCE删除defs.(作为旁注,我认为"+"中没有任何意义,"g"应该已经足够了).
类似的推理可以跟随内存加载和存储:如果没有到程序末尾的路径,则存储(def)已经死亡,其中包含来自该存储位置的加载(使用).由于跟踪任意内存位置比跟踪单个伪寄存器变量要困难得多,编译器保守地说 - 如果没有到程序末尾的路径,那么存储就会死亡,这可能会遇到使用该存储的问题.
一个这样的情况,是存储区域的存储,保证不存在别名 - 在释放内存之后,不可能使用该存储,这不会触发未定义的行为.IOW,没有这样的用途.
因此编译器可以消除v.push_back(42).但是有了它escape- 它会v.data()被认为是任意别名,正如上面描述的@Leon.
该clobber()示例中的目的是创建所有别名内存的人工使用.我们有一个商店(从push_back(42)),商店到一个全局别名的位置(由于escape(v.data())),因此clobber()可能包含该商店的使用(IOW,商店副作用是可观察的),因此编译器不是允许删除商店.
一些简单的例子:
例I:
void f() {
int v[1];
v[0] = 42;
}
Run Code Online (Sandbox Code Playgroud)
这不会生成任何代码.
例二:
extern void g();
void f() {
int v[1];
v[0] = 42;
g();
}
Run Code Online (Sandbox Code Playgroud)
这只会产生一个调用g(),没有内存存储.该函数g无法访问,v因为v没有别名.
例三:
void clobber() {
__asm__ __volatile__ ("" : : : "memory");
}
void f() {
int v[1];
v[0] = 42;
clobber();
}
Run Code Online (Sandbox Code Playgroud)
与上一个示例中一样,没有生成的商店,因为v没有别名,并且调用clobber内联到任何内容.
例四:
template<typename T>
void use(T &&t) {
__asm__ __volatile__ ("" :: "g" (t));
}
void f() {
int v[1];
use(v);
v[0] = 42;
}
Run Code Online (Sandbox Code Playgroud)
这个时间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()
}
Run Code Online (Sandbox Code Playgroud)
最后我们得到了商店,因为v转义和编译器必须保守地假设调用g可以访问存储的值.
(用于实验https://godbolt.org/g/rFviMI)
| 归档时间: |
|
| 查看次数: |
2296 次 |
| 最近记录: |