【问题标题】:Memory barriers force cache coherency?内存屏障强制缓存一致性?
【发布时间】:2015-09-06 15:08:40
【问题描述】:

我正在阅读this question about using a bool for thread control 并被@eran 的这个回答所吸引:

仅在单个内核上使用 volatile 就足够了,所有线程都使用相同的缓存。在多核上,如果在一个核上调用了 stop(),而在另一个核上执行了 run(),则 CPU 缓存可能需要一些时间来同步,这意味着两个核可能会看到两个不同的 isRunning_ 视图。

如果您使用同步机制,它们将确保所有缓存都获得相同的值,但代价是程序会暂停一段时间。性能还是正确性对您更重要,取决于您的实际需求。

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

关键字 volatile 不能保证内存屏障来强制缓存一致性。

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

但我没有足够的知识来确定是否相信这一点,并确保我没有误解它。

有人可以澄清一下吗?

【问题讨论】:

  • 没错,C 和 C++ 中的 volatile 关键字对线程同步没有任何作用(不记得 C#)。内存屏障确实强制缓存一致性。您可能想阅读强/弱内存模型和memory ordering

标签: multithreading caching cpu-architecture multicore memory-barriers


【解决方案1】:

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

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

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

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

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

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

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

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

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

【讨论】:

  • 尽管有失效队列,但大多数 ISA 都有一个内存模型,可以保证所有其他内核都同意两个存储的顺序(IRIW 石蕊测试)。 PowerPC 是一个值得注意的例外,它的硬件实际上可以做到这一点。 (ARMv7 在纸上允许它,但没有硬件做过;ARMv8 是多拷贝原子的)。 Will two atomic writes to different locations in different threads always be seen in the same order by other threads?
  • 失效队列是否引入了任何新的重新排序可能性,或者它们只是让阅读核心看起来比其他核心更“领先”吗?在考虑内存排序时,我一直不清楚为什么它们是相关的。 (但我对 PowerPC 不是很熟悉。)是否有一些试金石测试允许在某些机器上获得最终结果,这对于存储缓冲区、OoO exec / hit-under-miss of load 是不可能的,但是 使队列无效?我可能应该把它作为一个新问题来问。
【解决方案2】:

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

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

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

编辑:澄清您的疑问

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

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

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

【讨论】:

  • 我试图在 EDIT2 中澄清这一点,但我知道这可能会造成混淆。缓存一致性是一个硬件协议,用户无法控制它。 但是,在某些情况下,新值可能会延迟写入缓存。在这些情况下,没有一个缓存看到新值。在这里,您可以使用同步原语将存储队列刷新到缓存。一旦它在本地缓存中,缓存一致性协议将自动使新值对其他缓存可见。你看得到差别吗?需要注意的重要一点是缓存一致性≠内存一致性。
  • 所以,如果我们重新表述您的问题“为什么使用同步原语而不是布尔值来强制内存一致性?”,那么我们将得到一个有趣的地方。总结一个答案,您需要同步多个变量,并且那些变量需要特殊属性才能在单个处理器中序列化和刷新。即使这样,您也需要能够在离开之前刷新您的关键部分。阅读this,了解 Dekker 算法在没有同步原语的 x86 机器上运行时遇到的问题。
  • +1 - 这比我 4 岁的答案更正确。在大多数情况下,问题在于一致性,而不是连贯性,而这正是 volatile 惨遭失败的地方。如果可以的话,另外 +1 以引用这两篇论文,由计算机架构社区中一些最知名的研究人员撰写。
  • @Wad 您的最新链接很好,声明 "sync.primitives force all CPUs to see updated state" 很好。问题是您最初询问他们是否强制缓存一致性,但他们没有。澄清和讨论由此而来。
  • Wad,我同意上面 hayesti 的 cmets。我的时间有点短,现在无法阅读任何其他材料,因此无法对该链接发表评论。我确实知道答案中的论文很长一段时间,并认为它们是极好的资源。连贯性、一致性、记忆模型等都是非常复杂的主题,要想了解它们需要认真阅读。至于@usr 的回答,我不知道是谁投了反对票以及为什么。我只能说,我认为 haysti 的回答更好恕我直言。
【解决方案3】:

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

【讨论】:

  • 谢谢。我了解易失性,但我要求的是明确指出“适当的内存屏障将强制多个 CPU 内核之间的内存访问是一致的” - 你能指点我吗?
  • 这也令人困惑,因为我所读到的关于缓存同步的内容是它发生在硬件中 - 既然如此,软件“概念”如何强制它?
  • @Wad 一些例子是CLFLUSHMFENCE IA32 指令,a large pile of documentation can be found here
  • @Wad 我指出了 std::memory_order ,它与 std::atomic_thread_fence 一起可用于在代码中插入内存屏障。由于每个 CPU 架构都有自己的围栏,甚至有不同的严格要求(例如弱排序与强排序),您可以使用这个高级概念并让编译器为目标 CPU 插入正确的指令。当然,缓存是用硬件实现的,但 ALU 也是如此,它也可以由软件驱动。
猜你喜欢
  • 2017-08-02
  • 2014-09-03
  • 2020-03-18
  • 1970-01-01
  • 2012-07-02
  • 2021-01-06
  • 2016-12-09
  • 1970-01-01
  • 2014-02-04
相关资源
最近更新 更多