2

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
}

所以它实现了变量,如果是非常量,还告诉编译器忘记它之前的值。("+r"是一个 RMW 操作数)。

并且总是使用一个clobber,它是一个防止重新排序加载/存储的编译"memory"屏障,即确保所有全局可访问对象的内存与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)::);
    }
}
4

1 回答 1

4

你想知道"memory"clobber吗?是的,这可能会导致其他内容溢出,但有时这就是您在尝试重复循环的迭代之间想要的

请注意,"memory"clobber不会影响无法从全局变量访问的对象。(逃逸分析)。所以它不会导致像循环计数器这样的东西for(int i = ...)被溢出/重新加载。

在寄存器中具体化指定变量的值(并忘记它的值以用于常量传播或 CSE 目的)正是这个函数的重点,而且很便宜。除非东西真的在优化,否则该值将已经在寄存器中。

(除非是tmp1 = a+b;/的情况tmp2 = tmp1+c,但编译器宁愿先做b+c。在这种情况下,强制 tmp1 物化会强制它确实做a+b。通常这不是问题,因为人们通常不会在临时对象上使用 DoNotOptimize更大计算的一部分。)


我认为故意让这个错误发生在阻止更多东西的一边,比如提升循环不变量和其他CSE的负载,或者在迭代中降低强度,或者在基准测试中重复循环。看到人们benchmark::DoNotOptimize()只使用计算的最终结果或其他东西是很常见的。如果它没有“内存”clobber,那么就更不可能阻止编译器准备一次值(或一些不变的部分)并mov在每次迭代时将其具体化到寄存器中。

确切地了解他们正在尝试对什么进行基准测试以检查编译器生成的 asm 的人肯定可能希望使用asm("" : "+g"(var));它来使编译器实现它并忘记它对值的了解,而不会触发任何其他全局变量的溢出。

(这"+r,m"是 clang 的一种解决方法,它倾向于为"+rm"or发明一个临时内存。GCC 尽可能"+g"选择寄存器。)


"+m"用于指针

不,这会迫使编译器溢出指针本身,这是您不想要的。您只想确保指向的内存也同步,以防这是用户期望的,因此“内存”破坏器在那里是有意义的。

或者没有“记忆”破坏的另一种方式:

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

或者对于一个指向对象的整个数组(如何指示可以使用内联 ASM 参数指向的内存*?

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

但如果ptr为 NULL,则其中任何一个都可能会 break,因此泛型定义对所有指针使用其中任何一个都是不安全的。

手动使用这些,您可能会忽略+寄存器中的指针本身或指向的内存,以强制实现该值而不会忘记它。

您也可以省略"+r"(ptr)操作数,只需确保指向的内存是同步的,而不强制确切的指针存在于寄存器中。编译器仍然必须能够生成引用内存的寻址模式,您可以通过让 asm 模板扩展操作数来查看它选择了什么:

asm( "nop  # mem operand picked %0" : "+m" (*ptr) );

您不需要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 是不同的。它有一个内置的汇编器,可以在 inline asm 上运行。

于 2021-03-25T19:01:35.137 回答