缓存命中

缓存命中

游戏开发者可分为两类:在他们的游戏引擎中使用STL模板库之类的。以及不使用的。一些开发者认为STL内存分配模式(memory allocation pattern)不高效,也导致内存碎片问题,使STL不能在游戏中使用。一些开发者认为STL的强大和方便超过它的问题,而且大部分的问题还是可以变通解决,笔者个人认为STL在PC上可以无障碍使用。因为PC上可以无障碍使用虚拟内存(virtual memory)系统,谨慎的分配内存变得不那么紧要(虽然游戏开发者仍要非常谨慎)。在游戏主机上,只有有限(甚至没有)虚拟内存功能,而且内存命中失败(cache miss)的代价极高,游戏开发者最好编写自定义的数据结构,保证是可预期或者有限的内存分配模式。(在PC上做同样的事情,肯定也不也错不了。)【以上内容来自《游戏引擎架构》第29页】

什么是Cache?

Cache Memory也被称为Cache,是存储器子系统的组成部分,存放着程序经常使用的指令和数据,这就是Cache的传统定义。从广义的角度上看,Cache是快设备为了缓解访问慢设备延时的预留的Buffer,从而可以在掩盖访问延时的同时,尽可能地提高数据传输率。 快和慢是一个相对概念,与微架构(Microarchitecture)中的 L1/L2/L3 Cache相比, DDR内存是一个慢速设备;在磁盘 I/O 系统中,DDR却是快速设备,在磁盘 I/O 系统中,仍在使用DDR内存作为磁介质的Cache。在一个微架构中,除了有L1/L2/L3 Cache之外,用于虚实地址转换的各级TLB, MOB( Memory Ordering Buffers)、在指令流水线中的ROB,Register File和BTB等等也是一种Cache。我们这里的Cache,是狭义 Cache,是CPU流水线和主存储器的 L1/L2/L3 Cache。

好处是什么

提高计算机的运行速度。只要能存储数据的器件都可以称之为存储器,它的含义覆盖了寄存器,缓存,内存,硬盘。cpu访问快慢的速度依次为寄存器-> 缓存->内存->硬盘。
寄存器是中央处理器的组成部分,是一种直接整合到cpu中的有限的高速访问速度的存储器,它是有一些与非门组合组成的,分为通用寄存器和特殊寄存器。cpu访问寄存器的速度是最快的。那为什么我们不把数据都存储到寄存器中呢,因为寄存器是一种容量有限的存储器,并且非常小。因此只把一些计算机的指令等一些计算机频繁用到的数据存储在其中,来提高计算机的运行速度。
缓存其实是内存中高速缓存(cache),它之所以存在,是因为当cpu要频繁访问内存中的一些数据时,如果每次都从内存中去读,花费的时间会更多,因此在寄存器和内存之间有了缓存,把cpu要频繁访问的一些数据存储在缓冲中,这样效率就会更高,但需要注意的是,缓冲的大小也是很小的,不能存放大量的数据,并且缓存中存放的数据会因为cpu的访问而被替代,必须某个数据开始被cpu频繁访问,但后来不再频繁,那这个数据的空间会被其他访问频繁的数据所占据(那些数据会被暂时存储在缓存中是算法问题)。缓存又可以分为一级和二级缓存,一级的速度大一二级的速度。因此cpu在访问数据时,先到缓存中看有没有,没有的话再到内存中读取。

作为一个程序员,你需要理解存储器层次结构,因为它对应用程序的性能有着巨大的影响。如果你的程序需要的数据存储在CPU寄存器中的,那么在指令的执行期间,在零个周期内就能访问到它们。如果存储在高速缓存中,需要1~30个周期。如果存储在主存中,需要50~200个周期。而如果存储在磁盘上,需要大约几千万个周期。【以上内容来自《深入理解计算机系统(Computer Systems)》第382页】

如何知道Cache大小

如何知道自己CPU的L2、L3的容量多大呢?当然可以用CPU-z,但其实可以有个更加简单的办法,在命令行输入:
wmic cpu get L2CacheSize,L3CacheSize
我的笔记本得到这个结果:

怎么去写出缓存友好的代码

  • 如上一篇文章中struct 结构体类型的大小计算
    1
    2
    3
    4
    5
    6
    struct stu3
    {
      char c1;// 偏移量为0符合要求,首位本身不需要偏移。
      int i; // 偏移量为4, 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍),所以1+3
      char c2;// 偏移量为8(偏移量4+int大小4),符合要求
    }

总长度不考虑自身对齐的最大的那个值的倍数,所以长度是1+3+4+1 = 9,考虑倍数长度是12
优化后

1
2
3
4
5
6
struct stu3
{
  char c1;// 偏移量为0符合要求,首位本身不需要偏移。
char c2;// 偏移量为1(偏移量1+char大小1),符合要求
  int i; // 偏移量为4, 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍),所以
}

优化后大小是1+1+2+4= 8。

  • 让最常见的情况运行得快。大部分时间都花在少量的核心函数上,这些函数常把大部分时间都花在少量循环上。
  • 在每个循环内部缓存不命中数量最小。
    举例数组循环
    1
    2
    3
    4
    5
    6
    7
    8
    int sumarraycols(int a[M][N])
    {
    int i,j,sum =0;
    for(i=0;i<M;i++)
    for(j=0;j<N;j++)
    sum += a[i][j];
    return sum;
    }
1
2
3
4
5
6
7
8
int sumarraycols(int a[M][N])
{
int i,j,sum =0;
for(j=0;j<N;j++)
for(i=0;i<M;i++)
sum += a[i][j];
return sum;
}

第一个写法是常见的写法,缓存比较优好。第二个是修改后的。差异在于第一个是连续的内存块。第二个是每隔一段取一块。第二种miss的概率更高。Cache是CacheLine组成的。
——————————

参考

文章目录
  1. 1. 缓存命中
  2. 2. 什么是Cache?
  3. 3. 好处是什么
  4. 4. 如何知道Cache大小
  5. 5. 怎么去写出缓存友好的代码
  6. 6. 参考
|