[lab]csapp-cachelab
Cache lab
該lab主要是對應第六章儲存器層次結構.
分為兩部分,
A: cpu cache 命中分析,
B: cache 命中優化
Part A.
首先為了實現part A, 我們要安裝 valgrind 軟體, 它就是用來分析程式執行效率的, --trace-mem 能輸出對指定命令的記憶體讀寫操作, 命中分析基於它的輸出, 在給定 s E b 引數下輸出 hit, miss, eviction 的次數. 給出了一個輸出的例子
linux> ./csim-ref -v -s 4 -E 1 -b 4 -t traces/yi.trace L 10,1 miss M 20,1 miss hit L 22,1 hit S 18,1 hit L 110,1 miss eviction L 210,1 miss eviction M 12,1 miss eviction hit hits:4 misses:5 evictions:3
我們要實現基於LRU淘汰策略的快取記憶體, 相應的地址編碼, 及資料定義如下
31 b+s b 0
| CT | CI |CO |
int s, E, e, b, verbose, t;
#define CI(v) (((v) >> b) & ((1<<s) - 1))
#define CO(v) ((v) & ((1<<b) - 1))
#define CT(v) ((v) >> (s + b)) & ((1<<t) - 1)
首先分配 2^s-1
個cache組, 然後迴圈讀取檔案中的訪問資料, 對地址 addr, 計算出它所在的組和識別符號, 在組中查詢是否存在, 如果存在, 則更新其訪問時間, 否則插入到組中, 並輸出命中或者miss. 這裡要注意修改的情況, 實際是先將值取出, 再將修改的值寫入, 我們不需要真正管理cache的值, 直接預設第二次訪問命中即可.
// 獲取所在的組和組內標識. CacheGroupPtr group = cache_groups[CI(addr)]; int mask = CT(addr); // fprintf(stderr, "idx %d %d %d\n", CI(addr), mask, addr); verbose ? printf("%s %x,%d", mod, addr, size) : 0; if (find_item_in_group(group, mask)) { // 直接命中. hit++; verbose ? printf(" hit") : 0; } else { miss++; verbose ? printf(" miss") : 0; // 沒有命中. if (insert_item_into_group(group, mask)) { eviction++; verbose ? printf(" eviction") : 0; } } if (mod[0] == 'M') { hit++; // 修改的情況 而外加一次命中. verbose ? printf(" hit") : 0; } verbose ? puts("") : 0;
cache 我使用連結串列來模擬, 其中每個節點都是一個cache line, 其中的資料包括:
typedef struct CacheItem {
struct CacheItem* next;
int val;
} CacheItem, *CacheItemPtr;
typedef struct CacheGroup {
CacheItemPtr head;
int size;
} CacheGroup, *CacheGroupPtr;
連結串列中,節點的存放順序就是他們最近訪問的次數
當 CacheGroup.size > E 時執行淘汰, 刪除最後一個節點即可,
當節點被訪問或加入是, 直接插入到連結串列頭部.
CacheItemPtr init_cache_item(int v) {
CacheItemPtr i = (CacheItemPtr)malloc(sizeof(CacheItem));
if (i == NULL) {
exit(1);
}
i->val = v;
i->next = NULL;
return i;
}
void clear_cache_item(CacheItemPtr item) {
if (item == NULL) {
return;
}
clear_cache_item(item->next);
free(item);
}
CacheGroupPtr init_cache_group() {
CacheGroupPtr g = (CacheGroupPtr)malloc(sizeof(CacheGroup));
if (g == NULL) {
exit(1);
}
g->head = init_cache_item(0);
g->size = 0;
return g;
}
void clear_cache_group(CacheGroupPtr group) {
if (group == NULL) {
return;
}
clear_cache_item(group->head);
free(group);
}
int find_item_in_group(CacheGroupPtr group, int val) {
CacheItemPtr item = group->head->next;
CacheItemPtr pre_item = group->head;
while (item != NULL) {
if (item->val == val) {
// move item to first item.
pre_item->next = item->next;
item->next = group->head->next;
group->head->next = item;
return 1;
}
pre_item = item;
item = item->next;
}
return 0;
}
void evict_last_group(CacheGroupPtr group) {
CacheItemPtr item = group->head->next;
CacheItemPtr pre_item = group->head;
while (item->next != NULL) {
pre_item = item;
item = item->next;
}
clear_cache_item(item);
pre_item->next = NULL;
group->size--;
}
int insert_item_into_group(CacheGroupPtr group, int val) {
int res = 0;
if (group->size == E) {
evict_last_group(group);
res = 1;
}
CacheItemPtr item = init_cache_item(val);
item->next = group->head->next;
group->head->next = item;
group->size++;
return res;
}
Part B
為矩陣轉置演算法進行 cache 命中優化, cache 引數為 s = 5, E = 1, b = 5, 即塊大小32位元組, 組內只有一塊, 總共32個組, 原始的轉置程式碼如下:
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;
}
}
}
在解決時一開始沒有頭緒,走了很多彎路, 首先比較直觀的觀察
- int 大小 4位元組, 一個cache line 可以存放 8個位元組
- 矩陣記憶體是按行儲存, 因此 A[i][j] 行訪問可以很好的命中 cache, 而B[j][i] 列訪問需要我們進行優化.
- 三種情況 32:32, 64:64, 61:67 可以進行不同的優化.
因此我的第一版思路為對矩陣分成 8*8 的塊, 然後按對角線方式遍歷, 且函式最多有12個臨時變數, 4個作為迴圈+分塊變數, 8個可以用作訪問快取.
|10|13|15|16|
|6 |9 |12|14|
|3 |5 |8 |11|
|1 |2 |4 |7 |
但該方法對 64:64 的情況沒什麼效果, 這時我查閱了部落格, 發現解決問題的關鍵就是分組+避免衝突, 跟對角線訪問順序沒什麼關係, 64:64情況下按原來的8個一組會造成衝突, 從而降低效率, 要改進成4個一組.
對於61:67的情況, 由於矩陣大小沒有跟cache line對齊, 因此按8個一組就不會衝突. 我們先按8個一組訪問, 對不滿8個的邊界情況直接挨個訪問. 以下是我的解答程式碼
char transpose_64_64_desc[] = "Transpose for 64 64";
void transpose_64_64(int M, int N, int A[N][M], int B[M][N])
{
// 1653
int i,j,ii;
int jj;
int arr[8];
for (i = 0; i < N; i+=8) {
for (j = 0; j < M; j+=8) {
for (ii=0;ii<8;++ii) {
// 只在最裡層4步長訪問即可
for (jj=0;jj<4;++jj) {
arr[jj] = A[i+ii][j+jj];
}
for (jj=0;jj<4;++jj) {
B[j+jj][i+ii] = arr[jj];
}
}
for (ii=0;ii<8;++ii) {
for (jj=4;jj<8;++jj) {
arr[jj] = A[i+ii][j+jj];
}
for (jj=4;jj<8;++jj) {
B[j+jj][i+ii] = arr[jj];
}
}
}
}
}
char transpose_general_block8_desc[] = "Transpose for genernal, block is 8";
void transpose_general_block8(int M, int N, int A[N][M], int B[M][N])
{
// 61:67 2075
// 32:32 289
#ifndef BLOCK_SIZE
#define BLOCK_SIZE 8
int i, j, jj, ii;
int arr[BLOCK_SIZE];
for (i=0; i+BLOCK_SIZE<=N;i+=BLOCK_SIZE) {
for (j=0;j+BLOCK_SIZE<=M;j+=BLOCK_SIZE) {
for (ii=0;ii<BLOCK_SIZE;++ii) {
for (jj=0;jj<BLOCK_SIZE;++jj) {
// printf("%d %d\t", i+ii, jj+j);
arr[jj] = A[i+ii][jj+j];
}
for (jj=0;jj<BLOCK_SIZE;++jj) {
B[jj+j][i+ii] = arr[jj];
}
}
}
for (;j<M;++j) {
for (ii=0;ii<BLOCK_SIZE;++ii) {
// printf("%d %d\t", i+ii, jj);
arr[ii] = A[i+ii][j];
}
for (ii=0;ii<BLOCK_SIZE;++ii) {
B[j][i+ii] = arr[ii];
}
}
}
for (;i<N;i++) {
for (j=0;j+BLOCK_SIZE<=M;j+=BLOCK_SIZE) {
for (jj=0;jj<BLOCK_SIZE;++jj) {
// printf("%d %d\t", i, jj+j);
arr[jj] = A[i][jj+j];
}
for (jj=0;jj<BLOCK_SIZE;++jj) {
B[jj+j][i] = arr[jj];
}
}
for (;j<M;++j) {
B[j][i] = A[i][j];
}
// puts("");
}
#undef BLOCK_SIZE
#endif //BLOCK_SIZE
}
這次lab對partB的解答其實不夠深入, 如果更好的統計cache的 miss 情況, 應該能得到更好的解答.