内存屏障是否确保缓存一致性已经完成?

Does a memory barrier ensure that the cache coherence has been completed?

假设我有两个线程操作全局变量x。每个线程(或者我想是每个核心)都会有一个 x.

的缓存副本

现在说Thread A执行以下指令:

set x to 5
some other instruction

现在当set x to 5被执行时,x的缓存值将被设置为5,这将导致缓存一致性协议起作用并更新另一个的缓存新值 x.

的核心数

现在我的问题是:当 xThread A 的缓存中实际设置为 5 时,其他核心的缓存是否在 some other instruction 之前更新被执行?还是应该使用内存屏障来确保?:

set x to 5
memory barrier
some other instruction

注:假设指令是按顺序执行的,也假设当set x to 5执行时,5立即执行放置在线程 A 的缓存中(因此指令未放置在队列中或稍后执行的内容中)。

Now when set x to 5 is executed, the cached value of x will be set to 5, this will cause the cache coherence protocol to act and update the caches of the other cores with the new value of x.

有多个不同的 x86 CPUs 具有不同的缓存一致性协议(none、MESI、MOESI),以及不同类型的缓存(未缓存、写组合、只写、写-通过,回写)。

通常,当写入完成时(将 x 设置为 5 时),CPU 确定正在完成的缓存类型(来自 MTRRs 或 TLBs),如果缓存行可以被缓存,它会检查它自己的缓存来确定缓存行处于什么状态(从它自己的角度来看)。

然后根据缓存的类型和缓存行的状态来判断数据是直接写入物理地址space(绕过缓存),还是必须去取缓存行从其他地方同时告诉其他 CPUs 使旧副本无效,或者如果它在自己的缓存中具有独占访问权限并且可以在缓存中修改它而无需告诉任何东西。

A CPU 永远不会 "injects" 数据进入另一个 CPU 的缓存(并且只告诉其他 CPU 到 invalidate/discard 他们的缓存副本线)。告诉其他 CPU 到 invalidate/discard 他们的缓存行副本会导致他们获取它的当前副本 if/when 他们再次需要它。

请注意,其中 none 与内存障碍有关。

有 3 种类型的内存屏障(sfencelfencemfence),它们告诉 CPU 在允许以后完成存储、加载或两者兼而有之商店,负载或两者都发生。因为 CPU 通常是缓存一致的,所以这些内存 barriers/fences 通常是 pointless/unnecessary。然而,在某些情况下 CPU 缓存不一致(包括 "store forwarding",当使用写组合缓存类型时,当使用非临时存储时,等等)。需要内存 barriers/fences 来强制对这些 special/rare 个案例进行排序(如有必要)。

x86 架构上存在的内存屏障 - 但通常情况下是这样 - 不仅保证所有先前的1 加载或存储在任何后续加载之前完成或商店被执行 - 他们还保证商店已成为全球可见

全局可见意味着其他缓存感知代理 - 像其他 CPUs - 可以看到商店。
如果目标内存已标记为不强制立即写入内存的缓存类型,则其他不知道缓存的代理(如支持 DMA 的设备)通常不会看到存储。
这与屏障本身无关,这是 x86 架构的一个简单事实:程序员可以看到缓存,并且在处理硬件时它们通常被禁用。

英特尔有意对障碍进行通用描述,因为它不想将自己与特定实现联系起来。
您需要抽象地思考:全局可见意味着硬件将采取所有必要的步骤来使商店全局可见。期间。

然而,要了解这些障碍,值得看看当前的实施。
请注意,只要保持可见行为正确,英特尔就可以随意颠倒现代实施。

x86 中的存储 CPU 在核心中执行,然后放入 存储缓冲区
例如 mov DWORD [eax+ebx*2+4], ecx,一旦解码停止,直到 eaxebxecx 准备好 2,然后它被分派到执行能够计算其地址的单元。
执行完成后,存储已成为一对 (address, value) 并被移入 store buffer.
据说这家商店 在本地完成 (在核心)。

存储缓冲区允许 CPU 的 OoO 部分忘记存储并认为它已完成,即使尚未尝试写入也是如此。

发生特定事件时,如序列化事件、异常、屏障 的执行或缓冲区耗尽,CPU 会刷新存储缓冲区。
同花顺总是按顺序 - 先进先出。

存储从存储缓冲区进入缓存领域。
它可以组合到另一个称为 Write Combining buffer 的缓冲区(然后绕过缓存写入内存)如果目标地址被标记为 WC 缓存类型,它可以是如果缓存类型为WB或WT,则写入L1D缓存,L2,L3或LLC(如果不是前一个)。
如果缓存类型是UC或者WT,也可以直接写入内存


今天这就是全球可见的含义:离开商店缓冲区。
注意两件非常重要的事情:

  1. 缓存类型仍然影响可见性。
    全局可见并不意味着在内存中可见,它意味着在来自其他内核的负载将看到它的地方可见。
    如果内存区域是 WB 可缓存的,则加载可能会在缓存中结束,因此它在那里是全局可见的 - 仅对于知道缓存存在的代理。 (但请注意,现代 x86 上的大多数 DMA 都是缓存一致的)。
  2. 这也适用于非相干的 WC 缓冲区。
    WC 没有保持连贯——它的目的是将存储合并到顺序无关紧要的内存区域,如帧缓冲区。这还不是真正全局可见的,只有在写入组合缓冲区被刷新后,核心之外的任何东西才能看到它。

