1. 为何需要cache一致性
访问memory数据的速度相比core的运行速度来说,要花费更多的时钟周期,为了减轻这个差异引进了存储器层次结构,如图1所示。在层次结构中,越往上,读写速度越快,价格更贵,存储容量也越小。
图1 存储器结构层次
随着摩尔定律的发展,人们试图增加CPU的core数以提升系统整体性能,这类系统称之为多core系统。一个包含多core的系统中,每个core可能都有自己的私有cache,并且有可能会对相同的memory地址进行修改。如果多个core同时对同一memory地址进行读写操作,则很可能会导致cache内容不一致,从而导致数据不一致,最终造成系统出错和崩溃。因此,在设计多core系统时,必须考虑cache一致性(cache coherency)问题来确保数据的正确性和系统的稳定性。举个例子,假设2个core的系统,每个core私有的L1 cache的cacheline大小是64Bytes。两个core都读取0x80地址数据,导致0x80开始的64Bytes数据都分别加载到core0和core1的私有L1 cache中。
图2 两core系统读取0x80地址的数据
core0对0x80执行写操作,写入值为0x01。Core0的私有L1 cache更新对应cacheline的值。然后,core1读取0x80的数据,core1发现命中自己私有L1 cache,然后返回0x00值,并不是core0写入的0x01值,这就造成了core0和core1的私有L1 cache数据不一致现象。
图3 core0更新0x80地址的数据
按照正确的处理流程,可以使用以下两种方法保证多core cache的一致性:
- Core0修改0x80地址数据的时候,除了更新core0的私有cache之外,还应将修改的数据通知到core1的私有cache去更新0x80地址的数据。
- Core0修改0x80地址数据的时候,除了更新core0的私有cache之外,还应通知core1的私有cache将0x80地址所在的cacheline置为无效。保证core1读取数据时不会命中自己的cache。这样core1就必须重新从core0或memory中去读取0x80地址最新的数据。
Cache一致性可以通过软件和硬件来解决,通常来说,软件维护cache一致性维护成本太高,会导致性能下降。现在的实现方案基本都是使用硬件来自动维护多核cache的一致性,并且对软件和程序员来说是透明的。因此,本文只介绍硬件维护cache一致性。硬件维护cache一致性需要制定一致性协议,一致性协议是由系统内的各个分布式硬件参与者组件共同实现和维护的一组规则。一致性协议有许多版本,Arm的ACE(AXI Coherency Extensions)和CHI(Coherent Hub Interface),AMD的Coherent HyperTransport(HT),Intel的QuickPath Interconnect(QPI)等等。
2. 一致性协议的本质
虽然一致性协议众多,但本质上讲,所有一致性协议都通过将写入的数据传播到所有一致性cache来使一个core的写入对其它core可见。具体来说有两点原则:
- Single-Writer, Multiple-Read(SWMR):在任意时刻,对于任意的memory地址A,只有一个core可以写它(也可以读它),或者多个core读它。因此,永远不会有这样的情况:一个core可以写入memory地址A,而其它core可以同时读取或写入该memory地址A。如下图4所示,可以把时间会切割为无限小的切片,在每个切片中,对于memory地址A,要么单个core具有读写访问权限,要么若干core(可能为0)具有只读访问权限。比如T1和T4时刻,允许多个core对同一个memory进行读取,但在T2和T3种,只能存在1个core拥有读写的权限。
- Data-Value:在时间切片中,上一个时间切片结束的数据值与下一个时间切片开始的数据值相同,即使同一个切片中,每个core读取到的值必须相同。也就是memory地址的数据值需要被正确传播。比如在T1时刻,如果core2和core5可以读取到不同的值,那么就不符合一致性系统。同样的,如果T3的core1没有读到T2的core3最后写的数据值,或者T4的core1/core2/core3没有读到T3的core1最后写的数据值,那么也不符合一致性系统。
图4 不同core读写的时间顺序
对于SWMR还有另一种有意思的方式来解释,对于每个memory地址,存在固定数量的令牌,其数量至少与core数量一样。如果一个core拥有所有的令牌,它就可以写入memory地址,而且core必须至少有1个令牌,才能对memory地址进行读取数据。因此,在任何给定的时刻,当有core正在读写memory地址的数据时,其它core正在写入memory地址是不可能的。
一致性协议的两点原则在设计一致性协议时至关重要。举个比较常用的一致性无效协议(invalidate protocols)的设计方法:1.如果一个core想要读取memory地址A,它会向其它core发送消息,以获取memory地址A当前的最新值,并确保没有其它core缓存该memory地址A的cacheline拥有写权限的状态。2.如果一个core想要写入memory地址A,它会向其它core发送消息以获取memory地址A的当前值,并确保没有其它core缓存该memory地址A的cacheline拥有只读或读写权限的状态,也就是其它core都要无效掉自己缓存中memory地址A的cacheline。
3. 一致性协议的类别
有许多不同的方法来设计一致性协议,这些协议决定了在每个参与一致性的组件控制器上可能拥有哪些cache状态、发送或响应哪些一致性事务(transactions)操作、发生哪些事件(events)以及cache状态转换(transitions)等。
3.1 监听和目录
根据一个core的行为如何通知到其它core的方式可以分为监听协议(Snooping protocol)和目录协议(Directory protocol)。
1.监听协议
监听协议是第一类广泛使用的协议,在最初主导了商业市场,它们现在还有在各种小系统中使用。这种协议比较简单,当cache miss发生时,cache的控制器会发出仲裁请求给共享总线广播它的请求。共享总线确保所有cache控制器以相同的顺序观察到所有请求。监听协议依赖共享总线以一致的顺序向所有cache控制器发送广播消息,因此分布式cache控制器能够正确的更新自己的有限状态机,这些有限状态机共同代表了cache line的状态。
要求以一致的总顺序(total order)观察广播对于实现传统监听的共享总线具有重要意义。因为许多cache控制器可能同时尝试发出一致性请求,共享总线必须将这些请求序列化成某种总顺序(total order)。无论总线如何决定这个顺序,这个机制被称为监听协议的保序点(serialization point或ordering point)。在一般情况下,一个cache控制器发出一个一致性请求,总线在保序点将它排序并广播给所有cache控制器,包括发出请求的cache控制器,因此发出请求的cache控制器可以通过收到的监听请求流来判断它的请求在什么时候被处理了,在总顺序中排在哪个位置。
举个例子,监听协议总线使用仲裁逻辑来确保一次只在总线上广播一个请求,那么这个仲裁逻辑充当保序点,因为它有效地决定了各个一致性请求在总线上出现的顺序。有个微妙的点是,cache控制器的一致性请求是在仲裁逻辑顺序就排好序的,但是cache控制器只能通过监听来观察到这个顺序,进而判断自己请求的排序情况。因此,cache控制器的请求在保序点排好序后,可能会晚几个周期才能被cache控制器观察到请求顺序。
监听协议的latency比目录协议低,而且实现也比较简单,但是不易扩展。
图5 监听协议例子
2.目录协议
目录协议最初是为了解决监听协议缺乏可扩展性的问题而开发的。目录协议会创建一个目录来维护每一个cacheline的一致性状态的全局视图。该目录跟踪各cache保存了哪些cacheline以及处于什么状态。Cache控制器发出的一致性请求将直接单播给目录控制器(通常也称作home)。目录控制器要么直接响应请求,要么根据目录信息将请求转发给一个或多个其它cache控制器,然后由后者响应。目录协议使用间接层来避免有序广播网络和让每个cache控制器处理每个请求。一致性请求传输流程通常包括两个步骤(单播请求,然后是单播响应)或三个步骤(1个单播请求,K个转发请求和K个响应,其中K是共享者的数量)。有些协议甚至还有第四部,因为响应间接通过目录控制器,或者因为请求者在一致性流程完成时通知目录控制器。相比之下,监听协议将一个cacheline的状态分布在所有cache控制器上,由于这种分布式状态没有中央单元记录信息,一致性请求必须被广播到所有cache控制器上。因此监听协议的一致性请求流程总需要两个步骤(广播请求,然后是单播响应)。
在大多数目录协议中,一致性请求在目录控制器处排序,多个cache控制器可以同时向目录控制器发送一致性请求,目录控制器会序列化这些请求并排好序。如果两个请求同时到达目录控制器,目录控制器会选择首先处理哪个请求,第二个处理的请求的命运取决于目录协议和请求的类型。第二个处理的请求可能:(a)在第一个请求之后立即得到处理;(b)暂时保存在目录中等待第一个请求完成;(c)直接发送否定响应。在后一种情况下,目录控制器向请求者发送否定确认响应,请求者必须重新发出其一致性请求。
目录协议在目录控制器上排序,因为大多数目录协议不使用总顺序的广播,没有序列化的全局概念。更确切说,一个一致性请求者相对于可能又副本的所有cache,必须单独序列化。需要其它cache显示的消息通知请求者的请求已被相关cache处理和序列化。比如说,对于想获取写权限的一致性请求,有共享副本的每个cache控制器一旦完成序列化了无效消息,都必须显示地发送确认消息。
目录协议需要更少的总线带宽,实现了更大的可伸缩性,但代价是总线某些一致性请求的latency。
图6 目录协议例子
3.2 无效和更新
根据一个core决定写cache line时,一致性协议中其它core的cache line副本如何处理,可以分为:无效协议(Invalidate protocol)和更新协议(Update protocol)。这个分类的选择与协议是监听还是目录类型无关,它们可以两两交叉,组成4种类型的一致性协议。
1. 无效协议
当一个core需要往某cacheline写入一个值时,cache控制器会启动一个一致性请求来使所有其它cache中对应的cacheline副本无效掉。一旦所有副本都无效掉后,请求者就可以将数据写入该cacheline中,这样就确保其它core不会读取到旧值。如果另一个core希望在其副本无效后读取该cacheline的值,则必须启动一个新的一致性请求来获取该cacheline的数据,并且它将从写该cacheline的core处获取副本,从而保证了数据一致性。
图7 无效协议例子 (1->2->3->4->5->6)
2.更新协议
当一个core需要往某cacheline写入一个值时,它会启动一个一致性请求来更新所有其它cache中的副本,这样所有cache用到的都是新值。
图8 更新协议例子 (1->2->3->4->5->6)
这两种协议的选择需要权衡,更新协议减少了core读取新值的latency,因为core不需要初始化并等待重读一致性请求的完成。更新协议通常要比无效协议消耗更大的带宽,因为更新协议除了传输地址,还要传输新值数据。此外,更新协议会使许多memory一致性模型的实现变得非常复杂。例如,当多个cache必须对一个cacheline的多个副本进行多个更新时,保持写的原子性非常困难。由于更新协议的复杂性,它们很少被实现。
监听和目录与无效和更新,这些可以检查混合搭配,组成四种类型的一致性协议:监听&&无效、监听&&更新、目录&&无效、目录&&更新。Arm的ACE和CHI使用就是目录&&无效组合。
4. 一致性协议的设计
4.1 状态(states)选择
4.1.1 状态的特征
只有一个core的系统中,cache line的状态要么有效要么无效。如果需要区分dirty状态,则cacheline可能有两种有效状态。Dirty的cache line具有比memory更新的数据。具有多个core的系统也可以只使用这两三种状态,但通常需要区分不同类型的有效状态。cache line状态有四个主要特征:validity、dirtiness、exclusivity和ownership。后两个特征是具有多core的系统所特有的。
- Validity:一个有效的cache line具有该cache line的最新值。Cache line可以被读,但只有在它也是exclusivity的情况下才可以被写。
- Dirtiness:如果一个cache line的值是最新的,且这个值与memory中的值不一致,那么这个cache line就是dirty的,缓存控制器负责最终用这个新值更新memory内容。Clean通常被用作dirty的反义词。
- Exclusivity:如果一个cache line在系统中是唯一的cache line副本,则该cache line是exclusivity的,也就是该cache line除了可能在memory中,没有任何其它cache拥有这个cache line的值。
- Ownership:如果cache控制器(或memory控制器)负责响应cache line的一致性请求,那么它就是这个cache line的owner。在大多数协议中,一个给定cache line始终只有一个owner。如果没有将cache line的ownership交接给另一个一致性控制器,就算是由于cache容量或冲突缺失,这个cache line可能也不会从cache中被踢出去以腾出空间给另一个cache line。在某些协议中,非owned的cache line可能会被悄悄无效掉,不发送任何消息通知其它组件。
4.1.2 MOESI状态模型
许多一致性协议使用经典的五态MOESI模型的子集。MOESI是cache中line的状态,最基本的三种状态时MSI,也可以使用O和E态,但是它们不是最基本的。O和E态通常是协议中优化某些场景使用的。这些状态中的每一个都可以由上一小节描述的状态特征组合而成。
- M(odified):cache line是valid、exclusive、owned、并且可能是dirty的。这条line可以被读或写,且是cache中唯一的有效副本,拥有这条line的cache必须响应其它caches对这条line的请求,并且这条line在memory中的副本可能已经过期了。
- S(hared):cache line是valid,但不是exclusive、dirty和owned的。这条line只能被读,其它caches也可能具有这条line的有效只读副本。
- I(nvalid):cache 里呢是invalid,cache要么不包含这条line,要么包含可能无法读取或写入的陈旧副本。
- O(wned):cache line是valid,owned、且可能是dirty的,但不是exclusive的。这条line只能被读,并且拥有这条line的cache必须响应其它caches对这条line的请求。其它cache也可能有这条line的只读副本,但它们不是owners。这条line在memory中的副本可能已经过期了。
- E(xclusive):cache line是valid、owned、exclusive和clean的。这条line可以被读或写,没有其它cache拥有这条line的有效副本,并且这条line和memory中的数据是一致的,memory中的副本是最新的。(需要注意的是,某些协议中,Exclusive不被视为owned的)
图9用Venn图展示了MOESI状态。Venn图可以展示哪些状态共有哪些特征。除了I之外的所有状态都有效。M、O和E是ownership状态。M和E都是exclusivity,因为没有其它cache拥有该line的有效副本。M和O都是表示该cache line可能是dirty的。M、O、E和S组成了V(alid)态。
图9 MOESI状态的Venn图
MOESI状态模型虽然很常见,但不是所有的状态集合。例如,有些协议有F(orward)状态,它类似与O状态,除了它是clean的,即memory中的副本是最新的。Intel Core i7-9xx处理器中的QuickPath Interconnect(QPI)就使用了F状态,QPI支持MESIF的5个状态模型。
表1 MOESI的状态特征
4.2 事务(transaction)选择
一致性控制器组件的基本目标都是相似的,所以大多数一致性协议都有类似的transaction集合。例如,几乎所有协议都有一个获取Shared(只读) cache line的transaction。表2列出了一组常见的transactions,并且对于每个transaction,描述发起transaction的请求者目标。这些transactions都是由cache控制器发起的,这些cache控制器响应来自其对应core的读写请求。表3列出了一个core可以像其cache控制器发出的读写请求,以及这些core请求如何引导cache控制器发起一致性transactions。
表2 常见的transactions
尽管大多数协议使用类似的transactions集合,但它们在一致性控制器如何交互以执行transaction方面存在相当大的差异。正如我们之前提到,在监视协议中,cache控制器通过向系统中所有一致性控制器广播GetS请求来获取目标的cache line,并且当前line的owner的控制器需要提供所需数据去响应请求者。相反,在目录协议中,cache控制器通过向特定的目录控制器发送单播GetS请求,目录控制器可能直接响应,也可能将请求转发给其它一致性控制器去响应请求者的line数据。
表3 core对cache控制器的常见请求
5. 总结
为了弥补访问memory速度太慢的原因引进了cache。在多core系统中,每个core可能都有自己的私有cache,多个core会对自己的私有cache进行读写操作,如果不使用软件或硬件来维护cache数据,必然会造成cache一致性问题。为了硬件层面解决这个问题,大多数现在多core系统中都定义了一致性协议,让各个一致性控制器遵照” Single-Writer, Multiple-Read”和” Data-Value”这两条原则去维护存储内容。在一致性协议设计中,根据core的行为如何通知其它core分为监听协议和目录协议,根据core决定写cache line时,如何处理其它core的cache line副本,分为无效协议和更新协议。” 监听协议和目录协议”与” 无效协议和更新协议”可以两两交叉组成4种一致性协议,但目前最常用的是目录协议和无效协议的组合。随后抽象出cache line状态的四个主要特征:validity、dirtiness、exclusivity和ownership,以及这四个特征如何对应到经典的MOESI状态模型。最后介绍了一致性协议中常用的一致性transactions,以及core请求如何引导cache控制器发起一致性transactions。
为了使整篇文章内容不显得太冗长,本文没有对cache状态转换(transitions)过程过多讲述,也没有讲述异构系统和多芯片多处理器的一致性,现在计算机系统主要是异构的,不仅包含多core,还包含GPU和其它加速器(例如,神经网络硬件)。为了追求可编程性,这些异构系统也开始支持共享内存。
希望读者不管是从事软件还是硬件,本文能帮助大家了解更多的cache一致性知识。今天就写到这里了,有空继续写点memory consistency model。