author: τ
title: CSAPP Cache Lab 緩存實(shí)驗(yàn)
date: 2019-08-21
template: post
origin: csapp-cache
我最開始覺得這個實(shí)驗(yàn)就是寫一個緩存模擬器和利用緩存優(yōu)化代碼,應(yīng)該挺簡單的。結(jié)果發(fā)現(xiàn),這個實(shí)驗(yàn)設(shè)計(jì)得真的很好,并沒用那么簡單。所以,趕緊寫篇文章記錄自己的實(shí)驗(yàn)。
Part A是寫一個緩存的模擬器,相對而言比較簡單,我這里就不記錄Part A了。Part B是根據(jù)緩存的特性來優(yōu)化矩陣轉(zhuǎn)置的代碼,這應(yīng)該是這個實(shí)驗(yàn)最有意思的一部分,看似很簡單,但是要優(yōu)化到滿分難道不小。并且,Part B中對緩存是否命中的定量分析也是很有趣的。
[TOC]
實(shí)驗(yàn)介紹
Part B 是根據(jù)緩存的特性優(yōu)化矩陣轉(zhuǎn)置,目標(biāo)是miss次數(shù)盡量的小。
輸入是三個不同的固定大小的矩陣,矩陣分別為:
- 32 x 32,miss次數(shù)小于300滿分。
- 64 x 64,miss次數(shù)小于1300滿分。
- 61 x 67, miss次數(shù)小于2000滿分。
可以根據(jù)矩陣的大小做特定的優(yōu)化。實(shí)驗(yàn)還有如下限制:
- 最多12個
int
類型的本地變量,不能用其他整數(shù)類型來儲存int
類型的值。 - 不能修改
A
矩陣的內(nèi)容,B
矩陣可隨意。 - 不能用
malloc
類函數(shù)。
用來測試miss的是Part A的緩存模擬器,緩存的參數(shù)為s=5, E=1, b=5
, 即共有32個set, 每個set有一個cache line(直接映射), 每個塊的大小為32 bytes。
思路:分塊
CSAPP書上有介紹矩陣乘法的優(yōu)化,里面就用到了分塊的思路。我們這里也要使用分塊的思路來優(yōu)化矩陣轉(zhuǎn)置。那么為什么分塊會對緩存更加友好呢?
我們可以先看一下最簡單的轉(zhuǎn)置實(shí)現(xiàn):
void trans(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;
}
}
}
這里我們會按行優(yōu)先讀取A
矩陣,然后一列一列地寫入B
矩陣。在C語言,多維數(shù)組在內(nèi)存中是逐行逐行地排列的。也就是說,每一行的內(nèi)容是在同一塊連續(xù)內(nèi)存中的,并且相鄰行的內(nèi)存塊的地址是連續(xù)的。緩存每次會從內(nèi)存中加載固定大小的內(nèi)存塊。例如,程序在從內(nèi)存讀A[0][0]
的時候,除了A[0][0]
被加載到緩存中,它之后的A[0][1], A[0][2]...
也可能被加載進(jìn)緩存。
這里程序逐行地讀取A
矩陣的內(nèi)容對緩存是優(yōu)好的。但是內(nèi)容寫入B
矩陣的時候是一列一列地寫入,在列上相鄰的元素很有可能不在一個內(nèi)存塊上,這樣每次寫入都不能命中緩存,需要從內(nèi)存中加載待寫入的部分。緩存的大小是很有限的,一列寫完后,再返回第一行的下一列的時候原來在緩存中的內(nèi)容可能也被替換了,又要重新再從內(nèi)存中讀。所以,最壞的情況下,每次寫入內(nèi)容到B
矩陣都需要讀內(nèi)存。
矩陣B按列寫的過程, * 表示內(nèi)容加載到了緩存中。
假設(shè)每個緩存塊的大小為4個int的大小,B矩陣每相隔四行的行地址在緩存中的index會重復(fù)一次。
也就是說第一行的前四個元素和第五行的前四個元素會占據(jù)相同的緩存塊。
| | |
V V V
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | | | | | | | | | | |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | | | | | | | | | | |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | | | | | | | | | | |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | => | | | | | | | | | => |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | |*|*|*|*| | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | |*|*|*|*| | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | |*|*|*|*| | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | |*|*|*|*| | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
(1) (2) (3)
(1) 寫入第一列的前四行
(2) 寫入第一列后四行時,前四行緩存的內(nèi)容被替換
(3) 寫入第二列的時候,又需要重新從內(nèi)存中讀前四行的內(nèi)容
分塊的思路就是我們限制每次寫入B
矩陣的行數(shù),充分利用B
矩陣在緩存中的部分。比如在上面的例子中,我們可以把分塊限制為4 x 4
的大小,充分利用已經(jīng)加載到緩存中的內(nèi)容。
| |
V V
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|*|*|*|*| | | | | => |*|*|*|*| | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
(1) (2)
(1) 先寫入第一列的前四行,前四列的前四行都被加載到緩存中
(2) 再寫入第二列的前四行,不需要再從內(nèi)存中重新加載
A
矩陣也會以同樣的方式按照一個塊讀取內(nèi)容,同樣對緩存是友好的。
總的來說,分塊解決的是在矩陣轉(zhuǎn)置過程中,兩個矩陣的內(nèi)存的訪問順序不同導(dǎo)致的緩存不友好的問題,減少了同一個矩陣內(nèi)部緩存塊相互替換的問題。
32 x 32
緩存塊的大小為32 bytes,可以存8個int
類型的數(shù)據(jù),那么矩陣每行需要4個緩存塊。緩存塊的總數(shù)是32個,那么相隔8行矩陣的行開始的緩存塊的index
就會重復(fù)。
矩陣中緩存塊`index`的分布:
+--+--+--+--+
0 | 0| 1| 2| 3|
+--+--+--+--+
1 | 4| 5| 6| 7|
+--+--+--+--+
2 | 8| 9|10|11|
+--+--+--+--+
3 |12|13|14|15|
+--+--+--+--+
4 |16|17|18|19|
+--+--+--+--+
5 |20|21|22|23|
+--+--+--+--+
6 |24|25|26|27|
+--+--+--+--+
7 |28|29|30|31|
+--+--+--+--+
8 | 0| 1| 2| 3|
+--+--+--+--+
...
每一個小格子表示一個緩存塊,格子中的數(shù)字是緩存塊的index。
可以看到第0行和第8行緩存塊的index是重復(fù)的。
為了使得同一個矩陣的在緩存中的內(nèi)容不被相互替換,我們可以把分塊的大小設(shè)為8 x 8
。
普通分塊
實(shí)現(xiàn)
我們先直接按照8 x 8
分塊實(shí)現(xiàn)一個矩陣的轉(zhuǎn)置:
for (i = 0; i < N; i += 8) {
for (j = 0; j < M; j += 8) {
// 分塊
for (k = i; k < min(i + 8, N); k++) {
for (s = j; s < min(j + 8, M); s++) {
B[s][k] = A[k][s];
}
}
}
}
結(jié)果分析
在正式測試miss之前,我們先人工分析一下miss的次數(shù):
每個分塊的大小為8 x 8
, 所以每個塊的miss次數(shù)是8
。每個矩陣有16個分塊,有兩個矩陣,所以總的miss次數(shù)就是8*16*2=256
。
測試一下:
$ ./test-trans -M 32 -N 32
Function 2 (4 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 2 (8x8 block): hits:1710, misses:343, evictions:311
測試結(jié)果miss的次數(shù)是343
,和我們的分析相差很大,也沒有達(dá)到滿分的要求。
我們前面的分析漏掉了什么呢?我了調(diào)整代碼,輸出了A
,B
矩陣在內(nèi)存中的地址:
A:55ed51a670a0, B:55ed51aa70a0
我們可以看到A
,B
的地址最后16 bits都是相同的,而緩存的index
是由倒數(shù)5到10的bit組成,所以A
,B
在緩存中的內(nèi)容會沖突。因?yàn)?code>A, B
是互為轉(zhuǎn)置,所以沖突A
, B
只會發(fā)生在矩陣對角線上的塊。
我們來分析一下對角線上緩存塊的沖突。我用A[n]
表示第n的緩存塊, A[n][m]
表示第n個緩存塊上的第m個元素。
首先A
, B
固定的miss為16。
在對角線元素復(fù)制時B[m][m] = A[m][m]
, 會發(fā)生A[m]
,B[m]
之間的沖突。復(fù)制前, A[m]
開始在緩存中,B[m]
不在。 復(fù)制時, A[m]
被B[m]
取代。 下一個開始復(fù)制A[m]
又被重新加載進(jìn)入緩存取代B[m]
。這樣就會產(chǎn)生2次多余的miss。
最后一行和第一行情況有些不一樣: 第一行B[m]
被加載到緩存中是第一次,應(yīng)該算在那16次中, 但是同樣會發(fā)生A[0]
的重新加載, 所以額外產(chǎn)生的miss次數(shù)為1。 最后一行A[7]
被取代, 但是復(fù)制已經(jīng)完成,不需要再將A[7]
加載進(jìn)內(nèi)存,所以額外的miss也為1。
B[m]
被A[m]
取代, 在下一行A[m+1]
復(fù)制時需要重新加載B[m]
進(jìn)入緩存(第一行除外), 所以會除了第一行每行又多了一次miss。 所有總的額外的miss的數(shù)目為2*6+1*2+7=21
。
加上固定的miss次數(shù), 對角塊上的總的miss次數(shù)為37
次。
具體的過程:
緩存中的內(nèi)容:
+-----------------------+-------------------+
| opt | cache |
+-----------------------+-------------------+
|before B[0][0]=tmp | A[0] |---+
+-----------------------+-------------------+ |
|after B[0][0]=tmp | B[0] | |
+-----------------------+-------------------+ | A的第一行復(fù)制到B的第一列.
|after tmp=A[0][1] | A[0] | | 最終緩存中剩下A[0], B[1]...B[7].
+-----------------------+-------------------+ +--> A[0]被兩次加載進(jìn)入內(nèi)存,
|after B[1][0]=tmp | A[0] B[1] | | 總的miss次數(shù)是10.
+-----------------------+-------------------+ |
|... | | |
+-----------------------+-------------------+ |
|after B[7][0]=tmp | A[0] B[1..7] |---+
+-----------------------+-------------------+
|after B[0][1]=tmp | A[1] B[0] B[2..7] |---+
+-----------------------+-------------------+ | A的第二行復(fù)制到B的的二列.
|after B[1][1]=tmp | B[0..7] | | 其中發(fā)生的miss有:
+-----------------------+-------------------+ +--> A[0], B[0], A[1]與B[1]的相互取代.
|... | | | 總的miss次數(shù)為4.
+-----------------------+-------------------+ |
|after B[7][1]=tmp | A[1] B[0] B[2..] |---+
+-----------------------+-------------------+ 之后的三至七行與第二行類似,
|... | |------> miss的次數(shù)都是4.
+-----------------------+-------------------+
|after tmp=A[7][7] | A[7] B[0..6] |---+ 最后一行A[7]被A[8]取代后,
+-----------------------+-------------------+ +--> 不需要重新加載.
|after B[7][7]=tmp | B[0..7] |---+ 總的miss數(shù)為3.
+-----------------------+-------------------+
所以對角塊上的總的miss次數(shù)是10+4*6+3=37.
對角分塊有4個,普通的分塊12個,所以總的miss數(shù)是4*37+16*12=340
,和實(shí)際結(jié)果相差3
。3
是一個固定的偏差,程序可能在這個過程中有三次額外的內(nèi)存訪問,在后面的根據(jù)算法定量分析結(jié)果和實(shí)際結(jié)果中都會有3
次miss的偏差。
緩存分塊
實(shí)現(xiàn)
上面那種普通分塊的實(shí)現(xiàn)會在對角塊上產(chǎn)生太多的沖突,A
,B
矩陣的緩存塊相互替換的情況太多。我們可以考慮使用本地變量存下A
的一行后,再復(fù)制給B
,即用本地變量作為緩存存儲每個緩存塊中的內(nèi)容。本地變量數(shù)目不多的時候是放在寄存器上的,因此可以減少訪問內(nèi)存。
for (i = 0; i < 32; i += 8) {
for (j = 0; j < 32; j += 8) {
for (k = i; k < i + 8; k++) {
a0 = A[k][j];
a1 = A[k][j + 1];
a2 = A[k][j + 2];
a3 = A[k][j + 3];
a4 = A[k][j + 4];
a5 = A[k][j + 5];
a6 = A[k][j + 6];
a7 = A[k][j + 7];
B[j][k] = a0;
B[j + 1][k] = a1;
B[j + 2][k] = a2;
B[j + 3][k] = a3;
B[j + 4][k] = a4;
B[j + 5][k] = a5;
B[j + 6][k] = a6;
B[j + 7][k] = a7;
}
}
}
結(jié)果分析
同樣我們先來分析一下miss的次數(shù)。非對角線的分塊沒有A
, B
之間的沖突,miss次數(shù)是16
。
對于對角線上的分塊,復(fù)制A[m]
時會取代B[m]
(第一行除外),將數(shù)據(jù)寫入B[m]
的時候又會重新加載一次。所以,額外的miss次數(shù)為7
次。對角塊總的miss
次數(shù)為23
次。
總的miss次數(shù)應(yīng)該為23*4+16*12=284
次。
測試一下:
$ ./test-trans -M32 -N32
Function 0 (4 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:1766, misses:287, evictions:255
實(shí)際的miss次數(shù)為287
,和我們分析的剛好相差3
次。
對角線優(yōu)先復(fù)制
實(shí)現(xiàn)
這個思路來自于別人的一篇博客[1]。思路大概是這樣:A
矩陣按列優(yōu)先讀,B
矩陣按照行優(yōu)先寫,優(yōu)先處理每個分塊對角線上的元素。對于B[m]
行,因?yàn)榘阉虞d到緩存中會取代A[m]
, 而A[m]
中需要復(fù)制到B[m]
中的是A[m][m]
。我們通過可以優(yōu)先復(fù)制A[m][m]
來避免A
,B
之間的這次沖突。
這個思路通過改變數(shù)據(jù)復(fù)制的順序,巧妙地減少了兩個矩陣之間的緩存沖突。
for (i = 0; i < N; i += 8)
for (j = 0; j < M; j += 8)
for (k = 0; k < 8; k++) {
for (s = k; s < 8; s++)
B[k + j][s + i] = A[i + s][j + k];
for (s = k - 1; s >= 0; s--)
B[k + j][s + i] = A[i + s][j + k];
}
結(jié)果分析
復(fù)制B[m]
行的時候,A[m]
被替換,下一行復(fù)制的時候又需要將A[m]
加載到緩存中。所以miss的次數(shù)和上面的實(shí)現(xiàn)一樣,284
次。
測試結(jié)果:
$ ./test-trans -M32 -N32
Function 3 (4 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 3 (diagonal first): hits:1766, misses:287, evictions:255
實(shí)際miss次數(shù)為287
,和分析結(jié)果相差3
。
先復(fù)制后轉(zhuǎn)置
實(shí)現(xiàn)
前面提到的思路里, 在對角線的分塊處都無可避免的會產(chǎn)生A
, B
矩陣之間的緩存沖突。這個沖突真的不可避免嗎?答案是否定的。實(shí)驗(yàn)要求上寫了不能改變A
矩陣,但是B
可以隨意處理。我們可以考慮在B
矩陣上想點(diǎn)辦法來消除兩個矩陣之間的沖突。
前面提到的兩個實(shí)現(xiàn)中對角線分塊的沖突產(chǎn)生的原因是A
,B
矩陣訪問的順序不同,一個按列訪問,一個按行訪問,導(dǎo)致的。交叉的訪問順序無可避免地會導(dǎo)致緩存index
相同的塊替換。
為了消除兩個矩陣之間的緩存沖突,在把A
中分塊的內(nèi)容復(fù)制到B
的時候,我們按照都行優(yōu)先訪問順序訪問兩個矩陣。這樣的結(jié)果是,對應(yīng)分塊的內(nèi)容相同,沒有轉(zhuǎn)置。我們在分塊復(fù)制完成后,再在B
分塊里面完成轉(zhuǎn)置。
const int len = 8;
for (i = 0; i < N; i += len) {
for (j = 0; j < N; j += len) {
// copy
for (k = i, s = j; k < i + len; k++, s++) {
a0 = A[k][j];
a1 = A[k][j + 1];
a2 = A[k][j + 2];
a3 = A[k][j + 3];
a4 = A[k][j + 4];
a5 = A[k][j + 5];
a6 = A[k][j + 6];
a7 = A[k][j + 7];
B[s][i] = a0;
B[s][i + 1] = a1;
B[s][i + 2] = a2;
B[s][i + 3] = a3;
B[s][i + 4] = a4;
B[s][i + 5] = a5;
B[s][i + 6] = a6;
B[s][i + 7] = a7;
}
// transpose
for (k = 0; k < len; k++) {
for (s = k + 1; s < len; s++) {
a0 = B[k + j][s + i];
B[k + j][s + i] = B[s + j][k + i];
B[s + j][k + i] = a0;
}
}
}
}
為了消除對角線上分塊行的相互替換,在上面的實(shí)現(xiàn)中,每次先用本地變量緩存A
分塊的一行,再復(fù)制到B分塊對應(yīng)的行中。在復(fù)制完成后,B
的分塊全部在緩存中,轉(zhuǎn)置過程沒有miss。
結(jié)果分析
這個方法消除了所有的兩個矩陣之間的緩存沖突,所以miss的次數(shù)是16*16=256
。
測試結(jié)果:
$ ./test-trans -M32 -N32
Function 2 (4 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 2 (copy and then trans): hits:3586, misses:259, evictions:227
這大概是能夠達(dá)到的最好的結(jié)果了。
64 x 64
64 x 64
的矩陣每個行需要8個緩存塊,每四行緩存index
會重復(fù)一次。
矩陣中緩存`index`分布
+--+--+--+--+--+--+--+--+
0 | 0| 1| 2| 3| 4| 5| 6| 7|
+--+--+--+--+--+--+--+--+
1 | 8| 9|10|11|12|13|14|15|
+--+--+--+--+--+--+--+--+
2 |16|17|18|19|20|21|22|23|
+--+--+--+--+--+--+--+--+
3 |24|25|26|27|28|29|30|31|
+--+--+--+--+--+--+--+--+
4 | 0| 1| 2| 3| 4| 5| 6| 7|
+--+--+--+--+--+--+--+--+
5 | 8| 9|10|11|12|13|14|15|
+--+--+--+--+--+--+--+--+
6 |16|17|18|19|20|21|22|23|
+--+--+--+--+--+--+--+--+
...
顯然直接按照8 x 8
的分塊來做,同一個矩陣內(nèi)的緩存塊就會發(fā)生沖突。按照4 x 4
分塊,沒能充分利用加載進(jìn)入緩存內(nèi)的部分,測試結(jié)果也不能達(dá)到滿分的要求。為了滿分,我們要充分利用上面提到的兩個思路:用本地變量做緩存,先復(fù)制后轉(zhuǎn)置。
實(shí)現(xiàn)
我的這個實(shí)現(xiàn)有些復(fù)雜,我盡量講清楚我的具體步驟。
同樣,我們保持分塊為8 x 8
,在大的分塊下再分成4個4 x 4
的小分塊。我們先將A
的前四行全部復(fù)制到B
的前四行,這個時候B
的左上角的元素在最終正確的位置,B
的右上角元素是應(yīng)該放到左下角的元素。然后,我們在復(fù)制后A
的后四行到B
的過程中,利用本地變量將B
右上角的內(nèi)容復(fù)制到左下角。
具體步驟如下:
- 先將
A
的前四行按照(1)復(fù)制到B
中。 - 按照(2)將
A
中對應(yīng)位置的元素存到本地變量中。
-
buf1
的四個元素與B
右上角的第一行交換,將buf2
中的值存到B
右下角的對應(yīng)位置。此時緩存中B[4]
替換B[0]
。 - 將
buf1
中的元素存放到B
左下角對應(yīng)位置。 - 改變位置,重復(fù)(2),(3),(4),直到所有元素到達(dá)正確位置。
整個過程比較復(fù)雜,不過根據(jù)圖應(yīng)該可以看懂。下面是代碼的實(shí)現(xiàn):
for (i = 0; i < N; i += block_size) {
for (j = 0; j < M; j += block_size) {
for (k = 0; k < block_size / 2; k++) {
// A top left
a0 = A[k + i][j];
a1 = A[k + i][j + 1];
a2 = A[k + i][j + 2];
a3 = A[k + i][j + 3];
// copy
// A top right
a4 = A[k + i][j + 4];
a5 = A[k + i][j + 5];
a6 = A[k + i][j + 6];
a7 = A[k + i][j + 7];
// B top left
B[j][k + i] = a0;
B[j + 1][k + i] = a1;
B[j + 2][k + i] = a2;
B[j + 3][k + i] = a3;
// copy
// B top right
B[j + 0][k + 4 + i] = a4;
B[j + 1][k + 4 + i] = a5;
B[j + 2][k + 4 + i] = a6;
B[j + 3][k + 4 + i] = a7;
}
for (k = 0; k < block_size / 2; k++) {
// step 1 2
a0 = A[i + 4][j + k], a4 = A[i + 4][j + k + 4];
a1 = A[i + 5][j + k], a5 = A[i + 5][j + k + 4];
a2 = A[i + 6][j + k], a6 = A[i + 6][j + k + 4];
a3 = A[i + 7][j + k], a7 = A[i + 7][j + k + 4];
// step 3
tmp = B[j + k][i + 4], B[j + k][i + 4] = a0, a0 = tmp;
tmp = B[j + k][i + 5], B[j + k][i + 5] = a1, a1 = tmp;
tmp = B[j + k][i + 6], B[j + k][i + 6] = a2, a2 = tmp;
tmp = B[j + k][i + 7], B[j + k][i + 7] = a3, a3 = tmp;
// step 4
B[j + k + 4][i + 0] = a0, B[j + k + 4][i + 4 + 0] = a4;
B[j + k + 4][i + 1] = a1, B[j + k + 4][i + 4 + 1] = a5;
B[j + k + 4][i + 2] = a2, B[j + k + 4][i + 4 + 2] = a6;
B[j + k + 4][i + 3] = a3, B[j + k + 4][i + 4 + 3] = a7;
}
}
}
結(jié)果分析
這個實(shí)現(xiàn)會完全消除行同一個矩陣內(nèi)部的沖突,但是兩個矩陣之間的沖突沒能完全避免。
對于在對角線上的塊,步驟(1)會有3次額外miss,步驟(2)、(3)、(4)、(5)有7次額外miss。普通塊miss次數(shù)仍然為8。
總的miss次數(shù)為(3+7)*8 + 64*8*2=1104
。
$ ./test-trans -M64 -N64
Function 0 (4 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:9138, misses:1107, evictions:1075
(1)中的三次miss可以通過先復(fù)制后轉(zhuǎn)置的思路消除,可以減少3*8=24
次miss。
./test-trans -M64 -N64
Function 1 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 1 (64x64): hits:12234, misses:1083, evictions:1051
這個結(jié)果還不錯,比1300
的要求小了不少。
61 x 67
這個矩陣中緩存塊的分布比較奇怪,我根據(jù)觀察緩存塊的分布將分塊的大小設(shè)置為8 x 23
。
for (i = 0; i < N; i += 8) {
for (j = 0; j < M; j += 23) {
if (i + 8 <= N && j + 23 <= M) {
for (s = j; s < j + 23; s++) {
a0 = A[i][s];
a1 = A[i + 1][s];
a2 = A[i + 2][s];
a3 = A[i + 3][s];
a4 = A[i + 4][s];
a5 = A[i + 5][s];
a6 = A[i + 6][s];
a7 = A[i + 7][s];
B[s][i + 0] = a0;
B[s][i + 1] = a1;
B[s][i + 2] = a2;
B[s][i + 3] = a3;
B[s][i + 4] = a4;
B[s][i + 5] = a5;
B[s][i + 6] = a6;
B[s][i + 7] = a7;
}
} else {
for (k = i; k < min(i + 8, N); k++) {
for (s = j; s < min(j + 23, M); s++) {
B[s][k] = A[k][s];
}
}
}
}
}
測試結(jié)果:
./test-trans -M61 -N67
Function 0 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:6316, misses:1863, evictions:1831
因?yàn)?000的miss要求比較低,所以很容易就過了。
總結(jié)
總的來說,這個實(shí)驗(yàn)收獲還是很多的。尤其是對miss次數(shù)的定量分析,讓我很受益。之前學(xué)習(xí)算法之類的,只會大概估計(jì)一下復(fù)雜度的等級,完全定量地對程序分析對我來說還是比較少。在其他方面,如怎樣寫出對緩存友好的代碼,也有不少收獲。
深入理解計(jì)算機(jī)系統(tǒng)CacheLab-PartB實(shí)驗(yàn)報告