Linux 内核lock; addl $0,0(%%esp)
用作写屏障,而 RE2 库xchgl (%0),%0
用作写屏障。有什么区别,哪个更好?
x86 是否也需要读屏障指令?RE2 将其读取屏障功能定义为 x86 上的无操作,而 Linux 将其定义为要么lfence
或无操作,具体取决于 SSE2 是否可用。什么时候lfence
需要?
Linux 内核lock; addl $0,0(%%esp)
用作写屏障,而 RE2 库xchgl (%0),%0
用作写屏障。有什么区别,哪个更好?
x86 是否也需要读屏障指令?RE2 将其读取屏障功能定义为 x86 上的无操作,而 Linux 将其定义为要么lfence
或无操作,具体取决于 SSE2 是否可用。什么时候lfence
需要?
引用 IA32 手册(第 3A 卷,第 8.2 章:内存排序):
在定义为可回写高速缓存的内存区域的单处理器系统中,内存排序模型遵循以下原则 [..]
- 读取不会与其他读取重新排序
- 写入不会与较旧的读取一起重新排序
- 对内存的写入不会与其他写入一起重新排序,除了
CLFLUSH
用指令执行的写操作- 使用非临时移动指令执行的流式存储(写入)([此处的指令列表])
- 字符串操作(见第 8.2.4.1 节)
- 对不同位置的旧写入可能会重新排序读取,但对同一位置的旧写入则不会。
- 无法使用 I/O 指令、锁定指令或序列化指令重新排序读取或写入
- 读取不能通过
LFENCE
和MFENCE
指令- 写不能通过
SFENCE
和MFENCE
指令
注意:上面的“在单处理器系统中”有点误导。相同的规则分别适用于每个(逻辑)处理器;然后,该手册继续描述了多个处理器之间的附加排序规则。与这个问题有关的唯一一点是
- 锁定指令有一个总顺序。
简而言之,只要您正在写入回写内存(只要您不是驱动程序或图形程序员,您就会看到所有内存),大多数 x86 指令几乎是顺序一致的 - 唯一的重新排序x86 CPU 可以执行的是重新排序稍后(独立)读取以在写入之前执行。关于写屏障的主要事情是它们有一个lock
前缀(隐式或显式),它禁止所有重新排序并确保多处理器系统中的所有处理器以相同的顺序看到操作。
此外,在回写式内存中,读取永远不会重新排序,因此不需要读取屏障。最近的 x86 处理器对于流式存储和写入组合内存(通常用于映射图形内存)具有较弱的内存一致性模型。这就是各种fence
指令开始发挥作用;它们对于任何其他内存类型都不是必需的,但是 Linux 内核中的一些驱动程序确实处理写组合内存,因此它们只是以这种方式定义了它们的读屏障。每个内存类型的排序模型列表在第 11.3.1 卷中。IA-32 手册中的 3A。短版:Write-Through、Write-Back 和 Write-Protected 允许推测性读取(遵循上面详述的规则),Uncachable 和 Strong Uncacheable 内存具有强排序保证(没有处理器重新排序,读/写立即执行,用于 MMIO ) 和写入组合内存的排序较弱(即需要栅栏的宽松排序规则)。
如果我们在 (%%esp) 地址测试锁变量的 0 状态,“ lock; addl $0,0(%%esp) ”会更快。因为我们将 0 值添加到锁定变量,并且如果地址 (%%esp) 处的变量的锁定值为 0,则将零标志设置为 1。
来自英特尔数据表的lfence :
对在 LFENCE 指令之前发出的所有从内存加载指令执行序列化操作。这种序列化操作保证了在程序顺序中位于 LFENCE 指令之前的每条加载指令都是全局可见的,而在 LFENCE 指令之后的任何加载指令都是全局可见的。
(编者注:mfence
或者lock
ed 操作是唯一有用的栅栏(在存储之后),用于顺序一致性。 lfence
不会阻止存储缓冲区对 StoreLoad 重新排序。)
例如:像“mov”这样的内存写入指令是原子的(它们不需要锁定前缀),如果它们正确对齐的话。但是这条指令通常在 CPU 缓存中执行,此时对于所有其他线程不会全局可见,因为必须首先执行内存栅栏以使该线程等待,直到之前的存储对其他线程可见。
所以这两条指令的主要区别在于xchgl指令不会对条件标志产生任何影响。当然,我们可以使用lock cmpxchg指令测试锁变量状态,但这仍然比使用lock add $0指令更复杂。
lock addl $0, (%esp)
是替代品mfence
,不是lfence
。
用例是当您需要阻止 StoreLoad 重新排序(x86 的强内存模型允许的唯一类型),但您不需要对共享变量进行原子 RMW 操作。 https://preshing.com/20120515/memory-reordering-caught-in-the-act/
例如假设对齐std::atomic<int> a,b
:
movl $1, a a = 1; Atomic for aligned a
# barrier needed here
movl b, %eax tmp = b; Atomic for aligned b
您的选择是:
xchg
使用, 例如mov $1, %eax
/进行顺序一致性存储xchg %eax, a
,因此您不需要单独的屏障;它是商店的一部分。我认为这是大多数现代硬件上最有效的选择;除 gcc 之外的 C++11 编译器xchg
用于 seq_cst 存储。mfence
屏障。(gcc 将mov
+mfence
用于 seq_cst 存储)。用作lock addl $0, (%esp)
屏障。任何lock
ed 指令都是一个完整的障碍。 lock xchg 是否具有与 mfence 相同的行为?
(或到其他位置,但堆栈在 L1d 中几乎总是私有且热的,因此它是一个不错的候选者。但是这可能会使用堆栈底部的数据为某些东西创建依赖链。)
您只能xchg
通过将其折叠到存储中来用作屏障,因为它无条件地使用不依赖于旧值的值写入内存位置。
如果可能,xchg
最好使用 seq-cst 存储,即使它也从共享位置读取。 mfence
在最近的英特尔 CPU 上比预期的要慢(加载和存储唯一被重新排序的指令吗?),也以同样的方式阻止了独立非内存指令的乱序执行lfence
。
它甚至可能值得使用lock addl $0, (%esp)/(%rsp)
,而不是mfence
即使mfence
可用,但我还没有尝试过它的缺点。使用-64(%rsp)
or 可能会降低对热门事物(本地或返回地址)的数据依赖的可能性降低,但这会使 valgrind 等工具不高兴。
lfence
除非您从带有 MOVNTDQA 负载的视频 RAM(或其他一些 WC 弱排序区域)中读取,否则对于内存排序永远不会有用。
序列化乱序执行(但不是存储缓冲区)对于停止 StoreLoad 重新排序(x86 的强内存模型允许正常 WB(回写)内存区域的唯一类型)没有用。
现实世界的用例lfence
是阻止乱序执行,rdtsc
用于计时非常短的代码块,或者通过条件或间接分支阻止推测来缓解 Spectre。
另请参阅我何时应该使用 _mm_sfence _mm_lfence 和 _mm_mfence(我的答案和@BeeOnRope 的答案),了解更多关于为什么lfence
没有用以及何时使用每个屏障指令的信息。(或者在我的情况下,使用 C++ 而不是 asm 编程时的 C++ 内在函数)。
除了其他答案之外,HotSpot 开发人员发现lock; addl $0,0(%%esp)
零偏移可能不是最佳的,在某些处理器上它可能会引入错误的数据依赖性;相关的jdk 错误。
在某些情况下,触摸具有不同偏移量的堆栈位置可以提高性能。
lock; addl
和的重要部分xchgl
是lock
前缀。它是隐含的xchgl
。两者之间真的没有区别。我会看看他们如何组装并选择更短的(以字节为单位),因为对于 x86 上的等效操作通常更快(因此像这样的技巧xorl eax,eax
)
SSE2 的存在可能只是真实情况的代表,最终是cpuid
. 事实证明,SSE2 意味着 SSE2 的存在lfence
和可用性在启动时被检查/缓存。 lfence
可用时是必需的。