CacheLine *findLRU(CacheSet s) { unsigned id = 0; for (unsigned i = 1; i < ca.E; ++i) if (s.lines[i].lastVis < s.lines[id].lastVis) id = i; return &s.lines[id]; }
访问数据
大致思路就是先锁定组号,计算标记位。根据我们前面复习的,一个地址前 t 位是标记位,中间的 s 位是组号,最后的 b 位是偏移,因此标记位可以通过 addr >> (b + s) 计算,组号需要先右移 b 位以后对低 s 位取 and。
voidtrans(int M, int N, int A[N][M], int B[M][N]) { int i, j, tmp;
for (i = 0; i < N; i++) { for (j = 0; j < M; j++) { tmp = A[i][j]; B[j][i] = tmp; } }
}
一共分为 3 个子任务,每个子任务都需要达到一定的优化目标,具体的给分如下:
32×32
首先看一下如果不做优化的不命中次数是多少:
很显然,这个距离我们的目标还挺远的。先来理解一下这个 1183 是怎么计算出来的吧。
首先我们很容易发现,A 和 B 两个矩阵的首地址末 10 位都是相同的,也就是说,这两个矩阵的相同位置一定会出现在相同的高速缓存组中。
一个高速缓存块是连续的 25=32 字节,也就是 8 个 int 类型的变量,每行 8 个这样的块,所以整个高速缓存可以储存 432=8 行的矩阵。因此,如果按照外层枚举行内层枚举列的顺序,可以最高效率地使用缓存,一般可以达到每 8 个元素才会出现一次不命中,不命中总次数应该是 32×32÷8=128 次。这就是 A 矩阵的估计不命中次数。
但是,如果按照 B 矩阵的枚举顺序,外层枚举列,内层枚举行,那么每次访问都会产生不命中(因为每个缓存块的内容,只会被使用一次,在下次使用之前这个块早就被驱逐了),因此 B 矩阵的不命中次数是 32×32=1024 次。那么加起来,我们的预估总不命中次数应该是 1024+128=1152 次。比实际的要少 31 次,这是什么原因呢?
实际上,因为 A 和 B 矩阵是同时被访问的,因此 B 矩阵的操作会影响 A 矩阵的缓存。这种影响只会发生在转置对角线元素的时候,只有这个时候 A 和 B 会使用同一个缓存,对 B 的写会把 A 当前的缓存驱逐出去,因此下一次读 A 上同一个块的时候,会触发一次不命中。只有整个矩阵最后一个元素在写完以后不需要再读这个块,因此总共多了 31 次不命中。
知道了不命中的来源,怎么优化也就很明显了。可以观察到,主要的不命中都是 B 矩阵贡献的。如果我们能让 B 矩阵的缓存发挥左右,就可以解决这个问题。
每次在 B 矩阵上写 8 行就会触发一次驱逐,因此我们可以最多同时写 8 个块。所以思路很简单:
将 A 和 B 每个矩阵分成 4×4 个大小为 8×8 的块,每次将 A 上的一个块转置到 B 上对应的块中,一个块处理完再去处理下一个块。这样保证了 B 的缓存都被写完了才会驱逐。
代码很简单:
1 2 3 4 5 6 7
voidtranspose_32x32(int M, int N, int A[N][M], int B[M][N]) { for (int ii = 0; ii < 32; ii += 8) for (int jj = 0; jj < 32; jj += 8) for (int i = ii; i < ii + 8; ++i) for (int j = jj; j < jj + 8; ++j) B[j][i] = A[i][j]; }
然而很遗憾,这个做法的不命中数依然比 300 多:
为什么会这样呢?注意到我们之前的朴素矩阵转置中,为什么不命中数会多了 31?那是因为对角线上的元素会导致 A 和 B 两个矩阵的同时访问,干扰缓存。而在使用了分块策略之后,这个问题就变得更加突出而复杂。对于对角线上的块,A 和 B 在读写的时候会不断地相互驱逐,对角线上的每个元素应该都会带来三次多余的驱逐:A 的每一行第一个元素的取用会驱逐 B 的这一行,对角线上 B 的写会驱逐 A 的这一行,A 继续读这一行的元素会驱逐 B 的这一行……
想要解决这个问题,可以使用 8 个局部变量来一次性取完 A 的一整行,再写入 B 的相应位置,这样就避免了两次驱逐:写 B 的对角线驱逐 A 和读 A 这一行之后的元素又驱逐 B。虽然无法彻底解决对角线块冲突的问题,但是应该可以做到每行只会冲突一次左右。
考虑到我们在用 4×4 的块进行转置的时候,其实缓存也会存放右边的 4×4 的块。如图,我们不妨将这块缓存利用起来。首先,我们将一个 8×8 的区域划分为 4 个 4×4 的小区域,在 A 矩阵中依次编号为 I, II, III, IV。
在一开始,我们将 I 区域的转置做完,这个时候,A 的 II 区域和 B 矩阵的原本应该存放 III 的区域都已经进入缓存了。因此,我们可以利用这个缓存,将 A 矩阵的 II 区域的转置先放进 B 矩阵中原本应该放 III 的区域中,为了方便我们暂时称 B 的这个区域为 II’ 区域。
接下来,我们考虑如何将 B 中 II’ 区域的一行放进 III 区域的同时,将 A 中 III 区域的转置正确放进 B 的 II’ 区域,且不带来不必要的驱逐。如图,我们先将 II’ 区域中的一小行复制到临时变量中,然后从 A 矩阵的 III 区域复制一列到 II‘ 区域的一小行中。复制完成后,我们在将临时变量中取出的一小行放到 B 真正的 II 区域的对应位置。这个过程中,II’ 区域这一行将被驱逐,真正的 II 区域中这一行将被缓存,没有重复地驱逐再缓存再驱逐任何一行。
voidtranspose_61x67(int M, int N, int A[N][M], int B[M][N]) { int a0, a1, a2, a3, a4, a5, a6, a7; for (int ii = 0; ii < N; ii += 8) for (int jj = 0; jj < M; jj += 8) for (int i = ii; i < ii + 8 && i < N; ++i) { if (jj + 0 < M) a0 = A[i][jj]; if (jj + 1 < M) a1 = A[i][jj + 1]; if (jj + 2 < M) a2 = A[i][jj + 2]; if (jj + 3 < M) a3 = A[i][jj + 3]; if (jj + 4 < M) a4 = A[i][jj + 4]; if (jj + 5 < M) a5 = A[i][jj + 5]; if (jj + 6 < M) a6 = A[i][jj + 6]; if (jj + 7 < M) a7 = A[i][jj + 7]; if (jj + 0 < M) B[jj][i] = a0; if (jj + 1 < M) B[jj + 1][i] = a1; if (jj + 2 < M) B[jj + 2][i] = a2; if (jj + 3 < M) B[jj + 3][i] = a3; if (jj + 4 < M) B[jj + 4][i] = a4; if (jj + 5 < M) B[jj + 5][i] = a5; if (jj + 6 < M) B[jj + 6][i] = a6; if (jj + 7 < M) B[jj + 7][i] = a7; } }