1. 程式人生 > 其它 >[lab]csapp-cachelab

[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 情況, 應該能得到更好的解答.