内存障碍强制缓存一致性?

Memory barriers force cache coherency?

我正在阅读 this question about using a bool for thread control 并且对@eran 的这个回答很感兴趣:

Using volatile is enough only on single cores, where all threads use the same cache. On multi-cores, if stop() is called on one core and run() is executing on another, it might take some time for the CPU caches to synchronize, which means two cores might see two different views of isRunning_.

If you use synchronization mechanisms, they will ensure all caches get the same values, in the price of stalling the program for a while. Whether performance or correctness is more important to you depends on your actual needs.

我已经花了一个多小时搜索一些声明说同步原语强制缓存一致性但失败了。我最接近的是 Wikipedia:

The keyword volatile does not guarantee a memory barrier to enforce cache-consistency.

这表明内存屏障确实会强制缓存一致性,并且由于一些同步原语是使用内存屏障实现的(再次来自维基百科),这是一些“证据”。

但我所知道的还不足以确定是否相信这一点,请确保我没有曲解它。

有人可以澄清一下吗?

维基百科告诉您的是 volatile 并不意味着将插入内存屏障来强制缓存一致性。然而,适当的内存屏障将强制多个 CPU 内核之间的内存访问是一致的,您可能会发现阅读 std::memory_order 文档很有帮助。

据我了解,同步原语根本不会影响缓存一致性。 Cache 在法语中是 hidden 的意思,它不应该对用户可见。缓存一致性协议应该在没有程序员参与的情况下工作。

同步原语会影响内存排序,内存排序是明确定义的,并且通过处理器的 ISA 对用户可见。

计算机体系结构综合讲座集合中的 A Primer on Memory Consistency and Cache Coherence 提供了详细信息的良好来源。

编辑:澄清你的疑问

维基百科的说法有点错误。我认为混淆可能来自术语 内存一致性 缓存一致性 。他们不是同一件事。

C 中的 volatile 关键字意味着变量总是从内存中读取(与寄存器相反)并且 编译器 不会重新排序 loads/stores 围绕着它。这并不意味着 硬件 不会重新订购 loads/stores。这是一个内存一致性问题。当使用较弱的一致性模型时,程序员需要使用同步原语来强制执行特定的顺序。这与缓存一致性不同。例如,如果线程 1 修改了位置 A,那么在此事件之后线程 2 加载位置 A,它将收到一个更新的(一致的)值。如果使用缓存一致性,这应该会自动发生。内存排序是一个不同的问题。您可以查看著名的论文Shared Memory Consistency Models: A Tutorial for more information. One of the better known examples is Dekker's Algorithm,其中需要顺序一致性或同步原语。

EDIT2:我想澄清一件事。虽然我的缓存一致性示例是正确的,但在某些情况下内存一致性似乎与它重叠。这是当存储在处理器中执行但延迟进入缓存时(它们在存储 queue/buffer 中)。由于处理器的缓存没有收到更新值,其他缓存也不会。这可能 看起来 像一个缓存一致性问题,但实际上它不是并且实际上是 ISA 内存一致性模型的一部分。在这种情况下,可以使用同步原语将存储队列刷新到缓存中。考虑到这一点,您以粗体突出显示的维基百科文本是正确的,但另一个文本仍然略有错误:关键字 volatile 不保证内存屏障来强制缓存一致性。它应该说:关键字 volatile 不保证内存屏障来强制内存一致性

简答:缓存一致性在大多数情况下都有效,但并非总是如此。您仍然可以读取过时的数据。如果你不想冒险,那就使用内存屏障

长答案:CPU核心不再直接连接到主内存。所有加载和存储都必须通过缓存。每个 CPU 都有自己的私有缓存这一事实导致了新的问题。如果多个 CPU 正在访问同一内存,则仍必须确保两个处理器始终看到相同的内存内容。如果一个处理器上的高速缓存行是脏的(即,它还没有被写回主内存)并且第二个处理器试图读取相同的内存位置,则读取操作不能直接转到主内存。 .相反,需要第一个处理器的缓存行的内容。现在的问题是这个缓存行传输必须在什么时候发生?这个问题很容易回答:当一个处理器需要一个高速缓存行时,它在另一个处理器的高速缓存中是脏的,用于读取或写入。但是一个处理器如何确定另一个处理器缓存中的缓存行是否脏了呢?仅仅因为缓存行被另一个处理器加载就假设它是次优的(充其量)。通常大多数内存访问都是读访问,并且产生的缓存行不是脏的。缓存一致性协议来了。 CPU 通过 MESI 或其他一些缓存一致性协议在其缓存中保持数据一致性。

有了缓存一致性,我们是否应该始终看到缓存行的最新值,即使它已被另一个 CPU 修改?毕竟这是缓存一致性协议的全部目的。通常当缓存行被修改时,对应的 CPU 会向所有其他 CPU 发送“无效缓存行”请求。事实证明,CPU 可以立即向无效请求发送确认,但将缓存行的实际无效推迟到稍后的时间点。这是通过失效队列完成的。现在,如果我们不幸地读取了这个短 window 内的缓存行(在 CPU 确认无效请求和实际使缓存行无效之间),那么我们可以读取一个陈旧的值。现在为什么 CPU 会做这么可怕的事情。简单的答案是性能。因此,让我们看看失效队列可以提高性能的不同场景

  • 场景 1:CPU1 收到来自 CPU2 的无效请求。 CPU1 也有很多存储和负载排队等待缓存。这意味着请求的高速缓存行的失效需要时间,并且 CPU2 会停止等待确认

  • 场景 2:CPU1 在短时间内收到大量无效请求。现在 CPU1 需要时间使所有缓存行失效。

将条目放入无效队列本质上是 CPU 承诺在传输任何关于该缓存行的 MESI 协议消息之前处理该条目。所以失效队列是我们可能看不到最新值的原因,即使在对单个变量进行简单读取时也是如此。

现在敏锐的reader可能会想,当CPU想要读取缓存行时,它可以先扫描失效队列,然后再从缓存中读取。这应该可以避免这个问题。然而,CPU 和失效队列物理上位于缓存的相对两侧,这限制了 CPU 直接访问失效队列。 (一个 CPU 的缓存的失效队列由来自其他 CPU 的缓存一致性消息通过系统总线填充。因此将失效队列放置在缓存和缓存之间是有意义的系统总线)。所以为了真正看到任何共享变量的最新值,我们应该清空失效队列。通常读取内存屏障会这样做。

刚才讲了失效队列和读内存屏障。 [1]对于理解读写内存屏障的必要性和MESI缓存一致性协议的细节是一个很好的参考

[1] http://www.puppetmastertrading.com/images/hwViewForSwHackers.pdf