CSAPP 第六章 存储器层次结构(二)局部性和缓存系统

在计算机科学中只有两件难事:缓存失效和命名。

1. 局部性原理

  • 时间局部性: 程序倾向于引用最近引用过的数据。
  • 空间局部性: 程序倾向于引用最近引用过的数据附近的数据。

量化评价局部性的一些原则:

  • 重复引用相同变量的程序有良好的时间局部性。
  • 对数组以步长为k的模式进行引用时,k越小,空间局部性越好。
  • 对取指令而言,循环有好的时间和空间局部性,循环体越小,循环迭代次数越多,局部性越好。

2. 存储器层次结构

利用局部性原理组织存储器结构以期能够

  • 提供和最低成本存储相同数量级大小的空间
  • 提供和最高成本存储相同数量级大小的访问速度

基本思想: 第k层存储作为第k+1层存储的缓存。相邻两层之间使用固定的块大小作为传输单元。

  • 缓存命中
  • 缓存未命中: 需要从k+1层取出对应的块
    • 第k层未满
    • 第k层已经满了: replace/evict a victim block
  • 缓存未命中的种类
    • Cold (compulsory) miss: 对数据第一次引用时,缓存为空,一定会不命中
    • Capacity miss: 工作集(working set)比第k层缓存大
    • Conflict miss: 第k层缓存的空间足够放下工作集但是多个数据被映射到第k层的同一个地址块上
  • 缓存管理
    • 在每一层上,某种形式的逻辑需要管理内存,或是软件或是硬件
    • L1, L2, L3缓存: 硬件逻辑
    • DRAM: 作为对磁盘上数据块的缓存,操作系统软件和地址翻译硬件共同管理
    • AFS: 本地磁盘是缓存,由本地机器上的AFS客户端管理

  • 注意到cache缓存通常块大小是64字节
  • L1 cache访问需要4个时钟周期
  • L2 cache访问需要10个时钟周期
  • L3 cache访问需要50个时钟周期

3. Cache memories

假设每个内存地址为$m$位。高速缓存被组织成$S = 2^s$个高速缓存组(cache set),每个组包括$E$个高速缓存行。每行包括

  • $B = 2^b$字节的数据块
  • 有效位
  • $t = m - s - b$个标记位

因此,高速缓存的结构可以由四元组(S, E, B, m)描述。高速缓存的大小$C$指的是所有块大小之和,不包括有效位和tag,即$C = S * E * B$。

同时$m$位的地址被划分为三个部分如下所示

3.1. 直接映射

  • 直接映射: 每个组只有一行,$E = 1$
    • 组选择位(set index): 唯一映射到一个缓存行
  • 组选择: 通过$S$位进行选择
  • 行匹配: 通过$t$位进行匹配
  • 字抽取: 通过$m - s - t$位的值进行偏移

不命中

  • 可能会出现抖动(thrash)

3.2. 组相联

  • 组相联: 每个组有$1 < E < \frac{C}{B}$行
    • 相联存储器指的是(key,value)存储。组相联指的是多个组的相联存储器。
  • 组选择: 通过$S$位进行选择
  • 行匹配: $t$位(和标志位)作为键,相当于一个小的相联存储,块作为值进行匹配
  • 字抽取

不命中

  • 使用替换算法
    • LFU: 最不常用,即最近一段时间,引用次数最少
    • LRU: 最近最少使用,即最近一段时间,引用时间距离最远
  • 性能损失
    • 替换策略需要额外的时间和设备,但是越偏向存储层次结构的底层,一次不命中的开销远高于这种额外的时间和设备。

3.3. 全相联

  • 全相联: 只有1个组,即$S = 1, s = 0$,行数$E = \frac{C}{B}$
  • 由于需要并行匹配多个缓存行,全相联只适合做小的高速缓存,例如TLB。

3.4. 写缓存

写命中

  • 直写: 直接写入下一层
  • 写回: 需要额外的修改位

写不命中

  • 写分配(write-allocate): 先读下一层的块到当前层,然后写入当前层
    • 和写回搭配
    • 需要额外的一次读操作
  • 非写分配(not write-allocate): 直接写入下一层
    • 和直写搭配

3.5. 性能参数

  • miss rate: 不命中数量 / 引用数量
  • hit rate: 1 - miss rate
  • hit time: 从高速缓存传送一个字到CPU的时间,组选择、行选择和字选择的时间之和。
  • miss penalty: 由于不命中所需要的额外的时间

一些tradeoff

  • 高速缓存大小
    • 大:命中率大,命中时间(物理限制)长
  • 块大小
    • 大: 局部性容易利用,命中率容易变大,行数少
  • 相联度
    • 高: 命中率大, 命中时间长,由于更复杂,不命中处罚变大
    • 整体上而言,命中时间占比大而不命中处罚占比小,但是单次命中、不命中时间都变长
    • 离CPU近的高速缓存系统,因为命中、不命中相差不大,使用较低的相联度
  • 写策略
    • 直写可以使用写缓冲区,且非写分配在高层导致的读未命中惩罚不大。
    • 写回减少总线传输,允许更多的DMA
    • 越底层,写回越多

4. 高速缓存与性能

存储系统读性能测试

  • 读吞吐率/读带宽: 一般用MB每秒表示。

下面是测试内存系统读性能的函数

6-14

  • size: 读取元素的总字节数
  • elems: 读取的元素数目
  • stride: 读取的两个元素间的步长
  • 首先预热(因此有时间局部性),然后通过fcyc2测试执行周期数

内存山

  • 工作集越小,时间局部性越好
  • 步长越小,空间局部性越好

4.1. 综合:重排循环对矩阵乘法效率的影响

  • 矩阵乘法中: $C = A_{ik} * B_{kj}$
  • 按最内层循环分类,有三种遍历计算方式
    • 对i遍历: 内层遍历A的列和C的列
    • 对k遍历: 内层同时遍历A的行和B的列
    • 对j遍历: 内层遍历B的行和C的行
  • 假设
    • 数组都是double类型的
    • 只有一个高速缓存,块大小为32字节
    • 数组大小n很大,一个高速缓存行装不下
    • 编译器将局部变量存储在寄存器中,无加载或存储指令的开销

性能统计如下

  • 随着n的增大,最坏性能和最好性能相差了40倍
  • 该案例中和内存访问总数相比,缓存命中率是一个更好的指标。虽然按k迭代的版本内存访问次数减少,但是性能仍比按j迭代的慢
  • 随着n的增大,按j迭代的版本下性能变化不大,这是由于core i7预取硬件能够识别步长为1的访问模式

矩阵分块操作

  • 假设
    • cache block大小为8个doubles
    • cache size C « n

对于按k遍历的情况,首次迭代$n/8 + n$次未命中,之后的情况类似。总未命中数为 $9n/8n^2 = (9/8)n^3$

对于分块运算

  • $3B^2 < C$
    • 让2个输入块和1个输出块都能放入缓存中
  • 单个矩阵块有$B^2/8$次未命中
  • 首次迭代有$2\frac{n}{B}\frac{B^2}{8}$次未命中(内循环遍历一次的块数乘以每个块的为命中数)
  • 总未命中数为$nB/4 * (n/B)^2 = n^3/(4B)$

小结

  • 无分块: $(9/8)n^3$次未命中
  • 分块: $(1/(4B))n^3$次未命中
  • 性能差异的原因
    • 输入数据为$3n^2$,计算总共需要$2n^3$次(对k遍历,两次内存load操作,无store操作),因此常数因子的上限为2。每一个数组元素被访问了O(n)次,因此幂次3不变。