如何解决Google 基准框架 DoNotOptimize
我对 Google Benchmark Framework (definition from here) 的函数 void DoNotOptimize
的实现有点困惑:
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
}
所以它具体化了变量,如果是非常量的,也会告诉编译器忘记它之前的值。 ("+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)::);
}
}
解决方法
您想知道 "memory"
的破坏吗?是的,这可能会导致其他内容溢出,但有时这正是您想要在您尝试环绕重复循环的迭代之间的内容。
请注意,"memory"
破坏器不会影响无法从全局变量访问的对象。 (Escape analysis)。所以它不会导致 for(int i = ...)
中的循环计数器之类的东西被溢出/重新加载。
在寄存器中实现指定变量的值(并且为了常量传播或 CSE 目的而忘记它的值)正是这个函数的重点,而且很便宜。除非东西真的被优化掉了,否则这个值已经在寄存器中了。
(除非是 tmp1 = a+b;
/ tmp2 = tmp1+c
的情况,但编译器宁愿先执行 b+c
。在这种情况下,强制 tmp1 被物化将迫使它实际上执行 {{ 1}}。通常这不是问题,因为人们通常不会在属于较大计算的临时对象上使用 DoNotOptimize。)
我认为在阻止更多事情方面犯这种错误是有意的,例如提升循环不变量和其他 CSE 负载或跨迭代或在基准测试中重复循环。看到人们只对计算的最终结果或其他东西使用 a+b
是很常见的;如果它没有“内存”破坏,它就更不可能阻止编译器准备一次值(或某些不变的部分),并且每次迭代只benchmark::DoNotOptimize()
将它实现在寄存器中.
那些完全了解他们正在尝试进行基准测试以检查编译器生成的 asm 的人当然可能希望使用 mov
使编译器实现它并忘记它对值的了解,不会触发其他全局变量的任何溢出。
(asm("" : "+g"(var));
是 clang 的一种解决方法,它倾向于为 "+r,m"
或 "+rm"
发明一个临时内存。GCC 尽可能选择寄存器。)
"+g"
用于指针
不,这会迫使编译器溢出指针 value 本身,这是您不想要的。您只想确保指向的内存也是同步的,以防这是用户所期望的,因此“内存”破坏在那里是有意义的。
或者没有“记忆”破坏的另一种方式:
"+m"
或者对于一个完整的指向对象数组 (How can I indicate that the memory *pointed* to by an inline ASM argument may be used?)
asm volatile("" : "+r"(ptr),"+m"(*ptr));
但如果 // deref pointer-to-array of unspecified size
asm volatile("" : "+r"(ptr),"+m"( *(T (*)[]) ptr );
为 NULL,其中任何一个都可能会中断,因此通用定义对所有指针使用其中任何一个都不安全。
手动使用这些,您可能会在寄存器中的指针本身或指向的内存上省略 ptr
,以强制具体化该值,而不会在以后忘记它。
您也可以省略 +
操作数,并且只是确保指向的内存是同步的,而不强制将确切的指针存在于寄存器中。编译器仍然必须能够生成引用内存的寻址模式,您可以通过让 asm 模板扩展操作数来查看它选择的内容:
"+r"(ptr)
您不需要 asm( "nop # mem operand picked %0" : "+m" (*ptr) );
,它可以是像 nop
这样的纯 asm 注释行,但是 Godbolt 编译器资源管理器(本示例中为 https://godbolt.org/z/doPGsse9c)默认过滤注释,因此使用指令很方便。但是,如果您只想查看 GCC 的 asm 输出,它甚至不必是有效的。例如# hi mom,operand at %0
为 nop # mem operand picked 40(%rdi)
。
GCC 的 asm 模板纯粹是一种文本替换,如 printf 以将文本放入输出文件中 GCC 选择展开 asm 语句的位置。然而,Clang 是不同的。它有一个内置的汇编器,可以在内联汇编上运行。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。