【问题标题】:Why can `asm volatile("" ::: "memory")` serve as a compiler barrier?为什么 `asm volatile("" ::: "memory")` 可以用作编译器屏障?
【发布时间】:2021-06-11 21:16:22
【问题描述】:

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

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

"memory"clobber 告诉编译器汇编代码 对未列出的项目执行内存读取或写入 输入和输出操作数(例如,访问内存 由输入参数之一指向)。确保内存包含 正确的值,GCC 可能需要刷新特定的寄存器值以 执行asm 之前的内存。此外,编译器不假设 在 asm 之前从内存中读取的任何值在之后保持不变 那asm;它会根据需要重新加载它们。使用"memory" clobber 有效地为编译器形成了一个读/写内存屏障。

【问题讨论】:

  • 如果指令的效果未知,您不能重新排序。
  • IMO 它告诉编译器在下一条语句之前完成所有内存(以及其他操作)。就像在这个例子中:godbolt.org/z/fKv6GaGET
  • 我想你在一两天前问了一个非常相似的问题,和它一样,我不明白为什么引用的段落还没有回答这个问题。您能否举一个可能的重新排序的具体示例,您不确定引用的文本是否或为什么禁止它?比如说,C 源代码的 sn-p 以及您不确定编译器是否可以发出的汇编(伪)代码。那么可能有人可以更具体地解释一下。
  • @Nate Eldredge:您看不到它,因为您是专家,并且您认为这两个问题出于相同的原因非常相似。但是直到刚才我还没有看到 reads-everything / writes-everything 效果与编译器屏障效果的相似性——它们都是“要求内存同步的理想效果”。正是“同步”这句话让我想起了preshing.com/20120710/… 中的类比,然后我一下子就明白了你在评论中提到的相似之处。

标签: c gcc inline-assembly volatile memory-barriers


【解决方案1】:

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

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

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

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

任何读取或写入都不能与障碍在任一方向重新排序,因此障碍之前的任何操作都不能与障碍之后的任何操作重新排序,反之亦然。


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

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


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

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

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


相关:

【讨论】:

  • 在阅读“就像对“不透明”函数的函数调用一样”时,我一下子明白了。 preshing.com/20120625/memory-ordering-at-compile-time 给出了我想要的解释,但对于功能。由于它位于“隐含编译器障碍”部分,因此我未能在“显式编译器障碍”部分对asm("":::"memory") 应用相同的推理。现在我认为它们都是隐含的。谢谢你的详细回答。 PS,很酷指出volatile 是隐含的,所以我们不需要写它。
  • PS,与“可达”相关的示例可以在这里找到:gcc.gnu.org/legacy-ml/gcc-help/2019-09/msg00016.html
猜你喜欢
  • 1970-01-01
  • 2013-01-05
  • 1970-01-01
  • 2012-08-24
  • 2021-10-07
  • 1970-01-01
  • 2016-05-16
  • 1970-01-01
相关资源
最近更新 更多