是std :: atomic :: fetch_add在x86-64上进行序列化操作吗?

时间:2018-06-28 15:08:49

标签: c++ x86 atomic relaxed-atomics

考虑以下代码:

std::atomic<int> counter;

/* otherStuff 1 */
counter.fetch_add(1, std::memory_order_relaxed);
/* otherStuff 2 */

x86-64中是否存在一条指令(例如,使用不到5年的体系结构),该指令将允许其他东西1和2在fetch_add上重新排序,还是总是要序列化? >

编辑:

lock add是x86上的内存壁垒吗?”来概括一下。似乎不是,尽管我不确定在哪里可以找到它的参考。

3 个答案:

答案 0 :(得分:4)

首先让我们看一下使用std::memory_order_relaxed时允许编译器执行的操作。
如果otherStuff 1/2与原子操作之间没有依赖关系,则可以肯定地对语句重新排序。例如:

g = 3;
a.fetch_add(1, memory_order_relaxed);
g += 12;

clang ++生成以下程序集:

lock   addl $0x1,0x2009f5(%rip)        # 0x601040 <a>
movl   $0xf,0x2009e7(%rip)             # 0x60103c <g>

这里clang随意使用原子g = 3操作对fetch_add重新排序,这是合法的转换。

使用std::memory_order_seq_cst时,编译器输出变为:

movl   $0x3,0x2009f2(%rip)        # 0x60103c <g>
lock   addl $0x1,0x2009eb(%rip)   # 0x601040 <a>
addl   $0xc,0x2009e0(%rip)        # 0x60103c <g>

不会对语句进行重新排序,因为不允许编译器这样做。 读-修改-写(RMW)操作上的顺序一致顺序既是释放操作又是获取操作,因此,不允许在编译器和CPU级别上对语句进行(可见的)重新排序。

您的问题是,在X86-64上,std::atomic::fetch_add使用宽松的排序是否是序列化操作。.
答案是:是的,如果您不考虑编译器的重新排序。

X86架构上,RMW操作始终刷新存储缓冲区,因此实际上是序列化和顺序一致的操作。

您可以说,在X86 CPU上,每个RMW操作:

  • 是对其之前的内存操作的释放操作,并且是对其之后的内存操作的获取操作。
  • 以所有线程观察到的单个总顺序可见。

答案 1 :(得分:0)

目标体系结构

  

在X86架构上,RMW操作始终会刷新商店   缓冲区,因此实际上是序列化和顺序   一致的操作。

我希望人们不再这么说。

该语句甚至没有任何意义,因为不存在“顺序一致操作”之类的东西,因为“顺序一致”不是任何操作的属性。顺序一致的执行是最终结果是执行结果的操作交错的执行。

这些RMW操作可以怎么说:

  • RMW的R或W可见之前,RMW之前的所有操作必须全局可见
  • 在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 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功能都必须遵守这些传统:

  • 总存储订单:所有存储操作都是有序的,被隐式围起来;换句话说,每个内核中都有一个严格用于非推测性存储操作的存储缓冲区,该缓冲区不会重新排序,也不在内核之间共享;
  • 每个存储区都是一个释放操作(用于以前的常规内存操作);
  • 以推测方式启动的负载将按顺序完成,并在完成时得到验证:某个位置的任何早期负载都被缓存中的缓存取消,并且使用最近的值重新开始计算;
  • 负载是获取操作。

那么任何新的(但传统的)RMW操作都必须既是获取操作又是释放操作。

(将来可能添加的潜在假想RMW操作示例为xmultxdiv。)

但是,这是未来主义,将来添加较少顺序的指令不会违反任何安全性不变,除非可能针对基于定时的侧通道类似Spectre的攻击,否则我们通常都不知道如何建模和推理

这些问题(甚至是关于当前问题)的问题是,需要提供缺席证明,为此,我们需要了解CPU系列的每个变体。这并不总是可行的,如果在高级代码中使用正确的顺序,则不必要,如果不使用,则没用。

(1)保证内存操作的传统是CPU设计中的准则,而不是有关任何功能操作的保证:根据定义,尚不存在的操作除了保证内存完整性外,还不能保证其语义。是,保证将来没有操作会破坏先前建立的特权和安全保证(将来创建的任何非特权指令都不能访问未映射的内存地址...。

答案 2 :(得分:-1)

使用std::memory_order_relaxed时,唯一的保证就是操作是原子的。编译器或CPU均可随意对操作进行任何排序。

来自https://en.cppreference.com/w/cpp/atomic/memory_order

  

轻松的操作:没有同步或排序   施加于其他读取或写入的约束,仅此操作的   保证原子性(请参见下面的放宽顺序)