x86 上哪个写屏障更好:lock+addl 还是 xchgl?
-
26-09-2019 - |
题
Linux 内核使用 lock; addl $0,0(%%esp)
作为写屏障,而 RE2 库使用 xchgl (%0),%0
作为写屏障。有什么区别,哪个更好?
x86 也需要读屏障指令吗?RE2 将其读屏障函数定义为 x86 上的无操作,而 Linux 将其定义为 lfence
或无操作取决于 SSE2 是否可用。什么时候 lfence
必需的?
解决方案
这 ”锁;addl $0,0(%%esp)如果我们测试 (%%esp) 地址处的锁变量的 0 状态,速度会更快。因为我们将 0 值添加到锁定变量,并且如果地址(%%esp)处的变量的锁定值为 0,则零标志设置为 1。
栅栏 来自英特尔数据表:
对在Lfence指令之前发出的所有负载中的内存指令执行串行操作。这种序列化操作确保了在程序顺序上之前的每个负载指令在全球范围内可见的任何负载指令之前,在全球范围内可见。
(编者注: mfence
或一个 lock
ed 操作是对于顺序一致性唯一有用的栅栏(在存储之后). lfence
做 不是 阻止存储缓冲区进行 StoreLoad 重新排序。)
例如:如果正确对齐,像“mov”这样的内存写入指令是原子的(它们不需要锁前缀)。但该指令通常在CPU缓存中执行,此时对于所有其他线程来说不会是全局可见的,因为必须首先执行内存栅栏以使该线程等待,直到先前的存储对其他线程可见。
所以这两个指令之间的主要区别是 香格里拉 指令不会对条件标志产生任何影响。当然我们可以测试锁变量状态 锁定 cmpxchg 指令,但这仍然比使用更复杂 锁定加$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 卷第 11.3.1 节中。IA-32 手册的 3A。简洁版本:Write-Through、Write-Back 和 Write-Protected 允许推测性读取(遵循上面详述的规则),Uncachable 和 Strong Uncacheable 内存具有强大的排序保证(没有处理器重新排序,读/写立即执行,用于 MMIO)和 Write组合内存的排序较弱(即放宽了需要围栏的订购规则)。
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
作为障碍。(海湾合作委员会使用mov
+mfence
对于 seq_cst 存储)。 使用
lock addl $0, (%esp)
作为障碍。任何lock
ed指令是一个完整的障碍。 lock xchg 与 mfence 具有相同的行为吗?(或者到其他位置,但堆栈在 L1d 中几乎总是私有且热门的,因此它是一个不错的候选者。然而,这可能会使用堆栈底部的数据为某些东西创建依赖链。)
您只能使用 xchg
通过将其折叠到存储中作为屏障,因为它无条件地使用不依赖于旧值的值写入内存位置。
如果可能的话,使用 xchg
对于 seq-cst 存储来说可能是最好的,即使它也从共享位置读取。 mfence
在最新的 Intel CPU 上比预期慢(加载和存储是唯一需要重新排序的指令吗?),也以同样的方式阻止独立非内存指令的乱序执行 lfence
做。
它甚至可能值得使用 lock addl $0, (%esp)/(%rsp)
代替 mfence
即使当 mfence
可用,但我还没有尝试过它的缺点。使用 -64(%rsp)
或者某些东西可能会使其不太可能延长对热门内容(本地或返回地址)的数据依赖,但这可能会让像 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 可能意味着存在 lfence
并且在启动时检查/缓存 SSE2 的可用性。 lfence
当可用时需要它。