CPU 缓存

为什么需要CPU cache?

CPU从内存读写的速度与CPU处理速度间差异过大,引入寄存器作为缓存,能提高性能;

常见的有三级缓存,即L1cacheL2cacheL3cache,一般都采用SRAM

CPU - Cache - 内存的结构图?

image-20220912172456566

可以看到每个CPU核心有自己的L1、L2寄存器,L3寄存器在各个核心间共用。

CPU cache的数据结构与读取过程?

CPU cache缓存数据的基本单位是缓存行Cache Line,每个CacheLine由标志 Tag + 数据块 DataBlock组成;其中 Tag 主要用来缓存映射、标志该缓存行的状态

  • 对于缓存行号->内存块号的缓存映射,我们分别需要一个 索引组号 来完成映射:组号*缓存行数+索引= 内存块号(以直接映射为例);
  • 用一位有效位来 代表当前缓存行是否有效;

读取的过程:

  • CPU从cache读取的基本单位是,于是内存地址需要多一个 offset 保存当前行的偏移量
  • 看Tag中的有效位,若为1,则返回对应offset的字
  • 若为0,则需要到内存读取一个缓存行
  • 根据映射方式找到对应的内存块,读取到cache作为缓存行,返回对应offset的字

image-20220912175823699

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;

  1. 线程T1先读取变量A,则核心1的cache从内存读取到缓存行,发送广播并未收到回应,状态为 Exclusive
  2. 此时线程T2读取变量B,则核心2的cache从内存读取到缓存行,发送广播并收到回应,于是状态为 Shared
  3. 之后T1修改变量A,当前为共享状态,则发送广播让核心2的缓存行失效
  4. 再之后T2修改变量B,发现当前缓存行失效,则去内存重新读取,修改后发送广播,让T1失效
image-20220912183712246 image-20220912183742971

这样会让缓存命中率大大降低,效率大打折扣。

解决伪共享问题

主要思想是字节填充

  • 手动在类前后填充多个final的long数据
  • 是用提供的@sun.msic.Contended注解,默认在前后填充128字节数据

这样做可以让两个对象一定不会出现在同一缓存行,处于 Exclusive独占 状态,能解决因为所在行的其他数据修改而缓存失效的问题!

参考资料

Q.E.D.


记录 • 分享 • 日常