Ruixiang Li

Lecture12:Cache Memories

Cache memoty organization and operation

Cache缓存是小型的、由高速SRAM实现的存储结构,完全由硬件管理。其包含在CPU内,处于“寄存器组”附近的缓存的实质是存储有主存储器中经常访问的块。出于“局部性”原则,大多数请求会由Cache提供,仅花费几个时钟周期。

1.General Cache Organization-S,E,B

由于缓存完全由硬件管理,硬件需要知道如何查找有关块,并且知道是否为所需的块。所以必须用严格且简单的方式组织Cache。

关于查找逻辑,Cache由lines和sets组成。Cache由S=2^s个set组成,每个set由E=2^e个line组成,而每个line由B=2^b个字节的数据块data、一个vaild bit(指示数据位和数据块存在)以及一个tag标记位(帮助搜寻)组成。

Cache Size = S * E * B(组、行、块)

image

2.Cache Read

Address of Word:(在x86-64上为64位)

t bits s bits b bits
tag set index block offset

从缓存读取数据时,首先确定组,然后逐一检查组内的行/缓存块(对比Tag部分和检查valid bit ?= 1),确定数据是否命中(hit),然后根据块偏移定位准确位置。

example#1

E = 1,即Direct Mapped Cache直接映射缓存(每个set只有一个line)

address of int:t bits + 0…01 + 100

则组索引为1,块偏移为4(偏移从0开始),并且在读取4 Bytes(int)后返回CPU并存入寄存器。

如果未命中,则旧line会被清除后从主存中读取的新数据覆盖,之后再读取、返回。

example#2

E = 2(E > 1,即E-way Set Associative Cache E路相连高速缓存 or 组相连高速缓存),每个set中有2个line,每个block中有 8 bytes。

address of short int:t bits + 0…01 + 100

当查找到对应的set组后,Cache会并行查找所有的line,比较tag和valid。如果有匹配的line则缓存命中,否则缓存未命中。

对于未命中后需要被替换的块,最常用的算法是 “最近最少使用”策略。即将长时间不被引用(根据局部性,将来也不太可能被引用)的块替换。当替换组中的一个行时,如果数据已经更改,必须将其写回内存。

Inter系统的L3缓存是16组相联,其他大多为8路。

example#3

如果我们要求电路非常便宜,那么只能存在一个组S = 1,称为Fully Associative Cache全相联高速缓存

不用hash来确定组,直接挨个比对高位地址,来确定是否命中。可以想见这种方式不适合大的缓存。

如果4M 的大缓存,linesize为32Byte,采用全相联的话,就意味着4 * 1024 * 1024 / 32 = 128K 个line挨个比较,来确定是否命中,这是多要命的事情。高速缓存立马成了低速缓存了。

所以,比较复杂的搜索算法和硬件实现是有值得的。特别是在虚拟存储中,DRAM实现的完全关联的高速缓存,来自硬盘的块可以存储在任何地方。

3.Writes?

数据存在多份副本:L1、L2、L3、主存、硬盘

white-hit:

white-miss:

就是写入的字并不在缓存中。

在Inter Core i7的缓存结构中:

缓存 大小 E=? Access/cycles
访问/时钟周期
作用
L1 i-cache/d-cache 32KB 8 4 指令缓存/数据缓存,直接与寄存器交互
L2 unified cache 256KB 8 10 连接L1和L3
L3 unified cache 8MB 16 40-75 被所有核共享,连接主存和L2

4.Cache Performance Metrics

命中和未命中之间的差距是极大的,L1和主存之间的代价相差百倍。事实上,99%的命中率比97%的命中率好一倍(平均时间是一半)。

因此我们也需要编写缓存友好的代码,即具有更高的命中率,让常用部分更多。例如:重复引用、步长为1引用、

Performance impact of caches

1.The memory mountain

Read throughput(read bandwidth)吞吐量/带宽

每秒从内存中读取的字节数 MB/s。

Memory mountain存储器山:根据空间局部性和时间局部性测量读取吞吐量。是一种紧凑的方法,用于表征内存系统性能。其将该存储系统的性能在该范围内作为二维函数绘制出来。(参考课本封面)

aaa

其中:

步长的增加会减少空间局部性,而size的增加会导致可以容纳数据的缓存更少。

(注:上述分析来自知乎)

2.Rearranging loops to improve spatial locality

重排循环:以 N x N 矩阵乘法为例(改善空间局部性)

/* ijk */
for(i = 0; i < n; i++){
    for(j = 0; j < n; j++){
        cum = 0.0;
        for(k = 0; k < n; k++){
            sum += a[i][k] * b[k][j];
        }
        c[i][j] = sum;
    }
}

这段代码的时间复杂度为 O(n^3)。实际上,矩阵乘法不一定是这个顺序,可以证明改变循环顺序的另外5种排列依然可行。

对于ijk的顺序,在内循环中,矩阵A为行优先访问、矩阵B为列优先访问,在这种情况下,对于每次内循环的未命中次数,A每次会有0.25次未命中,而B有1次(每次都不会命中)(同jik

kij的顺序下,内循环的B、C都是行优先,平均每4次会出现一次未命中。(同ikj

/* 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];
    }
}

jki的顺序下,内循环的A、C均为列优先:(同kji

/* jki */
for(j = 0; j < n; j++){
    for(k = 0; k < n; k++){
        r = b[k][j];
        for(i = 0; i < n; i++)
            c[i][j] += a[i][k] * r;
    }
}

平均之下后,kij/ikj有着0.5次未命中,而最坏的jki/kji有2次未命中。(另外两种情况是1.25次)

3.Using blocking to improve temporal locality

Blocking阻塞:还是矩阵乘法(改善时间局部性)

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++)
        for (j = 0; j < n; j++)
            for (k = 0; k < n; k++)
                c[i*n + j] += a[i*n + k] * b[k*n + j];
}

假设矩阵元素为double,缓存大小为8 doubles,缓存大小远小于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; i1++)
                    for (j1 = j; j1 < j+B; j1++)
                        for (k1 = k; k1 < k+B; k1++)
                            c[i1*n+j1] += a[i1*n+k1] * b[k1*n+j1];
}

即将大矩阵分隔为更小的子矩阵后分别相乘。(线性代数告诉我们,分块矩阵乘法与原矩阵乘法等效)B = 1时即为原算法,其中保证了3B^2 < C。

这样总的未命中数为:n^3 / (4B)!