gcc 为什么会`asm volatile(“”:::“内存”)充当编译器屏障?

aoyhnmkz  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(309)

众所周知,asm volatile ("" ::: "memory")可以充当编译器屏障,以防止编译器在其上重新排序汇编指令。例如,在https://preshing.com/20120625/memory-ordering-at-compile-time/的“显式编译器屏障”一节中提到了它。
然而,在一些情况下,我所能找到的所有文章都只提到了asm volatile ("" ::: "memory")可以作为编译器屏障的事实,而没有给出为什么"memory" clobber可以有效地形成编译器屏障的原因。GCC在线文档只说,"memory" clobber所做的一切只是告诉编译器,汇编代码可能会执行操作数列表中指定之外的内存读写。但是,这样的语义是如何使编译器停止对内存指令进行重新排序的呢?我试图回答自己,但失败了,所以我在这里问:基于"memory" clobber的语义,为什么asm volatile ("" ::: "memory")可以作为编译器屏障?请注意,我问的是“编译器屏障”(在编译时有效),而不是更强的“内存屏障”(在运行时有效)。为方便起见,我摘录了GCC在线文档中"memory" clobber的语义如下:
"memory"乱码告诉编译器,汇编代码对输入和输出操作数中未列出的项执行内存读写操作为了确保内存包含正确的值,GCC可能需要在执行asm之前将特定的寄存器值刷新到内存中。此外,编译器不假定在asm之前从存储器读取的任何值在asm之后保持不变;使用"memory" clobber可以有效地为编译器形成读/写内存屏障。

bgibtngc

bgibtngc1#

如果一个变量可能被读取或写入,那么以什么顺序发生是很重要的。"memory"乱码的意义在于确保asm语句中的读取和/或写入发生在程序执行的正确点。
(Or更具体地说,在此 * 线程 * 的执行中,因为编译器屏障类似于atomic_signal_fence而不是atomic_thread_fence。除了在ISA(如x86 where acquire or release thread fences only require blocking compile-time reordering)上,以便利用硬件的强运行时排序。例如,asm("":::"memory")是x86上atomic_thread_fence(memory_order_release)的可能实现,而不是在AArch 64上。)
asm陈述式之后,任何在来源中发生的C变数值读取,都必须在目的机器编译器产生的组件输出中,位于内存破坏asm陈述式之后,否则可能会在asm陈述式变更值之前阅读值。
类似地,在asm语句 * 之前 * 读取源代码中的C var必须保持在之前的顺序,否则可能会错误地读取修改后的值。
类似的推理也适用于在任何asm语句之前/之后用"memory"乱码对C变量进行赋值(写入),就像对一个“不透明”函数的函数调用一样,编译器看不到它的定义。

任何读取或写入操作都不能在任一方向上对屏障进行重新排序(在编译时),因此,屏障之前的操作都不能对屏障之后的任何操作进行重新排序,反之亦然。

换个Angular 来看:实际的机器内存内容必须与C抽象机相匹配。编译器生成的asm必须遵守这一点,在asm("":::"memory")语句开始之前,将寄存器中的变量值存储到内存中,之后,它必须假设任何有变量值副本的寄存器可能不再是最新的。因此,如果需要,必须重新加载它们。
"memory"乱码的这种读取一切/写入一切的假设使得asm语句在编译时根本不对所有访问进行重新排序,即使是非volatile的访问。volatile已经隐含为没有"=..."输出操作数的asm()语句,并且是阻止它被完全优化掉的原因(以及随之而来的内存崩溃)。
注意,只有潜在的“可达”C变量会受到影响。例如,转义分析仍然可以让编译器在"memory"乱码中将本地int i保存在寄存器中,只要asm语句本身没有将该地址作为输入。
就像函数调用一样:for (int i=0;i<10;i++) {foobar("%d\n", i);}可以将循环计数器保存在一个寄存器中,并且在每次迭代时将它复制到foobar的第二个参数传递寄存器中。foobar不可能引用i,因为它的地址没有存储在任何地方,也没有传递到任何地方。
(This对于存储器屏障用例是良好的;其它 * 线程 * 也不能具有其地址。)
相关:

6mw9ycah

6mw9ycah2#

我要补充的是,: memory只是一个编译器指令。一个推测性的处理器 * 可能 * 重新排序指令。为了防止这种情况,显式的内存屏障调用是必要的。请参阅Linux文档中的内存屏障。

相关问题