在x86架构上存在的内存屏障 - 但这一点通常适用 - 不仅保证在执行任何后续的加载或存储之前,所有先前的加载或存储都已完成 - 它们还保证了存储已经变得“全局可见”。
所谓全局可见是指其他具有缓存感知能力的代理(如其他CPU)可以看到该存储。
对于不了解缓存的其他代理(如DMA设备),如果目标内存被标记为不强制立即写入内存的缓存类型,则通常不会看到该存储。
这与屏障本身无关,这是x86架构的一个简单事实:缓存对程序员是可见的,并且在处理硬件时它们通常是禁用的。
英特尔在屏障的描述上故意保持通用性,因为它不想将自己与特定的实现绑定在一起。
你需要以抽象的方式思考:全局可见意味着硬件将采取所有必要的步骤使存储全局可见。就这样。
然而,要理解屏障,值得看一下当前的实现。
请注意,英特尔可以随意颠倒现代实现,只要保持可见行为正确即可。
一个存储在x86 CPU中的指令在核心中执行,然后放置在“存储缓冲区”中。
例如,
mov DWORD [eax+ebx*2+4], ecx
,一旦解码完成,会等待
eax
、
ebx
和
ecx
准备就绪
2,然后被发送到能够计算其地址的执行单元中。
当执行完成后,存储变成了一个配对的形式
(地址, 值),并被移动到“存储缓冲区”中。
这个存储被称为在核心中“局部完成”。
“存储缓冲区”允许CPU的乱序执行部分忘记该存储,并将其视为已完成,即使尚未尝试写入。
在特定事件发生时,比如序列化事件、异常、执行“屏障”或缓冲区耗尽时,CPU会刷新存储缓冲区。
刷新总是按顺序进行 - 先进先写。
从存储缓冲区进入缓存领域。
如果目标地址标记为WC缓存类型,则可以将其合并到另一个称为
写组合缓冲区的缓冲区中(然后通过绕过缓存写入内存),如果缓存类型是WB或WT,则可以将其写入L1D缓存、L2、L3或LLC,如果不是前面提到的缓存类型,则也可以直接写入内存,如果缓存类型是UC或WT。
今天这就是成为全球可见的意义:离开存储缓冲区。
请注意两个非常重要的事情:
- 缓存类型仍然会影响可见性。
全球可见并不意味着在内存中可见,而是表示其他核心加载时可见。
如果内存区域是WB(写回)可缓存的,则加载可能会进入缓存,因此在那里是全球可见的,但只有知道缓存存在的代理能够看到它。(但请注意,现代x86上的大多数直接内存访问(DMA)都是具有缓存一致性的)。
- 这也适用于非一致的WC(写组合)缓冲区。
WC不保持一致性 - 它的目的是合并对内存区域的存储,其中顺序无关紧要,比如帧缓冲区。 这实际上还不是真正的全球可见,只有在刷新写组合缓冲区后,核外的任何东西才能看到它。
sfence
正是这样做的:等待所有先前的存储在本地完成,然后清空存储缓冲区。
由于存储缓冲区中的每个存储都有可能丢失,你可以看到这种指令有多重。 (但乱序执行,包括后续加载,可以继续进行。只有mfence
会阻止后续加载在存储缓冲区提交到缓存之后全局可见(从L1d缓存读取))。
但是,sfence
是否等待存储传播到其他缓存?
实际上不等待。
因为没有传播-让我们从高级角度来看看对缓存的写入意味着什么。
缓存通过MESI协议(对于多插槽Intel系统为MESIF,对于AMD系统为MOESI)在所有处理器之间保持一致。
我们将只看到MESI。
假设写入索引缓存行L,并且假设所有处理器在其缓存中具有相同值的该行L。
该行的状态是共享,在每个CPU中均如此。
当我们的存储器进入缓存时,L被标记为Modified,并在内部总线上进行特殊事务(或对于多插槽Intel系统,使用QPI)以使其他处理器中的线路L无效。
如果L最初不处于S状态,则协议相应地改变(例如,如果L处于Exclusive状态,则不执行总线上的事务[1])。
此时,写入操作完成,并完成sfence
。
这足以保持缓存一致。
当另一个CPU请求线路L时,我们的CPU监听该请求,并将L刷新到内存或内部总线,以便其他CPU可以读取更新后的版本。
L的状态再次设置为S。
因此,基本上是按需读取L - 这是有道理的,因为向其他CPU传播写入操作是昂贵的,而某些体系结构通过将L写回内存来实现(这有效,因为其他CPU的L处于Invalid状态,因此必须从内存中读取它)。
终于,不是说
sfence
等通常没有用,相反它们非常有用。
只是通常我们不关心其他CPU如何看待我们进行存储操作 - 但是在没有像C++中定义的
获取语义(并通过栅栏实现)的情况下获得锁定是完全不可理喻的。
你应该按照Intel所说的来考虑这些屏障:它们强制执行内存访问的全局可见性顺序。
你可以通过将屏障视为强制写入缓存的顺序来帮助自己理解。缓存一致性将确保对缓存的写入是全局可见的。
我不禁再次强调缓存一致性、全局可见性和内存排序是三个不同的概念。
第一个保证第二个,而第三个则强制执行这种保证。
Memory ordering -- enforces --> Global visibility -- needs -> Cache coherency
'.______________________________'_____________.' '
Architectural ' '
'._______________________________________.'
micro-architectural
脚注:
1. 按程序顺序。
2. 这只是一个简化。在英特尔CPU上,
mov [eax+ebx*2+4], ecx
解码为两个单独的微操作:存储地址和存储数据。存储地址微操作必须等待
eax
和
ebx
就绪,然后被分派到能够计算其地址的执行单元。该执行单元
将地址写入存储缓冲区,以便稍后的加载(按程序顺序)可以进行存储转发检查。
当
ecx
就绪时,存储数据微操作可以分派到存储数据端口,并将数据写入相同的存储缓冲区条目。
这可能发生在地址已知之前或之后,因为存储缓冲区条目按照程序顺序保留,所以存储缓冲区(也称为内存顺序缓冲区)可以跟踪加载/存储排序,一旦所有内容的地址最终确定,并检查是否有重叠。(对于那些最终违反了x86内存排序规则的推测性加载,如果另一个核心在它们被架构允许加载的最早时间点之前使其缓存行无效,就会导致
内存顺序错误的流水线清除。)
lock
来等待它并使其正确分发。如果没有锁定,则线程B可能会看到部分修改,甚至部分覆盖x
(甚至完全覆盖它或查看旧值)。如果两个线程都写入变量而没有锁定w / barrier,则内存屏障变体在我看来也无法帮助您,当每个线程将其不同部分写入时,您仍然可能最终得到两个线程的组合值。 - Ped7g线程A
解锁互斥锁时,解锁代码是否包含内存屏障,以确保其他核心的缓存在实际使互斥锁可用于其他线程锁定之前已被更新?因此,在线程B
锁定互斥锁时,线程B
可以确信线程A
对全局变量所做的所有修改都将被线程B
看到? - Christopher