CPU Cache伪共享问题

开发 架构
如果x和y中间加了128字节的填充,x和y不在同一个Cache line上,不同CPU之前不会影响,它俩都会频繁的命中自己的Cache,整个程序性能就会很高,这就是传说中的False Sharing问题。

先看下这两段代码:

代码段1:

const int row = 10240;
const int col = 10240;
int matrix[row][col];
int TestRow() {
//按行遍历
int sum_row = 0;
for (int r = 0; r < row; r++) {
for (int c = 0; c < col; c++) {
sum_row += matrix[r][c];
}
}
return sum_row;
}

代码段2:


int TestCol() {
//按列遍历
int sum_col = 0;
for (int c = 0; c < col; c++) {
for (int r = 0; r < row; r++) {
sum_col += matrix[r][c];
}
}
return sum_col;
}

两段代码的目的相同,都是为了计算矩阵中所有元素的总和。

但有些区别:一个是按行遍历元素做计算,一个是按列遍历元素做计算。

它俩的运行速度有什么区别吗?

如图:

图片

图片

图中可以看到,行遍历的代码速度比列遍历的代码速度快很多。

为什么按行遍历的代码比按列遍历的代码速度快?这里就是CPU Cache在起作用。

什么是CPU Cache?

可以先看下这个存储器相关的金字塔图:

图片

从下到上,空间虽然越来越小,但是处理速度越来越快,相应的,设备价格也越来越贵。

图中的寄存器和主存估计大家都知道,那中间的L1 、L2、L3是什么?它们起到了什么作用?

它们就是CPU 的Cache,如下图:

图片

可以理解为CPU Cache就是CPU与主存之间的桥梁。

当CPU想要访问主存中的元素时,会先查看Cache中是否存在,如果存在(称为Cache Hit),直接从Cache中获取,如果不存在(称为Cache Miss),才会从主存中获取。Cache的处理速度比主存快得多。

所以,如果每次访问数据时,都能直接从Cache中获取,整个程序的性能肯定会更高。

那,如何提高CPU Cache的命中率?

这里我不多介绍,感兴趣的直接移步到我这篇文章:https://mp.weixin.qq.com/s/iKWQZxn6XYKU9KnlBRynfg

但CPU Cache这里还有个小问题,看下这两段代码:

代码段1:


struct Point {
std::atomic<int> x;
// char a[128];
std::atomic<int> y;
};
void Test() {
Point point;
std::thread t1(
[](Point *point) {
for (int i = 0; i < 100000000; ++i) {
point->x += 1;
}
},
&point);
std::thread t2(
[](Point *point) {
for (int i = 0; i < 100000000; ++i) {
point->y += 1;
}
},
&point);
t1.join();
t2.join();
}

代码段2:

struct Point {
std::atomic<int> x;
char a[128];
std::atomic<int> y;
};
void Test() {
Point point;
std::thread t1(
[](Point *point) {
for (int i = 0; i < 100000000; ++i) {
point->x += 1;
}
},
&point);
std::thread t2(
[](Point *point) {
for (int i = 0; i < 100000000; ++i) {
point->y += 1;
}
},
&point);
t1.join();
t2.join();
}

两端代码的核心逻辑都是对Point结构体中的x和y不停+1。只有一点区别就是在中间塞了128字节的数组。

它们的执行速度却相差很大。

图片

图片

带128的比不带128的代码,执行速度快很多。

为什么?

看过我上面文章的同学应该就知道,每个CPU都有自己的L1和L2 Cache,而Cache line的大小一般是64字节,如果x和y之间没有128字节的填充,它俩就会在同一个Cache line上。

代码中开了两个线程,两个线程大概率会运行在不同的CPU上,每个CPU有自己的Cache。

当CPU1操作x时,会把y装载到Cache中,其他CPU对应的的Cache line失效。

然后CPU2加载y,会触发Cache Miss,它后面又把x装载到了自己的Cache中,其他CPU对应的Cache line失效。

然后CPU1操作x时,又触发Cache Miss。

它俩就会是大体这个流程:

图片

频繁的触发Cache Miss,导致程序的性能相当差。

而如果x和y中间加了128字节的填充,x和y不在同一个Cache line上,不同CPU之前不会影响,它俩都会频繁的命中自己的Cache,整个程序性能就会很高,这就是传说中的False Sharing问题。

所以我们写代码时,可以基于此做深一层思考,如果我们写单线程程序,最好保证访问的数据能够相邻,在一个Cache line上,可以尽可能的命中Cache。

如果写多线程程序,最好保证访问的数据有间隔,让它们不在一个Cache line上,减少False Sharing的频率。

上述内容源于前一段的技术分享,完整PPT在 一个优质的C++学习圈 里,来一起钻研C++吧。​

责任编辑:武晓燕 来源: 程序喵大人
相关推荐

2019-01-15 14:44:02

CPU Cache L共享存储器

2019-12-17 14:24:11

CPU缓存伪共享

2023-08-01 08:10:46

内存缓存

2022-02-02 21:50:25

底层伪共享CPU

2017-07-13 16:40:16

伪共享缓存行存储

2022-01-17 14:24:09

共享字节面试

2021-11-18 08:55:49

共享CPU内存

2013-07-30 10:46:39

CPU Cache并发

2017-08-23 13:21:31

2023-12-26 10:08:57

缓存伪共享修饰结构体

2021-03-01 11:53:15

面试伪共享CPU

2018-11-30 15:17:38

CPUCache缓存行

2021-06-30 21:13:49

CPUCache数据

2013-06-14 10:12:22

共享并行

2022-08-17 06:25:19

伪共享多线程

2023-08-02 09:28:28

计算机性能CPU

2023-09-07 14:04:58

计算机CPU内存

2019-01-04 10:53:59

CPUCache缓存

2015-08-17 14:53:44

2021-09-14 07:32:58

元宇宙VRAR
点赞
收藏

51CTO技术栈公众号