CMU 15-213 Intro to Computer Systems Lecture 12
课程主页:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html
课程资料:https://github.com/EugeneLiu/translationCSAPP
课程视频:https://www.bilibili.com/video/av31289365/
这一讲介绍了高速缓存存储器。
高速缓存存储器的组织和操作
高速缓存存储器
高速缓存存储器是小型,基于SRAM的快速存储器,可在硬件中自动管理
- 保留经常访问的主内存块
CPU首先在缓存中查找数据
典型系统结构:
高速缓存$(S,E,B)$的通用组织
高速缓存读取
利用上图的结构进行高速缓存读取,索引组判断属于哪个组,块偏移确定了$B$个字节的数据块中的字偏移,标记位确定标记。
例子
考虑$E=2$的情形:
首先通过$0…01$确定是第二个缓存,然后比较tag:
最后$100$确定偏移:
再来看一个具体例子:
如果读取的缓存中没有内容或者有效位为0,则记为miss,否则为hit。
写
- 存在多个数据副本:
- L1,L2,L3,主存,磁盘
- write-hit怎么办?
- Write-through(立即写入内存)
- Write-back(将写操作推迟到内存中,直到替换行为止)
- 需要一个dirty bit(行是否与内存不同)
- 发生写缺失该怎么办?
- write-miss(加载到缓存中,更新行在缓存中)
- 如果有更多写入该位置的信息,则很好
- No-write-allocate(直接写入内存,不加载到缓存中)
- write-miss(加载到缓存中,更新行在缓存中)
- 典型
- Write-through + No-write-allocate
- Write-back + Write-allocate
高速缓存性能指标
- 不命中率
- 在缓存中找不到的内存引用的比例(丢失/访问)$\text{= 1 – hit rate}$
- 典型数字(百分比):
- L1为3-10%
- 对于L2来说可能很小(例如<1%),具体取决于大小等。
- 命中时间
- 从高速缓存传送一个字到CPU所需时间
- 包括确定行是否在缓存中的时间
- 典型数字:
- L1需要4个时钟周期
- L2需要10个时钟周期
- 从高速缓存传送一个字到CPU所需时间
- 不命中处罚
- 因为错过而需要额外的时间
- 主存通常需要50-200个周期(趋势:正在增加!)
例子
比较99%命中率和97%命中率的平均读取时间:
- 97%:$1+0.03\times 100=4$
- 99%:$1+0.01\times 100=2$
这说明99%的命中率比97%命中率好两倍,所以我们关注不命中率。
编写高速缓存友好的代码
- 使最常见的情形快速运行
- 专注于核心功能的内循环
- 最小化内部循环中的不命中数量
- 重复引用变量是好的(时间局部性)
- 步长为$1$的引用模式很好(空间局部性)
缓存的性能影响
存储器山
- 读吞吐量(读带宽)
- 每秒从内存读取的字节数(MB / s)
- 存储器山:测量读吞吐量与空间和时间局部性的关系
- 表示储器系统性能的紧凑方法。
存储器山测试函数
long data[MAXELEMS]; /* Global array to traverse */
/* test - Iterate over first "elems" elements of
* array “data” with stride of "stride", using
* using 4x4 loop unrolling.
*/
int test(int elems, int stride) {
long i, sx2=stride*2, sx3=stride*3, sx4=stride*4;
long acc0 = 0, acc1 = 0, acc2 = 0, acc3 = 0;
long length = elems, limit = length - sx4;
/* Combine 4 elements at a time */
for (i = 0; i < limit; i += sx4) {
acc0 = acc0 + data[i];
acc1 = acc1 + data[i+stride];
acc2 = acc2 + data[i+sx2];
acc3 = acc3 + data[i+sx3];
}
/* Finish any remaining elements */
for (; i < length; i++) {
acc0 = acc0 + data[i];
}
return ((acc0 + acc1) + (acc2 + acc3));
}
重新排列循环以改善空间局部性
矩阵乘法
/* ijk */
for (i=0; i<n; i++) {
for (j=0; j<n; j++) {
sum = 0.0;
for (k=0; k<n; k++)
sum += a[i][k] * b[k][j];
c[i][j] = sum;
}
}
注意按这个方法写效率较低,从内存布局即可看出:
更换循环次序,一共可以得到6种循环,效率如下:
这里解释下为什么kij的效率较高,首先看代码:
/* kij */
for (k=0; k<n; k++) {
for (i=0; i<n; i++) {
r = a[i][k];
for (j=0; j<n; j++)
c[i][j] += r * b[k][j];
}
}
接着看示意图:
说明该代码的空间局部性较好。
使用分块以改善时间局部性
高速缓存未命中的分析
- 假设:
- 矩阵元素是双精度
- 高速缓存块 = 8 doubles
- 缓存大小$C << n$(比$n$小得多)
- 第一次迭代:
- $n / 8 + n = 9n / 8$次未命中
- 总未命中次数:
- $9n / 8 n^2 =(9/8) n^3$
分块矩阵乘法
c = (double *) calloc(sizeof(double), n*n);
/* Multiply n x n matrices a and b */
void mmm(double *a, double *b, double *c, int n) {
int i, j, k;
for (i = 0; i < n; i+=B)
for (j = 0; j < n; j+=B)
for (k = 0; k < n; k+=B)
/* B x B mini matrix multiplications */
for (i1 = i; i1 < i+B; i++)
for (j1 = j; j1 < j+B; j++)
for (k1 = k; k1 < k+B; k++)
c[i1*n+j1] += a[i1*n + k1]*b[k1*n + j1];
}
图示如下:
未命中的分析
- 假设:
- 高速缓存块 = 8 doubles
- 缓存大小$C << n$(比$n$小得多)
- 三个分块可以放入缓存,$3B^2<C$
- 第一次(块)迭代:
- 每个方块$B\times B/ 8=B^2/8$次未命中
- 总共:$2n / B * B^2 / 8$ = $nB/4$(忽略矩阵c)
- 总共未命中:
- $nB/4 * (n/B)^2 = n^3/(4B)$
在缓存中的示意图:
分块总结
- 无分块:$(9 / 8)^{*} n^{3}$
- 分块:$1 /(4 B)^{*} n^{3}$
- 建议选择最大的$B$,使得$3B^2<C$
- 产生巨大差异的原因:
- 矩阵乘法具有固有的时间局部性:
- 输入数据:$3n^2$,计算$2n^3$
- 每个数组元素使用$O(n)$次!
- 但是程序必须正确编写
- 矩阵乘法具有固有的时间局部性:
总结
- 缓存可能会对性能产生重大影响
- 您可以编写程序来利用它!
- 着眼于内部循环,其中发生大量的计算和内存访问。
- 尝试通过依次跨步读取数据对象来最大化空间局部性。
- 从内存中读取数据对象后,请尽可能多地使用它,以尽量提高时间局部性。
http://www.doraemonzzz.com/2020/06/10/CMU%2015-213%20Intro%20to%20Computer%20Systems%20Lecture%2012/
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Doraemonzzz!
评论
ValineLivere