Linux内核使用lock; addl $0,0(%%esp)
作为写屏障,而RE2库使用xchgl (%0),%0
作为写屏障。有什么区别,哪个更好?
x86还需要读屏障指令吗? RE2将其读屏障功能定义为x86上的无操作,而Linux将其定义为lfence
或no-op,具体取决于SSE2是否可用。什么时候需要lfence
?
如果我们在(%% esp)地址测试锁定变量的0状态,则“lock; addl $ 0,0(%% esp)”会更快。因为我们向lock变量添加0值,如果地址(%% esp)的变量的锁定值为0,则零标志设置为1。
英特尔数据表中的lfence:
对LFENCE指令之前发出的所有内存加载指令执行序列化操作。此序列化操作保证在LFENCE指令之后的任何加载指令全局可见之前,LFENCE指令在程序顺序之前的每个加载指令都是全局可见的。
(编者注:mfence
或lock
ed操作是连续一致性的唯一有用围栏(在商店之后).lfence
不会阻止StoreLoad由商店缓冲区重新排序。)
例如:如果正确对齐,像'mov'这样的内存写指令是原子的(它们不需要锁前缀)。但是此指令通常在CPU缓存中执行,并且此时对于所有其他线程不会全局可见,因为必须首先执行内存栅栏以使此线程等待,直到其他线程可以看到先前的存储。
因此,这两条指令之间的主要区别在于xchgl指令对条件标志没有任何影响。当然我们可以使用lock cmpxchg指令测试锁变量状态,但这仍然比使用lock add $ 0指令更复杂。
引用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并且写入组合内存具有弱排序(即,需要围栏的宽松排序规则)。
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指令都是完全障碍。 Does lock xchg have the same behavior as mfence?
(或者到其他一些位置,但是堆栈在L1d中几乎总是私有且很热,所以它是一个很好的候选者。但是这可能会使用堆栈底部的数据创建一个依赖链。)您只能将xchg
折叠成商店,因为它无条件地使用不依赖于旧值的值写入内存位置。
如果可能的话,使用xchg
作为seq-cst商店可能是最好的,即使它也从共享位置读取。 mfence
在最近的英特尔CPU(Are loads and stores the only instructions that gets reordered?)上比预期慢,同样阻止了lfence
的无序执行非独立非内存指令。
甚至当lock addl $0, (%esp)/(%rsp)
可用时,甚至可能值得使用mfence
而不是mfence
,但我没有尝试过这些缺点。使用-64(%rsp)
或某些东西可能会降低对热点(本地或返回地址)的数据依赖性的可能性,但这可能使像valgrind这样的工具不高兴。
lfence
对于存储器排序永远不会有用,除非您使用MOVNTDQA负载读取视频RAM(或其他一些WC弱序列区域)。
序列化乱序执行(但不是存储缓冲区)对于停止StoreLoad重新排序(x86的强内存模型允许普通WB(回写)内存区域的唯一类型)没有用。
lfence
的真实用例是用于阻止rdtsc
的无序执行,以便对非常短的代码块进行计时,或者通过阻止通过条件或间接分支的推测来阻止Spectre。
另请参阅When should I use _mm_sfence _mm_lfence and _mm_mfence(我的答案和@ BeeOnRope的答案),了解更多关于lfence
无效的原因,以及何时使用每个屏障指令。 (或者在我的C ++内部函数中用C ++而不是asm编程)。
除了其他答案之外,HotSpot开发人员发现零偏移的lock; addl $0,0(%%esp)
可能不是最佳的,在某些处理器上它可以introduce false data dependencies;相关的jdk bug。
触摸具有不同偏移的堆栈位置可以在某些情况下提高性能。
lock; addl
和xchgl
的重要部分是lock
前缀。它隐含于xchgl
。这两者之间确实没有区别。我会看看它们是如何组装并选择更短的(以字节为单位),因为对于x86上的等效操作通常更快(因此像xorl eax,eax
这样的技巧)
SSE2的存在可能只是真实条件的代表,最终是cpuid
的函数。事实证明,SSE2意味着存在lfence
,并且在启动时检查/缓存了SSE2的可用性。当lfence
可用时,它是必需的。