CPU Cache
我们知道计算机三大核心组件:
CPU
、内存和硬盘,其中
CPU
的处理速度是最快的,
CPU
的处理速度远远大于将数据从硬盘加载进来的速度,所以就导致
CPU
大部分都是空闲处于等待从硬盘加载数据这个流程上。然后就引入了内存,
CPU
从内存读取速度得到很大提升,然而依然存在很大瓶颈,为了提升
CPU
处理效率,生产厂商就在
CPU
上引入缓存
Cache
。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CZ4UmY0gTM2ATZwMjZ0EWO4ImYzQTZ4MDN0kTNhVTZy8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
CPU Cache
常见的如上图采用三层缓存架构,
L1、L2
一般位于
CPU核
内部,而
L3
位于
CPU核
外部,一般用于多个
CPU
核之间数据共享。 CPU Cache速度要远远大于内存的,所以,
CPU Cache
的出现主要是为了缓解
CPU
和
内存
之间速度不匹配问题。
MESI协议
为了提升
CPU
性能
CPU
厂商引入
CPU Cache
概念,但是会带来一个问题:缓存一致性。
L1、L2
都位于
CPU
核内部,
CPU
可能存在多个核,它们之间缓存可能就会存在一致性问题。
CPU Cache
会带来缓存一致性问题,那怎么去解决这个问题呢?有几种解决方案,其中比较通用的各种厂商通常都会支持的一种方案就是
MESI
协议,该协议就是用来解决
CPU Cache
之间缓存共享数据的一致性。
MESI
是由四个单词首字母简写来的,这四个单词是用来描述
cache line
在
CPU Cache
中的四种不同状态:
- 修改态(
):此Modified
已被当前cache line
修改过,内容和主存不一致;CPU
- 专有态(
):此Exclusive
只存在当前cache line
中,其它CPU缓存中没有,且和主存保持一致;CPU
- 共享态(
):此Shared
存在多个cache line
缓存中,且和主存保持一致;CPU
- 无效态(
):此Invalid
已被其它cache line
修改,导致当前CPU
缓存中的数据无效;CPU
cache line是CPU Cache管理数据最小单元,即CPU Cache和内存之间交换数据最小单位就是cache line,如果需要将某个变量加载到CPU中,会把该变量所处的cache line都统一一起加载进来,所以这里就引入了伪共享概念,即修改cache line中的一个变量导致处于该cache line中的其它变量也一起失效。
多处理器时,单个
CPU
对缓存中数据进行了改动,需要通知其它
CPU
,也就是意味着,
CPU
处理要控制自己读写操作,还需要监听其它CPU发出的通知,从而保证最终一致。
图片来源于网络
状态之间相互转换详细说明如下:
图片来源于网络
举个例子说明下,现在有个
cache line
位于
CPU0
和
CPU1
中,所以,这个
cache line
状态是
Shared
共享态,现在
CPU0
需要对
cache line
中的一个变量进行修改,大致流程如下:
- 修改前发现状态是
,这时就需要先发送S
失效通知给invalidate
;CPU1
-
收到CPU1
失效通知后进行数据失效处理,将invalidate
中对应的CPU1
状态标记为cache line
,然后发送I
给ack
进行确认;CPU0
-
收到CPU0
后,就开始对ack
中数据进行修改,修改完成后将cache line
状态标记成cache line
;M
- 等到有其它
需要读取这个变量值时,CPU
将处于CPU0
状态的M
同步到主存中,然后其它cache line
就可以从主存中读取到变量最新值,这时CPU
状态变成了cache line
。S
store-buffer
上面分析了如何通过
MESI
协议解决
CPU Cache
的一致性问题,但是却存在性能问题。如红色框框标记这个区间内,
CPU0
是一直处于等待状态的,现在计算机
CPU
核数都比较多,可能要等所有的
CPU
核都返回
ack
确认消息后才能继续工作,造成
CPU0
资源被白白的浪费。
如何去解决
MESI
带来的
CPU
性能问题呢?这时候
store-buffer
就出场了。
store-buffer
是处于
CPU
核中的另一个缓存,当存在修改时,把修改直接放到
store-buffer
中,
store-buffer
后台异步方式发送
invalidate
通知到其它
CPU
以及处理
ack
确认等工作,这样
CPU
就可以不用傻傻等待了。
还以刚才场景为例,
CPU0
修改
cache line
后,直接丢给
store-buffer
,让
store-buffer
处理后续和其它
CPU
同步问题,自己可以接着干下面工作,
store-buffer
采用异步方式发送
invalidate
通知和处理
ack
,这样
CPU0
就不会存在长时间阻塞问题,提示了
CPU
性能。
内存屏障
store-buffer
的引入虽然提升了
CPU
的性能,但是却引入了一个很大问题:数据不一致。
CPU0
中的
cache line
被修改后直接丢给
store-buffer
,
store-buffer
是异步处理方式,这时
CPU0
继续处理后续工作,其它
CPU
的
cache line
由于还没有来得及通知可能还是旧数据,这就出现数据不一致问题。
比如下面代码可能存在这样一种场景:
-
执行CPU0
这个方法,首先将cpu0()
值修改为value
,假如10
这个变量是value
状态,其在S
中也存在;CPU1
-
执行完CPU0
修改后,将修改直接丢给value
,然后执行store-buffer
,假如isFinish = true
变量只有isFinish
中有,其状态是CPU0
,然后修改后状态变成E
;M
-
执行CPU1
时,因为while(!isFinish)
中没有变量CPU1
,只有isFinish
中有最新数据,这时CPU0
会把自己缓存的CPU0
刷新到主存中,然后isFinish
从主存中读取到CPU1
最新值isFinish
;继续向下执行true
,虽然assert value == 10
已经把CPU0
设置成了value
,但是可能10
的CPU0
还没有发送出通知过来,导致store-buffer
中CPU1
还是旧值value
;3
上面分析场景来看:明明
cpu0()
方法中先执行
value=10
赋值,再去执行的
isFinish=true
赋值,但是在
cpu1()
方法中读取到了
isFinish
最新值,
value
却读到的是旧值。给人一种指令重排假象,这种就是伪指令重排,表面上像是发生了指令重排,实质上并没有进行指令重排,而是由于
CPU
缓存不一致造成的。
那怎么去解决这个问题呢?这里就引入了内存屏障。
在
cpu0()
方法中两个语句中间插入一个内存屏障指令
smp_mb
(伪代码),该指令作用就是保住
CPU0
的
store-buffer
中任务都同步完成后才能执行后续操作,也就保证
CPU0
上发生的修改对其它
CPU
都是可见的,然后再去执行后面语句。所以,这样就保证了
CPU1
中读取到
isFinish
最新值时,
value
也一定是最新值,从而解决了上面所说的问题。
invalidate-queues
内存屏障就是把
store-buffer
由异步执行变成同步执行的过程,
store-buffer
进行同步是个相当耗时的过程,需要发送
invalidate
通知到所有关联的
CPU
上,然后
CPU
接收到通知进行处理,处理完成后反馈
ack
,等获取到所有
CPU
反馈回来的
ack
才能继续向下执行。为了对内存屏障进行优化,又引入了
invalidate queues
(失效队列)概念。
如上图,
store-buffer
将
invalidate
通知发送到其它
cpu
,其它
cpu
接收到
invalidate
通知后放入到
invalidate queues
后直接反馈
ack
,因为处理
invalidate
也是比较耗时的工作,通过
invalidate queues
引入,缩短了
store-buffer
同步的时间。
读屏障、写屏障、全屏障
还是刚才那个场景,引入
invalidate queues
后,需要在
cpu0()
和
cpu1()
两个方法中都插入一条内存屏障才能实现之前效果。
CPU0
其实只需要把
store-buffer
同步出去即可,保证在
cpu0()
方法中的修改及时对其它
CPU
可见,插入内存屏障导致
CPU0
同时也会把
invalidate queues
处理掉,这是没有必要的一步;另一点,
CPU1
为了实现数据可见性,只需要把
invalidate queues
处理完就可以获取到
value
最新值,执行
assert value == 10
判断就没有问题了,插入内存屏障导致
store-buffer
中任务被处理同样是没必要的一步。
所以,对内存屏障进行优化,细分出三种类型:
- 写屏障:主要用来保证
中的任务都被处理完成,才能继续后续操作,避免因指令重排导致的后续的写操作提前到这个写操作之前;store-buffer
- 读屏障:主要用于保证
中的任务都被处理完成,才能继续后续操作;invalidate queues
- 全屏障:同时保证
和store-buffer
中的任务都被处理完成才能继续后续操作;invalidate queues
所以,对上述代码优化后就是如下情形,只需要在
cpu0
方法中插入写屏障,
cpu1
方法中插入读屏障即可。
长按识别关注,持续输出原创