2

关于std::atomic,存储到原子变量的 C++11 标准状态将在“合理的时间内”对该变量的加载可见。

从 29.3p13 开始:

实现应该使原子存储在合理的时间内对原子负载可见。

但是我很想知道在处理基于 MESI 缓存一致性协议(x86、x86-64、ARM 等)的特定 CPU 架构时实际发生了什么。

如果我对 MESI 协议的理解是正确的,一个核心总是会立即读取另一个核心之前写入/正在写入的值,可能是通过窥探它。(因为写一个值意味着发出一个 RFO 请求,这反过来又使其他缓存行无效)

这是否意味着当线程 A 将值存储到 a 时std::atomic,另一个连续对该原子进行加载的线程 B 实际上总是会观察 A 在 MESI 架构上写入的新值?(假设没有其他线程对该原子进行操作)

“成功”是指在线程 A 发出原子存储之后。(修改顺序已更新)

4

2 回答 2

3

我将回答在真实 CPU 上的实际实现中会发生什么,因为仅基于标准的答案几乎不能说出关于时间或“即时性”的任何有用信息。

MESI 只是 ISO C++ 没有什么可说的实现细节。ISO C++ 提供的保证只涉及顺序,而不涉及实际时间。ISO C++ 故意不特定,以避免假设它将在“普通”CPU 上执行。在需要显式刷新以实现存储可见性的非连贯机器上的实现在理论上可能是可能的(尽管对于发布/获取和 seq-cst 操作的性能可能很糟糕)

C++ 在时序方面不够具体,甚至允许在单核协作多任务系统(无抢占)上实现,编译器偶尔会插入自愿让步。(没有任何易失性访问或 I/O 的无限循环是 UB)。 假设您认为调度程序时间片仍然是“合理”的时间量,那么在一次实际上只能执行一个线程的系统上的 C++ 是完全可以和可能的。(如果您让步或以其他方式阻止,则更少。)

甚至 ISO C++ 用来提供关于排序的保证的形式主义模型也与硬件 ISA 定义其内存模型的方式大不相同。C++ 正式保证纯粹是在发生之前和同步方面,而不是“重新”排序石蕊测试或任何类似的东西。例如,如何在 C++11 中实现 StoreLoad 屏障?无法回答纯 ISO C++ 形式主义。它可以显示 C++ 的保证是多么的薄弱;例如,根据 C++ 形式,所有 seq_cst 操作的总顺序的存在不足以暗示基于它的发生之前。但在现实生活中,对于具有一致缓存和仅本地(在每个 CPU 内核内)内存重新排序的系统来说就足够了。

当线程 A 将值存储到std::atomic

这取决于您所说的“做”商店是什么意思。

如果您的意思是从存储缓冲区提交到 L1d 缓存,那么是的,那是存储变得全局可见的时刻,在使用 MESI 为所有 CPU 内核提供一致的内存视图的普通机器上。

尽管请注意,在某些 ISA 上,允许其他一些线程在通过 cache全局可见之前查看存储。(即硬件内存模型可能不是“多副本原子”,并允许 IRIW 重新排序。POWER 是我所知道的在现实生活中这样做的唯一示例。请参阅是否总是可以看到两个原子写入到不同线程中的不同位置其他线程的顺序相同吗?有关硬件机制的详细信息:SMT 线程之间的已退休 akagraded stores 的存储转发。)


如果您的意思是在本地执行以便稍后在该线程中加载可以看到它,那么不会。 std::atomic 可以使用弱于 seq_cst 的 memory_order。

所有主流 ISA 都有足够弱的内存排序规则,以允许存储缓冲区将指令执行从提交到缓存解耦。在我们确定它们是否在正确的执行路径上之前,这也允许通过在执行后将存储放在某个私有的地方来进行推测性的乱序执行。(在存储指令从后端的乱序部分退出之前,存储不能提交到 L1d,因此已知是非推测性的。)

如果您想等待其他线程可以看到您的商店,然后再进行任何后续加载,请使用atomic_thread_fence(memory_order_seq_cst);. (在标准选择 C++ 的“普通”ISA 上 -> asm 映射将编译为完全障碍)。

大多数ISA 上,seq_cst 存储(默认)也将停止该线程中的所有后续加载(和存储),直到该存储全局可见。但是在 AArch64 上,STLR 是一个顺序释放存储,以后加载/存储的执行不必停止,除非/直到 LDAR(获取加载)即将执行,而 STLR 仍在存储缓冲区中。这实现了尽可能弱的 SC 语义,假设 AArch64 硬件实际上以这种方式工作,而不是仅仅将其视为存储 + 全屏障。

但请注意,只需要阻止以后的加载/存储;寄存器上的 ALU 指令的乱序执行仍然可以继续。但是,例如,如果您期望由于 FP 操作的依赖链而产生某种时间效应,那么在 C++ 中您就无法依赖它。


即使您确实使用了 seq_cst ,所以在其他人可以看到存储之前,该线程中没有任何事情发生,这仍然不是即时的。例如,在主流现代 Intel x86上,真实硬件上的内核间延迟可能约为40ns 。(这个线程不必在内存屏障指令上停顿那么久;其中一些时间是另一个线程上的缓存未命中,试图读取被该内核的 RFO 无效的行以获得独占所有权。)或者当然对于共享物理内核的 L1d 缓存的逻辑内核来说要便宜得多:生产者-消费者在超同级与非超同级之间共享内存位置的延迟和吞吐量成本是多少?

于 2020-02-19T18:52:12.340 回答
0

从 29.3p13 开始:

实现应该使原子存储在合理的时间内对原子负载可见。

C 和 C++ 标准在线程上无处不在,因此不能用作正式规范。他们使用时间的概念,并在某种程度上暗示一切都按顺序按部就班地运行(如果不是,您将没有健全的程序语义),然后说某些构造可以看到无序的效果,而不会告诉哪个是哪个。

当看到效果乱序时,线程时间定义不正确,因为您没有一个也会乱序的计时器:如果动作执行乱序,您就不会做运动!

甚至“乱序”也表明有些事情是纯粹的顺序,而其他一些操作相对于第一个操作可能是“乱序的”。不是这样std::atomic定义的。

标准试图说的是每个线程都有一个进度的概念,有一个 CPU 时间或成本指数,并且随着更多的事情完成而增加,并且事情只能通过实现稍微重新排序:现在重新排序是定义明确,不是根据其他顺序指令,而是根据成本/周期/CPU时间。

因此,如果两条指令在顺序线程内执行中彼此接近,它们在 CPU 时间上也会接近。一个合理的编译器不应该将易失性操作、文件输出或原子操作移过非常昂贵的“纯”计算(没有外部可见副作用的计算)。

可悲的是,许多委员会成员甚至无法说出一个基本概念!

于 2020-02-19T02:41:02.820 回答