Google基准测试框架的DoNotOptimize函数

9

我有些困惑于 Google Benchmark Framework 的函数 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)::);
    }
}

请提供一个当前无法按预期工作的示例。 - rustyx
这更或多或少是一个普遍问题,即寄存器是否会被溢出(这将导致不必要的/与基准测试无关的开销)。 - Hymir
1个回答

6
你想知道 "memory" 损坏的原因?是的,这可能会导致其他东西被溢出,但有时这正是你想要在尝试包装重复循环的迭代之间发生的事情。
请注意,"memory" 损坏不会影响那些不可能从全局变量到达的对象。(逃逸分析)。因此,它不会导致像 for(int i = ...) 中的循环计数器被溢出/重新加载等问题。
将指定变量的值在一个寄存器中实现(并忘记其常量传播或CSE目的的值)正是这个函数的重点,并且是便宜的。除非真的优化了,否则该值已经在寄存器中了。
(除非是tmp1 = a+b; / tmp2 = tmp1+c这种情况,但编译器更愿意先执行b+c。在这种情况下,强制tmp1被实例化将迫使它实际上执行a+b。通常这不是问题,因为人们通常不会在较大计算的一部分使用DoNotOptimize临时变量。)
我认为故意这样做是为了阻止更多的东西,比如将大量循环不变量提升和其他CSE或跨迭代强度减少或重复循环中的基准测试。通常可以看到人们只在计算的最终结果上使用benchmark::DoNotOptimize(); 如果它没有"内存"破坏,那么它甚至更不可能停止编译器从准备值(或某些不变部分)一次性移动到寄存器中的过程。
理解他们试图进行基准测试的人可能会使用asm("" : "+g"(var));来使编译器材料化并忘记它所知道的值,而不触发其他全局变量的溢出。
"+r,m"是clang的一种解决方法,它倾向于为"+rm""+g"发明一个内存临时变量。GCC尽可能选择寄存器。)

"+m+"指针相关

不行,这会强制编译器溢出指针本身,而这并不是你想要的。你只想确保所指向的内存也同步,以防用户期望如此,因此在那里使用"memory" clobber是有意义的。

或者另一种没有"memory" clobber的方法:

asm volatile("" : "+r"(ptr), "+m"(*ptr));

或者针对一整个指向对象的数组(如何表明内联汇编参数所指向的内存可能被使用?)。

// deref pointer-to-array of unspecified size
asm volatile("" : "+r"(ptr), "+m"( *(T (*)[]) ptr  );

如果ptr为NULL,这两者中的任一个都可能出错,因此通用定义不能安全地使用它们来处理所有指针。
手动使用它们时,您可以在寄存器中的指针本身或指向的内存上省略+,以便只强制实现该值而不会忘记它。
您还可以省略"+r"(ptr)操作数,仅确保指向的内存与当前同步,而不强制存在准确的指针寄存器。编译器仍然必须能够生成引用内存的寻址模式,并且可以通过asm模板扩展操作数来查看所选择的内容。
asm( "nop  # mem operand picked %0" : "+m" (*ptr) );

你不需要一个nop,它可以是一个纯汇编注释行,比如# hi mom, operand at %0,但是Godbolt编译器资源管理器(https://godbolt.org/z/doPGsse9c 这个例子)默认过滤注释,所以使用指令很方便。即使它不是有效的,如果你只想查看GCC的汇编输出,也可以这样做。例如,对于int *ptr = func_arg+10;,可以使用nop # mem operand picked 40(%rdi)
GCC的汇编模板完全像printf一样进行文本替换,将文本放入输出文件中,位置由GCC选择来扩展汇编语句。然而,Clang是不同的;它有一个内置的汇编器来操作内联汇编。

更新:Google的`DoNotOptimize()`函数如何强制执行语句顺序 - "memory" clobber可以防止DoNotOptimizetime()重新排序,这可能是他们包含它的原因之一。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接