一、cache
cpu cache已经发展到了三级缓存结构,基本上现在买的个人电脑都是L3结构。
1. cache的意义
为什么需要CPU cache?因为CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu -> cache -> memory)。
CPU cache有什么意义?cache的容量远远小于主存,因此出现cache miss在所难免,既然cache不能包含CPU所需要的所有数据,那么cache的存在真的有意义吗?当然是有意义的——局部性原理。
A. 时间局部性:如果某个数据被访问,那么在不久的将来它很可能被再次访问;
B. 空间局部性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问;
2. cache和寄存器
存储器的三个性能指标——速度、容量和每位价格——导致了计算机组成中存储器的多级层次结构,其中主要是缓存和主存、主存和磁盘的结构。那么在主存之上,cache和寄存器之间的关系是?
举个例子,当你在思考一个问题的时候,寄存器存放的是你当前正在思考的内容,cache存放的是与该问题相关的记忆,主存则存放无论与该问题是否有关的所有记忆,所以,寄存器存放的是当前CPU执行的数据,而cache则缓存与该数据相关的部分数据,因此只要保证了cache的一致性,那么寄存器拿到的数据也必然具备一致性。
二、CPU cache结构
1. 单核CPU cache结构
在单核CPU结构中,为了缓解CPU指令流水中cycle冲突,L1分成了指令(L1P)和数据(L1D)两部分,而L2则是指令和数据共存。
2. 多核CPU cache结构
多核CPU的结构与单核相似,但是多了所有CPU共享的L3三级缓存。在多核CPU的结构中,L1和L2是CPU私有的,L3则是所有CPU核心共享的。
三、MESI(缓存一致性)
缓存一致性:在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题。而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致。
至于MESI,则是缓存一致性协议中的一个,到底怎么实现,还是得看具体的处理器指令集。
1. cache的写方式
cache的写操作方式可以追溯到大学教程《计算机组成原理》一书。
A. write through(写通):每次CPU修改了cache中的内容,立即更新到内存,也就意味着每次CPU写共享数据,都会导致总线事务,因此这种方式常常会引起总线事务的竞争,高一致性,但是效率非常低;
B. write back(写回):每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的时机才会更新到内存中;
无论是写通还是写回,在多核环境下都需要处理缓存cache一致性问题。为了保证缓存一致性,处理器又提供了写失效(write invalidate)和写更新(write update)两个操作来保证cache一致性。
A. 写失效:当一个CPU修改了数据,如果其他CPU有该数据,则通知其为无效;
B. 写更新:当一个CPU修改了数据,如果其他CPU有该数据,则通知其跟新数据;
写更新会导致大量的更新操作,因此在MESI协议中,采取的是写失效(即MESI中的I:ivalid,如果采用的是写更新,那么就不是MESI协议了,而是MESU协议)。
所以,现在常见的多核CPU写方式:写命中时,核内write back,核间write invalid + cache coherency protocol;写miss,先write allocated,再走前述的写命中流程。(write allocated和no-write allocated见评论区)
2. cache line
cache line是cache与内存数据交换的最小单位,根据操作系统一般是32byte或64byte。在MESI协议中,状态可以是M、E、S、I,地址则是cache line中映射的内存地址,数据则是从内存中读取的数据。
工作方式:当CPU从cache中读取数据的时候,会比较地址是否相同,如果相同则检查cache line的状态,再决定该数据是否有效,无效则从主存中获取数据,或者根据一致性协议发生一次cache-to--chache的数据推送(参见MESI协议,文章最后的链接);
工作效率:当CPU能够从cache中拿到有效数据的时候,消耗几个CPU cycle,如果发生cache miss,则会消耗几十上百个CPU cycle;
cache的工作原理以及在主板上的结构如下两图所示:
3. 状态介绍
MESI协议将cache line的状态分成modify、exclusive、shared、invalid,分别是修改、独占、共享和失效。
modify:当前CPU cache拥有最新数据(最新的cache line),其他CPU拥有失效数据(cache line的状态是invalid),虽然当前CPU中的数据和主存是不一致的,但是以当前CPU的数据为准;
exclusive:只有当前CPU中有数据,其他CPU中没有改数据,当前CPU的数据和主存中的数据是一致的;
shared:当前CPU和其他CPU中都有共同数据,并且和主存中的数据一致;
invalid:当前CPU中的数据失效,数据应该从主存中获取,其他CPU中可能有数据也可能无数据,当前CPU中的数据和主存被认为是不一致的;
对于invalid而言,在MESI协议中采取的是写失效(write invalidate)。
4. cache操作
MESI协议中,每个cache的控制器不仅知道自己的操作(local read和local write),每个核心的缓存控制器通过监听也知道其他CPU中cache的操作(remote read和remote write),今儿再确定自己cache中共享数据的状态是否需要调整。
local read(LR):读本地cache中的数据;
local write(LW):将数据写到本地cache;
remote read(RR):其他核心发生read;
remote write(RW):其他核心发生write;
下面摘自维基百科MESI协议Operation一节:
Processor Requests to Cache includes the following operations:
PrRd: The processor requests toreada Cache block.PrWr: The processor requests towritea Cache block
Bus side requests are the following:
BusRd: Snooped request that indicates there is areadrequest to a Cache block made by another processorBusRdX: Snooped request that indicates there is awriterequest to a Cache block made by another processor whichdoesn't already have the block.
因为MESI协议为了减少主存事务(主存总线竞争),会存在cache to cache的推送数据而不去内存读数据(This protocol reduces the number of Main memory transactions with respect to theMSI protocol. This marks a significant improvement in the performance.[2]),所以,还存在以下两种操作:
Flush: Snooped request that indicates that an entire cache block is written back to the main memory by another processor.FlushOpt: Snooped request that indicates that an entire cache block is posted on the bus in order to supply it to another processor(Cache to Cache transfers).
local read/write的操作(当前处理器自己的cache读写):
remote read/write的操作(其他处理器cache的读写操作对当前处理器cache line状态的影响):
5.状态转换和cache操作
如上文内容所述,MESI协议中cache line数据状态有4种,引起数据状态转换的CPU cache操作也有4种,因此要理解MESI协议,就要将这16种状态转换的情况讨论清楚。
初始场景:在最初的时候,所有CPU中都没有数据,某一个CPU发生读操作,此时必然发生cache miss,数据从主存中读取到当前CPU的cache,状态为E(独占,只有当前CPU有数据,且和主存一致),此时如果有其他CPU也读取数据,则状态修改为S(共享,多个CPU之间拥有相同数据,并且和主存保持一致),如果其中某一个CPU发生数据修改,那么该CPU中数据状态修改为M(拥有最新数据,和主存不一致,但是以当前CPU中的为准),其他拥有该数据的核心通过缓存控制器监听到remotewrite行文,然后将自己拥有的数据的cache line状态修改为I(失效,和主存中的数据被认为不一致,数据不可用应该重新获取)。
5.1 modify
场景:当前CPU中数据的状态是modify,表示当前CPU中拥有最新数据,虽然主存中的数据和当前CPU中的数据不一致,但是以当前CPU中的数据为准;
LR:此时如果发生local read,即当前CPU读数据,直接从cache中获取数据,拥有最新数据,因此状态不变;
LW:直接修改本地cache数据,修改后也是当前CPU拥有最新数据,因此状态不变;
RR:因为本地内存中有最新数据,当本地cache控制器监听到总线上有RR发生的时,必然是其他CPU发生了读主存的操作,此时为了保证一致性,当前CPU应该将数据写回主存,而随后的RR将会使得其他CPU和当前CPU拥有共同的数据,因此状态修改为S;
RW:同RR,当cache控制器监听到总线发生RW,当前CPU会将数据写回主存,因为随后的RW将会导致主存的数据修改,因此状态修改成I;
5.2 exclusive
场景:当前CPU中的数据状态是exclusive,表示当前CPU独占数据(其他CPU没有数据),并且和主存的数据一致;
LR:从本地cache中直接获取数据,状态不变;
LW:修改本地cache中的数据,状态修改成M(因为其他CPU中并没有该数据,因此不存在共享问题,不需要通知其他CPU修改cache line的状态为I);
RR:本地cache中有最新数据,当cache控制器监听到总线上发生RR的时候,必然是其他CPU发生了读取主存的操作,而RR操作不会导致数据修改,因此两个CPU中的数据和主存中的数据一致,此时cache line状态修改为S;
RW:同RR,当cache控制器监听到总线发生RW,发生其他CPU将最新数据写回到主存,此时为了保证缓存一致性,当前CPU的数据状态修改为I;
5.3 shared
场景:当前CPU中的数据状态是shared,表示当前CPU和其他CPU共享数据,且数据在多个CPU之间一致、多个CPU之间的数据和主存一致;
LR:直接从cache中读取数据,状态不变;
LW:发生本地写,并不会将数据立即写回主存,而是在稍后的一个时间再写回主存,因此为了保证缓存一致性,当前CPU的cache line状态修改为M,并通知其他拥有该数据的CPU该数据失效,其他CPU将cache line状态修改为I;
RR:状态不变,因为多个CPU中的数据和主存一致;
RW:当监听到总线发生了RW,意味着其他CPU发生了写主存操作,此时本地cache中的数据既不是最新数据,和主存也不再一致,因此当前CPU的cache line状态修改为I;
5.4 invalid
场景:当前CPU中的数据状态是invalid,表示当前CPU中是脏数据,不可用,其他CPU可能有数据、也可能没有数据;
LR:因为当前CPU的cache line数据不可用,因此会发生读内存,此时的情形如下。
A. 如果其他CPU中无数据则状态修改为E;
B. 如果其他CPU中有数据且状态为S或E则状态修改为S;
C. 如果其他CPU中有数据且状态为M,那么其他CPU首先发生RW将M状态的数据写回主存并修改状态为S,随后当前CPU读取主存数据,也将状态修改为S;
LW:因为当前CPU的cache line数据无效,因此发生LW会直接操作本地cache,此时的情形如下。
A. 如果其他CPU中无数据,则将本地cache line的状态修改为M;
B. 如果其他CPU中有数据且状态为S或E,则修改本地cache,通知其他CPU将数据修改为I,当前CPU中的cache line状态修改为M;
C. 如果其他CPU中有数据且状态为M,则其他CPU首先将数据写回主存,并将状态修改为I,当前CPU中的cache line转台修改为M;
RR:监听到总线发生RR操作,表示有其他CPU读取内存,和本地cache无关,状态不变;
RW:监听到总线发生RW操作,表示有其他CPU写主存,和本地cache无关,状态不变;
5.5 总结
MESI协议为了保证多个CPU cache中共享数据的一致性,定义了cache line的四种状态,而CPU对cache的4种操作可能会产生不一致状态,因此cache控制器监听到本地操作和远程操作的时候,需要对地址一致的cache line状态做出一定的修改,从而保证数据在多个cache之间流转的一致性。
四、storebuffer和invalidqueue
这里算是一些小的补充,为了加速MESI协议中的一些操作,硬件上引入storebuffer和invalidqueue的原因。
以下来自于/wiki/MESI_protocol
内存壁垒
MESI原型(naive MESI)直接的实现方式表现出两种特殊的低性能行为:首先,当写入无效的缓存行时,从另一个CPU提取该行时会存在较长的延迟;其次,将缓存行移至无效状态非常耗时。
因此,CPU实现了存储缓冲区并使队列无效。
写入无效的缓存行时使用存储缓冲区。由于无论如何都会进行写操作,因此CPU会发出一条读无效的消息(因此,有问题的高速缓存行以及存储该内存地址的所有其他CPU的高速缓存行都会无效),然后将写操作推入存储缓冲区当高速缓存行最终到达时执行。(CPU在尝试读取高速缓存行时会扫描自己的存储缓冲区,以防万一它有准备写入高速缓存的内容)。
因此,一个CPU可以写一些东西,但是还没有写在缓存中,因此其他CPU *看不到*-他们无法扫描其他CPU的存储缓冲区。
关于无效,CPU实施无效队列,从而立即确认传入的无效请求,但实际上不对其采取任何行动-它们只是输入无效队列,尽快进行处理(但不一定立即进行)。这样,CPU可以在其高速缓存中包含无效的行,但是尚不知道该行无效的行-无效队列包含尚未作用的无效。(失效队列在高速缓存的另一侧; CPU无法对其进行扫描,因为它可以对存储缓冲区进行扫描)。
结果,需要存储屏障。存储屏障将刷新存储缓冲区(确保所有写入均已进入CPU缓存)。读取屏障将刷新失效队列(确保其他CPU的所有写操作对刷新CPU可见)。
因此,MESI在实践中不太有效-如果您是单线程的,这不是问题,但如果不是,则绝对是问题。
以下来自于
/tag/invalidate-queue/
CPU 的速度与访问内存的速度差距巨大,为此 CPU 设计者使用了 cache,为每个 CPU/core 提供一个相对快速访问的存储空间,有了 cache 意味着存在 replication,这本质上跟分布式系统面临的问题类似,只是单机上我们可以使用一些硬件上的东西来提供某种 consistency modelcache 类似一个 hash table,但是其 hash 比较简单,通常是地址计算出来的 bit set为了使得 CPU 访问一致的数据,人们通过 cache coherence protocol 来同步 cache 里面的数据,常见的协议是 MESI 及其变种MESI 对应四个状态:modified(被修改过,要求对应内存不存在其他 cache 中)、exclusive(尚未被修改,但是仅有这个 CPU 拥有 copy,对应的也是 up-to-date 的数据)、shared(可能被多个 CPU 拥有,此时可以读但不可以写) 和 invalid(表示数据非法,也就是可以看成不可用);通常 CPU 需要用个 2bit 表示这四个状态;状态之间的转移通过某种 meesage 来实现一般对 MESI 的简单实现都是没有实际价值,这是因为发生写操作往往会带来很长时间的等候:首先需要写的 CPU 需要让别的 CPU 将状态转换到 invalid,收到 response 以后才能进行实际的写,为此硬件专家使用了 store buffer(Sutter 同志也说过,modern CPU 如果没有 store buffer 就不值得买,可见这个 feature 对整体性能的影响是不可忽略的)store buffer 的作用是让 CPU 需要写的时候仅仅将其操作交给 store buffer,然后继续执行下去,store buffer 在某个时刻就会完成一系列的同步行为;很明显这个简单的东西会违背 self consistency,因为如果某个 CPU 试图写其他 CPU 占有的内存,消息交给 store buffer 后,CPU 继续执行后面的指令,而如果后面的指令依赖于这个被写入的内存(尚未被更新,这个时候读取的值是错误的)就会产生问题,所以实际实现 store buffer 可能会增加 snoop 特性,即 CPU 读取数据时会从 store buffer 和 cache 两处读即便增加了 snoop,store buffer 仍然会违背 global memory ordering,导致的解决方案是 memory barrier:我们知道程序员书写两个写操作的时候,隐含的假定是如果能观察到后一个写的结果,那么前一个写的结果势必也会发生,这是一个非常符合人直觉的行为,但是由于 store buffer 的存在,这个结论可能并不正确:这是因为如果观察线程位于另一个 core,首先读取后一个写(该地址并不在 cache 内)需要向写入线程所在 core 要对应地址的值,由于该 core 从 store buffer 返回了新值的时候这个 buffer 里面的写操作可能尚未发生,所以观察线程在获取了后一个写的最新结果时,前一个写的结果依然无法观察到,这违背了 sequential consistency 的假定,往往程序员更倾向于这个 consistency model 下的 reasoning硬件 level 上很难揣度软件上这种前后依赖关系,因此往往无法通过某种手段自动的避免这种问题,因而只有通过软件的手段表示(对应也需要硬件提供某种指令来支持这种语义),这个就是 memory barrier,从硬件上来看这个 barrier 就是 CPU flush 其 store buffer 的指令,那么一种做法就是提供给程序员对应的指令(封装到函数里面)要求在合适的时候插入表达这种关系,另一种做法就是通过某种标识让编译器翻译的时候自动的插入这个指令往往 store buffer 都很小,针对连续写操作力不从心;类似的情况也发生在碰到 memory barrier 之后;开始写之前首先需要 invalidate 其他 cache 里面的数据,为了加速这个过程硬件设计者又加入了 invalidate queue,这个 queue 将 incoming 的 invalidate 消息存放,立即返回对应的 response 这样以便发起者能尽快做后面的事情,而这个 CPU 可以通过 invalidate queue 后续处理这些内存invalidate queue 的存在会使得我们有更多的地方需要 memory barrier(理由与 store buffer 类似)实际 memory barrier 又有一些细分,如 read/write 的,软件上会通过 smb_mb/rmb/wmb 等表示,对应的硬件指令不同平台下各不相同实际实现的时候由于某些指令集之间的关系使得 memory barrier 的实现不可能做到最优,很多常见的平台都使用了简单粗暴的 bus 锁(x86、amd64、armv7),这也就是 Sutter talk 里面认为硬件平台往往提供了一些“过度”的指令的原因,最终软件需要的 sequential consistency 尽管得以实现,但是产生了一些不必要的代价memory barrier 不是一个必需的东西,但是似乎如果有 real-time 的 requirement 似乎就不能避开,这一点有待证实实际的例子://04/12/why-memory-barrier%EF%BC%9F/
五、资源奉送
blog:点击打开链接
book:《大话处理器》(支持原著,请购买正版)
MESI(维基百科)
缓存相关的写策略:writeback/writethrough,writeallocateno-writeallocate
计算机体系结构:推荐国防科大张晨曦《计算机体系结构》
whatisstorebuffer(StackOverFlow问答)
附注:
本文如有错漏,烦请不吝指正,谢谢!