x86平台上的顺序一致原子加载

4

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

就我从编译器生成的汇编清单中所看到的,它被实现为在x86上的普通加载,然而据我所知,普通加载保证具有获取语义,而普通存储则保证具有发布语义。

顺序一致性存储是通过锁定xchg来实现的,而加载则是普通加载。这对我来说听起来很奇怪,请您详细解释一下?

添加

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

3个回答

10

在x86架构中,如果使用了“LOCK”指令进行SC存储,数值被正确对齐,并且使用了“normal”WB缓存模式,则普通的MOV指令足以实现原子顺序一致性加载。

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

如果您使用“WC”缓存模式或“non-temporal”指令(如MOVNTI),那么所有情况都无法保证,因为处理器不一定会及时将数据写回主内存。


1
请参见为什么在x86上对自然对齐的变量进行整数赋值是原子的?,以获取原子性保证(这与任何排序保证无关)。 - Peter Cordes
1
https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html讲解了顺序关系。在x86的acq_rel模型上,您需要在加载或存储之一上设置完整的屏障才能恢复顺序一致性(实际上是seq-cst +具有存储转发的存储缓冲区),正如问题中的链接所解释的那样。编译器选择将额外的成本放在存储上,以便纯加载可以更便宜。 - Peter Cordes

2

如果对齐,x86上的读取本质上是原子性的,因此在英特尔汇编手册vol 2A中的MOV指令下的部分以及LOCK前缀应该提到这一点。其他卷也可能提到这一点。

然而,如果您想要进行原子读取,可以使用_InterlockedExchangeAdd((LONG*)&var,0)(又称LOCK XADD),这将产生旧值,但不会更改其值。同样可以使用InterlockCompareExchange((LONG*)&var,var,var)(又称LOCK CMPXCHG),但我认为没有必要这样做。


1
这是关于获取seq-cst ordering,而不仅仅是原子性。如果您还希望在加载的一部分中使用完整的内存屏障,则只需使用原子RMW作为加载即可。顺便说一下,CAS(&var, 0,0)永远不会修改该值,并避免首先对共享变量进行非原子加载。 - Peter Cordes

1

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

阅读

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软件控制总线锁定

编辑2:总结评论中的信息。

虽然

"[...]处理器的锁定协议在交换操作的持续时间内自动实现,无论是否有LOCK前缀或IOPL的值的存在或不存在"

但存在限制

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


请阅读英特尔手册3A卷中的“8.1.1保证原子操作”。尽管如此,在地址对齐方面知之甚少的情况下,您的方法仍然很有用(在您的第二段代码中的LOCK是不必要的,因为处理器将始终使用XCHG指令进行锁定)。 - LocoDelAssembly
1
你正在断章取义。 "跨总线宽度、缓存行和页面边界的可缓存内存访问在没有lock前缀的情况下,如mov eax,[mem](对于对齐的mem是保证原子性的,这就是为什么编译器使用alignof( atomic<T> ) = sizeof(T)用于无锁原子操作。请参见为什么x86上自然对齐变量的整数赋值是原子的?)。 - Peter Cordes
1
你唯一使用慢速锁定操作进行加载和存储的原因是,如果你的原子变量跨越8字节边界。但更好的解决方法是对齐你的原子变量,这样你就可以使用廉价的加载!你只需要一个lock操作来进行原子RMW,或者用于加载或存储的seq-cst排序。你可以选择其中之一;通常的选择是廉价的加载。请参见https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html - Peter Cordes
@curiousguy 这完全是关于以不对齐的方式访问一块内存。这可以通过很多种方式来实现。 - bkausbk
1
如果你的数据跨越了缓存行,屏障是无法帮助你的;你需要一个lock前缀。但是分裂锁的性能是灾难性的。如果你坚持支持alignf(atomic<int>) = 1,那么最好使用自旋锁,即回退到不lock_free模式的操作。大多数系统都不会这样做;alignof(atomic<T>) = sizeof(T)和未对齐对象UB可能导致撕裂。此外,如果你依赖于未对齐的原子操作,你的程序将无法在非x86系统上移植;大多数LL/SC机器没有任何东西可以允许未对齐的原子操作。 - Peter Cordes
显示剩余6条评论

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