缓存组成

多核计算机中,一个物理插槽对应一个cpu,一个cpu包含一个L3 Cache、多个core;一个core包含 寄存器、L1 Cache、L2 Cache,如下图所示:
缓存 - 图1
其中越靠近core则,速度越快,容量则越小。L1和L2是只能给一个core进行共享,但是L3是可以给同一个槽内的core共享;而内存,是可以给所有的cpu共享,这就是内存的共享。

  • core执行运算的流程

首先在L1里面查找对应数据,如果没有则去L2、L3,如果都没有,则就会去内存中去拿,走的路越长,则耗费时间越久,性能就会越低。
需要注意的是,当线程之间进行共享数据的,需要将数据写回到内存中,而另一个线程通过访问内存获得新的数据。

  • 内存屏障

其实,线程间除了内存共享,还存在一些其他的内存共享方式。那么就可能出现另一个线程直接访问到修改之前的内存。
怎么办呢?这种数据我们可以通过设置缓存失效来保证缓存的最新,这个方式其实在cpu这里进行设置的,称为内存屏障(其实就是在cpu这里设置一条指令,这个指令就是禁止cpu重排序,这个屏障之前的不能出现在屏障之后,屏障之后的处理不能出现屏障之前,也就是屏障之后获取到的数据是最新的),对应到应用层面就是一个关键字volatile。

缓存行

缓存行(Cache line)是缓存的基本单位,一个缓存由很多个缓存行 组成,每个缓存行大小是32~128字节(通常是64字节)。
以64字节缓存行为例,java的一个Long类型是8字节,这样的话一个缓存行就可以存8个Long类型的变量,如下图所示:
image.png
因此,cup每次将内存中的数据加载到缓存中,最小都是一个缓存行。
例如 访问一个long类型的数组时,当数组中的一个值被加载到缓存中时,那么对应的后面的7个数据也会被加载到对应的缓存行中,这样虽然我只是加载了一个值,确也能快速的访问另外七个值了,这种现象称为 免费缓存加载
但是,如果使用的数据结构中的项在内存中不是彼此相邻的,比如链表,那么将得不到免费缓存加载带来的好处。

伪共享问题

缓存以行为单位,那么缓存失效也是以行单位,当缓存行中的某个数据失效,便会导致该缓存行中的其他数据也失效,这被称为伪共享
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能。

详见Disruptor中对其分析部分

缓存一致性协议

现在是一个多核cpu时代,而每个核心也都有自己独立的缓存,当多个核心同时操作多个线程对同一个数据进行更新时就会出现线程安全问题。例如:核心2在核心1还未将更新的数据刷回内存之前读取了数据,并进行操作,就会使程序的执行结果出现随机性的影响。

而总线加锁是对整个内存进行加锁,在一个核心对一个数据进行修改的过程中,其他的核心也无法修改内存中的其他数据,这样会导致多核CPU处理性能严重下降。

缓存一致性协议提供了一种高效的内存数据管理方案,它只会对单个缓存行(缓存行是缓存中数据存储的基本单元)的数据进行加锁,不会影响到内存中其他数据的读写。因此,缓存一致性协议能更高效的对内存数据的读写进行管理。

一言堂:内存加锁是粗粒度的加锁,cache line加锁是细粒度的加锁。

缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等,接下来我们主要介绍MESI协议。

MESI协议

MESI协议是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一。 也称 伊利诺伊州协议(Illinois协议),因为由于其在伊利诺伊大学厄巴纳 - 香槟分校发展。
其中MESI是高速缓存行(Cache line)的四种状态的首字母缩写(每个缓存行使用额外的两个bit表示)。通过对这四种状态的切换,来达到对缓存数据进行管理的目的。

MESI协议只能保证并发编程中的可见性,并未解决原子性和有序性的问题,所以只靠MESI协议是无法完全解决多线程中的所有问题。

回写高速缓存可以节省很多通常在写入缓存上浪费的带宽。 回写高速缓存中总是存在脏状态,表示高速缓存中的数据与内存中的数据不同。 如果块驻留在另一个缓存中,则要求缓存在未命中时缓存传输。 该协议相对于MSI协议减少了主存储器事务的数量。 这标志着性能的显着改善。

基本四态

状态 有效性 一致性 共享性 描述 状态转移
M
(Modified)
Y N N 只存在于当前core的缓存数据,在被修改后,与内存中不一致(dirty),就属于M状态。
1. 当core读取一个内存中的数据a到缓存时,该缓存a为E态;
1. 当core修改了缓存a,则转换为M态,因为此时该缓存数据与内存中的数据不一致,所以主内存中是脏数据(dirty),为保证数据安全,M态缓存对应的主存的数据会被时刻监听,在其他core读取该数据之前将该缓存数据回写(write back)内存中;
1. 当被写回内存之后,该缓存行会变成E状态(其他core是不能操作M状态的缓存行的,只有等其为E状态才行);
1. 当其他core读取了该内存数据,该缓存又会变成S状态;
1. 当其中任意core修改该缓存数据,该缓存数据变为I状态;
1. 一旦Cache line进入这个状态,core读数据就必须发出总线事务,从内存读。
E
(Exclusive)
Y 只存在于当前core的缓存数据,未被修改过,与内存中一致(clean),就属于E状态。
S
(Shared)
Y 存在于多个core的缓存数据,且与内存中数据一致,就属于S状态。
I
(Invalid)
N / / 由于S状态的缓存行被修改后,就属于I状态。
  • 有效性:指该缓存行数据是否有效,有效直接使用,无效则必须从主内存中重新读取;
  • 一致性:指该缓存行数据是否与内存中的数据一致;
  • 共享性:指该缓存行数据是否同时存在于其他缓存行中,只有不共享的状态才能对该缓存行写操作(保证数据安全);

