MESI 协议和 std::atomic - 它是否确保所有写入对其他线程立即可见?

A. *_* S. 5 c++ cpu-architecture memory-model stdatomic mesi

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

\n\n

从 29.3p13 开始:

\n\n
\n

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

\n
\n\n

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

\n\n

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

\n\n

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

\n\n

\xe2\x80\x9csuccessively\xe2\x80\x9d 我的意思是在线程 A 发出原子存储之后。(修改顺序已更新)

\n

Pet*_*des 8

我将回答真实 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++ 形式主义。Q&A 中的“选项 C”表明 C++ 的保证有多么薄弱;根据 C++ 形式主义,存储然后加载两个不同 SC 变量的情况不足以暗示基于它的发生之前,即使所有 SC 操作必须有一个总顺序。但在现实生活中,在具有一致缓存且仅本地(每个 CPU 核心内)内存重新排序的系统上就足够了,甚至在 AArch64 中,在 SC 存储之后立即加载 SC 本质上仍然给我们带来了 StoreLoad 屏障。

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

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

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

尽管请注意,在某些 ISA 上,允许其他一些线程在存储通过缓存变为全局可见之前查看存储。(即硬件内存模型可能不是“多副本原子”,并且允许 IRIW 重新排序。POWER 是我所知道的在现实生活中执行此操作的唯一示例。请参阅是否始终会看到对不同线程中不同位置的两个原子写入其他线程的顺序是否相同?有关硬件机制的详细信息:SMT 线程之间退休的又名分级存储的存储转发。)


如果您的意思是在本地执行,以便稍后在此线程中加载可以看到它,那么不行。 std::atomic 可以使用比 seq_cst 弱的内存顺序。

所有主流 ISA 的内存排序规则都足够弱,足以允许存储缓冲区将指令执行从提交到缓存分离。在我们确定存储位于正确的执行路径上之前,这还允许在执行后为存储提供私有的存储空间,从而允许推测性乱序执行。(在存储指令从后端的无序部分退出之前,存储不能提交给 L1d,因此已知是非推测性的。)

如果您想在执行任何后续加载之前等待您的存储对其他线程可见,请使用atomic_thread_fence(memory_order_seq_cst);. (在具有 C++ -> asm 映射标准选择的“普通”ISA 上,将编译为完全屏障)。

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

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


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


归档时间:

查看次数:

1011 次

最近记录:

4 年,2 月 前