DPDK
首发于DPDK
存储器层次结构(二):局部性与存储器层次结构

存储器层次结构(二):局部性与存储器层次结构

一、局部性(Principle Of Locality):

1、定义

局部性原理(Principle Of Locality):计算机程序倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。

局部性两种形式:时间局部性(Temporal Locality)和空间局部性(Spatial Locality)。

时间局部性(Temporal Locality):被引用过一次的内存位置很可能在不远的将来再被多次引用。

空间局部性(Spatial Locality):一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。

2、对程序数据引用的局部性

如下一个对二维数组元素求和函数的sumarrayrows,

int sumarrayrows(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;
}
数组a的引用模式(M=2,N=3)

双重嵌套循环按照行优先顺序读取数组元素。也就是,内层循环读取第一行的元素,然后读第二行,以此类推。函数sumarrayrows具有良好的空间局限性,因为它按照数组被存储的顺序来访问这个数组。


如下一个对二维数组元素求和函数的sumarraycols,

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;
}
数组a的引用模式(M=2,N=3)

函数sumarraycols的局部性很差,因为它按照列顺序来扫描数组而C语言的数组在内存中是按照行来顺序存放的。

3、取指令的局部性

对于取指令来说,循环有好的时间和空间局部性。循环体越小、循环迭代次数越多,局部性越好。

二、存储器层次结构

一个典型的存储器层次结构,从高层往底层走,存储设备变得更慢、更便宜和更大。在最高层次(L0),是少量快速的CPU寄存器,CPU可以在一个时钟周期内访问它们。接下来是一个或多个小型到中型的基于SRAM的高速缓存存储器,可以在几个CPU时钟周期内访问它们。然后是一个大的基于DRAM的主存,可以在几十到几百个时钟周期内访问它们。接下来是慢速但是容量很大的本地磁盘。最后,有些系统甚至包括一层附加的远程服务器上的磁盘,要通过网络来访问它们。

存储器层次结构

三、存储器层次结构中的缓存

存储器层次结构的中心思想是,对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一层都缓存来自较低一层的数据对象。例如,本地磁盘作为通过网络从远程磁盘取出的文件的缓存,主存作为本地磁盘上数据的缓存,以此类推,直到最小的缓存——CPU寄存器。

如下图,第k+1层的存储器被划分成连续的数据对象组块(chunk),称为块(block)。每个块都有一个唯一的地址或名字,使之区别于其他的块。图中第k+1层存储器被划分为16个大小固定的块,编号为0~15。类似的,第k层的存储器被划分成较少的块的集合,每个块的大小与k+1层的块的大小一样。任何时刻,第k层的缓存包含第k+1层块的一个子集副本。

数据总是以块大小为传送单位在第k层和第k+1层之间来回复制。虽然在层次结构中任何一对相邻的层次之间块大小是固定的,但是其他的层次对之间可以有不同的块大小。一般而言,层次结构中较低层设备的访问时间较长,为了补偿这些较长的访问时间,倾向于使用较大的块。

1、缓存命中

当程序访问第k+1层的某个数据对象d时,它首先在当前存储在第k层的一块中查找d。如果d刚好缓存在第k层中,那么就称为缓存命中(cache hit)。程序直接从第k层读取d,根据存储器层次结构的性质,这要比从第k+1层读取d更快。

2、缓存不命中

如果第k层中没有缓存数据对象d,那么被称为缓存不命中(cache miss)。当发生缓存不命中时,第k层的缓存从第k+1层缓存中取出包含d的那个块,如果第k层的缓存已经满了,可能就会覆盖现存的一个快。

覆盖一个现存的块的过程称为替换或驱逐这个块。被驱逐的这个块称为牺牲块。决定该替换哪个块是由缓存的替换策略来控制的。例如,一个具有随机替换策略的缓存会随机选择一个牺牲块。一个具有最近最少被使用(LRU)替换策略的缓存会选择那个最后被访问的时间距现在最远的块。

3、缓存不命中的种类

冷缓存(cold cache):如果第k层的缓存是空的,那么任何数据对象的访问都不会命中,空缓存被称为冷缓存。此类不命中称为强制性不命中或冷不命中。冷不命中通常是短暂的,不会在反复访问存储器使得缓存暖身之后的稳定状态中出现。

只要发生不命中,第k层的缓存就必须执行某个放置策略,以用来确定它从第k+1层中取出的块放在哪里。一般缓存使用比较严格的放置策略,这个策略是将第k+1层的某个块限制放置在第k层块的一个小的子集中。这种限制放置策略会引起一种不命中,称为冲突不命中,在这种情况中,缓存足够大,能够保持被引用的数据对象,但是因为这些对象会映射到同一个缓存块,缓存会一直不命中。例如,程序请求缓存块0,然后块8,然后块0,然后块8,以此类推,在第k层的缓存中,对这两个块的每次引用都会不命中。

例如,在一个嵌套循环中,可能反复访问同一个数组的元素。这个块的集合称为这个阶段的工作集。当工作集的大小超过缓存的大小时,缓存会经历容量不命中

四、缓存的管理

1、利用时间局部性:由于时间局部性,同一数据对象可能会被多次使用。一旦一个数据对象在第一次不命中时被复制到缓存中,我们就会期望后面对该目标有一系列的访问命中。因为缓存比低一层的存储设备更快,对后面的命中服务会比最开始的不命中快很多。

2、利用空间局部性:块通常包含多个数据对象。由于空间局部性,我们期望后面对该块中其他对象的访问能够补偿不命中后复制该块的花费。

下图是各种缓存类型以及缓存管理者的图:


参考:

《深入理解计算机系统》是2016年机械工业出版社的图书,作者是(美)布赖恩特(Bryant,R.E.)。

《Computer Systems A Programmer's perspective》:CSAPP。

编辑于 2018-08-29

文章被以下专栏收录