【问题标题】:Why Sequential Semantic on x86/x86_64 is using through MOV [addr], reg + MFENCE instead of + SFENCE?为什么 x86/x86_64 上的顺序语义通过 MOV [addr]、reg + MFENCE 而不是 + SFENCE 使用?
【发布时间】:2013-09-30 20:41:45
【问题描述】:

在 Intel x86/x86_64 系统有 3 种类型的内存屏障:lfence、sfence 和 mfence。就它们的使用而言的问题。 对于顺序语义 (SC),对于所有需要 SC 语义的存储单元,使用 MOV [addr], reg + MFENCE 就足够了。但是,您可以编写整个代码,反之亦然:MFENCE + MOV reg, [addr]。显然感觉,如果存储到内存的数量通常少于从中加载的数量,那么使用 write-barrier 的总成本会更低。并且在此基础上,我们必须对内存使用顺序存储,进行了另一个优化 - [LOCK] XCHG,这可能更便宜,因为“MFENCE inside in XCHG”仅适用于使用的内存缓存行XCHG (video where on 0:28:20 said that MFENCE more expensive that XCHG)。

http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html

C/C++11 操作 x86 实现

  • 加载 Seq_Cst:MOV(从内存中)
  • 存储序列 Cst: (LOCK) XCHG // 替代方案:MOV(进入内存),MFENCE

注意:有一个 C/C++11 到 x86 的替代映射,而不是锁定(或隔离) Seq Cst 存储锁定/隔离 Seq Cst 负载:

  • 加载 Seq_Cst: LOCK XADD(0) // 替代方法:MFENCE,MOV(从内存中)
  • 存储 Seq Cst: MOV(到内存中)

不同之处在于 ARM 和 Power 内存屏障仅与 LLC(Last Level Cache)交互,而 x86 与较低级别的缓存 L1/L2 交互。 在 x86/x86_64 中:

  • lfence 在 Core1 上:(CoreX-L1) -> (CoreX-L2) -> L3-> (Core1-L2) -> (Core1-L1)
  • sfence 在 Core1 上:(Core1-L1) -> (Core1-L2) -> L3-> (CoreX-L2) -> (CoreX-L1)

在 ARM 中:

  • ldr; dmb;: L3-> (Core1-L2) -> (Core1-L1)
  • dmb; str; dmb;: (Core1-L1) -> (Core1-L2) -> L3

GCC 4.8.2 编译的 C++11 代码 - x86_64 中的 GDB:

std::atomic<int> a;
int temp = 0;
a.store(temp, std::memory_order_seq_cst);
0x4613e8  <+0x0058>         mov    0x38(%rsp),%eax
0x4613ec  <+0x005c>         mov    %eax,0x20(%rsp)
0x4613f0  <+0x0060>         mfence

但是为什么在 x86/x86_64 顺序语义 (SC) 上使用 MOV [addr], reg + MFENCE 而不是 MOV [addr], reg + SFENCE,为什么我们需要全栅栏 MFENCE 而不是 SFENCE

【问题讨论】:

  • 我认为商店围栏只会与其他负载同步,而不是与其他商店同步。顺序一致性意味着您想要一个所有 CPU 都观察到的 total 订单,并且存储围栏并不意味着多个存储的排序。
  • @Kerrek 这适用于 ARM,但不适用于 x86,因为如果我们在第一个 CPU 内核上进行 SFENCE,那么在访问之前我们不再需要在另一个 CPU 内核上进行 LFENCE这个记忆细胞。因此,如果所有变量都需要顺序语义(SC),我们会使用 SFENCE,并且我们不需要任何地方都有 LFENCE。还是您的意思是 MFENCE 取消了处理器管道中双向的重新排序(乱序执行)?
  • 首先,我想我想说的是,单独的 sfence 无法提供所有 CPU 都观察到的 total 排序...
  • @Kerrek SB 所有 CPU 观察到的顺序语义和总排序是同义词。但是问题是为什么在每次存储操作之后SFENCE 不能提供所有 CPU 都观察到的总排序,即为什么我们需要在每次存储操作之后执行 LFENCE 包含在 MFENCE 中(不是在加载之前操作)?
  • 所以,我认为可能会发生以下情况。假设XY 为零。现在:[Thread 1: STORE X = 1, SFENCE][Thread 2: STORE Y = 1, SFENCE],在任何其他线程中,执行[LFENCE, LOAD X, LOAD Y]。现在另一个线程可以看到X = 1, Y = 0,另一个线程可以看到X = 0, Y = 1。栅栏只告诉你线程 1 中的 other, early 商店已经生效如果你看到X = 1。但没有与此一致的全球秩序。

标签: c++ multithreading assembly concurrency x86


【解决方案1】:

sfence 不会阻止 StoreLoad 重新排序。除非有任何 NT 商店在飞行中,否则它在架构上是无操作的。存储在它们自己提交到 L1d 并变得全局可见之前已经等待旧存储提交,因为 x86 不允许 StoreStore 重新排序。 (NT 存储/存储到 WC 内存除外)

对于 seq_cst,您需要一个完整的屏障来刷新存储缓冲区/确保所有旧存储在以后的任何加载之前全局可见。请参阅https://preshing.com/20120515/memory-reordering-caught-in-the-act/ 以获取示例在实践中未能使用mfence 会导致不一致的行为,即内存重新排序。


正如您所发现的,可以将 seq_cst 映射到 x86 asm,并在每个 seq_cst 加载而不是每个 seq_cst 存储/RMW 上使用完全障碍。在这种情况下,您不需要任何关于商店的屏障指令(因此它们具有发布语义),但您需要在每个 atomic::load(seq_cst) 之前使用 mfence

【讨论】:

    【解决方案2】:

    您不需要mfencesfence 确实足够了。事实上,在 x86 中你永远不需要lfence,除非你正在处理一个设备。但是英特尔(我认为 AMD)有(或至少有)与mfencesfence 共享的单一实现(即刷新存储缓冲区),因此使用较弱的sfence 没有性能优势。

    顺便说一句,请注意,您不必在每次写入共享变量后都刷新;您只需在不同共享变量的写入和后续读取之间刷新。

    【讨论】:

    • 谢谢!但我不同意这一点——我“在 x86 中从不需要 lfence”。你可以看到我关于这个的附加问题,看看我们可以在哪里使用它“3.MFENCE+LOAD 和 STORE(没有围栏)”stackoverflow.com/q/19047327/1558037我没有在任何地方放置任何围栏,但它正在为 C/C++ 编译器每个 std::memory_order_seq_cst(顺序语义变量)
    • SFENCE not 通常在 x86 上为您提供 seq-cst。它可能在与 MFENCE 一样强大的 IIRC 上的 AMD 上。正如您从 Alex 的链接问题的最终答案中看到的那样,您确实需要一个完整的障碍,因为您无法从 SFENCE+LFENCE 构建 mfence。 (正如您所说,您只需要在 SSE4.1 从 WC 内存中进行弱排序加载之后才需要 LFENCE,因此它基本上对内存排序没有用处,只是因为它的执行障碍效应。)
    猜你喜欢
    • 2015-02-22
    • 2015-02-20
    • 2014-09-14
    • 2019-10-30
    • 2014-12-28
    • 1970-01-01
    • 2012-10-18
    • 1970-01-01
    相关资源
    最近更新 更多