哪个是x86上更好的写屏障:lock + addl还是xchgl?

时间:2010-11-20 12:15:26

标签: assembly x86 memory-barriers

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

x86是否还需要读屏障指令? RE2将其读取屏障功能定义为x86上的无操作,而Linux将其定义为lfence或无操作,具体取决于SSE2是否可用。何时需要lfence

5 个答案:

答案 0 :(得分:10)

引自IA32手册(第3A卷,第8.2章:记忆订购):

  

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

     
      
  • 读取不会与其他读取重新排序
  •   
  • 写入不会与较旧的读取重新排序
  •   
  • 写入内存不会与其他写入重新排序,但   
        
    • 使用CLFLUSH指令执行的写入
    •   
    • 使用非时间移动指令([此处的指令列表])执行的流式存储(写入)
    •   
    • 字符串操作(参见第8.2.4.1节)
    •   
  •   
  • 可以使用较旧的写入对不同位置进行重新排序,但不能将较旧的写入重新排序到同一位置。
  •   
  • 读取或写入不能使用I / O指令,锁定指令或序列化指令重新排序
  •   
  • 读取无法通过LFENCEMFENCE说明
  •   
  • 写入无法通过SFENCEMFENCE说明
  •   

注意:上面的“在单处理器系统中”有点误导。每个(逻辑)处理器都有相同的规则;然后,手册继续描述多个处理器之间的附加排序规则。与此问题有关的唯一一点是

  
      
  • 锁定的说明总订单。
  •   

简而言之,只要您正在写回写内存(只要您不是驱动程序或图形程序员,就会看到所有内存),大多数x86指令几乎都是顺序一致的 - x86 CPU可以执行的唯一重新排序是在写入之后重新排序(独立)读取以执行。关于写入障碍的主要问题是它们具有lock前缀(隐式或显式),禁止所有重新排序,并确保多处理器系统中的所有处理器以相同的顺序查看操作。 / p>

此外,在回写存储器中,读取永远不会重新排序,因此不需要读取障碍。最近的x86处理器具有较弱的内存一致性模型,适用于流存储和写入组合内存(通常用于映射图形内存)。这就是各种fence指令发挥作用的地方;它们对于任何其他内存类型都不是必需的,但Linux内核中的某些驱动程序确实处理了写入组合内存,因此它们只是以这种方式定义了它们的读取障碍。每种存储器类型的排序模型列表在第11.3.1节中。 IA-32手册的3A。短版本:Write-Through,Write-Back和Write-Protected允许推测性读取(遵循上面详述的规则),Uncachable和Strong Uncacheable内存具有强大的排序保证(没有处理器重新排序,读取/写入立即执行,用于MMIO并且写入组合内存具有弱排序(即需要围栏的宽松排序规则)。

答案 1 :(得分:8)

如果我们在(%% esp)地址测试锁定变量的0状态,“锁定; addl $ 0,0(%% esp)”会更快。因为我们向lock变量添加0值,如果地址(%% esp)的变量的锁定值为0,则零标志设置为1。


来自英特尔数据表的

lfence

  

执行序列化操作   所有从内存加载的指令   在LFENCE之前发布   指令。这序列化   操作保证每次加载   程序之前的指令   命令LFENCE指令是   在任何负载之前全局可见   遵循LFENCE的指令   指令在全球范围内可见。

编者注:mfencelock ed操作是连续一致性唯一有用的围栏(在商店之后)lfence执行阻止StoreLoad由商店缓冲区重新排序。)


例如:如果正确对齐,像'mov'这样的内存写指令是原子的(它们不需要锁前缀)。但是这条指令通常在CPU缓存中执行,此时所有其他线程都不会全局可见,因为必须首先执行内存栅栏才能使此线程等待,直到其他线程可以看到以前的存储。


因此,这两条指令的主要区别在于 xchgl 指令对条件标志没有任何影响。当然我们可以使用 lock cmpxchg 指令测试锁定变量状态,但这仍然比使用 lock add $ 0 指令更复杂。

答案 2 :(得分:7)

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编译器对seq_cst存储区使用xchg
  • 使用mfence作为障碍。 (gcc将mov + mfence用于seq_cst存储)。
  • 使用lock addl $0, (%esp)作为障碍。任何lock版本的指令都是完全障碍。 Does lock xchg have the same behavior as mfence?

    (或到其他位置,但是堆栈在L1d中几乎总是私有且很热,因此它是一个不错的选择。但是,这可能会使用堆栈底部的数据为某些对象创建依赖链。)< / p>

您只能将xchg用作屏障,方法是将其折叠到存储区中,因为它无条件地使用不依赖于旧值的值来写入内存位置。

在可能的情况下,最好将xchg用于seq-cst存储,即使它也从共享位置读取也是如此。 mfence的速度比最近的Intel 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的无序执行,以计时非常短的代码块,或者通过有条件的或间接分支。

另请参阅When should I use _mm_sfence _mm_lfence and _mm_mfence(我的回答和@BeeOnRope的回答),以详细了解lfence为何无用的原因以及何时使用每个障碍说明。 (或者在我的情况下,当使用C ++而不是asm进行编程时,是C ++内部函数)。

答案 3 :(得分:6)

除了其他答案之外,HotSpot开发人员发现零偏移的lock; addl $0,0(%%esp)可能不是最佳的,在某些处理器上它可以introduce false data dependencies;相关的jdk bug

在某些情况下,触摸具有不同偏移量的堆栈位置可以提高性能。

答案 4 :(得分:2)

lock; addlxchgl的重要部分是lock前缀。它隐含在xchgl中。这两者之间确实没有区别。我会看看它们是如何组装并选择更短的(以字节为单位),因为对于x86上的等效操作通常更快(因此像xorl eax,eax这样的技巧)

SSE2的存在可能仅仅是真实条件的代理,它最终是cpuid的函数。事实证明,SSE2意味着存在lfence,并且在引导时检查/缓存了SSE2的可用性。 <{1}}在可用时是必需的。