【问题标题】:sequentially-consistent atomic load on x86x86 上的顺序一致的原子负载
【发布时间】:2025-11-23 05:20:04
【问题描述】:

我对 x86 上的顺序一致加载操作感兴趣。

据我从汇编程序列表中看到的,由编译器生成的它是作为 x86 上的普通加载实现的,但据我所知,普通加载保证具有获取语义,而普通存储保证具有释放。

p>

顺序一致的存储被实现为锁定的 xchg,而加载作为普通加载。这听起来很奇怪,你能详细解释一下吗?

添加

刚刚在互联网上发现,只要使用锁定的 xchg 完成存储,就可以像简单的 mov 一样完成顺序一致的原子加载,但没有证据,也没有文档链接。

【问题讨论】:

    标签: assembly x86 atomic memory-barriers memory-model


    【解决方案1】:

    x86 上的普通 MOV 足以满足原子顺序一致的加载,只要 SC 存储使用 LOCKed 指令完成,值正确对齐,并使用“正常”WB 缓存模式。

    有关完整映射,请参阅我在 http://www.justsoftwaresolutions.co.uk/threading/intel-memory-ordering-and-c++-memory-model.html 的博客文章,有关允许订购的详细信息,请参阅 http://developer.intel.com/products/processor/manuals/index.htm 的英特尔处理器文档。

    如果您使用“WC”缓存模式或“非临时”指令(例如 MOVNTI),那么所有赌注都将失败,因为处理器不一定会及时将数据写回主内存。

    【讨论】:

    【解决方案2】:

    x86 上的读取本质上是原子的,只要它们对齐,英特尔汇编手册第 2A 卷中 MOV 指令下的部分应该提到这一点,与 LOCK 前缀相同。其他卷也可能提到这一点

    然而,如果你想要一个atomic read,你可以使用_InterlockedExchangeAdd((LONG*)&var,0) aka LOCK XADD,这将产生旧值,但不会改变它的值,同样可以使用InterlockCompareExchange((LONG*)&var,var,var) aka @ 987654327@,但是IMO,不需要这个

    【讨论】:

    • 这是关于获得 seq-cst ordering,而不仅仅是原子性。如果您还希望将完整的内存屏障作为负载的一部分,您只想使用原子 RMW 作为负载。顺便说一句,CAS(&var, 0,0) 永远不会修改该值并避免首先对共享变量进行非原子加载。
    【解决方案3】:

    寄存器到内存传输,反之亦然,在多处理器环境中不一定是原子的。

    阅读

    XOR EAX, EAX
    LOCK XADD [address], EAX
    

    第一条指令将 EAX 寄存器归零,第二条指令将两个 EAX 的内容与 [address] 交换,并将两者的总和再次存储在 [address] 中。由于之前 EAX 寄存器为零,因此没有任何变化。

    写作

    XCHG [address], EAX
    

    EAX 寄存器将获取值存储到指定地址。

    编辑:LOCK ADD EAX,[address] 将导致“无效操作码异常”,因为目标操作数不是内存地址。

    当 LOCK 前缀与任何其他代码一起使用时,将生成无效操作码异常 (#UD) 指令或当没有对内存进行写操作时。 8.1.2.2 Software Controlled Bus Locking

    编辑 2:汇总来自 cmets 的信息。

    虽然

    "[...] 处理器的锁定协议是自动实现的 在交换操作期间,无论是否存在 或缺少 LOCK 前缀或 IOPL 的值。”

    这是有限制的

    “对跨总线宽度、缓存行和页面边界的可缓存内存的访问不保证是原子的”

    【讨论】:

    • 它们是,阅读英特尔手册第 3A 卷中的“8.1.1 保证原子操作”。尽管如此,在对地址对齐知之甚少的情况下,您的方法仍然很有用(第二个代码中的 LOCK 是不需要的,因为处理器总是会使用 XCHG 指令锁定)
    • 好吧,你是对的“如果引用了内存操作数,处理器的锁定协议会在交换操作期间自动执行,无论是否存在LOCK前缀或值IOPL 的。”但该文档还说“对跨总线宽度、缓存行和页面边界的可缓存内存的访问不能保证是原子的”
    • 你在断章取意。 “对跨总线宽度、缓存行和页面边界的可缓存内存的访问不保证是原子的”仅适用于加载或存储没有lock前缀,例如 mov eax, [mem](对于对齐的内存,保证是原子的,这就是为什么编译器使用 alignof( atomic<T> ) = sizeof(T) 来实现无锁原子。参见 Why is integer assignment on a naturally aligned variable atomic on x86?
    • 对加载和存储都使用慢速锁定操作的唯一原因是您的原子变量是否跨越了 8 字节边界。 但是更好的解决方法是对齐你的原子变量,这样你就可以使用便宜的负载!!你只需要一个locked 原子 RMW 操作,或者对于 seq-加载或存储的 cst 排序。您可以选择其中之一;正常的选择是便宜的负载。见cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
    • @bkausbk:如果您的数据在缓存行中拆分,那么栅栏将无法帮助您;你需要一个lock前缀。但是分裂锁的性能是灾难性的。如果您坚持支持alignf(atomic<int>) = 1,您不妨使用自旋锁,即回退到未对齐的atomic<T> 的非lock_free 操作模式。大多数系统都没有; alignof(atomic<T>) = sizeof(T) 和未对齐的对象 UB 可能导致撕裂。此外,如果您的程序依赖于未对齐的原子,您的程序将无法移植到非 x86 系统;大多数 LL/SC 机器没有任何允许原子错位的东西。
    最近更新 更多