CPU Cache与MESI缓存一致协议

Posted 苍山有雪,剑有霜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CPU Cache与MESI缓存一致协议相关的知识,希望对你有一定的参考价值。

CPU Cache

在看小林文章的时候,边看边查,加深了对CPU Cache了解。

包括什么是Cache?Cache的分级?缓存一致性?write through与wrte back?等等

什么是Cache

小林开篇提了一个问题:请问以下两种初始化数组的方式,哪种方式执行速度更快?

回答这个问题,必须得有Cache相关的知识储备。

现阶段,计算机内存的速度远远低于CPU的访问速度,为了协调CPU与内存之间的速度差异,就在两者之间加了一个中间层——缓存Cache。

CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度

如今常见的CPU Cache架构大致如下:

即一个CPU中有三级缓存,每一个CPU核具有L1、L2两级缓存,这是每个核独占的,其他核无法对其进行操作;而所有CPU公用一个缓存L3。L1、L2、L3的访问速度依次下降,存储空间大小依次上升

为什么要设计多层Cache而且容量还这么小?

简单来说,是由于只靠增加单个Cache的容量来提升性能的方式,性价比太低了!大容量的Cache的工艺难度以及成本上面都有许多问题,不利用大面积推广,只有少部分定制计算机才会采用较大的缓存。

Windows系统下可以直接打开任务管理器-性能-CPU,看到自己的CPU 各级Cache大小

CPU Cache 的数据是从内存中读取过来的,它是以一小块一小块读取数据的,而不是按照单个数组元素来读取数据的,在 CPU Cache 中的,这样一小块一小块的数据,称为 Cache Line(缓存块,Cache的最小单位,主流的CPU Cache的Cache Line大小都是64Bytes)

如果 L1 Cache Line 大小是 64 字节,也就意味着 L1 Cache 一次载入数据的大小是 64 字节。

通过这个命令可以查询自己电脑的L1 Cache Line

CPU Cache内部其实由一个一个的Cache Line所构成的,可以看作是一行行的“数据”,如下:

来自:https://mp.weixin.qq.com/s/PDUqwAIaUxNkbjvRfovaCg

头标志Tag中存放着一些有关于该缓存数据的信息,例如缓存是否修改等,在后面会继续讲。而数据块Data Block即是存放缓存数据的地方。

回到最开始的问题,我们需要知道的是,对于数组而言,在内存中是顺序存放的,而且是这种顺序array[0][0] array[0][1]…array[1][0] array[1][1]…

因此,形式一显然更快。因为形式一的代码充分利用了Cache,即将某一块数组读取到内存中,而第二种方式涉及缓存的切换,就会造成从内存中读取数据到缓存,这种操作时很耗时的。

Write Through与Write Back

之前我们提到Cache可以加快CPU访问数据的速度,那么这里就有一个问题:CPU如果对缓存中的数据进行了修改,那么应该以什么方式、什么时候将其写回内存呢?即如何处理Cache与内存的数据同步问题

有关Cache的写入,有两种方式:

  • Write Back(写回)
  • Write Through(写直达)

Write Through

如何保证Cache中修改的数据与内存中原数据的一致性问题?

直观上最简单的方式就是,修改数据的时候,同时将数据写入Cache与内存

来自:https://mp.weixin.qq.com/s/PDUqwAIaUxNkbjvRfovaCg

简单来说,就是先更新Cache中的数据(如果该数据在Cache)中,而后再将数据写入内存。

这种方式最直观、最简单,但是也是最损耗性能的。因为每次对数据修改之后都需要将数据写入内存,这就不能很好的发挥出缓存的性能,得不偿失。

Write Back

写直达的操作无法很好的发挥出缓存应有的性能,那么如何对其进行优化?

能不能……每次只更新Cache中的数据,只有当Cache必须得写回内存时,我们才将Cache写入内存。

可以预想,这种方式肯定比之前的方法效率更高。那么,什么叫做Cache必须得写回内存时?——就是Cache被替换的时候。

Cache的容量就那么小(以KB计量),读取的Cache数据不可能一直占据着空间,这其中也涉及Cache Line的替换,类似内存页置换,常用算法有LRU、LFU。

也就是说,当Cache Line没有被替换的时候,我们不需要将更新的数据写回内存,只需要更新Cache中数据即可,只有当该CacheLine被新的Cache数据给替换时,我们才将Cache中的数据写回内存。整体流程如下:

来自:https://mp.weixin.qq.com/s/PDUqwAIaUxNkbjvRfovaCg

注意,图中所说的标记当前Cache Data Block为脏(dirty)即是在Cache Line中的Tag进行标记,后面在内存一致性中会进一步讲解。

缓存一致性协议MESI

写回机制可以充分发挥缓存的性能,于是如今大部分缓存机制都是采用这种方式。但是这又会带来一个问题:那就是如何保证多核场景下的缓存与内存的一致性

假设CPU有两核,A,B,并且在他们的L1、L2是相互独立的,假设在A、B的缓存中都读入了变量i,此时i=0

假设在A核中将i的值改为了1,但是B核所知道的i还是等于0,这就可能造成未知的错误。

来自:https://mp.weixin.qq.com/s/PDUqwAIaUxNkbjvRfovaCg

这就是多核场景下缓存数据不一致问题。如何解决?

