【问题标题】:Which is a better write barrier on x86: lock+addl or xchgl?x86 上哪个写屏障更好:lock+addl 还是 xchgl?
【发布时间】:2011-05-13 01:42:54
【问题描述】:

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

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

【问题讨论】:

    标签: assembly x86 memory-barriers


    【解决方案1】:

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

    SSE2 的存在可能只是真实情况的代表,最终是cpuid 的函数。事实证明,SSE2 意味着存在lfence,并且在启动时检查/缓存了 SSE2 的可用性。 lfence 可用时是必需的。

    【讨论】:

    • 指令 lfence 是 SSE2 指令集的一部分。它不是代理。
    • lfence 不是内存排序所必需的,除非您从 WC 内存(例如,从视频 RAM)执行 movntdqa 弱排序加载。 mfence 是一个替代的完整屏障,您可以替代 addl $0, (%esp),但 lfence 的强度不足以阻止 StoreLoad 重新排序。你绝对不需要两者。 (顺便说一句,mfence 非常慢,并且对 OoO exec 的影响比英特尔 CPU 上的 xchglocked 指令更大:Are loads and stores the only instructions that gets reordered?
    【解决方案2】:

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


    lfence 来自英特尔数据表:

    执行序列化操作 所有从内存加载的指令 在 LFENCE 之前发布 操作说明。这个序列化 操作保证每个负载 程序前面的指令 命令 LFENCE 指令是 在任何加载之前全局可见 LFENCE 之后的指令 指令是全局可见的。

    (编者注:mfencelocked 操作是唯一有用的栅栏(在存储之后),以实现顺序一致性lfence 不是通过存储缓冲区阻止 StoreLoad 重新排序。)


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


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

    【讨论】:

    • 如果我写入共享内存并调用lock; addl $0,0(%%esp)sfence,在读取内存之前是否需要在其他进程/线程中调用lfence?还是 lock/sfence 指令本身已经保证了其他 CPU 可以看到数据?
    • 是的,锁前缀保证指令的结果是立即全局可见的。
    • 假设 CPU 支持 SSE 但不支持 SSE2。我使用sfence,但不能使用lfence。我需要使用lock; add 作为读取屏障,还是可以不使用读取屏障而侥幸?
    • 取决于 haw 以及您的指令在哪个环中执行。指令 lfence 通常用于内核(环 0)。如果 CPU 不支持 lfence 指令,则程序应用程序和线程必须在使用 mov 执行锁定后使用 sfence,因为内核可以在任何 CPU 指令之后中断程序应用程序和线程,并且更改的数据内存和指令仍然可以在缓存中。因此,您可以在内核中使用“lock add $0,...”,在程序应用程序和线程中使用“mov $1,... sfence”。
    • 我的指令在用户空间执行。所以如果我使用'lock; add' 作为写屏障,那么在读取方面我不必使用任何特殊的读屏障指令,一个简单的编译器屏障就足够了,对吧?
    【解决方案3】:

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

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

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

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

    • 锁定指令有一个总顺序。

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

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

    【讨论】:

    • 哪个更快?为什么使用 lock;addl vs fence?
    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

      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) 作为障碍。任何locked 指令都是一个完整的障碍。 Does lock xchg have the same behavior as mfence?

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

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

      如果可能,将xchg 用于 seq-cst 存储可能是最好的,即使它也从共享位置读取。 mfence 在最新的 Intel CPU (Are loads and stores the only instructions that gets reordered?) 上比预期的要慢,同时也像 lfence 一样阻止了独立非内存指令的乱序执行。

      即使mfence 可用,也可能值得使用lock addl $0, (%esp)/(%rsp) 而不是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++ 而不是 asm 编程时的 C++ 内在函数)。

      【讨论】:

        猜你喜欢
        • 2020-06-05
        • 2012-04-25
        • 2010-11-16
        • 2010-11-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-07
        • 1970-01-01
        相关资源
        最近更新 更多