2
  • MESI 的重点是保留共享内存系统的概念。
  • 然而,使用存储缓冲区,事情变得复杂了:
  • 一旦数据到达 MESI 实现的缓存,内存就会在下游保持一致。
  • 但是,在此之前,每个内核可能不同意内存位置 X 中的内容,具体取决于每个内核的本地存储缓冲区中的内容。
  • 因此,从每个核心的角度来看,内存的状态似乎是不同的——它是不连贯的。
  • 那么,为什么我们要“部分”地执行与 MESI 的一致性呢?

编辑:在进一步缩小了真正让我困惑的地方之后,进行了实质性的编辑。我试图保持问题的一般概念不变,以保持收到的重要答案的相关性。

4

2 回答 2

4

x86 上的 MESI 的意义与几乎任何多核/CPU 系统上的相同:强制缓存一致性。x86 上等式的缓存一致性部分没有使用“部分一致性”:缓存是完全一致的。因此,可能的重新排序是连贯缓存系统和与核心本地组件(例如加载/存储子系统(尤其是存储缓冲区)和其他无序机制)交互的结果。

这种交互的结果是 x86 提供的架构强大的内存模型,只有有限的重新排序。如果没有一致的缓存,您根本无法合理地实现这个模型,或者几乎任何模型都不是完全弱1

你的问题似乎嵌入了这样一个假设,即只有可能的状态“连贯”和“其他一切”。此外,缓存一致性(主要处理缓存,并且主要是隐藏的细节)的想法与架构定义并将由每个架构2实现的内存一致性模型有一些混合。维基百科解释说,缓存一致性和内存一致性之间的一个区别是前者的规则一次只适用于一个位置,而一致性规则适用于不同的位置。在实践中,更重要的区别是内存一致性模型是唯一在架构上记录的模型。

简而言之,英特尔(和 AMD)定义了一个特定的内存一致性模型x86-TSO 3 - 就内存模型而言,它相对强大,但仍然比顺序一致性弱。与顺序一致性相比,削弱的主要行为是:

  • 以后的负载可以通过较早的商店。
  • 可以以与商店总订单不同的顺序查看该商店,但只能通过执行其中一个商店的核心来查看。

为了实现这个内存模型,各个部分必须按照规则来实现它。在所有最近的 x86 上,这意味着有序的加载和存储缓冲区,这避免了不允许的重新排序。使用存储缓冲区会导致上面提到的两个重新排序:如果不允许这些,实现将受到很大限制并且可能会慢得多。在实践中,它还意味着完全一致的数据缓存,因为如果没有它,许多保证(例如,没有加载-加载重新排序)将很难实现。

总结一下:

  • 内存一致性不同于缓存一致性:前者是记录在案的内容,并构成编程模型的一部分。
  • 在实践中,x86 实现具有完全一致的缓存,这有助于他们实现其 x86-TSO 内存模型,该模型相当强大,但比顺序一致性弱。
  • 最后,也许您正在寻找的答案,换句话说:弱于顺序一致性的内存模型仍然非常有用,因为您可以针对它进行编程,并且在您需要某些特定操作的顺序一致性的情况下,您插入正确的记忆障碍4
  • 如果您针对语言提供的内存模型(例如JavaC++11)进行编程,则无需担心硬件细节,而不必担心语言内存模型,并且编译器会插入匹配语言内存模型所需的障碍语义到硬件之一。硬件模型越强大,所需的障碍就越少。

1如果您的内存模型完全弱,即没有真正对跨核重新排序设置任何限制,我想您可以直接在非缓存一致系统上以廉价的方式实现正常操作,但是内存障碍可能会变成非常昂贵,因为他们需要刷新可能很大一部分本地私有缓存。

2各种芯片的内部实现方式可能不同,特别是一些芯片可能实现比模型更强的语义(即,一些允许的重新排序永远不会被观察到),但没有错误不会实现更弱的。

3这是那篇论文中给它的名称,我之所以使用这个名称,是因为英特尔自己没有给它命名,而且这篇论文是一个比英特尔给出的不太正式的模型作为一系列试金石测试的更正式的定义。

4它在 x86 上练习,您通常使用锁定指令(使用lock前缀)而不是单独的屏障,尽管也存在独立的屏障。这里我将只使用术语barries来指代独立屏障和嵌入到锁定指令中的屏障语义。

于 2018-04-16T00:21:14.003 回答
2

回复:您的编辑,这似乎是一个新问题:对,存储转发“违反”一致性。一个核心可以比任何其他核心更早地看到自己的商店。存储缓冲区不一致。

x86 内存排序规则要求加载按程序顺序全局可见,但允许核心在全局可见之前从其自己的存储加载数据。它不必假装它等待并检查内存顺序错误推测,就像它在其他情况下比内存模型所说的更早进行加载一样。

也相关;x86 是否可以重新排序具有更广泛负载的狭窄存储以完全包含它?是存储缓冲区 + 存储转发违反通常的内存排序规则的特定示例。请参阅Linus Torvalds 的邮件列表帖子集合,解释存储转发对内存排序的影响(以及它如何意味着建议的锁定方案不起作用)。


如果完全没有一致性,您将如何以原子方式递增共享计数器,或实现其他对于实现锁或直接在无锁代码中使用必不可少的原子读-修改-写操作。(参见Can num++ be atomic for 'int num'?)。

lock add [shared_counter], 1同时在多个线程中永远不会丢失实际 x86 的任何计数,因为lock前缀使核心保持缓存行的独占所有权,直到存储提交到 L1d(因此变得全局可见)。

没有一致缓存的系统会让每个线程增加自己的共享计数器副本,然后内存中的最终值将来自最后刷新该行的线程。

即使在发生其他加载/存储时,允许不同的缓存长期保存同一行的冲突数据,并且跨越内存障碍,将允许各种奇怪的事情。

这也违反了纯存储立即对其他内核可见的假设。如果您根本没有一致性,那么核心可以继续使用共享变量的缓存副本。因此,如果您希望读者注意到更新,则必须clflush在每次读取共享变量之前进行,这会使常见情况变得昂贵(当您上次检查后没有人修改数据时)。

MESI 就像一个推送通知系统,而不是强迫每个读者在每次阅读时重新验证他们的缓存。

在未修改共享数据结构的情况下,MESI(或一般的一致性)允许诸如 RCU(读取-复制-更新)之类的读取器的开销为零(与单线程相比)。请参阅https://lwn.net/Articles/262464/https://en.wikipedia.org/wiki/Read-copy-update。基本思想是,作者不是锁定数据结构,而是复制整个事物,修改副本,然后更新共享指针以指向新版本。所以读者总是完全无需等待;他们只是取消引用一个(原子)指针,并且数据在他们的 L1d 缓存中保持热状态。


硬件支持的一致性非常有价值,几乎每个共享内存 SMP 架构都使用它。即使是内存排序规则比 x86 弱得多的 ISA,如 PowerPC,也使用 MESI。

于 2018-04-16T00:09:35.313 回答