x86架构下的内存排序限制

4
在他的伟大著作《C++ Concurrency in Action》中,Anthony Williams写道(第309页):
例如,在x86和x86-64架构上,原子加载操作始终相同,无论是标记为memory_order_relaxed还是memory_order_seq_cst(参见5.3.3节)。这意味着使用松散内存顺序编写的代码可能适用于具有x86架构的系统,在具有更精细的内存顺序指令集(如SPARC)的系统上则会失败。
我理解得对吗?在x86架构上,所有原子加载操作都是memory_order_seq_cst吗?此外,在cppreference的std::memory_order网站上提到,在x86上,发布-获取排序是自动的。
如果这个限制是有效的,那么这些排序是否仍然适用于编译器优化?

“_所有原子加载操作都是memory_order_seq_cst吗?_”这句话甚至不正确,它是一个毫无意义的陈述。没有任何操作是有序或无序的,程序中的操作才有顺序。 - curiousguy
5个回答

8
是的,编译器优化仍然适用于顺序。
另外,在x86上,“原子加载操作始终相同”并非完全准确。
在x86上,使用mov执行的所有加载具有获取语义,并且使用mov执行的所有存储具有释放语义。因此,acq_rel、acq和relaxed加载是简单的mov,同样acq_rel、rel和relaxed存储(acq存储和rel加载始终等于relaxed)也是如此。
然而,对于seq_cst来说,这并不一定成立:架构不保证mov具有seq_cst语义。实际上,x86指令集没有任何特定指令用于顺序一致性加载和存储。仅在x86上的原子读取-修改-写入操作将具有seq_cst语义。因此,您可以通过使用参数为0的fetch_and_add操作(lock xadd指令)获得加载的seq_cst语义,并通过进行seq_cst exchange操作(xchg指令)并丢弃先前的值来获得存储的seq_cst语义。
但您不需要同时进行两者!只要所有的seq_cst存储都使用xchg完成,seq_cst加载就可以简单地使用mov实现。同样,如果所有加载都使用lock xadd完成,则seq_cst存储可以简单地使用mov实现。 xchglock xaddmov慢得多。因为程序通常具有更多的加载而不是存储,所以方便使用xchg进行seq_cst存储,以便(更频繁的)seq_cst加载可以简单地使用mov。这种实现细节在x86应用程序二进制接口(ABI)中被编码。在x86上,合规的编译器必须将seq_cst存储编译为xchg,以便可以使用更快的mov指令执行seq_cst加载(可能出现在另一个使用不同编译器编译的翻译单元中)。
因此,在x86上,seq_cst和acquire加载使用相同的指令并不总是正确的。这只是因为ABI指定seq_cst存储应编译为xchg

“只有在x86上的原子读-修改-写操作才具有seq_cst语义。” 不,它们不会。那是一个毫无意义的说法。 只有程序执行可以是顺序的,指令则不能。 - curiousguy
@curiousguy:更准确地说:仅在x86上锁定的读取-修改-写入操作将具有与C11原子操作和seq_cst排序相对应的语义。加载将具有与C11获取排序相对应的语义,而存储将具有C11发布排序的语义。 - Paolo Bonzini

2
编译器当然必须遵循语言规则,不论运行在哪种硬件上。
他所说的是,在x86上你没有松散排序,所以即使你没有要求,你也会得到更严格的排序。这也意味着,在x86上测试的代码可能在具有松散排序的系统上无法正常工作。

0

这本书中的句子写得有些误导人。在体系结构上获得的排序不仅取决于如何翻译原子加载,还取决于如何翻译原子存储。

在x86上实现seq_cst的通常方法是在任何seq_cst存储和同一线程的后续seq_cst加载之间的某个时刻刷新存储缓冲区。编译器保证这样做的通常方法是在存储后刷新,因为存储比加载少。在这种翻译中,seq_cst加载不需要刷新。

如果您只使用普通的加载和存储来编写x86程序,则可以保证加载提供acquire语义,而不是seq_cst

至于编译器优化,在C11/C++11中,编译器根据特定原子的语义进行代码移动的优化,然后考虑底层硬件。(硬件可能提供更强的排序,但没有理由让编译器因此限制其优化。)


请注意,在存储和加载之间只需要刷新;连续的多个存储操作之间不需要栅栏。如果您的代码生成是针对每个原子操作独立进行的,那么当然无法实现这一点。 - curiousguy

0
值得记住的是,尽管在x86上,load relaxed和seq_cst load可能映射到相同的指令,但它们并不相同。load relaxed可以自由地被编译器重新排序,跨越到不同内存位置的内存操作,而seq_cst load不能跨越其他内存操作重新排序。

0
我是否理解正确,x86 架构上的所有原子加载操作都是使用 memory_order_seq_cst 顺序一致性吗?
只有执行(程序的执行或程序中某些线程间可见的操作的执行)可以是顺序的。单个操作本身不是顺序的。
询问单个孤立操作的实现是否是顺序的是一个没有意义的问题。
需要某些保证的所有内存操作的翻译必须遵循使该保证生效的策略。可能有不同的策略,具有不同的编译器复杂度成本和运行时成本。
[就像有不同的策略来实现虚拟函数:唯一 OK 的策略(适合我们对速度、可预测性和简单性的所有期望)是使用虚函数表,因此所有编译器都使用虚函数表,但虚函数并没有定义为通过虚函数表进行。]
实际上,在给定 CPU 上实现 memory_order_seq_cst 操作时,使用的策略并不会有太大差异(据我所知)。编译器之间的差异很小,不会影响二进制兼容性。但是,潜在的差异以及多线程程序的高级全局优化可能会为原子操作的更高效代码生成打开新的机会。

根据您的编译器,仅包含放松加载和std::atomic<>对象的memory_order_seq_cst修改的程序可能会在强有序CPU上表现出仅有的顺序行为,也可能不会。


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