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 情况, 应该能得到更好的解答.