单核环境下高速缓存被首先改写而导致cache与主存不一致问题的解决方案,简单来说有两种方法:通写法和回写法。
通写法(Write Through):
即CPU在对cache写入数据时,同时也直接写入主存,这样就能使得主存和cache中的数据始终保存一致。
通写法的优点是简单,硬件上容易实现,同时在调度缓存单元时不会有脏数据,调度速度快;缺点是每次cache写入操作时都增加了写主存的等待时间,效率较低。
回写法(Write Back):
回写法和通写法的主要区别在于,回写法在CPU写cache时并不实时同步写主存,而是在进行调度被覆盖前整体的写回主存。如果被调度出的cache单元并没有被写入过,则直接被覆盖无需写回主存。
回写法的优点是写入cache时无需同步主存,总体效率比通写法高。缺点是硬件实现较为复杂。
通常多核并行架构的CPU,每个核虽然都独自工作,但与外部存储器的交互依然是共用同一总线进行的。通过总线,每个核心都能够监听、接收到来自其它核心的消息通知,这一机制被称为总线侦听或是总线嗅探。
基于总线侦听的写传播:
每个核心在对自己独有的高速缓存行进行修改时,需要将修改通知送至总线进行广播。其它核心在监听到总线上来自其它核心的远程写通知时,需要查询本地高速缓存中是否存在同样内存位置的数据。如果存在,需要选择将其设置为失效状态或是更新为最新的值。
基于总线侦听的写串行化:
总线上任意时间只能出现一个核的一个写通知消息。多个核心并发的写事件会通过总线仲裁机制将其转换为串行化的写事件序列(可以简单理解为逻辑上的一个FIFO事件队列),在每个写事件广播时,必须得到每个核心对事件的响应后,才进行下一个事件的处理,这一机制被称作总线事务。
而本文的主角MESI协议便是基于总线侦听机制,采用回写法、写传播失效策略的高速缓存一致性协议,其另一个更精确的名称是四态缓存写回无效协议。
通过MESI协议,在强制串行化的总线事务帮助下能够始终保持一个全局高速缓存一致的稳定状态。
MESI协议依赖总线侦听机制,在某个核心发生本地写事件时,为了保证全局只能有一份缓存数据,要求其它核对应的缓存行统统设置为Invalid无效状态。为了确保总线写事务的强一致性,发生本地写的高速缓存需要等到远端的所有核心都处理完对应的失效缓存行,返回Ack确认消息后才能继续执行下面的内存寻址指令(阻塞)。
原始MESI协议实现时的性能问题:
1.对于进行本地写事件的核心,远端核心处理失效并进行响应确认相对处理器自身的指令执行速度来说是相当耗时的,在等待所有核心响应的过程中令处理器空转效率并不高。
2.对于响应远程写事件的核心,在其高速缓存压力很大时,要求实时的处理失效事件也存在一定的困难,会有一定的延迟。
不进行优化的MESI协议在实际工作中效率会非常的低下,因此CPU的设计者在实现时对MESI协议进行了一定的改良。
针对上述本地写事件需要等待远端核心ACK确认,阻塞本地处理器的问题,引入了存储缓存机制。
存储缓存是属于每个CPU核心的。当使用了存储缓存后,每当发生本地写事件时,本地核心不再阻塞的等待远程核的确认响应,而是将写入的新值放入存储缓存中,继续执行后面的指令。存储缓存会替处理器接受远端核心的ACK确认,当对应本地写事件广播得到了全部远程核心的确认后,再提交事务,将其新值写入本地高速缓存中。存储缓存的大小是十分有限的,当堆积的事务满了之后,依然会阻塞CPU,直到有事务提交释放出新的空间。
存储缓存的引入将本地写事件—>等待远程写通知确认消息并提交这一事务,从同步、强一致性变成了异步、最终一致性,提高了本地写事件的处理效率。
本地处理器在进行本地读事件时,由于可能存储缓存中新修改的数据还未提交到本地缓存中,这就会造成一个核心内,对于同一缓存行其后续指令的读操作无法读取到之前写操作的最新值。为此,在进行本地读操作时,处理器会先在存储缓存中查询对应记录是否存在,如果存在则会从存储缓存中直接获取,这一机制被称为Store Fowarding。
针对上述远端核心响应远程写事件,实时的将对应缓存行设置为Invalid无效状态延迟高的问题,引入了失效队列机制。
失效队列同样是属于每个CPU核心的。当使用了失效队列后,每当监听到远程写事件时,对应的高速缓存不再同步的处理失效缓存行后返回ACK确认信息,而是将失效通知存入失效队列,立即返回ACK确认消息。对于失效队列中的写失效通知,会在空闲时逐步的进行处理,将对应的高速缓存中的缓存行设置为无效。失效队列的引入在很大程度上缓解了存储缓存空间有限,容易阻塞的问题。
失效队列的引入将监听到远程写事件处理失效缓存行—>返回ACK确认消息这一事务,从同步、强一致性变成了异步、最终一致性,提高了远程写事件的处理效率。
存储缓存和失效队列的引入在提升MESI协议实现的性能同时,也带来了一些问题。由于MESI的高速缓存一致性是建立在强一致性的总线串行事务上的,而存储缓存和失效队列将事务的强一致性弱化为了最终一致性,使得在一些临界点上全局的高速缓存中的数据并不是完全一致的。
对于一般的缓存数据,基于异步最终一致的缓存间数据同步不是大问题。但对于并发程序,多核高速缓存间短暂的不一致将会影响共享数据的可见性,使得并发程序的正确性无法得到可靠保证,这是十分致命的。但CPU在执行指令时,缺失了太多的上下文信息,无法识别出缓存中的内存数据是否是并发程序的共享变量,是否需要舍弃性能进行强一致性的同步。
CPU的设计者提供了内存屏障机制将对共享变量读写的高速缓存的强一致性控制权交给了程序的编写者或者编译器。
内存屏障分为读屏障和写屏障两种,内存屏障以机器指令的形式进行工作。
写屏障用于保证高速缓存间写事务的强一致性。当CPU执行写屏障指令时,必须强制等待存储缓存中的写事务全部处理完再继续执行后面的指令。相当于将存储缓存中异步处理的本地写事务做了强一致的同步。
写屏障指令执行完后,当前核心位于写屏障执行前的本地写事务全部处理完毕,其它的核心都已经接收到了当前所有的远程写事件的写无效通知。
读屏障用于保证高速缓存间读事务的强一致性。当CPU执行读屏障指令时,必须先将当前处于失效队列中的写无效事务全部处理完,再继续的执行读屏障后面的指令。相当于将异步队列中异步处理的远程写事务做了强一致的同步。
读屏障指令执行完后,当前核心位于读屏障执行前的远程写无效事务全部处理完毕,对于读屏障之后的共享数据读取会得到最新的值。
在进行并发程序的开发时,针对关键的任务间共享变量的读写需要使用内存屏障保证其在多核间高速缓存的一致性。在对共享变量的写入指令后,加入写屏障,令新的数据立即对其它核心可见;在对共享变量的读取指令前,加入读屏障,令其能获取最新的共享变量值。
通过在指令中的适当位置加入读/写内存屏障,虽然一定程度上降低了效率,但保证了并发程序在多核高速缓存条件下对于共享变量的可见性,是一个很好的折中解决方案。
https://www.cnblogs.com/xiaoxiongcanguan/p/13184801.html