CPU缓存伪共享

2023-11-02 10:49:29 浏览数 (1)

CPU缓存什么东西?当然这个问题很多人有可能觉得比较傻,CPU缓存什么,肯定是缓存数据(代码)啊,要不然还能缓存啥,这个确实没问题,但是CPU到底缓存什么样的数据呢?因为对CPU来说,无论是指令,还是数据,都是数据,他如果要缓存,缓存的单位是啥?要缓存的内容是啥呢?

接下来咱们一点点解析这部分的内容,首先看一个比较有意思的代码

代码语言:javascript复制
#include <thread>
#include <iostream>
#include <sys/time.h>

//设置一个10亿的执行次数
#define MAX_NUM  1000000000

struct TestLine {
  int x;
  int y;
};
int GetTimeCost(const timeval &beg,const timeval &end) {
  return (end.tv_sec-beg.tv_sec) * 1000   (end.tv_usec-beg.tv_usec)/1000;
}
void T1Func(TestLine* tl){
  for( int it = 0; it< MAX_NUM ;   it) {
    tl->x  =1;
  }
  return;
}
void T2Func(TestLine *tl) {
  for ( int it = 0;it < MAX_NUM ;   it) {
    tl->y  =1;
  }
  return ;
}
int main(){
  timeval beg,end;
  gettimeofday(&beg,NULL);
  TestLine tl = {0,0};
  std::thread t1(T1Func,&tl);
  std::thread t2(T2Func,&tl);
  t1.join();
  t2.join();
  gettimeofday(&end,NULL);
  std::cout << "cost = "<< GetTimeCost(beg,end) << "ms ,x="<< tl.x << ",y="<< tl.y << std::endl;
  return 0;
}

这是一段很简单的程序,启动两个线程,分别对结构体中的x变量与y变量进行操作,循环10亿次,在大家看来,这段代码是两个线程分别对两个变量进行操作,其实是两者毫无关联的动作,我们执行一下这段代码,看下耗时

线程执行耗时

接下来咱们再看一下下面的代码

代码语言:javascript复制
#include <thread>
#include <iostream>
#include <sys/time.h>

//设置一个10亿的执行次数
#define MAX_NUM  1000000000

struct TestLine {
  int x;
  long long buf[8]; // 新增一个8个long long 类型的数组
  int y;
};
int GetTimeCost(const timeval &beg,const timeval &end) {
  return (end.tv_sec-beg.tv_sec) * 1000   (end.tv_usec-beg.tv_usec)/1000;
}
void T1Func(TestLine* tl){
  for( int it = 0; it< MAX_NUM ;   it) {
    tl->x  =1;
  }
  return;
}
void T2Func(TestLine *tl) {
  for ( int it = 0;it < MAX_NUM ;   it) {
    tl->y  =1;
  }
  return ;
}
int main(){
  timeval beg,end;
  gettimeofday(&beg,NULL);
  TestLine tl = {0,0};
  std::thread t1(T1Func,&tl);
  std::thread t2(T2Func,&tl);
  t1.join();
  t2.join();
  gettimeofday(&end,NULL);
  std::cout << "cost = "<< GetTimeCost(beg,end) << "ms ,x="<< tl.x << ",y="<< tl.y << std::endl;
  return 0;
}

这段代码与上面的那段代码最大的区别就是:在结构体中增加了一个8个long long类型的数组,接下来我们看下这段代码的执行情况:

注:以上代码是在C 11环境下进行编译运行,大家可以通过 g -lpthread -std=c 11 http://xxx.cc方式进行编译

大家可以看到,这段代码与上段代码最终x与y的输出是一致的,但是耗时上,这段代码要比第一段代码执行时间降低50%以上,为啥?

为什么我已经把数据缓存到本地了,通过增加一个数组,就可以提升程序的运行时间呢?

cache line(缓存行)

通过上面的一个小程序,大家是不是会存在很多疑惑,为啥只是增加了一个数组,整个程序的运行时间就大幅度提高了,下面我们来简单解释一下这里面的原因。

通过上文我们知道,CPU为了提升运行速度,是存在缓存的,但是CPU的缓存,到底缓存了啥呢?数据 指令

CPU的缓存单位是啥呢?cache line,cache line 到底是个啥东西呢?cache line 就是CPU执行时,从内存中读取内容的最小单位,那cache line大小是多少呢?一般是64个字节(当然不同的体系结构及厂商设定的cache line大小是不一样的),为啥是64个字节,不是其他值呢?哈哈,我也不知道,大概率就是测试出来64的性价比 性能是最优的吧。

典型的cache line结构如下:

tag用于标识这个缓存行,data字段用于存储实际的内容数据(这就是我们所说的64字节大小的部分),flag用于标记这个缓存行的状态

如何获取到系统的缓存行大小信息呢?

代码语言:javascript复制
getconf -a | grep CACHE

上述可以看到,计算机有三层缓存,并且每层缓存中的cache line都是64字节。

现在我们来解释一下上两段代码的差异吧。我们知道CPU缓存数据是以cache line为单位,并且每个cache line的大小大概是64个字节,我们就可以看出,在第一段代码中,结构体中的x成员与y成员,大概率会在同一个cache line 中,如果这两个线程分别对这个cache line进行操作,那么很有可能会造成读写这段cache line临界区的程序变成串行,因为两个线程同时操作一个cache line,肯定会存在覆盖写的问题,为了解决覆盖写,所以这段数据只能是串行(你写完之后,我在读取,然后我在写)这种模式

那再来分析一下第二段代码,第二段代码在两个变量中间增加了long long类型的数组,数组大小为8,为啥是这个数字内,因为上文中简单介绍了,一般cache line的大小是64个字节,当然大家也可以增加一个64个char型的数组,效果其实是一样的。当我们增加了这么第一段看似没有用的数据之后,我们就可以猜测出来,x与y,大概率不会再同一个cache line中了,所以这两个线程操作的是两个不同的cache line,完全可以实现线程的并行执行(因为已经不存在临界区的东西了),所以第二段代码的执行时间也是第一段代码的50%(可解释)

所以通过分析可以得出,CPU虽然把数据进行了缓存,但是这些缓存有时候并不能完全做到数据共享,而是有部分数据发生变化之后,其余CPU的数据也必须跟着发生变化,这就是所谓的CPU缓存的“伪共享”问题。

0 人点赞