加载中...
知识点整理(十)——cpu的缓存一致性和伪共享
发表于:2021-09-18 | 分类: 程序人生
字数统计: 2.2k | 阅读时长: 7分钟 | 阅读量:

L1、L2、L3、内存

要了解cache的缓存一致性和伪共享问题,就要先了解L1 cache 、L2 cache 、L3 cache和内存之间的关系。

通常cpu内有3级缓存,即L1、L2、L3缓存。其中L1缓存分为数据缓存和指令缓存,cpu先从L1缓存中获取指令和数据,如果L1缓存中不存在,那就从L2缓存中获取。每个cpu核心都拥有属于自己的L1缓存和L2缓存。如果数据不在L2缓存中,那就从L3缓存中获取。而L3缓存就是所有cpu核心共用的。如果数据也不在L3缓存中,那就从内存中获取了。当然,如果内存中也没有那就只能从硬盘中获取了。

为什么要设计成这个样子呢?因为从不同的设备中获取数据的耗时是不一样的。

设备(模块) 硬件类型 成本(美元/MB) 通常大小 随机读取耗时
L1 cache SRAM 7 几十到几百KB 2~4个周期 1纳秒左右
L2 cache SRAM 7 几百KB到几MB 10~20个周期 5纳秒左右
L3 cache SRAM 7 几MB到几十MB 20~60个周期 10纳秒左右
内存 DRAM 0.015 几GB至几十GB 200~300个周期 100纳秒左右
固态硬盘 SSD 0.0004 几百GB至几TB 1000倍内存耗时 150微妙左右
机械硬盘 HDD 0.00004 几百GB至几十TB 10~100倍固态硬盘耗时 10毫秒左右

L1、L2、L3 都是SRAM(静态内存),只要通电就一直储存数据。通常由6个晶体管组成,电路比较简单,所以访问速度快,但密度不高,所以同样物理空间下能存储的数据有限。
其中L1距离cpu最近,所以效率最高,几乎24个时钟周期就能访问到数据。而L3距离cpu较远,访问需要2060个时钟周期。

内存在cpu外部,属于DRAM(动态内存),一般使用一个晶体管和一个电容,因为电容会漏电,所以需要定时刷新电容,所以被称为动态内存。数据访问和刷新的电路都比较复杂,所以访问速度会比较慢。通常在100纳秒左右。

SSD(固态硬盘),使用类似内存的结构,但是不再需要刷新,所以断电后数据还在,同时成本较低。随机读取大约在一百微妙左右。

HDD(机械硬盘),通过磁头定位和读写,所以随机读写速度更慢。

cache的读取和写入

读取

数据是怎么从内存读取到cpu cache中的呢?
如果程序需要读取一个a变量,cpu cache并不是只从内存中读取一个a变量的数据。
在L1、L2、L3中,通常定义64字节为一个cache line,当cache中不存在数据,需要从内存中读取时,也是每次至少读取一个cache line的数据加载到cache中。

为什么这么设计呢?其一,每次只读写1个字节或者4个字节(int)有点浪费。内存速度要比cpu慢很多,中间需要经过各种环节(设置内存地址,控制总线等等)只为了取1个字节的数据太过浪费。其二,很多情况下我们访问了一个数据,很有可能会立刻访问附近的数据。(例如数组等顺序申明的结构)

例如定义一个int[16] a ,那cpu取a[0]的时候,会顺序取入64个字节的数据,于是接下来遍历a数组的时候,由于cpu cache中已经有了,就不用再频繁从内存中获取了。

写入

读取可以通过cache line来提高效率,那修改数据怎么办呢?如果每次都写入内存,那效率又会降低。

其实cpu在写入数据时,只是修改了cache中的数据,并标记为”脏”状态。等待cache中的数据需要被覆盖时,再将cache中的数据写回内存。这样有2个好处,如果频繁修改某个变量,cpu不会一直不停的回写内存,而只是修修改cache,真正写回可能只发生一次。其二,修改顺序数据的时候也能将多次修改合并成一次写回。(例如数组等顺序申明的结构)