CPU的读取遵循下面几点

  • 如果cpu1的缓存数据为M或E,当cpu2读该数据时,cpu1就把自己的缓存数据回写到内存中,并将自己的状态设置为S;
  • 缓存行可以随时主动被作废(变成I状态),而M状态的缓存行会在作废前自动写回主存。

而S状态的缓存行被主动作废,即使另一个core实际上已经独享了该缓存行,但是该缓存行确不会转移为E状态。因为其它core不会广播他们作废掉该缓存行的通知,同样由于core并没有保存该缓存行的副本数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。
iShot_2022-05-07_18.42.47.png

状态转移

MESI协议状态切换过程分析.svg
MESI保证多CPU/多核系统的cache一致性。即多线程程序读写共享变量的时候,每个CPU看到的一定是这个变量的最新值。

原子性:当前进程读写共享变量的操作不能被打断,通常通过锁总线或者锁缓存(MESI协议)来实现,具体采用锁总线还是锁缓存,取决于读写的变量位于内存还是cache中。

缓存一致性协议通过监控独立的loads和stores指令来监控缓存同步冲突,并确保不同的处理器对于共享内存的状态有一致性的看法。当一个处理器loads或stores一个内存地址a时,它会在bus总线上广播该请求,其他的处理器和主内存都会监听总线(也称为snooping)。

  1. core1从内存中将变量a加载到缓存中,会将变量a的状态改为E(独享),并通过总线嗅探机制对内存中变量a的操作进行嗅探(监听);
  2. 后,core2读取内存变量a,总线嗅探机制会将core1中的缓存变量a的状态置为S(共享),并将变量a加载到core2的缓存中,状态为S;
  3. core1对缓存a进行修改操作,core1中的缓存a会被置为M状态,而core2中的缓存a会被通知,改为I状态。此时core2中操作变量a需要重新去内存中读(高并发情况下可能出现两个core同时修改各自的缓存a,并同时向总线发出将各自的缓存行更改为M状态的情况,此时总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效);
  4. 当core1将修改后的缓存a写回内存后,缓存a转换为E状态;
  5. 当core2操作变量a时,会重新去内存中加载变量a,同时core1和core2中的缓存a都会转换为S状态。

如果缓存满了,则可能需要驱逐一个缓存行。如果该缓存行是S|E状态,那么它可以直接简单的被丢弃。但是如果该缓存行是M状态,那么它必须被先写回内存之后再丢弃。

  • 状态转移情况分析 | 状态 | 事件 | 转移 | 说明 | | —- | —- | —- | —- | | E | l-r | E | | | | l-w | M | | | | r-r | S | | | | r-w | I | | | S | l-r | S | | | | l-w | M | | | | r-r | S | | | | r-w | I | | | M | l-r | M | | | | l-w | M | | | | r-r | S | 其他core是不能直接操作M状态的缓存行的,当其他core读取该数据时,会触发该缓存行被回写到内存中变为E状态,才能读取。所有的写,都必须在读之后。 | | | r-w | I | | | I | l-r | E | | | | l-w | M | | | | r-r | I | 缓存行已经无效,其他core的操作与它无关 | | | r-w | I | |

  • l-r(local read):当前core,从Cache中读取数据;

  • l-w(local write):当前core,修改Cache中数据;
  • r-r(remote read):其他core,读取该数据(从内存中读取);
  • r-w(remote write):其他core,修改该数据(在RR之后)。

优劣分析

vs MSI的优势

两种协议之间最显着的差异是MESI协议中存在的额外 Exclusive状态。添加了这个额外状态,因为它有许多优点。
如果处理器需要读取其他处理器都没有的块然后写入它,那么在MSI的情况下将发生两个总线事务。首先是BusRd请求,在写入块之前读取块,然后是BusRdX请求。这种情况下的BusRdX请求是无用的,因为没有其他缓存具有相同的块,但是一个缓存无法知道这一点。因此,MESI协议通过添加Exclusive状态克服了这一限制,从而节省了总线请求。这在顺序应用程序运行时会产生巨大差异。由于只有一个处理器正在处理它,所有访问都将是独占的。 MSI在这里表现得非常糟糕。即使在高度并行的应用程序中,数据共享最少,MESI也会快得多 [1] 。
编辑播报

缺点

如果由特定块上的各种高速缓存执行连续读取和写入操作,则每次都必须将数据刷新到总线上。因此,主存储器将在每次冲洗时拉动它并保持清洁状态。但这不是一项要求,只是由于MESI的实施而导致的额外开销。 MOESI协议克服了这一挑战。