CPU 缓存
为什么需要CPU cache?
CPU从内存读写的速度与CPU处理速度间差异过大,引入寄存器作为缓存,能提高性能;
常见的有三级缓存,即L1cache
、L2cache
、L3cache
,一般都采用SRAM
CPU - Cache - 内存的结构图?
可以看到每个CPU核心有自己的L1、L2寄存器,L3寄存器在各个核心间共用。
CPU cache的数据结构与读取过程?
CPU cache缓存数据的基本单位是缓存行Cache Line,每个CacheLine由标志 Tag + 数据块 DataBlock组成;其中 Tag 主要用来缓存映射、标志该缓存行的状态;
- 对于
缓存行号->内存块号
的缓存映射,我们分别需要一个 索引 和 组号 来完成映射:组号*缓存行数+索引= 内存块号(以直接映射为例); - 用一位有效位来 代表当前缓存行是否有效;
读取的过程:
- CPU从cache读取的基本单位是字,于是内存地址需要多一个 offset 保存当前行的偏移量
- 看Tag中的有效位,若为1,则返回对应offset的字
- 若为0,则需要到内存读取一个缓存行
- 根据映射方式找到对应的内存块,读取到cache作为缓存行,返回对应offset的字
CPU如何向CPU cache写入数据?
主要有两种方式,其中第二种广泛采用
写直达
检查cache中是否有对应数据(映射定位)
- 无,直接写入内存
- 有,先写入cache,再写入内存
可以看出这种方式需要频繁地将数据写入内存,性能不够好
写回
这种方式引入了 脏缓存行 的概念
检查cache中是否有对应数据(映射定位)
- 有:写入cache,并标志其为脏缓存行
- 无:
- 若当前缓存行是脏缓存行,则将其写回内存
- 从内存读取对应数据块到cache作为缓存行
- 将该缓存行标记为脏缓存行
CPU缓存一致性问题
在多核CPU以及写回策略的情况下,引入L1、L2 cache
后会出现缓存与内存间数据一致性的问题
要解决这个问题,需要保证两个点:
- 写传播:当前CPU核心的写操作会通知其他的CPU核心
- 事务串行化:各个核心观察到的对数据的操作顺序一致
总线嗅探
实现写传播可以通过总线来实现,每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面,如果 某个 CPU 核心的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache上;
缺点:
- 不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,加重总线的负载
- 未实现事务的串行化
MESI协议
在总线嗅探的基础上是用状态机的思想改进;
MESI四种状态:
- Modified:已修改,修改数据不需要广播
- Exclusive:独占,修改数据不需要广播
- Shared:共享,修改数据发送广播让其他核心的cache行都失效
- Invalidated:已失效,修改数据需要重新从内存读取
伪共享问题
问题分析
主要原因是【数据所在行的其他数据修改而导致的缓存失效】,案例如下
假设现在又两个线程T1、T2分别对内存上相邻的两个变量A、B进行读写,核心1绑定了线程T1,核心2绑定了线程T2;
- 线程T1先读取变量A,则核心1的cache从内存读取到缓存行,发送广播并未收到回应,状态为 Exclusive
- 此时线程T2读取变量B,则核心2的cache从内存读取到缓存行,发送广播并收到回应,于是状态为 Shared
- 之后T1修改变量A,当前为共享状态,则发送广播让核心2的缓存行失效
- 再之后T2修改变量B,发现当前缓存行失效,则去内存重新读取,修改后发送广播,让T1失效…


这样会让缓存命中率大大降低,效率大打折扣。
解决伪共享问题
主要思想是字节填充
- 手动在类前后填充多个final的long数据
- 是用提供的
@sun.msic.Contended
注解,默认在前后填充128字节数据
这样做可以让两个对象一定不会出现在同一缓存行,处于 Exclusive独占 状态,能解决因为所在行的其他数据修改而缓存失效的问题!
参考资料
Q.E.D.