cpu知识学习

本文深入探讨了CPU缓存的工作原理,包括L1、L2、L3缓存,缓存行和way的概念,以及MESI协议。通过实例解释了缓存缺失的原因,并提出了预取和补齐策略。伪共享(False Sharing)问题在多线程编程中尤为突出,当多个线程同时修改同一缓存行的不同部分时,会引发不必要的缓存同步,降低性能。文章通过代码示例展示了如何通过使用临时变量来避免伪共享,从而优化多线程程序的执行效率。
摘要由CSDN通过智能技术生成

收藏了coolshell 皓子写的程序员应该知道的cpu知识好久了,最近终于翻完了,顺便搜了几篇关于CPU缓存、伪共享的文章,在此记录一下。

cpu缓存时cpu核心处理器与内存之间的一个缓冲/缓存,一般的CPU有3级缓存,也就是L1、L2、L3,L1最快也最小,L3最大也最慢。

缓存行、8-way

cpu读取缓存的最下单位不是字节,而是缓存行cacheline,一个缓存行是64个字节(不同处理器有区别,这里是指主流CPU)

即使是有了缓存行,读取速度也不快,为了提高缓存命中率(快速判断内存地址的数据是否在缓存中),有了一个way的概念,一般cpu是8-way,也就是L1会被8等分,每一路是有64条cacheline。

为了方便索引内存地址,

  • Tag:每条 Cache Line 前都会有一个独立分配的 24 bits来存的 tag,其就是内存地址的前24bits
  • Index:内存地址后续的6个bits则是在这一Way的是Cache Line 索引,2^6 = 64 刚好可以索引64条Cache Line
  • Offset:再往后的6bits用于表示在Cache Line 里的偏移量

查找的时候先按照index查找,如果查找到了,那现在就查询范围就缩小到了每一个way里一个固定索引位置的数据,然后再遍历这个8个cacheline就行了。

cache miss的三种原因

1. 第一次访问数据, 在cache中根本不存在这条数据, 所以cache miss, 可以通过prefetch解决.
2. cache冲突, 需要通过补齐来解决.
3. 就是我示例的这种, cache满, 一般情况下我们需要减少操作的数据大小, 尽量按数据的物理顺序访问数据.

MESI协议

Modify 修改

exclusive 独占

share 共享

invalid 失效

缓存数据的四种状态,根据四种不同的状态在使用数据的时候做不同的操作,实际也很好理解。modify 说明数据被修改了,需要拿最新的数据。exclusive 是独占,说明没有其他的核在使用这个数据是可以放心使用的。invalid 是失效,说明这个数据已经是脏数据了。不能再用。 

当前操作

CPU0

CPU1

Memory

说明

1) CPU0 read(x)

x=1 (E)

x=1

只有一个CPU有 x 变量,
所以,状态是 Exclusive

2) CPU1 read(x)

x=1 (S)

x=1(S)

x=1

有两个CPU都读取 x 变量,
所以状态变成 Shared

3) CPU0 write(x,9)

x=9 (M)

x=1(I)

x=1

变量改变,在CPU0中状态
变成 Modified,在CPU1中
状态变成 Invalid

4) 变量 x 写回内存

x=9 (M)

X=1(I)

x=9

目前的状态不变

5) CPU1 read(x)

x=9 (S)

x=9(S)

x=9

变量同步到所有的Cache中,
状态回到Shared

MOESI协议

MOESI协议,Owner(宿主)-> 从宿主cpu读取最新数据,不需要从内存中读数据,从内存中读取更慢

伪共享

我们再来看一下另外一段代码:我们想统计一下一个数组中的奇数个数,但是这个数组太大了,我们希望可以用多线程来完成这个统计。下面的代码中,我们为每一个线程传入一个 id ,然后通过这个 id 来完成对应数组段的统计任务。这样可以加快整个处理速度

int total_size = 16 * 1024 * 1024; //数组长度

int* test_data = new test_data[total_size]; //数组

int nthread = 6; //线程数(因为我的机器是6核的)

int result[nthread]; //收集结果的数组

void thread_func (int id) {

result[id] = 0;

int chunk_size = total_size / nthread + 1;

int start = id * chunk_size;

int end = min(start + chunk_size, total_size);

for ( int i = start; i < end; ++i ) {

if (test_data[i] % 2 != 0 ) ++result[id];

}

}

然而,在执行过程中,你会发现,6个线程居然跑不过1个线程。因为根据上面的例子你知道 result[] 这个数组中的数据在一个Cache Line中,所以,所有的线程都会对这个 Cache Line 进行写操作,导致所有的线程都在不断地重新同步 result[] 所在的 Cache Line,所以,导致 6 个线程还跑不过一个线程的结果。这叫 False Sharing

优化也很简单,使用一个线程内的变量。

void thread_func (int id) {

result[id] = 0;

int chunk_size = total_size / nthread + 1;

int start = id * chunk_size;

int end = min(start + chunk_size, total_size);

int c = 0; //使用临时变量,没有cache line的同步了

for ( int i = start; i < end; ++i ) {

if (test_data[i] % 2 != 0 ) ++c;

}

result[id] = c;

}

我们把两个程序分别在 1 到 32 个线程上跑一下,得出的结果画一张图如下所示(横轴是线程数,纵轴是完成统的时间,单位是微秒):

上图中,我们可以看到,灰色的曲线就是第一种方法,橙色的就是第二种(用局部变量的)方法。当只有一个线程的时候,两个方法相当,基本没有什么差别,但是在线程数增加的时候的时候,你会发现,第二种方法的性能提高的非常快。直到到达6个线程的时候,开始变得稳定(前面说过,我的CPU是6核的)。而第一种方法无论加多少线程也没有办法超过第二种方法。因为第一种方法不是CPU Cache 友好的。也就是说,第二种方法,只要我的CPU核数足够多,就可以做到线性的性能扩展,让每一个CPU核都跑起来,而第一种则不能

 重点:使用临时变量避免cacheline共享问题

参考链接:

https://coolshell.cn/articles/20793.html

http://ifeve.com/from-javaeye-cpu-cache/

http://ifeve.com/from-javaeye-false-sharing/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值