asm、asm volatile和clobbering memory的区别

106

在实现无锁数据结构和计时代码时,通常需要抑制编译器的优化。通常人们使用带有memory的asm volatile来做到这一点,但有时只会看到带有memory的asm volatile或只是一个简单的asm清除内存。

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

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

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

2
有人似乎在过于接近底层进行搞事情 :-) (而在另一个地方,@Mysticial正在打着一个非常详细的答案...) - Kerrek SB
3个回答

89
请参阅GCC文档中的"扩展汇编"页面

您可以在asm之后写入关键字volatile,以防止删除asm指令。[...] volatile关键字表示该指令具有重要的副作用。如果可达,则GCC不会删除volatile asm。

以及

没有任何输出操作数的asm指令将与volatile asm指令处理方式相同。

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

这并不完全等同于什么也不做。请参考this question以获取一个更改代码生成的虚拟asm示例 - 在该示例中,循环1000次的代码被向量化为一次计算16次循环的代码; 但循环内部存在asm会抑制优化(必须达到1000次asm)。 "memory"占位符使得GCC认为任何内存都可以被虚拟asm读写,因此将防止编译器在asm的左右重新排序加载或存储:

这将导致GCC在汇编指令周围不保留内存值缓存,不对该存储进行优化或加载。

(这并不能防止CPU在另一个CPU方面重新排序加载和存储; 您需要真正的内存屏障指令来实现这一点。)

这实际上非常有趣,没有意识到gcc将没有输出的 asm 块视为易失性是我知识上的一大空白。 - jleahy
所以,无论在什么上下文中使用(变量或汇编语言),volatile都会成为性能杀手。只有在绝对必要的情况下才使用goto关键字。 - etherice
“任何内存”是指内存中的任何对象吗? - curiousguy
4
"memory" clobber仅适用于全局可访问的内存,或可通过asm语句的任何指针输入访问的内存。就需要在内存中“同步”的哪些C对象和仍然可以在寄存器中的对象而言,这就像非内联函数调用一样。因此,从未将其地址传递到函数外部(例如循环计数器)的本地变量通常仍可以保留在寄存器中,这要归功于逃逸分析 - Peter Cordes
1
这种优化是安全的,因为已经不允许像 asm("incl -16(%%rbp)" ::: "memory") 这样做来访问 gcc 放置本地变量的堆栈空间(没有使用 "+m" 操作数来获取编译器生成的寻址模式)。堆栈帧布局并不是您可以做出任何假设的东西;不同的编译器选项会改变它。所以无论如何,"memory" 破坏符合这个答案所说的内容,但是性能损失并不完全糟糕。 - Peter Cordes
显示剩余6条评论

13

asm("") 什么也不做(或者至少,它不应该做任何事情)。

asm volatile("") 同样什么也不做。

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

asm volatile ("" ::: "memory") 根据我所知道的与前面的相同。 volatile 关键字告诉编译器不允许移动此汇编块位置。例如,如果编译器决定每次调用时输入值都相同,就可能将其提升出循环。我不确定编译器在什么条件下会认为它足够了解汇编以尝试优化其位置,但是 volatile 关键字完全抑制了这一点。话虽如此,如果没有声明输入或输出,则我会非常惊讶编译器会尝试移动一个 asm 语句。

顺便说一下,volatile 还可以防止编译器删除表达式,如果编译器决定输出值未使用。不过,这只适用于有输出值的情况,因此不适用于 asm ("" ::: "memory")


13
Matthew Slattery的回答指出,asm volatile("")并不完全等同于不做任何操作,因为它可能会对编译器优化产生重大影响。使用asm volatile("" ::: "memory")作为编译器屏障也会有相同的性能影响。 - etherice
编译器不理解汇编语言! - curiousguy
3
不,但它会理解asm块声明的输入/输出,这告诉编译器它依赖哪些寄存器以及它将修改哪些寄存器,因此,如果某些计算不影响输入/输出,编译器可以重新排列它们。 - Lily Ballard
1
至少对于GCC而言,asm volatile并不会阻止一般指令重排,它只是防止可达的asm块因为(表面上)缺乏有意义的副作用而被删除(在较新的GCC上,即使编译器确定输入始终相同,也会防止其被提升出循环)。否则,指令重排仅由声明的输入和输出(以及伪输出,如“memory”)所禁止。请在文档中了解更多信息。 - ShadowRanger
是的,如果asm语句没有输出约束,则它们隐式地是volatile。(https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html)。因此,`asm("":::"memory")`与`asm volatile("":::"memory")完全相同。如果结果从未被使用,则可以删除非易失性asm语句,或者如果它在重复使用相同的输入时被提升(或以其他方式[CSE](https://en.wikipedia.org/wiki/Common_subexpression_elimination)),则可以将其提升。因此,您需要明确的volatile来包装像rdtscrdrand`这样的东西,因为您会得到具有相同(空)输入集的不同输出。 - Peter Cordes
但正如ShadowRanger所说:“volatile”并不能完全解决所有其他指令的问题,甚至无法解决私有局部变量访问(逃逸分析已经证明这些变量无法通过全局变量的指针访问)。可以将其视为非内联函数调用:必须同步mem,但只有可能被全局访问或作为指针参数传递的mem。对于没有输入的asm语句,只适用于全局可达部分。 - Peter Cordes

3

为了完整起见,关于 Lily Ballard的答案,Visual Studio 2010提供了_ReadBarrier()_WriteBarrier()_ReadWriteBarrier()来完成相同的功能(VS2010不允许64位应用程序使用内联汇编)。

这些不生成任何指令,但会影响编译器的行为。一个好的例子可以在这里找到。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接