Hym*_*mir 9 c++ assembly inline-assembly microbenchmark google-benchmark
void DoNotOptimize我对Google Benchmark Framework 的功能实现有点困惑(定义来自这里):
template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp const& value) {
asm volatile("" : : "r,m"(value) : "memory");
}
template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) {
#if defined(__clang__)
asm volatile("" : "+r,m"(value) : : "memory");
#else
asm volatile("" : "+m,r"(value) : : "memory");
#endif
}
Run Code Online (Sandbox Code Playgroud)
因此,它具体化了变量,如果变量非常量,也会告诉编译器忘记有关其先前值的任何信息。("+r"是 RMW 操作数)。
并且总是使用"memory"clobber,这是编译器对重新排序加载/存储的障碍,即确保所有全局可访问的对象的内存与 C++ 抽象机同步,并假设它们也可能已被修改。
我距离成为低级代码专家还很远,但据我了解其实现,该函数充当读/写屏障。因此,基本上,它确保传入的值位于寄存器或内存中。
虽然如果我想保留函数的结果(应该进行基准测试),这似乎是完全合理的,但我对编译器留下的自由度感到有点惊讶。
我对给定代码的理解是,编译器可能会在DoNotOptimize调用时插入物化点,这意味着重复执行时(例如,在循环中)会产生大量开销。当不应优化的值只是单个标量值时,如果编译器确保该值驻留在寄存器中似乎就足够了。
区分指针和非指针不是一个好主意吗?例如:
template< class T >
inline __attribute__((always_inline))
void do_not_optimize( T&& value ) noexcept {
if constexpr( std::is_pointer_v< T > ) {
asm volatile("":"+m"(value)::"memory");
} else {
asm volatile("":"+r"(value)::);
}
}
Run Code Online (Sandbox Code Playgroud)
你对"memory"破坏者感到好奇吗?是的,这可能会导致其他东西溢出,但有时这就是你想要的 between iterations of something you're trying to wrap a repeat loop around.
请注意,"memory"破坏不会影响无法从全局变量访问的对象。(逃逸分析)。所以它不会导致像循环计数器这样的东西for(int i = ...) to be spilled/reloaded.
在寄存器中具体化指定变量的值 (and forgetting about its value for constant-propagation or CSE purposes) is exactly the point of this function, and is cheap. Unless stuff really is optimizing away, the value will already be in a register.
tmp1 = a+b;(除非是/的情况tmp2 = tmp1+c,但编译器宁愿b+c先执行。在这种情况下,强制 tmp1 被具体化将迫使它实际执行a+b。通常这不是问题,因为人们通常不会在临时对象上使用 DoNotOptimize更大计算的一部分。)
我认为故意犯这样的错误是为了阻止更多的东西,比如提升循环不变量和其他CSE的负载,或者跨迭代或基准测试中重复循环的东西的强度减少。人们经常benchmark::DoNotOptimize()只使用计算的最终结果或其他东西;如果它没有“内存”破坏,则更不可能阻止编译器一次性准备值(或某些不变部分)moving it to materialize it in a register every iteration.
那些准确理解他们想要进行基准测试的内容足以检查编译器生成的 asm 的人肯定可能想要使用asm("" : "+g"(var)); to make the compiler materialize it and forget what it knows about the value, without triggering any spilling of other globals.
(这"+r,m"是 clang 的一种解决方法,它倾向于为"+rm"或发明一个临时内存"+g"。GCC 在可能的情况下选择寄存器。)
"+m"用于指针不,这会迫使编译器溢出指针值 itself, which you don't want. You only want to make sure the pointed-to memory is also in sync, in case that's what a user expects, so a "memory" clobber makes sense there.
或者没有“内存”破坏的另一种方式:
asm volatile("" : "+r"(ptr), "+m"(*ptr));
Run Code Online (Sandbox Code Playgroud)
或者对于指向对象的整个数组(如何指示可以使用内联 ASM 参数*指向*指向的内存?)
// deref pointer-to-array of unspecified size
asm volatile("" : "+r"(ptr), "+m"( *(T (*)[]) ptr );
Run Code Online (Sandbox Code Playgroud)
但如果ptr为 NULL,则其中任何一个都可能会中断,因此通用定义对所有指针使用其中任何一个都是不安全的。
手动使用这些,您可能会忽略+寄存器中的指针本身或指向的内存,以强制具体化该值而不会在以后忘记它。
您也可以省略"+r"(ptr)操作数,只确保指向的内存同步,而不强制精确的指针存在于寄存器中。编译器仍然必须能够生成引用内存的寻址模式,并且您可以通过让 asm 模板扩展操作数来查看它选择的内容:
asm( "nop # mem operand picked %0" : "+m" (*ptr) );
Run Code Online (Sandbox Code Playgroud)
您不需要nop,它可以是纯 asm 注释行,例如# hi mom, operand at %0,但 Godbolt 编译器资源管理器(本例为https://godbolt.org/z/doPGsse9c)默认会过滤注释,因此使用指令很方便。不过,如果您只想查看 GCC 的 asm 输出,它甚至不必有效。例如nop # mem operand picked 40(%rdi)对于int *ptr = func_arg+10;.
GCC 的 asm 模板纯粹是像 printf 一样的文本替换,用于将文本放入输出文件中 GCC 选择扩展 asm 语句的位置。不过,Clang 不同;它有一个内置的汇编器,可以在内联汇编上运行。