有上面这个
局部性
原理为理论指导,为了解决二者速度不匹配问题就可以在
CPU
和内存之间加一个缓存层,于是就有了如下的结构:
三、何时更新缓存
在
CPU
中引入缓存中间层后,虽然可以解决和内存速度不一致的问题,但是同时也面临着一个问题:当 CPU 更新了其缓存中的数据之后,要什么时候去写入到内存中呢?比较容易想到的一个解决方案就是,
CPU
更新了缓存的数据之后就立即更新到内存中,也就是说当
CPU
更新了缓存的数据之后就会从上到下更新,直到内存为止,英文称之为
write through
,这种方式的优点是比较简单,但是缺点也很明显,由于每次都需要访问内存,所以速度会比较慢。还有一种方法就是,当
CPU
更新了缓存之后并不马上更新到内存中去,在
适当的时候
再执行写入内存的操作,因为有很多的缓存只是存储一些中间结果,没必要每次都更新到内存中去,英文称之为
write back
,这种方式的优点是
CPU
执行更新的效率比较高,缺点就是实现起来会比较复杂。
上面说的
在适当的时候写入内存
,如果是单核
CPU
的话,可以在缓存要被新进入的数据取代时,才更新内存,但是在多核
CPU
的情况下就比较复杂了,由于
CPU
的运算速度超越了 1 级缓存的数据
I\O
能力,
CPU
厂商又引入了多级的缓存结构,比如常见的 L1、L2、L3 三级缓存结构,L1 和 L2 为
CPU
核心独有,L3 为
CPU
共享缓存。
如果现在分别有两个线程运行在两个不同的核
Core 1
和
Core 2
上,内存中
i
的值为 1,这两个分别运行在两个不同核上的线程要对
i
进行加 1 操作,如果不加一些限制,两个核心同时从内存中读取
i
的值,然后进行加 1 操作后再分别写入内存中,可能会出现相互覆盖的情况,解决的方法相信大家都能想得到,第一种是只要有一个核心修改了缓存的数据之后,就立即把内存和其它核心更新。第二种是当一个核心修改了缓存的数据之后,就把其它同样复制了该数据的
CPU
核心失效掉这些数据,等到合适的时机再更新,通常是下一次读取该缓存的时候发现已经无效,才从内存中加载最新的值。
四、缓存一致性协议
不难看出第一种需要频繁访问内存更新数据,执行效率比较低,而第二种会把更新数据推迟到最后一刻才会更新,读取内存,效率高(类似于
懒加载
)。
缓存一致性协议(MESI)
就是使用第二种方案,该协议主要是保证缓存内部数据的一致,不让系统数据混乱。
MESI
是指 4 种状态的首字母。每个缓存存储数据单元(Cache line)有 4 种不同的状态,用 2 个 bit 表示,状态和对应的描述如下:
下面看看基于
缓存一致性协议
是如何进行读取和写入操作的, 假设现在有一个双核的
CPU
,为了描述方便,简化一下只看其逻辑结构:
单核读取步骤
:
Core 0
发出一条从内存中读取 a 的指令,从内存通过
BUS
读取 a 到
Core 0
的缓存中,因为此时数据只在
Core 0
的缓存中,所以将
Cache line
修改为
E
状态(独享),该过程用示意图表示如下:
双核读取步骤
:首先
Core 0
发出一条从内存中读取 a 的指令,从内存通过
BUS
读取 a 到
Core 0
的缓存中,然后将
Cache line
置为
E
状态,此时
Core 1
发出一条指令,也是要从内存中读取 a,当
Core 1
试图从内存读取 a 的时候,
Core 0
检测到了发生地址冲突(其它缓存读主存中该缓存行的操作),然后
Core 0
对相关数据做出响应,a 存储于这两个核心
Core 0
和
Core 1
的缓存行中,然后设置其状态为
S
状态(共享),该过程示意图如下:
假设此时
Core 0
核心需要对
a
进行修改了,首先
Core 0
会将其缓存的
a
设置为
M
(修改)状态,然后通知其它缓存了
a
的其它核
CPU
(比如这里的
Core 1
)将内部缓存的
a
的状态置为
I
(无效)状态,最后才对
a
进行赋值操作。该过程如下所示:
细心的朋友们可能已经注意到了,上图中内存中
a
的值(值为
1
)并不等于
Core 0
核心中缓存的最新值(值为
2
),那么要什么时候才会把该值更新到内存中去呢?就是当
Core 1
需要读取
a
的值的时候,此时会通知
Core 0
将
a
的修改后的最新值同步到内存(
Memory
)中去,在这个同步的过程中
Core 0
中缓存的
a
的状态会置为
E
(独享)状态,同步完成后将
Core 0
和
Core 1
中缓存的