目标架构
在 X86 架构上,RMW 操作总是刷新存储缓冲区,因此实际上是一个序列化和顺序一致的操作。
我希望人们不要这么说。
该声明甚至没有意义,因为没有“顺序一致操作”之类的东西,因为“顺序一致性”不是任何操作的属性。顺序一致的执行是最终结果是一个交错的操作给出该结果的执行。
关于这些 RMW 操作可以说些什么:
- 在 RMW 的 R 或 W 可见之前,RMW 之前的所有操作都必须是全局可见的
- 并且在RMW可见之前RMW之后没有操作可见。
即之前的部分、RMW 和之后的部分是顺序运行的。换句话说,在RMW之前和之后有一个完整的围栏。
这是否会导致整个程序的顺序执行取决于程序的所有全局可见操作的性质。
可见性与执行顺序
那是在可见性方面。我不知道这些处理器是否会尝试在 RMW 之后推测性地执行代码,如果与并行执行的副作用发生冲突,则操作会回滚的正确性要求(这些细节对于不同的供应商和给定家庭的世代,除非明确指定)。
您的问题的答案可能会有所不同
- 您需要保证副作用集的正确性(如顺序一致性要求),
- 或保证基准是可靠的,
- 或独立于 CPU 版本的比较时序:保证不同执行的时序比较结果(对于给定的 CPU)。
高级语言与 CPU 功能
问题标题是“是std::atomic::fetch_add
x86-64 上的序列化操作吗?” 一般形式:
“OP 是否在 ARCH 上提供保证 P”
在哪里
- OP是高级语言中的高级操作
- P 是所需的属性
- ARCH 是特定的 CPU 或编译器目标
通常,规范的答案是:这个问题没有意义,OP 是高级别的并且与目标无关。这里存在低电平/高电平不匹配。
编译器受语言标准(或更合理的解释)、文档扩展、历史……而不是目标体系结构标准的约束,除非该特性是高级语言的低级、透明特性.
在 C/C++ 中获得低级语义的规范方法是使用 volatile 对象和 volatile 操作。
在这里,您必须使用 avolatile std::atomic<int>
才能提出有关架构保证的有意义的问题。
当前代码生成
您问题的有意义的变体将使用以下代码:
volatile std::atomic<int> counter;
/* otherStuff 1 */
counter.fetch_add(1, std::memory_order_relaxed);
该语句将生成一个原子 RMW 操作,在这种情况下,该操作在 CPU 上“是一个序列化操作”:之前在汇编代码中执行的所有操作在 RMW 启动之前完成;RMW 之后的所有操作都等到 RMW 完成才能启动(就可见性而言)。
然后您需要了解 volatile 语义的不愉快之处:volatile 仅适用于这些 volatile 操作,因此您仍然无法获得有关其他操作的一般保证。
无法保证在易失性 RMW 操作之前的高级 C++ 操作在汇编代码中排序在之前。你需要一个“编译器屏障”来做到这一点。这些屏障是不可移植的。(这里不需要,因为无论如何这是一种愚蠢的方法。)
但是,如果您想要该保证,则可以使用:
- 发布操作:确保之前的全局可见操作是完整的
- 获取操作:确保以下全局可见操作之前不会开始
- 对多个线程可见的对象的 RMW 操作。
那么为什么不让你的 RMW 操作 ack_rel 呢?那么它甚至不需要是易变的。
处理器系列中可能的 RMW 变体
x86-64(比如不到 5 年的架构)中是否有一条指令可以
指令集的潜在变体是另一个子问题。供应商可以引入新的指令,以及在运行时测试其可用性的方法;编译器甚至可以生成代码来检测它们的可用性。
任何遵循现有传统(1)的任何 RMW 特征,即该家族中通常的内存操作的强排序都必须尊重这些传统:
- Total Store Order:所有的 store 操作都是有序的,隐式隔离的;换句话说,每个内核中都有一个严格用于非推测性存储操作的存储缓冲区,它不会重新排序,也不会在内核之间共享;
- 每个存储都是一个释放操作(用于之前的正常内存操作);
- 推测性开始的加载按顺序完成,并在完成时进行验证:任何在缓存中被破坏的位置的早期加载都将被取消,并以最近的值重新开始计算;
- 负载是获取操作。
那么任何新的(但传统的)RMW 操作必须既是获取操作又是释放操作。
(未来可能添加的虚拟 RMW 操作的示例是xmult
和xdiv
。)
但这是未来学,并且在未来添加较少有序的指令不会违反任何安全不变量,除非可能针对基于时间的、类似旁通道 Spectre 的攻击,我们通常不知道如何建模和推理。
这些问题的问题,即使是现在,也需要提供缺席证明,为此我们需要了解 CPU 系列的每个变体。这并不总是可行的,而且,如果您在高级代码中使用正确的顺序,那也是不必要的,如果您不这样做,那将毫无用处。
(1) 保证内存操作的传统是 CPU 设计中的指导方针,而不是任何特征操作的保证:根据定义,尚不存在的操作除了保证内存完整性之外,不保证其语义,即保证未来的任何操作都不会破坏先前建立的特权和安全保证(将来创建的非特权指令都不能访问未映射的内存地址......)。