转 Cache一致性和内存模型

本文深入探讨了CPU Cache的结构、一致性问题,包括Cache一致性协议MESI、Store Buffer、Store Forwarding和Memory Barrier。通过实例解释了内存屏障在解决数据竞争和指令重排序中的作用,并介绍了编程中volatile、atomic和mutex的用法,强调了原子性、可见性和顺序性在并发编程中的重要性。
摘要由CSDN通过智能技术生成

卢本伟牛逼,写得很好

https://wudaijun.com/2019/04/cpu-cache-and-memory-model/

 

 

本文主要谈谈CPU Cache的设计,内存屏障的原理和用法,最后简单聊聊内存一致性。

我们都知道存储器是分层级的,从CPU寄存器到硬盘,越靠近CPU的存储器越小越快,离CPU越远的存储器越大但越慢,即所谓存储器层级(Memory Hierarchy)。以下是计算机内各种存储器的容量和访问速度的典型值。

存储器类型 容量 特性 速度
CPU寄存器 几十到几百Bytes 数据电路触发器,断电丢失数据 一纳秒甚至更低
Cache 分不同层级,几十KB到几MB SRAM,断电丢失数据 几纳秒到几十纳秒
内存 几百M到几十G DRAM,断电丢失数据 几百纳秒
固态硬盘(SDD) 几十到几百G SSD,断电不丢失数据 几十微秒
机械硬盘(HDD) 上百G 磁性介质和磁头,断电不丢失数据 几毫秒

从广义的概念上来说,所有的存储器都是其下一级存储器的Cache,CPU Cache缓存的是内存数据,内存缓存的是硬盘数据,而硬盘缓存的则是网络中的数据。本文只谈CPU Cache,一个简单的CPU Cache示意图如下:

图中忽略了一些细节,现代的CPU Cache通常分为三层,分别叫L1,L2,L3 Cache, 其中L1,L2 Cache为每个CPU核特有,L3为所有CPU核共有,L1还分为缓存指令的i-cache(只读)和缓存程序数据的d-cache,L2 L3 Cache则不区分指令和程序数据,称为统一缓存(unified cache)。本文主要讨论缓存命中和缓存一致性的问题,因此我们只关注L1 Cache,不区分指令缓存和程序数据缓存。

Cache Geometry

当CPU加载某个地址上的数据时,会从Cache中查找,Cache由多个Cache Line构成(通常L1 Cache的Cache Line大小为64字节),因此目标地址必须通过某种转换来映射对应的Cache Line,我们可以很容易想到两种方案:

  1. 指定地址映射到指定Cache Line,读Cache时对地址哈希(通常是按照Cache Line数量取模,即在二进制地址中取中间位)来定位Cache Line,写Cache时如果有冲突则丢掉老的数据。这种策略叫直接映射
  2. 任何地址都可以映射到任何Cache Line,读Cache时遍历所有Cache Line查找地址,写Cache时,可以按照LFU(最不常使用)或LRU(最近最少使用)等策略来替换。这种策略叫全相联

直接映射的缺点在于在特定的代码容易发生冲突不命中,假设某CPU Cache的Cache Line大小为16字节,一共2个Cache Line,有以下求向量点乘的代码:

1
2
3
4
5
6
7
8
9
// 代码片段1
float dotprod(float x[8], float y[8])
{
float sum = 0.0;
int i;
for (i = 0; i < 8; i ++)
sum += x[i] * y[i];
return sum;
}

由于x和y在函数栈中是连续存放的,x[0..3]y[0..3]将映射到同一个Cache Line, x[4..7]y[4..7]被映射到同一个Cache Line,那么在for循环一次读取x[i],y[i]的过程中,Cache Line将不断被冲突替换,导致Cache “抖动”(thrashing)。也就是说,在直接映射中,即使程序看起来局部性良好,也不一定能充分利用Cache。

那么同样的例子,换成全相联,则不会有这个问题,因为LRU算法会使得y[0..3]不会替换x[0..3]所在的Cache Line,也就不会造成Cache抖动。全相连的缺点是由于每一次读Cache都需要遍历所有的Cache Line进行地址匹配,出于效率考虑,它不适用于太大的Cache。

So,现代OS的操作系统是取两者折中,即组相连结构: 将若干Cache Line分为S个组,组间直接映射,组内全相连,如下图:

通用的Cache映射策略,将目标地址分为t(标记位),s(组索引),b(块偏移)三个部分。我在Linux Perf 简单试用中也有例子说明程序局部性对效率的影响。

Cache Coherency

前面我们谈的主要是Cache的映射策略,Cache设计的最大难点其实在于Cache一致性: 即所有CPU看到的指定地址的值是一致的。比如在CPU尝试修改某个地址值时,其它CPU可能已有该地址的缓存,甚至可能也在执行修改操作。因此该CPU需要先征求其它CPU的”同意”,才能执行操作。这需要给各个CPU的Cache Line加一些标记(状态),辅以CPU之间的通信机制(事件)来完成, 这可以通过MESI协议来完成。MESI是以下四个状态的简称:

M(modified): 该行刚被 CPU 改过,并且保证不会出现在其它CPU的Cache Line中。即CPU是该行的所有者。CPU持有该行的唯一正确参照。
E(exclusive): 和M类似,但是未被修改,即和内存是一致的,CPU可直接对该行执行修改(修改之后为modified状态)。
S(shared): 该行内容至少被一个其它CPU共享,因此该CPU不能直接修改该行。而需要先与其它CPU协商。
I(invalid): 该行为无效行,即为空行,前面提到Cache策略会优先填充Invalid行。

除了状态之外,CPU还需要一些消息机制:

Read: CPU发起读取数据请求,请求中包含需要读取的数据地址。
Read Response: 作为Read消息的响应,该消息可能是内存响应的,也可能是某CPU响应的(比如该地址在某CPU Cache Line中为Modified状态,该CPU必须返回该地址的最新数据)。
Invalidate: 该消息包含需要失效的地址,所有的其它CPU需要将对应Cache置为Invalid状态
Invalidate Ack: 收到Invalidate消息的CPU在将对应Cache置为Invalid后,返回Invalid Ack
Read Invalidate: 相当于Read消息+Invalidate消息,即取得数据并且独占它,将收到一个Read Response和所有其它CPU的Invalid Ack
Writeback: 写回消息,即将状态为Modified的行写回到内存,通常在该行将被替换时使用。现代CPU Cache基本都采用”写回(Write Back)”而非”直写(Write Through)”的方式。

1
思考: 为什么要有专门的Read Invalidate消息,而不直接用Read + Inv
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值