asm、asm volatile 和 clobbering memory 的区别

Posted

技术标签:

【中文标题】asm、asm volatile 和 clobbering memory 的区别【英文标题】:The difference between asm, asm volatile and clobbering memory 【发布时间】:2013-01-05 03:03:47 【问题描述】:

在实现无锁数据结构和时序代码时,通常需要抑制编译器的优化。通常人们使用asm volatilememory 在clobber 列表中执行此操作,但有时您只会看到asm volatile 或只是一个普通的asm 破坏内存。

这些不同的语句对代码生成有什么影响(特别是在 GCC 中,因为它不太可能是可移植的)?

仅供参考,以下是有趣的变化:

asm ("");   // presumably this has no effect on code generation
asm volatile ("");
asm ("" ::: "memory");
asm volatile ("" ::: "memory");

【问题讨论】:

似乎有人在离金属太近了 :-) (在其他地方,@Mysticial 正在输入一个可笑的详细答案......) 【参考方案1】:

请参阅"Extended Asm" page in the GCC documentation。

您可以通过在asm 之后写入关键字volatile 来防止删除asm 指令。 [...] volatile 关键字表明该指令具有重要的副作用。如果volatile asm 可访问,GCC 不会删除它。

没有任何输出操作数的asm 指令将被视为与易失性asm 指令相同。

您的示例都没有指定输出操作数,因此asmasm volatile 表单的行为相同:它们在代码中创建了一个不能删除的点(除非它被证明是不可访问的)。

这不等于什么都不做。请参阅 this question 以获取更改代码生成的虚拟 asm 示例 - 在该示例中,循环 1000 次的代码被矢量化为一次计算 16 次循环迭代的代码;但是循环中存在asm 会抑制优化(asm 必须达到 1000 次)。

"memory" 破坏器使 GCC 假定任何内存都可以被asm 块任意读取或写入,因此将阻止编译器重新排序加载或存储:

这将导致 GCC 不会在整个汇编指令中将内存值缓存在寄存器中,并且不会优化对该内存的存储或加载。

(不过,这不会阻止 CPU 相对于另一个 CPU 重新排序加载和存储;您需要真正的内存屏障指令。)

【讨论】:

这实际上很有趣,我没有意识到 gcc 将没有输出的 asm 块视为 volatile 是我的知识空白。 所以volatile = 性能杀手,无论它在什么上下文中使用(变量或 asm)。使用 goto 关键字归档 - 仅在绝对必要时使用。 “任何内存”是指内存中的任何对象? A "memory" clobber 仅适用于全局可访问的内存,或可通过asm 语句的任何指针输入访问的内存。至于哪些 C 对象必须在内存中“同步”,哪些仍然可以在寄存器中,这就像一个非内联函数调用。因此,由于escape analysis,从未将其地址传递到函数之外的本地变量(例如循环计数器)通常仍可以保留在寄存器中。 这种优化是安全的,因为它已经不安全/不允许执行类似asm("incl -16(%%rbp)" ::: "memory") 的操作来访问 gcc 碰巧放置本地的堆栈空间(不使用 "+m" 操作数来获取编译器生成寻址模式)。堆栈框架布局不是您可以做出任何假设的东西。不同的编译器选项会改变它。所以无论如何,"memory" clobber 会按照这个答案所说的那样做,但性能损失并没有相当那么糟糕。【参考方案2】:

asm ("") 什么都不做(或者至少,它不应该做什么。

asm volatile ("") 也不做任何事情。

asm ("" ::: "memory") 是一个简单的编译器栅栏。

asm volatile ("" ::: "memory")AFAIK 和前面的一样。 volatile 关键字告诉编译器不允许移动这个程序集块。例如,如果编译器确定每次调用中的输入值都相同,它可能会被提升出循环。我不确定在什么条件下编译器会决定它对程序集有足够的了解以尝试优化其位置,但volatile 关键字完全抑制了这一点。也就是说,如果编译器试图移动没有声明输入或输出的asm 语句,我会感到非常惊讶。

顺便说一句,volatile 还会阻止编译器在确定输出值未使用时删除表达式。这只有在有输出值的情况下才会发生,因此不适用于asm ("" ::: "memory")

【讨论】:

Matthew Slattery 的回答指出asm volatile ("") 与什么都不做并不完全相同,因为它会对编译器优化产生巨大影响。同样的性能影响也适用于使用asm volatile ("" ::: "memory") 作为编译器栅栏。 编译器看不懂汇编语言! @curiousguy 不,但它确实理解 asm 块何时声明了输入/输出,它告诉编译器它依赖于哪些寄存器以及它将修改哪些寄存器,因此编译器可以随机播放如果它们不影响输入/输出,则进行某些计算。 至少对于 GCC,asm volatile 确实 not 禁止一般指令重新排序,它只会防止可访问的 asm 块由于(明显)缺乏有意义的内容而被删除副作用(在最近的 GCC 上,即使编译器确定输入始终相同,也可以防止它被提升出循环)。否则,指令重新排序只会被声明的输入和输出(以及伪输出,如"memory")所禁止。在the docs 中阅读更多信息。 是的,asm 语句如果没有输出约束,则隐式为 volatile。 (gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html)。所以asm("":::"memory")asm volatile("":::"memory")完全相同。如果从不使用结果,则可以删除非易失性 asm 语句,如果重复使用相同的输入运行,则可以提升(或以其他方式 CSEd)。因此,您需要明确的 volatile 来包装 rdtscrdrand 之类的内容,因为您确实会获得具有相同(空)输入集的不同输出。【参考方案3】:

为了对 Lily Ballard's answer 的完整性,Visual Studio 2010 提供了 _ReadBarrier()_WriteBarrier()_ReadWriteBarrier() 来做同样的事情(VS2010 不允许 64 位应用程序的内联汇编)。

这些不会生成任何指令,但会影响编译器的行为。一个很好的例子是here。

MemoryBarrier() 生成lock or DWORD PTR [rsp], 0

【讨论】:

以上是关于asm、asm volatile 和 clobbering memory 的区别的主要内容,如果未能解决你的问题,请参考以下文章

使用内联 PTX asm() 指令时,'volatile' 有啥作用?

如何使用“asm volatile”编写 btr 指令

asm volatile ("B .")

gcc asm

一个小的asm

翻译 | “扩展asm”——用C表示操作数的汇编程序指令