3

在他的伟大著作“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 上发布-获取排序是自动的。

如果此限制有效,那么这些排序是否仍适用于编译器优化?

4

5 回答 5

6

是的,排序仍然适用于编译器优化。

此外,在 x86 上“原子加载操作始终相同”并不完全准确。

在 x86 上,完成的所有加载都mov具有获取语义,并且完成的所有存储都mov具有释放语义。所以acq_rel、acq 和relaxed 加载都是simple movs,acq_rel、rel 和relaxed 存储类似(acq 存储和rel 加载总是等于relaxed)。

然而,这对于 seq_cst 不一定是正确的:架构不保证seq_cst 的语义mov。事实上,x86 指令集没有任何特定指令用于顺序一致的加载和存储。只有 x86 上的原子读取-修改-写入操作才会具有 seq_cst 语义。因此,您可以通过执行lock xadd参数为 0 的 fetch_and_add 操作(指令)来获得加载的 seq_cst 语义,并通过执行 seq_cst 交换操作(xchg指令)并丢弃先前的值来获得存储的 seq_cst 语义。

但你不需要两者都做!只要所有 seq_cst 存储都用 完成xchg,seq_cst 加载就可以简单地用mov. 双重的,如果所有的加载都是用 完成的lock xadd,seq_cst 存储可以简单地用mov.

xchg并且lock xadd比 慢得多mov。因为程序的负载(通常)比存储多,所以使用 seq_cst 存储很方便,这样xchg(更频繁的) seq_cst 负载可以简单地使用 a mov。此实现细节在 x86 应用程序二进制接口 (ABI) 中进行了编码。在 x86 上,兼容的编译器必须编译 seq_cst 存储,xchg以便可以使用更快的mov指令完成 seq_cst 加载(可能出现在另一个翻译单元中,使用不同的编译器编译)。

因此,一般来说,seq_cst 和获取加载在 x86 上使用相同的指令完成是不正确的。这是正确的,因为 ABI 指定 seq_cst 存储被编译为xchg.

于 2013-08-29T13:21:57.480 回答
2

编译器当然必须遵循语言的规则,无论它运行在什么硬件上。

他说的是,在 x86 上,您没有宽松的排序,因此即使您不要求,您也会得到更严格的排序。这也意味着在 x86 上测试的此类代码可能无法在具有宽松排序的系统上正常工作

于 2012-05-10T16:18:45.570 回答
0

值得记住的是,虽然负载放松和 seq_cst 负载可能映射到 x86 上的相同指令,但它们并不相同。编译器可以跨内存操作自由地重新排序加载放松到不同的内存位置,而不能跨其他内存操作重新排序 seq_cst 加载。

于 2013-09-03T05:38:13.613 回答
0

书中的句子以一种有点误导的方式写成。在架构上获得的排序不仅取决于您如何转换原子负载,还取决于您如何转换原子存储。

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

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

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

于 2013-11-07T13:58:19.850 回答
0

我是否理解在 x86 架构上所有原子加载操作都是正确的memory_order_seq_cst

只有(程序的,程序中的一些线程间可见操作的)执行可以是顺序的。单个操作本身不是顺序的。

询问单个隔离操作的实现是否是顺序的,是一个毫无意义的问题。

所有需要保证的内存操作的转换必须遵循启用该保证的策略。可能有不同的策略具有不同的编译器复杂性成本和运行时成本。

[只是说实现虚函数有不同的策略:唯一可以(符合我们对速度、可预测性和简单性的所有期望)的策略是使用 vtables,所以所有编译器都使用 vtable,但没有定义虚函数就像通过 vtable 一样。]

实践中,用于在给定 CPU(据我所知)上实现操作的策略并没有大相径庭。编译器之间的差异很小,不会妨碍二进制兼容性。但是存在潜在的差异,多线程程序的高级全局优化可能会为更高效的原子操作代码生成开辟新的机会。memory_order_seq_cst

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

于 2019-12-12T03:44:50.653 回答