在x86架构中,哪种写屏障更好:lock+addl还是xchgl?

30

Linux内核使用lock; addl $0,0(%%esp)作为写障碍,而RE2库使用xchgl (%0),%0作为写障碍。它们有什么区别,哪一个更好?

x86是否也需要读屏障指令? 在x86上,RE2将其读屏障函数定义为无操作,而Linux将其定义为lfence或无操作,具体取决于SSE2是否可用。什么时候需要使用lfence?

5个回答

12

引用自IA32手册(Vol 3A,第8.2章:内存排序):

 

对于定义为写回缓存的内存区域的单处理器系统,内存排序模型遵守以下原则[...]

    
     
  • 读取不与其他读取重新排序
  •  
  • 写入不与旧读取重新排序
  •  
  • 写入到内存不会与其他写入重新排序,但有以下例外:  
       
    • 使用CLFLUSH指令执行的写入
    •  
    • 使用非临时移动指令([此处列出的指令])执行的流式写入操作
    •  
    • 字符串操作(参见第8.2.4.1节)
    •  
  •  
  • 读取可以与不同位置的旧写入重新排序,但不能与相同位置的旧写入重新排序。
  •  
  • 读取或写入无法与I/O指令、锁定指令或序列化指令重新排序
  •  
  • 读取不能通过LFENCEMFENCE指令
  •  
  • 写入不能通过SFENCEMFENCE指令
  •  

注:上面的“在单处理器系统中”略微具有误导性。同样的规则适用于每个(逻辑)处理器;然后手册继续描述了多个处理器之间的其他排序规则。与问题相关的唯一部分是

 
     
  • 锁定指令具有全局顺序。
  •  
简而言之,只要你在写回内存(也就是除了驱动程序或图形编程人员外的所有内存),大多数x86指令几乎是按顺序一致的 - x86 CPU能够执行的唯一重排序是将后面(独立的)读取重新排序以在写入之前执行。关于写屏障的主要问题在于它们具有lock前缀(隐式或显式),这禁止了所有重排,并确保操作在多处理器系统中被所有处理器以相同的顺序看到。
此外,在写回内存中,读取永远不会被重新排序,因此不需要读取屏障。最近的x86处理器对于流式存储和写组合内存(常用于映射的图形内存)具有较弱的内存一致性模型。这就是各种fence指令发挥作用的地方。它们对于任何其他类型的内存都不是必需的,但是Linux内核中的某些驱动程序确实处理写组合内存,因此它们只定义了自己的读取屏障。每种内存类型的排序模型列表在IA-32手册的第3A卷11.3.1节中。简而言之:Write-Through、Write-Back和Write-Protected允许推测性读取(遵循如上所述的规则),Uncachable和Strong Uncacheable内存具有强有力的排序保证(无处理器重新排序,读取/写入立即执行,用于MMIO),Write Combined内存具有弱排序(即放松的排序规则需要使用fences)。

哪个更快?为什么使用lock;addl而不是fence? - Alexis

9
"lock; addl $0,0(%%esp)" 这个指令在测试地址为 (%%esp) 的锁变量的值是否为 0 时更快。因为我们将 0 值加到锁变量上,如果地址为 (%%esp) 的锁变量的值为 0,那么零标志位就会被设置为 1。请保留 HTML 标签。

Intel数据手册中的lfence指令:

对于在LFENCE指令之前发出的所有从内存加载的指令执行序列化操作。此序列化操作保证先于LFENCE指令在程序顺序中的每个加载指令在后面的任何加载指令之前全局可见。

(编辑说明:mfence或锁定操作是顺序一致性(sequential consistency)下唯一有用的栅栏(在存储操作之后)。lfence不能阻止存储缓冲区进行StoreLoad重排序。)


例如:像“mov”这样的内存写入指令如果正确对齐,则是原子性的(它们不需要锁前缀)。但是,这个指令通常在CPU缓存中执行,并且此时不会对所有其他线程全局可见,因为必须首先执行内存栅栏,使得该线程等待直到先前的存储对其他线程可见。
所以这两个指令的主要区别在于,xchgl指令不会对条件标志产生任何影响。当然,我们可以使用lock cmpxchg指令测试锁变量状态,但这仍然比使用lock add $0指令更复杂。

1
如果我写入共享内存并调用lock; addl $0,0(%%esp)sfence,在另一个进程/线程读取内存之前,我需要调用lfence吗?或者锁定/sfence指令本身已经保证其他CPU可以看到数据了吗? - Hongli
3
是的,锁定前缀可以保证指令的结果立即在全局范围内可见。 - GJ.
1
假设CPU支持SSE但不支持SSE2。 我使用 sfence 但无法使用 lfence。我需要使用 lock; add 作为读取屏障,还是可以不使用读取屏障而逃脱呢? - Hongli
1
取决于指令在哪个环中执行。指令lfence通常在内核(ring 0)中使用。如果CPU不支持lfence指令,则程序应用和线程必须在使用mov执行锁定后使用sfence,因为内核可以在任何CPU指令之后中断程序应用和线程,并更改数据内存和指令仍然可以在缓存中。因此,在内核中可以使用“lock add $0,...”,在程序应用和线程中可以使用“mov $1,... sfence”。 - GJ.
1
我的指令在用户空间中执行。因此,如果我使用“lock; add”作为写入屏障,那么在读取方面,我就不必使用任何特殊的读取屏障指令,简单的编译器屏障就足够了,对吗? - Hongli
显示剩余7条评论

8

lock 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用作屏障,通过将其折叠到存储中,因为它无条件地使用不依赖于旧值的值写入内存位置。
可能最好在seq-cst存储中使用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++ 内置函数而非汇编语言)。


6
作为其他答案的附注,HotSpot开发人员发现,在一些处理器上,使用零偏移量的lock; addl $0,0(%%esp)可能不是最优的,它可能会引入错误的数据依赖性;相关jdk bug
在某些情况下,触及具有不同偏移量的堆栈位置可以提高性能。

2
lock; addlxchgl的重要部分是lock前缀。对于xchgl,它是隐式的。实际上两者之间没有区别。建议查看它们是如何组装的,并选择字节更短的一个(例如xorl eax,eax),因为在x86上等效操作通常更快。

SSE2的存在可能只是cpuid的实际条件,其代理。很可能SSE2意味着存在lfence,并且在启动时检查/缓存了SSE2的可用性。如果可用,则需要lfence


4
指令lfence是SSE2指令集的一部分,它不是代理。 - Pascal Cuoq
除非您从WC内存(例如从视频RAM)进行movntdqa弱排序加载,否则不需要使用lfence来进行内存排序。 mfence是一种替代的完整屏障,可以替换为addl $0,(%esp),但lfence不足以阻止StoreLoad重新排序。 您绝对不需要两者都使用。(顺便说一下,mfence相当慢,并且对于Intel CPU上的OoO exec具有比xchglock指令更大的影响:Are loads and stores the only instructions that gets reordered? - Peter Cordes

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