Linux内核使用lock; addl $0,0(%%esp)
作为写障碍,而RE2库使用xchgl (%0),%0
作为写障碍。它们有什么区别,哪一个更好?
x86是否也需要读屏障指令? 在x86上,RE2将其读屏障函数定义为无操作,而Linux将其定义为lfence
或无操作,具体取决于SSE2是否可用。什么时候需要使用lfence
?
Linux内核使用lock; addl $0,0(%%esp)
作为写障碍,而RE2库使用xchgl (%0),%0
作为写障碍。它们有什么区别,哪一个更好?
x86是否也需要读屏障指令? 在x86上,RE2将其读屏障函数定义为无操作,而Linux将其定义为lfence
或无操作,具体取决于SSE2是否可用。什么时候需要使用lfence
?
引用自IA32手册(Vol 3A,第8.2章:内存排序):
对于定义为写回缓存的内存区域的单处理器系统,内存排序模型遵守以下原则[...]
- 读取不与其他读取重新排序
- 写入不与旧读取重新排序
- 写入到内存不会与其他写入重新排序,但有以下例外:
- 使用
CLFLUSH
指令执行的写入- 使用非临时移动指令([此处列出的指令])执行的流式写入操作
- 字符串操作(参见第8.2.4.1节)
- 读取可以与不同位置的旧写入重新排序,但不能与相同位置的旧写入重新排序。
- 读取或写入无法与I/O指令、锁定指令或序列化指令重新排序
- 读取不能通过
LFENCE
和MFENCE
指令- 写入不能通过
SFENCE
和MFENCE
指令
注:上面的“在单处理器系统中”略微具有误导性。同样的规则适用于每个(逻辑)处理器;然后手册继续描述了多个处理器之间的其他排序规则。与问题相关的唯一部分是
简而言之,只要你在写回内存(也就是除了驱动程序或图形编程人员外的所有内存),大多数x86指令几乎是按顺序一致的 - x86 CPU能够执行的唯一重排序是将后面(独立的)读取重新排序以在写入之前执行。关于写屏障的主要问题在于它们具有lock前缀(隐式或显式),这禁止了所有重排,并确保操作在多处理器系统中被所有处理器以相同的顺序看到。
- 锁定指令具有全局顺序。
Intel数据手册中的lfence指令:
对于在LFENCE指令之前发出的所有从内存加载的指令执行序列化操作。此序列化操作保证先于LFENCE指令在程序顺序中的每个加载指令在后面的任何加载指令之前全局可见。
(编辑说明:mfence
或锁定操作是顺序一致性(sequential consistency)下唯一有用的栅栏(在存储操作之后)。lfence
不能阻止存储缓冲区进行StoreLoad重排序。)
lock; addl $0,0(%%esp)
或sfence
,在另一个进程/线程读取内存之前,我需要调用lfence
吗?或者锁定/sfence指令本身已经保证其他CPU可以看到数据了吗? - Honglisfence
但无法使用 lfence
。我需要使用 lock; add
作为读取屏障,还是可以不使用读取屏障而逃脱呢? - Honglilock addl $0, (%esp)
是 mfence
的替代品,而不是 lfence
。
(在现代 CPU 上,特别是更新了微码的 Intel Skylake 上,lock add
通常更快,其中 mfence
也像 lfence
一样阻止寄存器上的指令乱序执行。 这就是为什么 GCC 最近在需要完整屏障时使用虚拟的 lock add
而不是 mfence
的原因。)
使用情况是当您需要阻止 StoreLoad 重排序(x86 强内存模型允许的唯一类型),但您不需要对共享变量进行原子 RMW 操作时。 https://preshing.com/20120515/memory-reordering-caught-in-the-act/
例如,假设已对齐std::atomic<int> a,b
,其中默认的 memory_order 是 seq_cst
。movl $1, a # a = 1; Atomic for aligned a
# barrier needed here between seq_cst store and later loads
movl b, %eax # tmp = b; Atomic for aligned b
你的选项是:
使用xchg
进行顺序一致存储,例如mov $1, %eax
/ xchg %eax, a
,这样您就不需要单独的障碍;它是存储的一部分。我认为这是现代大多数硬件上最有效的选项;除gcc以外的C++11编译器使用xchg
进行seq_cst存储。(有关性能和正确性,请参见为什么std::atomic存储顺序一致性使用XCHG?)
使用mfence
作为障碍。(gcc对于seq_cst存储使用mov
+mfence
,但最近为了提高性能改用xchg
。)
使用lock addl $0, (%esp)
作为障碍。任何lock
指令都是完整的障碍,但这个指令对寄存器或内存内容除了FLAGS没有影响。请参见 lock xchg 是否与 mfence 具有相同的行为?
(或者其他位置,但堆栈几乎总是私有的并且在L1d中很热,所以它是一个好的选择。使用该空间的任何后续重新加载都无法在原子RMW之前开始,因为它是完整的障碍。)
xchg
用作屏障,通过将其折叠到存储中,因为它无条件地使用不依赖于旧值的值写入内存位置。xchg
,即使它也从共享位置读取。 在最近的英特尔CPU上,mfence
比预期慢(仅有负载和存储指令会被重新排序吗?),还会像lfence
一样阻止独立非内存指令的乱序执行。mfence
时,使用lock addl $0, (%esp)/(%rsp)
而不是mfence
可能是值得的,但我还没有尝试过缺点。 使用-64(%rsp)
或类似的东西可能会减少对某些热点(本地变量或返回地址)的数据依赖性,但这可能会使valgrind之类的工具感到不满意。
lfence
除非你使用 MOVNTDQA 载入从显存(或其他 WC 弱序区域)读取的数据,否则在内存排序方面是没有用处的。
对于防止 StoreLoad 重排序(这是 x86 现有的强内存模型允许的唯一类型),乱序执行的序列化(但不包括存储缓冲区)并不实用。
lfence
的真实用例是为了在计时非常短的代码块时阻止 rdtsc
的乱序执行,或通过条件或间接分支阻止推测以进行 Spectre 缓解。
有关为什么 lfence
不实用以及何时使用每个屏障指令,请参见 When should I use _mm_sfence _mm_lfence and _mm_mfence (我的回答和 @BeeOnRope 的回答)。 (或在编写 C++ 时使用 C++ 内置函数而非汇编语言)。
lock; addl $0,0(%%esp)
可能不是最优的,它可能会引入错误的数据依赖性;相关jdk bug。lock; addl
和xchgl
的重要部分是lock
前缀。对于xchgl
,它是隐式的。实际上两者之间没有区别。建议查看它们是如何组装的,并选择字节更短的一个(例如xorl eax,eax
),因为在x86上等效操作通常更快。
SSE2的存在可能只是cpuid
的实际条件,其代理。很可能SSE2意味着存在lfence
,并且在启动时检查/缓存了SSE2的可用性。如果可用,则需要lfence
。
lfence
是SSE2指令集的一部分,它不是代理。 - Pascal Cuoqmovntdqa
弱排序加载,否则不需要使用lfence
来进行内存排序。 mfence
是一种替代的完整屏障,可以替换为addl $0,(%esp)
,但lfence
不足以阻止StoreLoad重新排序。 您绝对不需要两者都使用。(顺便说一下,mfence
相当慢,并且对于Intel CPU上的OoO exec具有比xchg
或lock
指令更大的影响:Are loads and stores the only instructions that gets reordered?) - Peter Cordes