sfence 正是这样做的:等待所有先前的存储在本地完成,然后清空存储缓冲区。
由于存储缓冲区中的每个存储都可能丢失,因此您会看到这样的指令有多么繁重。 (但是乱序执行包括后面的加载可以继续。只有 mfence 会阻止后面的加载全局可见(从 L1d 缓存读取)直到存储缓冲区完成提交到缓存。)

但是 sfence 是否等待存储传播到其他缓存中?
嗯,没有。
因为没有传播——让我们从高层次的角度看看写入缓存意味着什么。

使用 MESI 协议(MESIF 用于多路 Intel 系统,MOESI 用于 AMD 系统),缓存在所有处理器之间保持一致。
我们只会看到MESI。

假设写入索引缓存行 L,并假设所有处理器在其缓存中都具有相同值的这一行 L。
在每个 CPU 中,此行的状态为 Shared

当我们的商店登陆缓存时,L 被标记为 已修改 并且在内部总线(或多插槽英特尔系统的 QPI)上进行特殊事务以使无效其他处理器中的 L 行。

如果 L 最初不处于 S 状态,则协议会相应更改(例如,如果 L 处于状态 独占总线上没有交易完成[1]).

至此写入完成,sfence完成。

这足以保持缓存的一致性。
当另一个 CPU 请求行 L 时,我们的 CPU 侦听该请求并将 L 刷新到内存或内部总线,以便另一个 CPU 将读取更新的版本。
L的状态再次设置为S

所以基本上 L 是按需读取的 - 这是有道理的,因为将写入传播到其他 CPU 是昂贵的,并且一些架构通过将 L 写回内存来实现(这是有效的,因为另一个 CPU 的 L 处于 Invalid 状态,因此它必须从内存中读取)。


最后,sfence 等并不是一般情况下都没有用,相反,它们非常有用。
只是通常我们不关心其他CPUs如何看待我们创建我们的商店——而是在没有获取语义的情况下获取锁,例如在C++中定义的,并用栅栏实现,完全是疯了。

您应该像 Intel 所说的那样考虑障碍:它们强制执行内存访问的全局可见性顺序。
您可以通过将障碍视为执行顺序或写入缓存来帮助您自己理解这一点。然后缓存一致性将确保对缓存的写入是全局可见的。

忍不住再强调一次,缓存一致性、全局可见性和内存排序是三个不同的概念。
第一个保证第二个,由第三个强制执行。

Memory ordering -- enforces --> Global visibility -- needs -> Cache coherency
'.______________________________'_____________.'                            '
                 Architectural  '                                           '
                                 '._______________________________________.'
                                             micro-architectural

脚注:

  1. 按节目顺序。
  2. 这是一种简化。在 Intel CPUs 上,mov [eax+ebx*2+4], ecx 解码为两个独立的微指令:存储地址和存储数据。存储地址 uop 必须等到 eaxebx 准备就绪,然后它被分派到能够计算其地址的执行单元。该执行单元 writes the address into the store buffer,因此以后的加载(按程序顺序)可以检查存储转发。

    ecx就绪时,store-data uop可以分派到store-data端口,并将数据写入同一个store buffer entry。

    这可能发生在地址已知之前或之后,因为存储缓冲区条目可能是按程序顺序保留的,因此存储缓冲区(也称为内存顺序缓冲区)可以跟踪地址后的加载/存储顺序一切最终都是已知的,并检查重叠。 (对于最终违反 x86 内存排序规则的推测性加载,如果另一个内核使它们在架构上允许加载的最早点之前加载的缓存行无效。这导致 。)

,内存屏障不能确保缓存一致性 "completed"。它通常 根本不涉及任何一致性操作 并且可以推测性地执行或作为空操作执行。

它只强制执行屏障中描述的排序语义。例如,一个实现可能只是在存储队列中放置一个标记,这样存储到加载的转发就不会发生在比该标记更早的存储上。

尤其是英特尔,已经为正常的加载和存储(编译器生成的类型和您在汇编中使用的类型)提供了强大的内存模型,其中唯一可能的重新排序是稍后加载通过较早的存储。在 SPARC 内存屏障的术语中,除 StoreLoad 之外的每个屏障都已经是

实际上,x86 上的 有趣 障碍附加到 LOCKed 指令,并且执行此类指令根本不需要任何缓存一致性.如果该行已经处于独占状态,CPU 可以简单地执行指令,确保在操作进行时不释放该行的独占状态(即,在读取参数和写回之间)结果的),然后只处理防止存储到加载转发破坏 LOCK 指令附带的总顺序。目前他们通过清空存储队列来做到这一点,但在未来的处理器中,即使这样也可能是推测性的。

内存屏障或屏障+op 的作用是确保操作以遵守屏障所有限制的相对顺序被其他代理看到。当然,这通常不涉及 将结果推送 到其他 CPU 作为你所暗示的连贯性操作。

如果没有其他处理器的缓存中有 X,则在处理器 A 上执行 x=5 不会更新任何其他处理器中的缓存。如果处理器 B 读取变量 X,处理器 A 将检测到该读取(这称为侦听)并在总线上为处理器 B 提供数据 5。现在处理器 B 的缓存中将具有值 5。如果没有其他处理器读取变量 X,那么它们的缓存将永远不会用新值 5 更新。