assembly 内存屏障是否确保该高速缓存一致性已完成?

tf7tbtn2  于 2023-10-19  发布在  其他
关注(0)|答案(4)|浏览(94)

假设我有两个线程操作全局变量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的缓存中(因此指令没有被放置在队列或稍后执行的东西中)。
4ngedf3f

4ngedf3f1#

x86体系结构上存在的内存屏障(但这通常是正确的)不仅保证所有先前的1加载或存储在任何后续加载或存储执行之前完成,而且还保证存储已成为全局可见
全局可见意味着其他缓存感知代理(如其他CPU)可以看到存储。
其他不知道缓存的代理(如DMA设备)通常不会看到存储,如果目标内存已标记为不强制立即写入内存的缓存类型。
这与屏障本身无关,这是x86架构的一个简单事实:高速缓存对于程序员是可见的,并且当处理硬件时,它们通常被禁用。
英特尔故意对障碍的描述是通用的,因为它不想把自己绑在一个特定的实现上。
你需要抽象地思考:全局可见意味着硬件将采取所有必要的步骤来使存储全局可见。期
然而,为了理解这些障碍,有必要看看当前的实现。
请注意,英特尔可以自由地将现代实现颠倒过来,只要它保持可见的行为正确。
x86 CPU中的存储在核心中执行,然后放置在 * 存储缓冲区 * 中。
例如mov DWORD [eax+ebx*2+4], ecx,一旦解码就停止,直到eaxebxecx就绪2,然后将其分派到能够计算其地址的执行单元。
当执行完成时,存储已成为一对 (地址,值),被移动到 * 存储缓冲区 *。
商店据说是 * 在本地完成 *(在核心)。
存储缓冲区允许CPU的乱序执行部分忘记存储并认为它已经完成,即使还没有尝试写入。
在特定事件发生时,如序列化事件、异常、执行 * 屏障 * 或缓冲区耗尽,CPU会刷新存储缓冲区。
刷新总是按顺序进行-先入先写。
存储从存储缓冲区进入该高速缓存的域。
如果目标地址被标记为WC缓存类型,则可以将其组合到另一个名为 Write Combining buffer 的缓冲区中(并且稍后绕过缓存写入内存中),如果缓存类型为WB或WT,则可以将其写入L1 D缓存、L2、L3或LLC(如果它不是先前的缓存之一)。
如果该高速缓存类型为UC或WT,也可以直接将其写入内存。

今天,这就是成为全球可见的含义:离开存储缓冲区。

注意两件非常重要的事情:
1.该高速缓存类型仍然会影响可见性。
全局可见并不意味着在内存中可见,它意味着在其他内核的加载中可见。
如果内存区域是WB可缓存的,则加载可以在该高速缓存中结束,因此它在那里是全局可见的-仅对于意识到该高速缓存存在的代理。(但请注意,现代x86上的大多数DMA都是缓存一致的)。
1.这也适用于非相干的WC缓冲器。
WC不保持一致性-其目的是将存储合并到顺序无关紧要的内存区域,如帧缓冲区。这还不是真正全局可见的,只有在写组合缓冲区被刷新后,核心外部的任何东西才能看到它。
sfence就是这样做的:等待所有先前的存储在本地完成,然后耗尽存储缓冲区。
由于存储缓冲区中的每个存储都有可能丢失,因此您可以看到这样的指令有多么繁重。(但包括稍后加载的乱序执行可以继续。只有mfence会阻止以后的加载全局可见(从L1 d缓存阅读),直到存储缓冲区完成提交到缓存之后。)
但是sfence会等待存储传播到其他缓存中吗?
嗯,不。
因为没有传播-让我们从高级的Angular 来看看写入该高速缓存意味着什么。
该高速缓存通过MESI协议(MESIF用于多插槽Intel系统,MOESI用于AMD系统)在所有处理器之间保持一致性。
我们只会看到MESI。
假设写操作索引了该高速缓存行L,并假设所有处理器的高速缓存中都有相同值的该行L。
这一行的状态是 Shared,在每个CPU中。
当我们的存储到达该高速缓存时,L被标记为 Modified,并在内部总线(或多插槽英特尔系统的QPI)上进行特殊事务,以使其他处理器中的L行无效。
如果L最初不处于 S 状态,则相应地改变协议(例如,如果 L 处于状态 Exclusive,则不进行总线上的事务[1])。
此时写入完成,sfence完成。
这足以使该高速缓存保持一致。
当另一个CPU请求行L时,我们的CPU会监听该请求,L会被刷新到内存或内部总线,以便另一个CPU读取更新的版本。
L的状态再次被设置为 S