要想解决这个问题需要保证两点

  • 某个CPU核对数据进行更新操作时,必须将该操作“告诉”其他的CPU核以及内存,这就是所谓的“写传播write propagation”(在多人协同文档中,也需要这种处理机制),也可以将其称为“广播”。实现这一点的常见方式是总线嗅探(Bus snooping),当某个核更新了数据时,会将数据通过总线来广播到其他核,其他核会时刻监督总线的动态,当收到数据更新时,会查看自己的缓存中是否存在对应的数据,如果有就进行更新。如果每次更新数据,都在总线上进行广播,这种方式会对总线产生巨大的负载压力。

  • 多核对于数据的修改必须**是串行事务化的。**简单来说,如果A核对i进行了修改,B核也对数据进行修改,它们会将相应的修改“传播”给其他核,这就必然涉及到AB核的消息谁先到达的问题。用小林画的图:

来自:https://mp.weixin.qq.com/s/PDUqwAIaUxNkbjvRfovaCg

等等,这种类似的数据同步问题是不是在多线程的数据同步中也遇到过?当时我们引入了来解决,同理,在缓存一致性问题上我们同样可以引入机制,只有获取到锁的核,才能够对数据进行修改。

上面提到如果只靠总线嗅探的方式会加大总线负载,使用效果不好,实际广泛使用的是MESI协议,它将缓存中的数据标记为M、E、S、I四种状态,并通过这四种状态之间的关系来实现缓存一致。

  • M,modified,表示该数据已修改,即上述所说的“脏”数据,需要在合适的时机写回内存;

  • E,Exclusive,表示该数据独占,意指此时该数据只存在某个核中,其他核没有该数据,于是就不需要所谓的广播给其他的核,也就没有缓存一致性的问题;

  • S,Shared,表示数据共享,是从数据独占状态转移过来的。意指多个核中都有该数据,这个时候就会存在缓存一致性的问题;

  • I,Invalidated,表示数据已失效,可以丢弃掉Cache Block的数据了(高并发情况下可能出现两个CPU同时修改变量a,并同时向总线发出将各自的缓存行更改为M状态的情况,此时总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效);

MESI四种状态标志是存储在Cache Line中的

相信有些朋友已经隐约有点感觉了,这就是一个有限状态机:

来自:https://mp.weixin.qq.com/s/PDUqwAIaUxNkbjvRfovaCg

单看有限状态机或许还比较难理解,我们以下面这个例子来一步一步分析MESI协议是如何工作的,借鉴链接

首先,CPU 1核将内存中的变量i加载到自己的缓存中,并在缓存中将状态标记为E(独占):

注:为简洁未画出L1、L2、L3缓存,可以认为图上的缓存表示L12,L3并入了内存中

随后,CPU 2读取了**变量i,**此时CPU1通过总线嗅探了解到CPU2的动作,将自身缓存I的状态标记为S(共享),CPU2的缓存也标记为S:

**而后,**CPU 1对变量i进行了修改,将缓存中的状态改为M(修改),CPU 2通过总线嗅探将自身的数据i标记为I(无效的):

再之后,CPU1将变量i写回内存,并将自身状态标记为E(独占),注意,CPU2的变量a被置为I(无效)状态后,只是保证变量a的修改不会被写回内存,但CPU2有可能会在CPU1将变量a置为E(独占)状态之前重新读取内存中的变量a,这个取决于汇编指令是否要求CPU2重新加载内存:

**最后,**某个时刻CPU2需要访问变量i,但是发现已失效,于是需要重新去内存读取变量i,此时状态为:

上述只是一个MESI协议工作过程的简单描述,实际工作过程极其复杂,有三个比较重要的点:

  • 当一个处理器请求使用exclusive模式加载load一个缓存行时,其他的处理器会将所有它们自己关于该缓存行的副本都置为invalid。任何一个已修改过自己本地的该对应缓存行的处理器都需要首先将其写回到内存中,之后第一个处理器的load请求才可以被满足

  • 当一个处理器请求使用shared模式加载load一个缓存行时,任何一个以exclusive模式加载该line的处理器都必须将其状态置为shared,并且任何一个已经修改过自己本地对应缓存行的处理器都必须将该line写回主内存,之后第一个处理器的load请求才可以被满足

  • 如果缓存满了,则可能需要替换一个缓存行。如果该line是shared或exclusive状态,那么它可以直接简单的被丢弃。但是如果该line被修改过,那么它必须被首先写回内存之后再丢弃

注意,MESI中对状态位的修改是基于总线嗅探机制实现的,并且整个过程在硬件层面上实现

原文链接
手写操作系统1——helloOS
手写操作系统2——代码是如何运行的?

以上是关于CPU Cache与MESI缓存一致协议的主要内容,如果未能解决你的问题,请参考以下文章

CPU Cache 一致性:Cache 结构同步方式Cache 一致性总线嗅探MESI 协议

CPU Cache 一致性:Cache 结构同步方式Cache 一致性总线嗅探MESI 协议

CPU Cache 一致性:Cache 结构同步方式Cache 一致性总线嗅探MESI 协议

CPU Cache 一致性:Cache 结构同步方式Cache 一致性总线嗅探MESI 协议

缓存一致性协议MESI

高速缓存一致性协议MESI与内存屏障