CPU cacheline

发布于:2024-12-18 ⋅ 阅读:(158) ⋅ 点赞:(0)

一、CPU 如何读写数据的

先来认识CPU的架构,只有理解了CPU的架构,才能更好地理解CPU是如何读写数据的,对于现代的CPU架构图如下:

可以看到,一个CPU里通常会有多个CPU核心,比如上图中的1号和2号核心,并且每个CPU核心都有自己的L1 Cache和L2 Cache,而L1 Cache通常分为Dcache(数据缓存)和Icache(指令缓存),L3 Cache则是多个核心共享的,这就是CPU典型的缓存层次。

上面提到的都是CPU内部的Cache,放眼外部的话,还会有内存和硬盘,这些存储设备共同构成了金字塔存储层次。如下图所示:

从上图也可以看到,从上往下,存储设备的容量会越大,而访问速度会越慢。至于每个存储设备的访问延时,可以看下图的表格:

可以看到,CPU访问L1 Cache速度比访问内存快100倍,这就是为什么CPU里面会有L1~L3 Cache的原因,目的就是把Cache作为CPU与内存之间的缓存层,以减少对内存的访问频率。

CPU从内存中读取数据到Cache的时候,并不是一个字节一个字节读取,而是一块一块的方式来读取数据的,这一块一块的数据被称为CPU Cache Line(缓存行),所以CPU Cache Line是CPU从内存读取数据到Cache的单位。

至于CPU Cache Line大小,在Linux系统中可以用下面的方式查看到:

那么对数组的加载,CPU就会加载数组里面连续的多个数据到Cache里,因此我们应该按照物理内存地址分布的顺序去访问元素,这样访问数组元素的时候,Cache命中率就会很高,于是就能减少从内存读取数据的频率,从而可提高程序的性能。

但是,在我们不适用数组,而是使用单独的变量的时候,则会有Cache伪共享的问题,Cache伪共享问题上是一个性能杀手,我们应该要规避它。

接下来,就来看看Cache伪共享是什么?又如何规避这个问题?

现在假设有一个双核心的CPU,这两个CPU核心并行运行着两个不同的线程,它们同时从内存中读取两个不同的数据,分别是类型为long的变量A和B,这两个数据的地址在物理内存上是连续的,如果Cache Line的大小是64字节,并且变量A在Cache Line的开头位置,那么这两个数据是位于同一个Cache Line中,又因为CPU Cache Line是CPU从内存中读取数据到Cache的单位,所以这两个数据会被同时读入到了两个CPU核心各自Cache中。

我们来思考一个问题,如果这两个不同核心的线程分别修改不同的数据,比如1号CPU的核心的线程只修改了变量A,或2号CPU核心的现场只修改了变量B,会发生什么呢?

二、分析伪共享的问题

现在我们结合保证多核缓存一致的MESI协议,来说明这一整个的过程。

1、最开始变量A和B都还不在Cache里面,假设1号核心绑定了线程A,2号核心绑定了线程B,线程A只会读写变量A,线程B只会读写变量B。

2、1号核心读取变量A,由于CPU从内存读取数据到Cache的单位是Cache Line,也正好变量A和变量B的数据归属于同一个Cache Line,所以A和B的数据都会被加载到Cache,并将次Cache Line标记为【独占】状态。

3、接着,2号核心开始从内存里读取变量B,同样的也是读取Cache Line大小的数据到Cache中,此Cache Line中的数据也包含了变量A和B,此时1号和2号核心的Cache Line状态变为【共享】状态。

4、1号核心需要修改变量A,发现此Cache Line的状态是【共享】状态,所以先需要通过总线发送消息给2号核心,通知2号核心把Cache中对应的Cache Line标记为【已失效】状态,然后1号核心对应的Cache Line状态变成【已修改】状态,并且修改变量A。

5、之后,2号核心需要修改变量B,此时2号核心的Cache中对应的Cache Line是【已失效】状态,另外由于1号核心的Cache也有此相同的数据,且状态为【已修改】状态,所以要先把1号核心的Cache对应的Cache Line写回到内存,然后2号核心再从内存读取Cache Line大小的数据到Cache中,最后把变量B修改到2号核心的Cache中,并将状态标记为【已修改】状态。

所以,可以发现如果1号和2号CPU核心这样持续交替的分别修改变量A和B,就会重复4和5这两个步骤,Cache并没有起到缓存的效果,虽然变量A和B之间其实并没有任何的关系,但是因为同时归属于一个Cache Line,这个Cache Line中的任意数据被修改后,都会相互影响,从而出现4和5这两个步骤。

因此,这种以为多个线程同时读写同一个Cache Line的不同变量时,而导致CPU Cache失效的现象称为伪共享。

三、避免伪共享的方法

一次,对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个Cache Line中,否则就会出现伪共享的问题。

接下来,看看在实际项目中是用什么方式来避免伪共享的问题的。

在Linux内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。

从上面的宏定义,我们可以看到:

  1. 如果在多个系统里,该宏定义是 __cacheline_aligned,也就是Cache Line的大小;
  2. 而如果在单核系统里,该宏定义是空的;

因此,针对在同一个Cache Line中共享的数据,如果在多核之间竞争比较严重,为了防止伪共享现象的发生,可以采用上面的宏定义使得变量在Cache Line里是对齐的。

举个例子,有下面这个结构体:

结构体里的两个成员变量a和b在物理内存地址上是连续的,于是它们可能会位于同一个Cache Line中,如下图:

所以,为了防止前面提到的Cache伪共享问题,我们可以使用上面介绍的宏定义,将b的地址设置为Cache Line对齐地址,如下:

这样a和b变量就不会再同一个Cache Line中了,如下图:

所以,避免Cache伪共享实际上是用空间换时间的思想,浪费一部分Cache空间,从而换来性能的提升。