缓存一致性问题

当cpu有多个内核的时候,由于L1、L2缓存是每个内核各自独有的,这就造成了可能内核1修改了某个数据,但内核2根本不知道。

例如,我们有一个a变量值为1。首先内核1读取了这个变量,并修改为2。由于修改只发生在L1的cache中,并没有触发策略写回内存。接着内核2也读取这个变量,此时内存的a还是1,于是内核2读取到了旧值。

为解决缓存一致性问题,就有了MESI协议。

M(Modified 已修改)
E(Exclusive 独占)
S(Shared 共享)
I(Invalidated 已失效)

就是用这四个状态来标记cache line的四个不同的状态。
已修改,就是cache中的数据被修改了,但还没有写回到内存里。已失效表示这个数据不能使用,不可以读取数据。独占和共享代表数据是干净的,和内存一直的。区别在于独占表示只有自己内核里有这个数据,共享表示还有其他内核里有这个数据。

当前状态 动作 处理
I(已失效) Local Read 其他内核无数据,则自己标记为E(独占)
其他内核有数据并且状态为M(已修改),则先把数据写回内存,然后读取内存数据标记自己和其他内核状态为S(共享)
其他内核有数据,状态为E(独占)或者S(共享),则自己读取后标记自己和其他内核状态为S(共享)
I(已失效) Local Write 其他内核无数据,则自己标记为M(已修改)
其他内核有数据并且状态为M(已修改),则先把数据写回内存,然后读取并修改数据标记自己状态为M(已修改),标记其他内核数据为I(已失效)
其他内核有数据,状态为E(独占)或者S(共享),则自己读取并修改数据后标记自己状态为M(已修改),标记其他内核数据为I(已失效)
I(已失效) Remote Read 自身内核数据已失效,其他内核读取数据不影响,状态不变
I(已失效) Remote Write 自身内核数据已失效,其他内核修改数据不影响,状态不变
E(独占) Local Read 直接读取cache中数据,状态不变
E(独占) Local Write 直接修改cache中数据,状态变成M(已修改)
E(独占) Remote Read 状态修改为S(共享)
E(独占) Remote Write 状态修改为I(已失效)
S(共享) Local Read 直接读取cache中数据,状态不变
S(共享) Local Write 直接修改cache中数据,状态变成M(已修改),其他核心共享状态变成I(已失效)
S(共享) Remote Read 状态不变
S(共享) Remote Write 状态修改为I(已失效)
M(已修改) Local Read 直接读取cache中数据,状态不变
M(已修改) Local Write 直接修改cache中数据,状态不变
M(已修改) Remote Read 把数据写入内存,状态变成S(共享)
M(已修改) Remote Write 先写入内存,然后其他内核读取修改,自己内核状态变为I(已失效)

伪共享

cache无论是读取、写入还是按照MESI协议标记,最小的颗粒都是cache line(通常64字节)。但是如果我们有2个变量a和b,分别在不同的cache line中,内核1只读取修改a变量,内核2只读取修改b变量。按照MESI协议,会频繁的发生标记失效,写回内存,重新读取操作。但实际上程序并没有让2个内核竞争同一个资源。

  • 如何避免?

将多个频繁访问的变量设置成不同的cache line上。linux中使用__cacheline_aligned_in_smp宏定义。
Java 并发框架 Disruptor 使⽤「字节填充 + 继承」的⽅式,来避免伪共享的问题。

p1~p7 是为了填充cache line的,加上真正使用的long变量,一共正好8个long类型,64个字节。防止不同的变量分配给同一个cache line上。

上一篇:
知识点整理(十一)——DH秘钥交换协议
下一篇:
知识点整理(九)——mysql的MVCC
本文目录
本文目录