我试图确切地了解GoogleDoNotOptimize()
应该如何工作。
为了完整起见,这里是它的定义(对于 clang 和非常量数据):
template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) {
asm volatile("" : "+r,m"(value) : : "memory");
}
据我了解,我们可以在这样的代码中使用它:
start_time = time();
bench_output = run_bench(bench_inputs);
result = time() - start_time;
确保基准保持在临界区:
start_time = time();
DoNotOptimize(bench_inputs);
bench_output = run_bench(bench_inputs);
DoNotOptimise(bench_output);
result = time() - start_time;
具体是什么我不明白的是为什么这种担保(是吗?)那run_bench()
是不会的上方移动start_time = time()
。
据我了解,上面DoNotOptimze()
做了几件事:
value
到堆栈,因为它是由 C++ 引用传递的。你不能有一个指向寄存器的指针,所以它必须在内存中。value
现在的堆栈,随后重挫内存(如ASM限制做)在将强制编译器假定value
既读,并通过调用写DoNotOptimize(value)
。+r,m
约束是否相关。据我所知,这表示指针本身可能存储在寄存器或内存中,但指针值本身可能被读取和/或写入。)这就是事情对我来说变得模糊的地方。
如果start_time
也是堆栈分配的,内存破坏DoNotOptimize()
将意味着编译器必须假设DoNotOptimize()
可能读取start_time
. 因此,语句的顺序只能是:
start_time = time(); // on the stack
DoNotOptimize(bench_inputs); // reads start_time, writes bench_inputs
bench_output = run_bench(bench_inputs)
但是如果start_time
不是存储在内存中,而是存储在寄存器中,那么 clobbering memory 就不会 clobber start_time
,对吧?在这种情况下,start_time = time()
和的所需顺序DoNotOptimize(bench_inputs)
丢失,编译器可以自由地执行以下操作:
DoNotOptimize(bench_inputs); // only writes bench_inputs
bench_output = run_bench(bench_inputs)
start_time = time(); // in a register
显然我误解了一些东西。谁能帮忙解释一下?谢谢 :)
我想知道这是否是因为重新排序优化发生在寄存器分配之前,因此所有内容都被假定为当时堆栈分配。但如果是这样的话,那DoNotOptimize()
就是多余的,ClobberMemory()
就足够了。
总结:DoNotOptimize
是订购wrt。time()
由"memory"
clobbers,就好像它是另一个可以修改任何全局状态的不透明函数的函数调用。
DoNotOptimize
订购wrt。正如 Chandler Carruth 在您链接的问答中解释的那样,通过输入计算的数据依赖性以及计算的输出来计算输入的输出。该"memory"
撞是无关紧要的这一部分。
"memory"
clobber 就像一个非内联函数调用DoNotOptimize
的asm
声明包含一个"memory"
破坏。就优化器而言,这相当于一个不透明的函数调用:必须假设它读取和写入每个全局可访问的对象1。(即使是这个编译单元可能不知道的。)
由于time()
它本身在任何头文件中都没有内联定义,因此它不能DoNotOptimize
在编译时重新排序,原因与编译器无法重新排序调用foo()
以及bar()
当它看不到这些函数的定义时相同。同样的原因编译器不需要任何特殊的逻辑来阻止它们重新排序puts("hi"); puts("mom");
.
(一个time()
可以内联并且只包含一个asm
语句的假设必须用来asm volatile
确保重复调用不只是使用第一个的输出。asm volatile
语句不能相互重新排序或访问volatile
变量,所以这也可以,出于不同的原因。)
脚注 1:全局可达 = 任何假设的全局变量可能指向的任何对象。即,除了这个函数内的局部变量,或者用 新分配的内存之外的任何东西new
,如果逃逸分析可以证明这个函数之外的任何东西都不能有指向它们的指针。
asm
语句的工作我认为您严重误解了 asm 的工作原理。"+r,m"
告诉编译器在寄存器(或内存,如果需要)中实现该值,然后使用(空)asm 模板末尾的值作为该 C++ 对象的新值。
所以它迫使编译器在某处实际实现(产生)值,这意味着它必须被计算。这意味着必须忘记它之前知道的值(例如,它是一个编译时常量 5,或非负数,或任何东西),因为"+"
修饰符声明了一个读/写操作数。
DoNotOptimize
输入的要点是击败会让基准优化掉的恒定传播。
并在输出上确保最终结果实际在寄存器(或内存)中实现,而不是优化掉所有导致未使用结果的计算。(这就是存在asm volatile
相关的地方;击败恒定传播仍然适用于 non-volatile asm
。)
因此,您要进行基准测试的计算必须在两个DoNotOptimize()
语句之间进行,并且这两个语句不能分别使用time()
.
编译器必须假设 asm 语句像val ^= random
它所知道的一样修改值,同时更改任何/每个其他对象的内存中的值,除了不是操作数的私有本地变量,因此例如,"memory"
clobber 不会停止编译器将本地循环计数器保留在内存中。(这里不是一个空的 asm 模板字符串的特殊情况;程序不会偶然包含这样的 asm 语句,所以没有人希望它们被优化掉。)
"m"
"+r,m"
在决定从头开始解释可能更好之前,我只是部分地了解了您尝试推理操作数和参考函数 arg的细节。正确的原因并不复杂。但有几件事值得特别纠正:
包含该asm
语句的 C++ 函数可以内联,让 by-reference 函数 arg 优化掉。(它甚至被声明inline __attribute__((always_inline))
为即使禁用优化也强制内联,尽管在这种情况下引用变量不会优化掉。)
最终结果就像直接在传递给 DoNotOptimize 的 C++ 变量上使用 asm 语句一样。例如DoNotOptimize(foo)
就像asm volatile("" : "+r,m"(foo) :: "memory")
如果需要,编译器总是可以选择寄存器,例如选择在asm
语句之前将变量的值加载到寄存器中。(如果 C++ 语义要求更新内存中变量的值,还要在 asm 语句之后发出一条存储指令。)
例如,我们可以看到 GCC 确实选择这样做。(我想我可以用作incl %0
示例,但我只是选择了nop
一种方式来显示编译器为操作数位置选择的内容作为# %0
纯注释的替代方法,因此Godbolt 编译器资源管理器不会将其过滤掉。)
void foo(int *p)
{
asm volatile("nop # operand picked %0" : "+r,m" (p[4]) );
}
# GCC 11.2 -O2
foo(int*):
movl 16(%rdi), %eax
nop # operand picked %eax
movl %eax, 16(%rdi)
ret
与 clang 选择将值留在内存中,因此 asm 模板中的每条指令都将访问内存而不是寄存器。(如果有任何说明)。
# clang 12.0.1 -O2 -fPIE
foo(int*): # @foo(int*)
nop # operand picked 16(%rdi)
retq
有趣的事实:"r,m"
试图解决 clang 未命中优化错误,该错误使其始终为"rm"
约束选择内存,即使该值已经在寄存器中。首先溢出它,即使它必须为表达式的值创建一个临时位置作为输入。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句