前言

本文是15-213 CSAPP系列课程配套实验cachelab的题解,实验分为两个部分:
一、编写缓存模拟器(cache simulator),模拟地址与缓存之间的映射关系
二、编写缓存友好(cache-friendly)代码,从而优化矩阵转置

碎碎念:cachelab可以用c语言写,终于不用在gdb一行行看汇编了。

参考资料:

课程视频链接:2015 CMU 15-213 CSAPP 深入理解计算机系统 课程视频

实验文档:cachelab.dvi (cmu.edu)

getopt编译错误:编译错误 error: implicit declaration of function ‘getopt’ [-Werror=implicit-function-declaration] 解决方法-CSDN博客

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目录下的每个文件中,有一行行的操作指令,每条指令格式如下:

1
[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的:

1
./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile>

-t 指定一个trace file,然后在分析里面每个操作对缓存的影响,是hit还是miss,有没有eviction
-h 表示打印帮助信息,-v表示打印每个操作的分析结果
-s 指定组索引的位数-E 指定每组的行数-b 指定块偏移的位数

示例如下:

没有-v:

1
2
linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace
hits:4 misses:5 evictions:3

有-v:

1
2
3
4
5
6
7
8
9
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
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

错误输入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

文件不存在:
1
2
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函数的参数,第三个参数为一个字符串,格式如下:

1
char opts[16] = "hvs:E:b:t:";

其中,不带冒号表示无参数,带一个冒号表示必须有参数,带两个冒号则表示参数可选

在本例中,-h和-v不带参数,-s -E -b 和 -t必须带参数。

注意:实验提供的编译参数无法使用getopt,应当在Makefile中将CFLAGS作如下修改

1
2
# CFLAGS = -g -Wall -Werror -std=c99 -m64
CFLAGS = -g -Wall -Werror -std=gnu99 -m64

getopt()返回得到匹配的字符,并且设置全局变量optarg为匹配到的参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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:

1
2
3
4
5
6
// 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无法打开:

1
2
3
4
5
6
7
// 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) 定义缓存

已知$S = 2^s$为缓存总共的组数,$E$为每一组的行数,而每一行包含有效位valid标记tag以及长度为$B = 2^b$的高速缓存块。

由于在本题中,我们不需要处理缓存保存的实际内容,所以高速缓存块是什么可以忽略。

此外,文档指定缓存的替换策略为最近最少使用(Least-recently used, LRU),也就是说,会替换最后一次访问时间最久远的那一行。因此需要在定义 行 的结构体时,增加一个时间戳stamp表示最后一次访问该行的时间

定义 行 的结构体如下:

1
2
3
4
5
typedef struct{
int valid;
int tag;
long stamp;
}line;

获取当前时间的时间戳的函数:

1
2
3
4
5
6
7
#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动态分配内存如下:

1
2
3
4
5
6
7
8
9
// 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的可以直接跳过

1
2
3
4
5
6
// 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地址addrsize用于 -v时候的输出
1
2
3
4
5
6
7
8
// 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

1
2
3
4
5
6
//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的一行

1
2
3
4
5
6
7
8
9
// 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计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    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的内存当然是好习惯啦:

1
2
3
4
// output the result and release memories
fclose(fp);
free(cache);
printSummary(hits, misses, evictions);

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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. 实验目标

实验提供了验证矩阵转置函数的正确性及其表现的打分器,将编写的函数在缓存参数为

的条件下运行,并跟踪hit,miss和eviction的次数。一共有三个关卡:

  1. 对$32 \times 32$的矩阵作转置,$miss < 300$满分,$miss > 600$零分
  2. 对$64 \times 64$的矩阵作转置,$miss < 1300$满分,$miss > 2000$零分
  3. 对$61 \times 67$的矩阵作转置,$miss < 2000$满分,$miss > 3000$零分

2. 实验过程

实验环境有点小坑:

  1. test-trans运行超时:将实验文件夹从共享文件夹中移动到Linux单独的目录中
  2. 移动后Permission Denied:使用命令chmod 777 xx(文件名称)

实话说要自己想出来优化方法真的很难,本人做个分块之后就手足无措了,然后搜刮资料看别人的优秀思路,听取妙声一片(

(1) 32 $\times$ 32

由于$b = 5$,那么$B = 2^b = 32$,缓存中每一行保存32字节的数据,又int占4个字节,那么缓存中每一行能存8个int

由于$s = 5$且$E = 1$,那么共有$S = 2^s = 32$组,每一组只有一行,故缓存总共能存32 * 8 个int

本例中,处理的矩阵是32 $\times$ 32,故缓存最多能保存矩阵的8行

那正好,按8 * 8分块的话,8列能完美地利用每一行缓存中的8个int,8行又恰好保证了每一行有不同的set index,不会互相冲突。

分块写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
}
}
}
}
}

测试结果如下:
1
2
3
4
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中,长这样:

1
2
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

改进写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

1
2
3
4
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中每一块作转置

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
2
3
4
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 $\times$ 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的块做转置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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

1
2
3
4
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的右下的转置就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
1
2
3
4
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 $\times$ 67

实在想不到有什么特别的办法,那就在8 * 8的分块的基础上,把边角料给处理了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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];
}

1
2
3
4
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,那就在处理边角料那里再怼几个局部变量

来点循环展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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;
}

1
2
3
4
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比较近,不过应该没关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
}

1
2
3
4
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爽过