前言
本文是15-213 CSAPP系列课程配套实验cachelab的题解,实验分为两个部分:
一、编写缓存模拟器(cache simulator),模拟地址与缓存之间的映射关系
二、编写缓存友好(cache-friendly)代码,从而优化矩阵转置
碎碎念:cachelab可以用c语言写,终于不用在gdb一行行看汇编了。
个人博客页:【CMU 15-213 CSAPP】详解cachelab------模拟缓存、编写缓存友好代码 | Andrew的个人博客 (andreww1219.github.io)
参考资料:
课程视频链接:2015 CMU 15-213 CSAPP 深入理解计算机系统 课程视频
PartB思路参考:
CSAPP Cache Lab 缓存实验 - 简书 (jianshu.com)
CSAPP - Cache Lab的更(最)优秀的解法 - 知乎 (zhihu.com)
Part A: 编写缓存模拟器
1. 实验目标
在csim.c中编写代码,后编译生成可执行文件,要求该文件接收traces目录下的文件作为输入,得到的输出与参考文件csim-ref执行同样操作得到的结果一致。
那不就跟打OJ一样嘛!而且每个样例都给我们了,直接面向样例编程不就好了(bushi
言归正传,既然是给定了输入输出,那么csim这个黑盒做了什么事情呢?
(1) Trace Files
首先,traces目录下的每个文件中,有一行行的操作指令,每条指令格式如下:
css
[space]operation address,size
operation 代表操作,包括:L 读操作(Load) ,S 写操作(Save) ,M 读操作 + 写操作(Modify)
address 代表64位地址 ,我们需要将该地址分割成 有效位valid ,组索引set index ,块偏移block offset,再映射到缓存中进行以上操作
size 代表上述操作涉及到的字节数
(2) csim-ref
既然要模拟csim-ref,那就看看csim-ref的输入输出长什么样,在命令行中是这么调用csim-ref的:
shell
./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile>
-t 指定一个trace file,然后在分析里面每个操作对缓存的影响,是hit还是miss,有没有eviction
-h 表示打印帮助信息,-v 表示打印每个操作的分析结果
-s 指定组索引的位数 ,-E 指定每组的行数 ,-b 指定块偏移的位数
示例如下:
没有-v:
shell
linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace
hits:4 misses:5 evictions:3
有-v:
shell
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
使用-h:
shell
root@Andrew:/mnt/d/.c/csapp/cachelab-handout# ./csim-ref -h
Usage: ./csim-ref [-hv] -s <num> -E <num> -b <num> -t <file>
Options:
-h Print this help message.
-v Optional verbose flag.
-s <num> Number of set index bits.
-E <num> Number of lines per set.
-b <num> Number of block offset bits.
-t <file> Trace file.
Examples:
linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace
linux> ./csim-ref -v -s 8 -E 2 -b 4 -t traces/yi.trace
错误输入:
shell
root@Andrew:/mnt/d/.c/csapp/cachelab-handout# ./csim-ref -v -s 4 -E 1 -t traces/andrew.trace
./csim-ref: Missing required command line argument
Usage: ./csim-ref [-hv] -s <num> -E <num> -b <num> -t <file>
Options:
-h Print this help message.
-v Optional verbose flag.
-s <num> Number of set index bits.
-E <num> Number of lines per set.
-b <num> Number of block offset bits.
-t <file> Trace file.
Examples:
linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace
linux> ./csim-ref -v -s 8 -E 2 -b 4 -t traces/yi.trace
文件不存在:
shell
root@Andrew:/mnt/d/.c/csapp/cachelab-handout# ./csim-ref -v -s 4 -E 1 -b 4 -t traces/andrew.trace
traces/andrew.trace: No such file or directory
2. 编写代码
(1) 读取命令行输入
unistd.h提供了getopt()用于解析命令行参数,getopt的前两个参数同main函数的参数,第三个参数为一个字符串,格式如下:
c
char opts[16] = "hvs:E:b:t:";
其中,不带冒号 表示无参数 ,带一个冒号 表示必须有参数 ,带两个冒号 则表示参数可选
在本例中,-h和-v不带参数,-s -E -b 和 -t必须带参数。
注意:实验提供的编译参数无法使用getopt,应当在Makefile中将CFLAGS作如下修改
ini
# CFLAGS = -g -Wall -Werror -std=c99 -m64
CFLAGS = -g -Wall -Werror -std=gnu99 -m64
getopt()返回得到匹配的字符,并且设置全局变量optarg为匹配到的参数,如下:
c
char helper[1024] = "Usage: ./csim-ref [-hv] -s <num> -E <num> -b <num> -t <file>\n"
"Options:\n"
"-h Print this help message.\n"
"-v Optional verbose flag.\n"
"-s <num> Number of set index bits.\n"
"-E <num> Number of lines per set.\n"
"-b <num> Number of block offset bits.\n"
"-t <file> Trace file.\n\n"
"Examples:\n"
"linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace\n"
"linux> ./csim-ref -v -s 8 -E 2 -b 4 -t traces/yi.trace\n";
int display = 0;
int s = 0, E = 0, b = 0;
char tracefile[MAX_FILENAME_LENGTH];
int main(int argc, char *argv[])
{
// get params from command line
int ch;
while ((ch = getopt(argc, argv, opts)) != -1){
switch (ch) {
case 'h':
printf("%s", helper);
return 0;
case 'v':
display = 1;
break;
case 's':
s = atoi(optarg);
break;
case 'E':
E = atoi(optarg);
break;
case 'b':
b = atoi(optarg);
break;
case 't':
sscanf(optarg, "%s", tracefile);
break;
}
}
return 0;
}
上述步骤便完成了对命令行参数的解析和保存
更多异常处理如下:
如果缺少s或E或b:
c
// make sure that s, E and b are valid
if(!s || !E || !b){
printf("./csim-ref: Missing required command line argument\n");
printf("%s", helper);
return 0;
}
如果tracefile无法打开:
c
// make sure that the tracefile is valid
FILE *fp = NULL;
fp = fopen(tracefile, "r");
if(!fp){
printf("%s: No such file or directory\n", tracefile);
return 0;
}
(2) 定义缓存
已知 <math xmlns="http://www.w3.org/1998/Math/MathML"> S = 2 s S = 2^s </math>S=2s为缓存总共的组数, <math xmlns="http://www.w3.org/1998/Math/MathML"> E E </math>E为每一组的行数,而每一行包含有效位valid ,标记tag 以及长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> B = 2 b B = 2^b </math>B=2b的高速缓存块。
由于在本题中,我们不需要处理缓存保存的实际内容,所以高速缓存块是什么可以忽略。
此外,文档指定缓存的替换策略为最近最少使用(Least-recently used, LRU) ,也就是说,会替换最后一次访问时间最久远 的那一行。因此需要在定义 行 的结构体时,增加一个时间戳stamp表示最后一次访问该行的时间
定义 行 的结构体如下:
c
typedef struct{
int valid;
int tag;
long stamp;
}line;
获取当前时间的时间戳的函数:
c
#include <sys/time.h>
long get_stamp(){
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_usec;
}
注意不是 最不常使用(least-frequently-used,LFU):替换在过去某个时间窗口内引用次数最少的那一行,如果是这个策略的话,需要记录的是被使用的次数cnt,一开始没分清然后几个样例过不去。
那么,缓存cache 就是元素为结构体line的二维数组,用malloc动态分配内存如下:
c
// init our cache before process with each row
unsigned long int setSize = sizeof(line) * E;
int S = 1 << s;
line **cache;
cache = (line**)malloc(sizeof(line*) * S);
for(int i = 0; i < S; ++i){
cache[i] = (line*)malloc(setSize);
memset(cache[i], 0, setSize); // valid = 0, stamp = 0
}
注意将有效位和时间戳都初始化为0
(3) 处理操作
由于我们只需要分析数据对缓存的影响,可以忽略对指令的操作,也就是读到第一个字符是I的可以直接跳过
c
// process with each row
unsigned long int rowSize = 64;
char *row = (char*)malloc(sizeof(char) * rowSize);
while(fgets(row, rowSize, fp) != NULL){
if(row[0] == 'I')
continue;
对于操作L,S 和M,就需要保存操作类型op ,地址addr 和size用于 -v时候的输出
c
// get operation, address and size from each row
char op;
long addr;
int size;
sscanf((row[0] == ' ') ? row + 1: row, "%c %lx,%d", &op, &addr, &size);
if(display){
printf("%c %lx, %d ", op, addr, size);
}
这里使用 (row[0] == ' ') ? row + 1: row 是因为tracefile中有I操作时,其他操作前面会多一个空格
将addr分割为 有效位valid ,组索引set index 和块偏移block offset
c
//get tag, set index and block offset;
// int block_offset = (int) (addr % (1 << b));
addr /= (1 << b);
int set_index = (int)(addr % (1 << s));
addr /= (1 << s);
int tag = (int)addr;
block_offset用不到,其实也可以不用存,存了会报错unused variable
根据set_index定义到缓存中对应组,找到组中tag与地址的tag相等,并且有效位valid为1的一行
c
// find line in cache
int hitTag = 0, hitIndex = -1;
for(int i = 0; i < E; ++i ){
if(cache[set_index][i].valid && cache[set_index][i].tag == tag){
hitTag = 1;
hitIndex = i;
break;
}
}
处理hit和miss
hit很好处理,直接修改对应行的时间戳,需要打印结果就打印结果
miss就得根据LRU找到需要覆盖的行,如果该行是valid的,说明出现了eviction
由于M操作 相当于L操作+S操作,也就是一读一写,并且观察到写操作肯定是hit。因此如果发现是M操作,就在后边加一次hit计数
c
if(hitTag){ // hit
++hits;
if(display)
printf("hit ");
cache[set_index][hitIndex].stamp = get_stamp();
}else{ // miss
++misses;
if(display)
printf("miss ");
// find LRU line
hitIndex = 0;
for(int i = 1; i < E; ++i){
if(cache[set_index][i].stamp < cache[set_index][hitIndex].stamp)
hitIndex = i;
}
if(cache[set_index][hitIndex].valid){
++evictions;
if(display)
printf("eviction ");
}
cache[set_index][hitIndex].valid = 1;
cache[set_index][hitIndex].tag = tag;
cache[set_index][hitIndex].stamp = get_stamp();
}
if(op == 'M'){
++hits;
if(display)
printf("hit ");
cache[set_index][hitIndex].stamp = get_stamp();
}
if(display)
putchar('\n');
}
(4) 输出结果
程序末尾释放malloc的内存当然是好习惯啦:
c
// output the result and release memories
fclose(fp);
free(cache);
printSummary(hits, misses, evictions);
测试结果如下:
shell
root@Andrew:/mnt/d/.c/csapp/cachelab-handout# ./test-csim
Your simulator Reference simulator
Points (s,E,b) Hits Misses Evicts Hits Misses Evicts
3 (1,1,1) 9 8 6 9 8 6 traces/yi2.trace
3 (4,2,4) 4 5 2 4 5 2 traces/yi.trace
3 (2,1,4) 2 3 1 2 3 1 traces/dave.trace
3 (2,1,3) 167 71 67 167 71 67 traces/trans.trace
3 (2,2,3) 201 37 29 201 37 29 traces/trans.trace
3 (2,4,3) 212 26 10 212 26 10 traces/trans.trace
3 (5,1,5) 231 7 0 231 7 0 traces/trans.trace
6 (5,1,5) 265189 21775 21743 265189 21775 21743 traces/long.trace
27
TEST_CSIM_RESULTS=27
Part B: 优化矩阵转置
1. 实验目标
实验提供了验证矩阵转置函数的正确性及其表现的打分器,将编写的函数在缓存参数为
<math xmlns="http://www.w3.org/1998/Math/MathML"> s = 5 , E = 1 , b = 5 s = 5, E = 1, b = 5 </math>s=5,E=1,b=5
的条件下运行,并跟踪hit,miss和eviction的次数。一共有三个关卡:
- 对 <math xmlns="http://www.w3.org/1998/Math/MathML"> 32 × 32 32 \times 32 </math>32×32的矩阵作转置, <math xmlns="http://www.w3.org/1998/Math/MathML"> m i s s < 300 miss < 300 </math>miss<300满分, <math xmlns="http://www.w3.org/1998/Math/MathML"> m i s s > 600 miss > 600 </math>miss>600零分
- 对 <math xmlns="http://www.w3.org/1998/Math/MathML"> 64 × 64 64 \times 64 </math>64×64的矩阵作转置, <math xmlns="http://www.w3.org/1998/Math/MathML"> m i s s < 1300 miss < 1300 </math>miss<1300满分, <math xmlns="http://www.w3.org/1998/Math/MathML"> m i s s > 2000 miss > 2000 </math>miss>2000零分
- 对 <math xmlns="http://www.w3.org/1998/Math/MathML"> 61 × 67 61 \times 67 </math>61×67的矩阵作转置, <math xmlns="http://www.w3.org/1998/Math/MathML"> m i s s < 2000 miss < 2000 </math>miss<2000满分, <math xmlns="http://www.w3.org/1998/Math/MathML"> m i s s > 3000 miss > 3000 </math>miss>3000零分
2. 实验过程
实验环境有点小坑:
- test-trans运行超时:将实验文件夹从共享文件夹中移动到Linux单独的目录中
- 移动后Permission Denied:使用命令chmod 777 xx(文件名称)
实话说要自己想出来优化方法真的很难,本人做个分块之后就手足无措了,然后搜刮资料看别人的优秀思路,听取妙声一片(
(1) 32 <math xmlns="http://www.w3.org/1998/Math/MathML"> × \times </math>× 32
由于 <math xmlns="http://www.w3.org/1998/Math/MathML"> b = 5 b = 5 </math>b=5,那么 <math xmlns="http://www.w3.org/1998/Math/MathML"> B = 2 b = 32 B = 2^b = 32 </math>B=2b=32,缓存中每一行保存32字节的数据,又int占4个字节,那么缓存中每一行能存8个int
由于 <math xmlns="http://www.w3.org/1998/Math/MathML"> s = 5 s = 5 </math>s=5且 <math xmlns="http://www.w3.org/1998/Math/MathML"> E = 1 E = 1 </math>E=1,那么共有 <math xmlns="http://www.w3.org/1998/Math/MathML"> S = 2 s = 32 S = 2^s = 32 </math>S=2s=32组,每一组只有一行,故缓存总共能存32 * 8 个int
本例中,处理的矩阵是32 <math xmlns="http://www.w3.org/1998/Math/MathML"> × \times </math>× 32,故缓存最多能保存矩阵的8行
那正好,按8 * 8分块的话,8列能完美地利用每一行缓存中的8个int,8行又恰好保证了每一行有不同的set index,不会互相冲突。
分块写法如下:
c
char trans_blocking_decr[] = "blocking 8 by 8";
void trans_blocking(int M, int N, int A[N][M], int B[M][N]){
int i, j;
int k, t;
int size = 8;
int maxI = N / size, maxJ = M / size;
for(i = 0; i < maxI; ++i){
for(j = 0; j < maxJ; ++j){
int minK = i * size, maxK = (i + 1) * size;
int minT = j * size, maxT = (j + 1) * size;
for(k = minK; k < N && k < maxK; ++k){
for(t = minT; t < M && t < maxT; ++t){
B[t][k] = A[k][t];
}
}
}
}
}
测试结果如下:
shell
Function 2 (3 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 2 (blocking 8 by 8): hits:1709, misses:344, evictions:312
仍然有344次miss,这是怎么回事呢?
对分块进行理论值分析:块长宽为8,矩阵长宽为32,那么A和B各有(32 / 8) ^ 2 = 16块,每一块有八分之一miss,也就是 8 * 8 * (1 / 8) = 8 下miss。综上,应该总共有 2 * 16 * 8 = 256次miss才对。344比理论多了不少。
原来是因为,定义M和N这两个数组的代码在tracegen.c中,长这样:
c
static int A[256][256];
static int B[256][256];
这说明,A和B都是连续的256 * 256个int,是缓存容量的整数倍。因此,取A和B中下标相同的位置时,它们的地址总是相差256 * 256个int的长度,地址会被分割为tag,set index和block offset。故它们的set index相同但tag不同,而一个组只有一行,需要eviction。
这么一来,对于对角线上的块,就会反复地出现miss 和 eviction,需要对其特殊处理。
这怎么想得到(小声
特殊处理就是,遍历位于A中的块的每一行时,把一整行用局部变量存到寄存器中,就不怕写入B导致的eviction会让下一次读A出现miss了。也就是通过保存A,减少冲突情况下读取A导致的miss
改进写法如下:
c
void trans_blocking(int M, int N, int A[N][M], int B[M][N]){
int i, j, k, t;
int a0, a1, a2, a3, a4, a5, a6, a7;
for(i = 0; i < N; i += 8){
for(j = 0; j < M; j += 8){
if(i == j){
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;
}
}else{
for(k = i; k < i + 8; ++k)
for(t = j; t < j + 8; ++t)
B[t][k] = A[k][t];
}
}// for j
}// for i
}// trans_blocking
shell
Function 2 (3 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 2 (blocking 8 by 8): hits:1765, misses:288, evictions:256
漂亮的很馁,结果是288次miss。
其实还可以再改进,我们看到前面减少的是读取A时的miss,其实还可以减少写入B时的miss 。由于对于A是行操作,对于B是列操作(需要8行缓存),当两者交错在一起时,必然会导致每次对A的读取都会使下一次对B的写入多一次miss。所以引申出新思路(太nb了):先按行读A ,并按行写入到B对应块 中,再对B中每一块作转置
代码如下:
c
void trans_blocking_advanced(int M, int N, int A[N][M], int B[M][N]){
int i, j, k, t;
int a0, a1, a2, a3, a4, a5, a6, a7;
for(i = 0; i < N; i += 8){
for(j = 0; j < M; j += 8){
// 将A中每一块按行复制到B中对应块
// A 是从 i, j 开始递增
// 那么B是从 j, i 开始递增
for(k = i, t = j; k < i + 8; ++k, ++t){
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[t][i] = a0;
B[t][i + 1] = a1;
B[t][i + 2] = a2;
B[t][i + 3] = a3;
B[t][i + 4] = a4;
B[t][i + 5] = a5;
B[t][i + 6] = a6;
B[t][i + 7] = a7;
}
// 对每一块内作转置
// 注意B的块是在 j, i 到 j + 8, i + 8 之间
for(k = 0; k < 8; ++k){
for(t = k + 1; t < 8; ++t){
a0 = B[j + k][i + t];
B[j + k][i + t] = B[j + t][i + k];
B[j + t][i + k] = a0;
}
}
}// for j
}// for i
}// trans_blocking_advanced
shell
Function 3 (4 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 3 (advanced version of blocking 8 by 8): hits:3585, misses:260, evictions:228
太美丽辣,misses低至260!!!只比理论值高出4点
(2) 64 <math xmlns="http://www.w3.org/1998/Math/MathML"> × \times </math>× 64
前面说到,缓存能保存32 * 8 个int,也就是32 * 32的int矩阵的8行。也就是说,每过8行,set index就会重新计数。由于前面取的是8 * 8的块,所以块内不会出现set index冲突的情况。
而现在,矩阵的规格是64 * 64,也就是说,每过4行,set index就会重新计数。如果取8 * 8的块,那么块内前4行跟后4行的set index就会出现冲突!
但是为了充分利用缓存又不得不用8 * 8的块,因此聪明的大佬们就想到了把8 * 8的块再分成4个4 * 4的块再处理
先展示一下本人失败的思路:
先跟32 * 32一样,把每个块按行复制到B中,再在B中把每个块分成4 * 4的块做转置:
c
void trans_blocking_pp(int M, int N, int A[N][M], int B[M][N]){
int i, j, k, t;
int a0, a1, a2, a3, a4, a5, a6, a7;
for(i = 0; i < N; i += 8){
for(j = 0; j < M; j += 8){
// 同32*32, 从A按行复制到B
for(k = i, t = j; k < i + 8; ++k, ++t){
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[t][i] = a0;
B[t][i + 1] = a1;
B[t][i + 2] = a2;
B[t][i + 3] = a3;
B[t][i + 4] = a4;
B[t][i + 5] = a5;
B[t][i + 6] = a6;
B[t][i + 7] = a7;
}
// 对4个 4 * 4的块做转置
// 右上和左下对换
// 右上从 j, i + 4 开始数
// 左下从 j + 4, i 开始数
for(k = 0; k < 4; ++k){
a0 = B[j + k][i + 4];
a1 = B[j + k][i + 5];
a2 = B[j + k][i + 6];
a3 = B[j + k][i + 7];
B[j + k][i + 4] = B[j + 4 + k][i];
B[j + k][i + 5] = B[j + 4 + k][i + 1];
B[j + k][i + 6] = B[j + 4 + k][i + 2];
B[j + k][i + 7] = B[j + 4 + k][i + 3];
B[j + 4 + k][i] = a0;
B[j + 4 + k][i + 1] = a1;
B[j + 4 + k][i + 2] = a2;
B[j + 4 + k][i + 3] = a3;
}
// 再对四个4 * 4矩阵各自转置
// 左上然后右上, 左下然后右下 能充分利用缓存
for(k = 0; k < 4; ++k){
for(t = k + 1; t < 4; ++t){
a0 = B[j + k][i + t];
B[j + k][i + t] = B[j + t][i + k];
B[j + t][i + k] = a0;
}
}
// 右上
for(k = 0; k < 4; ++k){
for(t = k + 1; t < 4; ++t){
a0 = B[j + k][i + 4 + t];
B[j + k][i + 4 + t] = B[j + t][i + 4 + k];
B[j + t][i + 4 + k] = a0;
}
}
// 左下
for(k = 0; k < 4; ++k){
for(t = k + 1; t < 4; ++t){
a0 = B[j + 4 + k][i + t];
B[j + 4 + k][i + t] = B[j + 4 + t][i + k];
B[j + 4 + t][i + k] = a0;
}
}
// 右下
for(k = 0; k < 4; ++k){
for(t = k + 1; t < 4; ++t){
a0 = B[j + 4 + k][i + 4 + t];
B[j + 4 + k][i + 4 + t] = B[j + 4 + t][i + 4 + k];
B[j + 4 + t][i + 4 + k] = a0;
}
}
}// for j
}// for i
}// trans_blocking_pp
shell
Function 2 (3 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 2 (plus plus version of blocking trans): hits:14337, misses:4100, evictions:4068
结果不尽人意,分析是因为完整地复制了8行过去,B的后4行的缓存会覆盖B的前4行,这时再做转置会反复miss。
于是尝试先复制前4行 过去,转置 。然后别着急,先别把后4行复制过去,全部在B中就不好处理了。想想现在缓存里是什么:A的前4行 和 B的前4行。
其中B的前4行的后4列(右上)是需要移动的,移动后是A的后4行前4列来补充(左下),所以趁着B的前4行缓存还在,赶紧把这个操作完成了。局部变量按行存B的右上 ,然后按列取出A的左下转置到B的右上空出的那一行 ,再把保存的那一行转移到B的左下
这时,B的前4行已经完全不在缓存中了,只要放心地把B的右下等于A的右下的转置就行了
c
void trans_blocking_pp(int M, int N, int A[N][M], int B[M][N]){
int i, j, k, t;
int a0, a1, a2, a3, a4, a5, a6, a7;
for(i = 0; i < N; i += 8){
for(j = 0; j < M; j += 8){
// 同32*32, 从A按行复制到B
for(k = i, t = j; k < i + 4; ++k, ++t){
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[t][i] = a0;
B[t][i + 1] = a1;
B[t][i + 2] = a2;
B[t][i + 3] = a3;
B[t][i + 4] = a4;
B[t][i + 5] = a5;
B[t][i + 6] = a6;
B[t][i + 7] = a7;
}
// 左上
for(k = 0; k < 4; ++k){
for(t = k + 1; t < 4; ++t){
a0 = B[j + k][i + t];
B[j + k][i + t] = B[j + t][i + k];
B[j + t][i + k] = a0;
}
}
// 右上
for(k = 0; k < 4; ++k){
for(t = k + 1; t < 4; ++t){
a0 = B[j + k][i + 4 + t];
B[j + k][i + 4 + t] = B[j + t][i + 4 + k];
B[j + t][i + 4 + k] = a0;
}
}
for(k = 0; k < 4; ++k){
// 局部变量按行存B的右上
a0 = B[j + k][i + 4];
a1 = B[j + k][i + 5];
a2 = B[j + k][i + 6];
a3 = B[j + k][i + 7];
// 按列取出A的左下转置到B的右上空出的那一行
B[j + k][i + 4] = A[i + 4][j + k];
B[j + k][i + 5] = A[i + 5][j + k];
B[j + k][i + 6] = A[i + 6][j + k];
B[j + k][i + 7] = A[i + 7][j + k];
// 把保存的那一行转移到B的左下
B[j + 4 + k][i] = a0;
B[j + 4 + k][i + 1] = a1;
B[j + 4 + k][i + 2] = a2;
B[j + 4 + k][i + 3] = a3;
}
// B的右下等于A的右下的转置
for(k = 4; k < 8; ++k){
for(t = 4; t < 8; ++t){
B[j + k][i + t] = A[i + t][j + k];
}
}
}// for j
}// for i
}// trans_blocking_pp
shell
Function 2 (3 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 2 (plus plus version of blocking trans): hits:12073, misses:1244, evictions:1212
仅仅1244 misses!!!小于满分的要求1300,虽然别人题解更低的也不少,但是由于我是受到把8 * 8分成4块4 * 4的启发后自己探索拿到的满分,还是很有成就感!!!
(3) 61 <math xmlns="http://www.w3.org/1998/Math/MathML"> × \times </math>× 67
实在想不到有什么特别的办法,那就在8 * 8的分块的基础上,把边角料给处理了:
c
char trans_notsquare_decr[] = "trans a matrix not square";
void trans_notsquare(int M, int N, int A[N][M], int B[M][N])
{
int i, j, k;
int a0, a1, a2, a3, a4, a5, a6, a7;
for(i = 0; i + 8 <= N; i += 8){
for(j = 0; j + 8 <= M; 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;
}
}// for j
}// for i
for(i = N - N % 8; i < N; ++i)
for(j = 0; j < M - M % 8; ++j)
B[j][i] = A[i][j];
for(i = 0; i < N - N % 8; ++i)
for(j = M - M % 8; j < M ; ++j)
B[j][i] = A[i][j];
for(i = N - N % 8; i < N; ++i)
for(j = M - M % 8; j < M; ++j)
B[j][i] = A[i][j];
}
shell
Function 1 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 1 (trans a matrix not square): hits:6102, misses:2077, evictions:2045
苦亚西,苦亚西------------满分是2000以下,现在2077,那就在处理边角料那里再怼几个局部变量
来点循环展开:
c
for(i = N - N % 8; i < N; ++i)
for(j = 0; j < M - M % 8; ++j)
B[j][i] = A[i][j];
for(i = 0; i < N - N % 8; ++i){
a0 = A[i][M - 5];
a1 = A[i][M - 4];
a2 = A[i][M - 3];
a3 = A[i][M - 2];
a4 = A[i][M - 1];
B[M - 5][i] = a0;
B[M - 4][i] = a1;
B[M - 3][i] = a2;
B[M - 2][i] = a3;
B[M - 1][i] = a4;
}
for(i = N - N % 8; i < N; ++i){
a0 = A[i][M - 5];
a1 = A[i][M - 4];
a2 = A[i][M - 3];
a3 = A[i][M - 2];
a4 = A[i][M - 1];
B[M - 5][i] = a0;
B[M - 4][i] = a1;
B[M - 3][i] = a2;
B[M - 2][i] = a3;
B[M - 1][i] = a4;
}
shell
Function 1 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 1 (trans a matrix not square): hits:6115, misses:2064, evictions:2032
2064!!!距离目标又近了一点,但是也太少了吧!!!所以我决定,放弃8 * 8,来点玄学分块
试一下8 * 13(选13是因为想到5 * 13 = 65离67比较近,不过应该没关系
c
void trans_notsquare(int M, int N, int A[N][M], int B[M][N])
{
int i, j, k, t;
int a0, a1, a2, a3, a4, a5, a6, a7;
for(i = 0; i < N; i += 8){
for(j = 0; j < M; j += 13){
if(i + 8 <= N && j + 13 <= M){
for (t = j; t < j + 13; ++t) {
a0 = A[i][t];
a1 = A[i + 1][t];
a2 = A[i + 2][t];
a3 = A[i + 3][t];
a4 = A[i + 4][t];
a5 = A[i + 5][t];
a6 = A[i + 6][t];
a7 = A[i + 7][t];
B[t][i + 0] = a0;
B[t][i + 1] = a1;
B[t][i + 2] = a2;
B[t][i + 3] = a3;
B[t][i + 4] = a4;
B[t][i + 5] = a5;
B[t][i + 6] = a6;
B[t][i + 7] = a7;
}
}else{
for(k = i; k < N && k < i + 8; ++k)
for(t = j; t < M && t < j + 13; ++t)
B[t][k] = A[k][t];
}
}// for j
}// for i
}
shell
Function 1 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 1 (trans a matrix not square): hits:6357, misses:1822, evictions:1790
1822爽过