为什么 asm volatile("" :::: "memory") 可以作为编译器屏障?

Why can `asm volatile("" ::: "memory")` serve as a compiler barrier?

众所周知,asm volatile ("" ::: "memory") 可以作为编译器屏障,以防止编译器重新排序跨它的汇编指令。例如,在 https://preshing.com/20120625/memory-ordering-at-compile-time/ 的“显式编译器障碍”部分中提到了它。

然而,我能找到的所有文章都只提到了 asm volatile ("" ::: "memory") 可以作为编译器障碍的事实,而没有给出为什么 "memory" 破坏器可以有效地形成编译器障碍的原因。 GCC online documentation 只是说所有特殊的 clobber "memory" 所做的就是告诉编译器汇编代码可能会执行除操作数列表中指定的操作之外的内存读取或写入。但是这样的语义如何导致编译器停止任何对内存指令重新排序的尝试呢?我试图回答自己但失败了,所以我在这里问:为什么 asm volatile ("" ::: "memory") 可以作为编译器屏障,基于 "memory" clobber 的语义?请注意,我问的是“编译器屏障”(在编译时有效),而不是更强的“内存屏障”(在 运行 时有效)。为了方便起见,我在下面的 GCC 在线文档中摘录了 "memory" clobber 的语义:

The "memory" clobber tells the compiler that the assembly code performs memory reads or writes to items other than those listed in the input and output operands (for example, accessing the memory pointed to by one of the input parameters). To ensure memory contains correct values, GCC may need to flush specific register values to memory before executing the asm. Further, the compiler does not assume that any values read from memory before an asm remain unchanged after that asm; it reloads them as needed. Using the "memory" clobber effectively forms a read/write memory barrier for the compiler.

如果一个变量可能被读取或写入,那么发生的顺序很重要。"memory" 破坏的目的是确保读取 and/or 写入 asm 语句发生在程序执行的正确位置。

asm 语句之后在源中发生的任何 C 变量值读取必须在编译器为目标机器生成的汇编输出中的内存破坏 asm 语句之后,否则它可能会在 asm 语句更改它之前读取一个值。

之前 一个 asm 语句之前对源中 C var 的任何读取同样必须保持顺序在前,否则它可能会错误地读取修改后的值。

类似的推理适用于对 C 变量 before/after 的赋值(写入)任何带有 "memory" 破坏的 asm 语句。就像对“不透明”函数的函数调用一样,编译器看不到它的定义。

没有任何读取或写入可以在任何方向上使用屏障重新排序,因此屏障之前的任何操作都不能使用屏障之后的任何操作重新排序,反之亦然。


另一种看待它的方式:实际的机器内存内容必须与此时的 C 抽象机相匹配。编译器生成的 asm 必须尊重这一点,通过在 asm("":::"memory") 语句开始之前将寄存器中的任何变量值存储到内存中,然后它必须假设任何具有变量值副本的寄存器可能不会启动至今为止。因此,如果需要,必须重新加载它们。

"memory" 破坏器的这种读取所有内容/写入所有内容的假设是阻止 asm 语句在编译时完全重新排序的原因。所有访问,甚至是非 volatile 访问。 volatile 已经隐含为没有 "=..." 输出操作数的 asm() 语句,并且阻止它被完全优化(以及内存破坏)。


请注意,只有可能“可达”的 C 变量会受到影响。例如,只要 asm 语句本身没有地址作为输入,逃逸分析仍然可以让编译器在 "memory" 破坏的寄存器中保留本地 int i

就像一个函数调用:for (int i=0;i<10;i++) {foobar("%d\n", i);} 可以将循环计数器保存在一个寄存器中,并且每次迭代都将它复制到 foobar 的第二个参数传递寄存器。 foob​​ar 不可能引用 i 因为它的地址没有存储在任何地方或传递到任何地方。

(这对于内存屏障用例来说很好;其他 线程 也不能有它的地址。)


相关:

  • - 为什么 opaque 函数调用会成为编译器障碍。
  • - 非空 asm 语句(或其他虚拟操作数告诉 asm 语句 which内存被读取/写入。)