因此,基本上L是按需读取的-这是有意义的,因为将写入传播到其他CPU是昂贵的,并且一些架构通过将L写回内存来实现这一点(这是可行的,因为其他CPU的L处于 * 无效 * 状态,因此它必须从内存中读取它)。
最后,并不是说sfence等都是无用的,相反,它们非常有用。
只是通常我们不关心其他CPU如何看待我们的存储-但是在没有 * 获取语义 * 的情况下获取锁,例如,在C++中定义,并使用围栏实现,这完全是疯了。
你应该像英特尔所说的那样思考这些障碍:它们强制执行存储器访问的全局可见性的顺序。
您可以通过将屏障视为强制执行命令或写入该高速缓存来帮助自己理解这一点。然后,该高速缓存一致性将确保对高速缓存的写入是全局可见的。
我不得不再次强调,缓存一致性、全局可见性和内存排序是三个不同的概念。
第一个保证了第二个,这是由第三个强制执行。

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

脚注:
1.按节目顺序
1.这是一种简化。在Intel CPU上,mov [eax+ebx*2+4], ecx解码为两个独立的uop:存储地址和存储数据。存储地址uop必须等到eaxebx准备好,然后才能被分派到能够计算其地址的执行单元。该执行单元writes the address into the store buffer,因此稍后的加载(按程序顺序)可以检查存储转发。
ecx准备就绪时,存储数据uop可以分派到存储数据端口,并将数据写入同一存储缓冲区条目。
这可以发生在地址已知之前或之后,因为存储缓冲区条目可能是按程序顺序保留的,因此一旦最终知道所有内容的地址,存储缓冲区(又名内存顺序缓冲区)可以跟踪加载/存储顺序,并检查重叠。(对于那些最终违反x86内存排序规则的推测性加载,如果另一个核心在架构上允许加载的最早点之前使它们加载的该高速缓存行无效。这导致a memory-order mis-speculation pipeline clear。)

7ivaypg9

7ivaypg92#

现在,当执行将x设置为5时,x的缓存值将被设置为5,这将导致缓存一致性协议采取行动并使用新的x值更新其他核心的缓存。
有多个不同的x86 CPU具有不同的缓存一致性协议(无,MESI,MOESI),以及不同类型的缓存(未缓存,写组合,只写,直写,回写)。
一般来说,当正在进行写入时(当将x设置为5时),CPU确定正在进行的高速缓存的类型(从MTRR或TLB),并且如果可以高速缓存该高速缓存行,则CPU检查其自己的高速缓存以确定该高速缓存行处于什么状态(从其自己的Angular )。
然后,缓存的类型和该高速缓存行的状态被用来确定数据是否直接写入物理地址空间(绕过缓存),或者是否必须从其他地方获取该高速缓存行,同时告诉其他CPU使旧副本无效,或者它是否在自己的缓存中具有独占访问权,并且可以在该高速缓存中修改它而不告诉任何东西。
CPU从不将数据“注入”到另一个CPU的缓存中(并且只告诉其他CPU使缓存行的副本无效/丢弃)。告诉其他CPU使其缓存行的副本无效/丢弃会导致它们在需要时再次获取当前副本。
请注意,这些都与记忆障碍无关。
有3种类型的内存屏障(sfencelfencemfence),它们告诉CPU在允许以后的存储,加载或两者发生之前完成存储,加载或两者。因为CPU通常是高速缓存一致的,所以这些内存屏障/围栏通常是无意义/不必要的。然而,存在CPU不是高速缓存一致性的情况(包括“存储转发”,当使用写组合高速缓存类型时,当使用非临时存储时,等等)。对于这些特殊/罕见的情况,需要内存屏障/围栏来强制排序(如果必要)。

iqjalb3h

iqjalb3h3#

,内存屏障并不能确保缓存一致性已经“完成”。它通常不涉及任何相干操作 *,并且可以推测地或作为无操作执行。

它只强制执行屏障中描述的排序语义。例如,一个实现可能只是在存储队列中放置一个标记,这样存储到加载的转发就不会发生在比标记更早的存储上。
特别是英特尔,已经有了一个强大的内存模型用于正常的加载和存储(编译器生成的那种,你会在汇编中使用),其中唯一可能的重新排序是稍后的加载传递先前的存储。在DRAM内存屏障的术语中,除了StoreLoad之外的每个屏障都已经是no-op
在实践中,x86上的 interesting barrier被附加到LOCKed指令上,并且执行这样的指令根本不一定涉及任何缓存一致性。如果行已经处于独占状态,CPU可以简单地执行指令,确保在操作进行时(即在读取参数和回写结果之间)不释放行的独占状态,然后只处理防止存储到加载转发破坏LOCK指令的总顺序。目前,它们通过清空存储队列来实现这一点,但在未来的处理器中,即使这样做也可能是推测性的。
内存屏障或屏障+op所做的是确保操作以遵守屏障所有限制的相对顺序被其他代理看到。这当然不会像你问的那样,把结果推到其他CPU上作为一个一致性操作。

lhcgjxsq

lhcgjxsq4#

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

相关问题