文章目录
CS:APP Lab4 : cachelab
一.Part A : 编写缓存模拟器
本部分要求用C编写一个缓存模拟器,用trace中的数据作输入模拟缓存的命中/未命中行为,并正确计算命中次数,未命中次数和驱逐次数.(按照参考缓存模拟器的测试结果)
由于测试模拟缓存需要用命令行参数,因此分为以下流程来逐步实现:
(一).解析并读取命令行参数
输入命令行参数采用如下的格式:
-h选项打印帮助栏信息,不带选项参数;
-v选项显示每个测试用例的缓存行为情况,不带选项参数;
-s,-E,-b,-t选项都需要带参数,分别指组索引位数,缓存行数,块偏移位数,测试目标文件名
首先设置一些基本变量如下:
/**
*一些基本变量
*/
int i=0,j=0;//循环变量
int hit = 0,miss = 0,eviction = 0;/*命中、不命中、驱逐的次数*/
int opt;//获取调用getopt()函数的返回值
int s = 0;//s位组索引,高速缓存组S=2^s
int E = 0;//E行高速缓存行
int b = 0;//b位块偏移,数据块B=2^b
int v = 0;//是否用"v"选项,是就设为1
char *file_name = NULL;//文件名
FILE *file = NULL; //文件
/*"帮助栏"信息*/
char *help[] = {"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",
};
然后利用getopt()
函数以解析命令行参数.利用getopt()
函数的特性,设计以下代码段来解析并读取命令行参数:
/**
*读取命令行参数,选项h、v不用参数;选项s、E、b、t需要参数
*/
while((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
switch (opt){
case 'h':{ //打印帮助栏
for (int i = 0; i<11; i++) {
fprintf(stderr, "%s", help[i]);
}
exit(EXIT_SUCCESS); // 打印完帮助栏后退出程序
}
case 'v':{ //显示详细信息
v = 1; //设置为1
break;
}
case 's':{ //获取组索引位
s = atoi(optarg);
break;
}
case 'E':{ //获取行数
E = atoi(optarg);
break;
}
case 'b':{ //获取块偏移位
b = atoi(optarg);
break;
}
case 't':{ //获取文件名
file_name = optarg; // 直接从optarg获取文件名
file = fopen(file_name, "r");//打开文件
/*文件为空*/
if (file == NULL) {
perror(file_name);
exit(EXIT_FAILURE);
}
break;
}
default:{
fprintf(stderr, "Invalid option: %c\n", opt);
break;
}
}
}
(二).设计模拟缓存结构
首先以结构体来抽象高速缓存行:
/*高速缓存行结构*/
typedef struct cacheROW{
int valid_bit; //有效位
int tag_bit; //标记位
int LRU; //LRU计录器
}Cache;
由于不用考虑数据的真实处理,本次lab的模拟缓存只需考虑有效位和标记位,高速缓存块部分无需考虑.
又由于采用LRU策略进行驱逐,因此额外加一个成员LRU
以作为LRU记录器.(LRU算法下文专门阐述)
为模拟缓存分配内存以及初始化:
/**
*此处开始设计模拟高速缓存
*/
int time = 0;//缓存计时器
/*初始化模拟高速缓存*/
int S = 1<<s; //高速缓存组数S=2^s=1<<s
Cache **cache_p = (Cache **)malloc(LEN1*S);//二级指针 ,分配E组缓存行结构体的内存
for(i = 0;i < S;i++){
cache_p[i] = (Cache *)malloc(LEN2*E);//一级指针,分配E行缓存行结构体的内存
}
for(i=0;i<S;i++){//为每个缓存行的数据赋初值
for(j=0;j<E;j++){
cache_p[i][j].valid_bit = 0;
cache_p[i][j].tag_bit = -1;
cache_p[i][j].LRU = 0;
}
}
(三).从文件中读取操作和地址
接下来就是读取操作以进行模拟缓存行为.其中,文件中共有四种以共同格式输入的测试用例,每行的格式为:
I 0400d7d4,8
M 0421c7f0,4
L 04f6b868,8
S 7ff0005c8,8
I指加载指令,本次实验中不用考虑;
L和S分别指加载数据和存储数据,本次实验中可视为同一种操作;
M指更改数据,即先加载数据再存储数据,相当于连续进行L和S操作.
读取相关信息的实现代码如下:
/**
*分析地址
*为了获取组索引和块偏移,
*需要引入一个s位全为1、其他位全0
*以及一个b位全为1、其他位全0的工具数
*/
int tool_b = (int)pow(2,b)-1;
int tool_s = ((int)pow(2,s)-1)<<b;
/*从文件中读取操作和地址*/
char str[20];//字符数组文件中的一行数据
char *token = NULL;//临时子串
char *tokens[3];//三个字串,分别存储操作、地址、字节数的字符串
char split[3] = " ,";//分隔符
/*获取操作、地址、字节数*/
while(fgets(str, 20, file) != NULL){
tokens[0] = token = strtok(str, split);
if(!strcmp(tokens[0], "I")){//"I"操作不用考虑
continue;
}
int i = 0;
while(token != NULL){
tokens[++i] = token = strtok(NULL, split);
}
int index; //组索引
int bias; //块偏移
int tag; //标记位
long adr = HtoI(tokens[1]);//地址字符串转换为十进制整数
index = (adr & tool_s) >> b;//获得组索引
tag = adr >> (s+b);//获得标记位
bias = adr & tool_b;//获得块偏移
int size = atoi(tokens[2]);//获得字节大小
index = index-index+tag-tag+bias-bias;
/*tips:
* bias这个变量其实并不需要用到,但是不使用变量会被警告通不过编译
* 因此随便用一下,可忽略
* 同时由于暂时没用上其他变量,也进行类似操作如上,仅仅是方便调试
*/
if(v){ //-v选项显示每个测试用例的详细情况
printf("%s %s,%d \n",tokens[0],tokens[1],size);
}
}
其中,需要讲十六进制字符串转化为十进制整数,写一个函数HtoI()
以实现该功能,函数代码如下:
/*将字符转换成数字*/
int cast(char c)
{
if(c >= '0'&&c <= '9')
return (c-'0');
else
return (c-'a'+10);
}
/*将十六进制字符串转化成十进制长整型*/
unsigned long HtoI(char *str_16)
{
unsigned long x_10 = 0;
int i = strlen(str_16)-1;
int d = 1;
for(;i>=0;i--){
x_10 += (cast(str_16[i])*d);
d *= 16;
}
return x_10;
}
程序编写到此,检查测试一下.
如果需要打印帮助栏,运行如下:
添加-v
选项,并为-s
,-E
,-b
,-t
选项依次赋值选项参数,运行如下:
(四).模拟缓存行为(多路组相联时采用LRU替换策略)
本实验中缓存行为有三种结果:
命中 不命中(并将数据写入空行) 不命中(没有空行需要按LRU策略驱逐掉某一行再写入)
前两种很好说,当索引到某一组,按行寻找到有效位为1,标记位一致的就是命中;不满足命中条件,就寻找有效位设为0(也就是空行)的那一行写入数据.
关键是后一种,不满足命中条件,并且没有多余的空行了,这时就得按一定策略驱逐.
本实验中采取LRU(Least Recently Used)策略,通俗地解释就是:最近一次访问距离现在最久的缓存行.
下面来设计LRU实现算法:
每个缓存行的结构体中设有一个LRU记录器,我们可以这样认为:每次对某一缓存行进行命中,不命中(包含驱逐)时,都是访问了一次这个缓存行,虽然每次访问哪一个缓存行是不确定的,总是变化的,但是可以从一开始就设置一个从零开始的计时器time,每次进行缓存行为都会加1,相当于时间轴一直在走,并且time越大说明越靠近当前时刻(选择驱逐哪一行时),然后每次缓存行为后对应的缓存行的LRU记录器记录下此次被访问的时刻,也就是最近一次访问的时刻.这样一来,每个缓存行都相当于记录了它们最近一次被访问的时刻,当需要驱逐时,就应该选择当前组中LRU记录器最小的那一行(也就是最近一次访问时距离当前时刻最久的)来驱逐.
缓存行为总实现代码如下:
/*开始模拟缓存行为*/
int is_M = (strcmp(tokens[0], "M")==0)?2:1;
do{
int i,flag = 0;
do{
/*1、命中*/
for(i=0;i<E;i++){
if(cache_p[index][i].valid_bit && cache_p[index][i].tag_bit == tag){
hit++;//命中+1
cache_p[index][i].LRU = time++;//更新LRU计录器
if(v){
printf("hit ");
}
flag = 1;
break;
}
}
if(flag) break;
/*2、不命中*/
for(i=0;i<E;i++){
if(!cache_p[index][i].valid_bit){
miss++;//不命中+1
cache_p[index][i].LRU = time++;//更新LRU计录器
cache_p[index][i].valid_bit = 1;//加载后有效位设为1
cache_p[index][i].tag_bit = tag;//标记位设为载入数据的标记
if(v){
printf("miss ");
}
flag = 1;
break;
}
}
if(flag) break;
/*3、不命中且需要驱逐*/
miss++;//不命中+1
eviction++;//驱逐+1
int evict_line = min_LRU_line(index, E, cache_p);//得到应该驱逐的缓存行
cache_p[index][evict_line].valid_bit = 1;//驱逐并写入后有效位设为1
cache_p[index][evict_line].tag_bit = tag;//标记位设为写入数据的标记
cache_p[index][evict_line].LRU = time++; //更新LRU计录器
if(v){
printf("miss eviction ");
}
}while(0);
}while(--is_M);//如果是M指令,就要循环两次
if(v) printf("\n");
其中用了得到某一缓存组中所有缓存行的LRU最小时所对应的行下标的函数min_LRU_line()
,函数实现代码如下:
/*求LRU记录器最小所对应的缓存行*/
int min_LRU_line(int set_index, int total_E, Cache **p){
int i = 0,min = INT_MAX,min_LRU_line = 0;
for(;i<total_E;i++){
if(p[set_index][i].LRU<min){
min = p[set_index][i].LRU;
min_LRU_line = i;
}
}
return min_LRU_line;
}
(五).合并成完整代码
#include "cachelab.h"
#include <getopt.h>
#include <stdlib.h>
#include <malloc.h>
#include <unistd.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <limits.h>
#define LEN1 sizeof(Cache *)
#define LEN2 sizeof(Cache)
/*高速缓存行结构*/
typedef struct cacheROW{
int valid_bit; //有效位
int tag_bit; //标记位
int LRU; //LRU计录器
}Cache;
/*声明需要用到的函数*/
int cast(char c);
unsigned long HtoI(char *str_16);
int min_LRU_line(int set_index, int total_E, Cache **p);
/**
*main函数传入参数:
* argc---参数个数
* argv---参数数组字符串
*/
int main(int argc, char *argv[])
{
/**
*一些基本变量
*/
int i=0,j=0;//循环变量
int hit = 0,miss = 0,eviction = 0;/*命中、不命中、驱逐的次数*/
int opt;//获取调用getopt()函数的返回值
int s = 0;//s位组索引,高速缓存组S=2^s
int E = 0;//E行高速缓存行
int b = 0;//b位块偏移,数据块B=2^b
int v = 0;//是否用"v"选项,是就设为1
char *file_name = NULL;//文件名
FILE *file = NULL; //文件
/*"帮助栏"信息*/
char *help[] = {"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",
};
/**
*读取命令行参数,选项h、v不用参数;选项s、E、b、t需要参数
*/
while((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
switch (opt){
case 'h':{ //打印帮助栏
for (int i = 0; i<11; i++) {
fprintf(stderr, "%s", help[i]);
}
exit(EXIT_SUCCESS); // 打印完帮助栏后退出程序
}
case 'v':{ //显示详细信息
v = 1; //设置为1
break;
}
case 's':{ //获取组索引位
s = atoi(optarg);
break;
}
case 'E':{ //获取行数
E = atoi(optarg);
break;
}
case 'b':{ //获取块偏移位
b = atoi(optarg);
break;
}
case 't':{ //获取文件名
file_name = optarg; // 直接从optarg获取文件名
file = fopen(file_name, "r");//打开文件
/*文件为空*/
if (file == NULL) {
perror(file_name);
exit(EXIT_FAILURE);
}
break;
}
default:{
fprintf(stderr, "Invalid option: %c\n", opt);
break;
}
}
}
/**
*此处开始设计模拟高速缓存
*/
int time = 0;//缓存计时器
/*初始化模拟高速缓存*/
int S = 1<<s; //高速缓存组数S=2^s=1<<s
Cache **cache_p = (Cache **)malloc(LEN1*S);//二级指针 ,分配E组缓存行结构体的内存
for(i = 0;i < S;i++){
cache_p[i] = (Cache *)malloc(LEN2*E);//一级指针,分配E行缓存行结构体的内存
}
for(i=0;i<S;i++){//为每个缓存行的数据赋初值
for(j=0;j<E;j++){
cache_p[i][j].valid_bit = 0;
cache_p[i][j].tag_bit = -1;
cache_p[i][j].LRU = 0;
}
}
/**
*分析地址
*为了获取组索引和块偏移,
*需要引入一个s位全为1、其他位全0
*以及一个b位全为1、其他位全0的工具数
*/
int tool_b = (int)pow(2,b)-1;
int tool_s = ((int)pow(2,s)-1)<<b;
/*从文件中读取操作和地址*/
char str[20];//字符数组文件中的一行数据
char *token;//临时子串
char *tokens[3];//三个字串,分别存储操作、地址、字节数的字符串
char split[3] = " ,";//分隔符
/*获取操作、地址、字节数*/
while(fgets(str, 20, file) != NULL){
tokens[0] = token = strtok(str, split);
if(!strcmp(tokens[0], "I")){//"I"操作不用考虑
continue;
}
int i = 0;
while(token != NULL){
tokens[++i] = token = strtok(NULL, split);
}
int index; //组索引
int bias; //块偏移
int tag; //标记位
unsigned long addr = HtoI(tokens[1]);//地址字符串转换为十进制整数
index = (addr & tool_s) >> b;//获得组索引
tag = addr >> (s+b);//获得标记位
bias = addr & tool_b;//获得块偏移
int size = atoi(tokens[2]);//获得字节大小
bias = bias + 0;//这行代码可忽略
if(v){ //加了-v选项就打印过程
printf("%s %s,%d ",tokens[0],tokens[1],size);
}
/*开始模拟缓存行为*/
int is_M = (strcmp(tokens[0], "M")==0)?2:1;
do{
int i,flag = 0;
do{
/*1、命中*/
for(i=0;i<E;i++){
if(cache_p[index][i].valid_bit && cache_p[index][i].tag_bit == tag){
hit++;//命中+1
cache_p[index][i].LRU = time++;//更新LRU计录器
if(v){
printf("hit ");
}
flag = 1;
break;
}
}
if(flag) break;
/*2、不命中*/
for(i=0;i<E;i++){
if(!cache_p[index][i].valid_bit){
miss++;//不命中+1
cache_p[index][i].LRU = time++;//更新LRU计录器
cache_p[index][i].valid_bit = 1;//加载后有效位设为1
cache_p[index][i].tag_bit = tag;//标记位设为载入数据的标记
if(v){
printf("miss ");
}
flag = 1;
break;
}
}
if(flag) break;
/*3、不命中且需要驱逐*/
miss++;//不命中+1
eviction++;//驱逐+1
int evict_line = min_LRU_line(index, E, cache_p);//得到应该驱逐的缓存行
cache_p[index][evict_line].valid_bit = 1;//驱逐并写入后有效位设为1
cache_p[index][evict_line].tag_bit = tag;//标记位设为写入数据的标记
cache_p[index][evict_line].LRU = time++; //更新LRU计录器
if(v){
printf("miss eviction ");
}
}while(0);
}while(--is_M);//如果是M指令,就要循环两次
if(v) printf("\n");
}
/*关闭文件*/
if(file != NULL){
fclose(file);
}
/*释放内存空间*/
for(i = 0;i < S;i++){
free(cache_p[i]);
}
free(cache_p);
/*最终打印命中、未命中、逐出次数*/
printSummary(hit, miss, eviction);
return 0;
}
/*将字符转换成数字*/
int cast(char c)
{
if(c >= '0'&&c <= '9')
return (c-'0');
else
return (c-'a'+10);
}
/*将十六进制字符串转化成十进制长整型*/
unsigned long HtoI(char *str_16)
{
unsigned long x_10 = 0;
int i = strlen(str_16)-1;
int d = 1;
for(;i>=0;i--){
x_10 += (cast(str_16[i])*d);
d *= 16;
}
return x_10;
}
/*求LRU记录器最小所对应的缓存行*/
int min_LRU_line(int set_index, int total_E, Cache **p){
int i = 0,min = INT_MAX,min_LRU_line = 0;
for(;i<total_E;i++){
if(p[set_index][i].LRU<min){
min = p[set_index][i].LRU;
min_LRU_line = i;
}
}
return min_LRU_line;
}
输入./text-csim
以对所有测试用例进行测试,结果如下,得到27分满分:
二.Part B : 优化矩阵转置
本部分实验前须知:
-
本实验指定评判矩阵转置效率时,规定s=5,E=1,b=5,也就是说使用直接映射高速缓存(32, 1, 32, 12),即有32个缓存组,每组一行,即一个缓存块,每个缓存块存储32个字节,刚好是8个int型数据.分析缓存结构,不难看出每个cache行刚好能存储8个数组中的元素.
-
数组A和数组B的起始地址的关系(首元素被映射到同一个块)决定了每个元素索引到的缓存行是一样的.
(一).32*32矩阵
对于32*32矩阵,由于A,B矩阵访问方向不一样,A矩阵按行访问,B矩阵按列访问,他们共用同一个缓存.
由于矩阵的前8行元素填满了整个cache,而8个元素又占满一个缓存块,因此不妨按照下图将矩阵分块,对于每8行元素,按每列8个元素分,一共分出16个 8 x 8 的子矩阵,每个子矩阵的一行元素属于同一个缓存块,竖直方向上每个子矩阵的对应行的组号相同,而水平方向上每个子矩阵的对应行的组号不同.
不难发现,如果按照对角线翻转过去,这两个对称的子矩阵对应行的组号是不相同的,这样刚好能保证当转置过去的时候,访问A时的缓存块号与访问B时的缓存块号是完全错开的.
- 上图来源于网络
首先,应该清楚的是,只要是某一次复制时矩阵A,B先后访问对角线上的元素,那就必然会造成冲突不命中(组索引相同,但是标记位不同).要想优化对角线冲突不命中的情况,也不是不能做到,但就是会很繁琐,也大大降低了代码的可读性,因此不妨把目光放在其他情况.
在A矩阵中,我们可以通过一次连续访问8个元素,这8个元素属于同一个缓存块,因此每当访问该8个元素时,访问第一个元素一定是冷不命中或冲突不命中,然而访问后面7个元素时全为命中;此时对于矩阵B,虽然复制A子矩阵的第一行到B子矩阵的第一列时,一定会冷不命中或冲突不命中,但是完成第一列的转置后,缓存已经加载了整个B子矩阵的元素,之后再转置剩余7*8个元素时,访问B时每次都是命中.
也就是说,每次对称子矩阵的转置,在访问A时,每行第一个元素是不命中,共8次;访问B时,只有访问第一列元素时是全都不命中(共8次),剩余其他元素都是命中.合起来就是8+8=16次.这大大降低了不命中的次数.
因此,按照此思路编写实现代码,为了能连续访问A子矩阵的一行元素,可添加8个临时变量,一个一个转存下来,然后再传给B子矩阵的对应元素,完整代码如下:
void transpose_submit(int M, int N, int A[N][M], int B[M][N])
{
int i,j;//定义循环变量
int t1,t2,t3,t4,t5,t6,t7,t8;//局部变量用于中转
if((M == 32) && (N == 32)){ /* 32*32情况 */
for(j = 0;j < 32;j+=8) { /* 按列每8个遍历 */
for(i = 0;i < 32;i++){
t1 = A[i][j+0];t2 = A[i][j+1];t3 = A[i][j+2];t4 = A[i][j+3];
t5 = A[i][j+4];t6 = A[i][j+5];t7 = A[i][j+6];t8 = A[i][j+7];
B[j+0][i] = t1;B[j+1][i] = t2;B[j+2][i] = t3;B[j+3][i] = t4;
B[j+4][i] = t5;B[j+5][i] = t6;B[j+6][i] = t7;B[j+7][i] = t8;
}
}
}
}
在Linux终端验证,不命中次数为288次,小于300次,说明优化算是成功了.
(二).64*64矩阵
同上分析,由矩阵一行有64个元素知,矩阵前4行元素会填满整个cache,故若仍按照上述32*32矩阵的 8 x 8 分块,那么在访问矩阵B时,上4行和下4行的索引缓存情况是一样的,这两块对应访问同一个缓存组,上下对应访问同一个缓存块,但标记位不同,这样就会发生冲突不命中.因此还想按照 8 x 8 分块显然是不行的.
因此不难接着往 4 x 4 分块方向考虑,若仅仅这样分块,访问B矩阵时能够利用子矩阵的4行缓存命中,但是只利用到了前4列,后4列并没有利用到,也就是说,缓存块只利用了前一半,这样冲突不命中还是会有很多.
继续优化,还是基于 4 x 4 分块,但是由于一个缓存块存8个元素,故将 8 x 8 作为一个整体,如下图:
按以下步骤进行优化分析:
(1)步骤一:将A矩阵的前4行(A1,A2两个小块)全搬运到B对应位置处(每个小块已经过转置成A1T),虽然A2T的位置不对,但这个位置暂时不用,可以先转存在这,这样做有利于一次性用到A1,A2这两个块所在的同一片缓存块,减少在访问A矩阵时的不命中次数(事实上,访问B时也是一次性访问前4行,除了第一列,其他全都命中);
(2)步骤二:
这一步非常关键,它需要同时实现两个结果:
-
1.从A矩阵中取出A3块,转置后放入原A2T块所在的位置;
-
2.将A2T从B中取出,再转移到其在B中正确的位置.
但是为了避免访问B的前,后4行时造成冲突不命中,应确保两次访问B的前4行的后4列操作(一次是取出A2T,一次是存入A3T)相连在一起的,这就需要中间变量来实现.
这样一来,只会在访问A取出A3时造成4次不命中,访问B后4行的前4列时造成4次不命中.
(3)步骤三:最后将A的A4取出,转置后存到B中的对应位置,这一步全都命中.
由此来编写实现代码如下:
else if((M == 64) && (N == 64)){ /* 64*64情况 */
for(i = 0;i < N;i+=8){
for(j = 0;j < M;j+=8){
/*步骤一*/
for(x = i;x < i+4;x++){
t1 = A[x][j+0];t2 = A[x][j+1];t3 = A[x][j+2];t4 = A[x][j+3];
t5 = A[x][j+4];t6 = A[x][j+5];t7 = A[x][j+6];t8 = A[x][j+7];
B[j+0][x] = t1;B[j+1][x] = t2;B[j+2][x] = t3;B[j+3][x] = t4;
B[j+0][x+4] = t5;B[j+1][x+4] = t6;B[j+2][x+4] = t7;B[j+3][x+4] = t8;
}
/*步骤二*/
for(y = j;y < j+4;y++){
t1 = A[i+4][y];t2 = A[i+5][y];t3 = A[i+6][y];t4 = A[i+7][y];
t5 = B[y][i+4];t6 = B[y][i+5];t7 = B[y][i+6];t8 = B[y][i+7];
B[y][i+4] = t1;B[y][i+5] = t2;B[y][i+6] = t3;B[y][i+7] = t4;
B[y+4][i+0] = t5;B[y+4][i+1] = t6;B[y+4][i+2] = t7;B[y+4][i+3] = t8;
}
/*步骤三*/
for(x = i+4;x < i+8;x++){
t1 = A[x][j+4];t2 = A[x][j+5];t3 = A[x][j+6];t4 = A[x][j+7];
B[j+4][x] = t1;B[j+5][x] = t2;B[j+6][x] = t3;B[j+7][x] = t4;
}
}
}
}
在Linux终端验证,不命中次数为1180次,小于1300次,说明优化算是成功了.
(三).61*67矩阵
矩阵的行,列数非特殊数字,故只能按多种分块方法逐一进行尝试,经过尝试,最终选择 16 x 16 分块方法.
实现代码如下:
else if((M == 61) && (N == 67)){ /* 61*67情况 */
for( i = 0;i < N;i+=16) {
for(j = 0;j < M;j+=16){
for(x = i;x < N && x < i+16;x++){
for(y = j;y < M && y < j+16;j++){
t1 = A[x][y];
B[y][x] = t1;
}
}
}
}
}
在Linux终端验证,不命中次数为1993次,小于2000次,说明优化算是成功了.
(四).合并成完整代码
将上述三种情况的部分代码按照条件分支,以及其他情况用普通版本,合并成如下的总代码:
void transpose_submit(int M, int N, int A[N][M], int B[M][N])
{
int i,j,x,y;//定义循环变量
int t1,t2,t3,t4,t5,t6,t7,t8;//局部变量用于中转
if((M == 32) && (N == 32)){ /* 32*32情况 */
for(j = 0;j < M;j+=8) {
for(i = 0;i < N;i++){
t1 = A[i][j+0];t2 = A[i][j+1];t3 = A[i][j+2];t4 = A[i][j+3];
t5 = A[i][j+4];t6 = A[i][j+5];t7 = A[i][j+6];t8 = A[i][j+7];
B[j+0][i] = t1;B[j+1][i] = t2;B[j+2][i] = t3;B[j+3][i] = t4;
B[j+4][i] = t5;B[j+5][i] = t6;B[j+6][i] = t7;B[j+7][i] = t8;
}
}
}
else if((M == 64) && (N == 64)){ /* 64*64情况 */
for(i = 0;i < N;i+=8){
for(j = 0;j < M;j+=8){
/*步骤一*/
for(x = i;x < i+4;x++){
t1 = A[x][j+0];t2 = A[x][j+1];t3 = A[x][j+2];t4 = A[x][j+3];
t5 = A[x][j+4];t6 = A[x][j+5];t7 = A[x][j+6];t8 = A[x][j+7];
B[j+0][x] = t1;B[j+1][x] = t2;B[j+2][x] = t3;B[j+3][x] = t4;
B[j+0][x+4] = t5;B[j+1][x+4] = t6;B[j+2][x+4] = t7;B[j+3][x+4] = t8;
}
/*步骤二*/
for(y = j;y < j+4;y++){
t1 = A[i+4][y];t2 = A[i+5][y];t3 = A[i+6][y];t4 = A[i+7][y];
t5 = B[y][i+4];t6 = B[y][i+5];t7 = B[y][i+6];t8 = B[y][i+7];
B[y][i+4] = t1;B[y][i+5] = t2;B[y][i+6] = t3;B[y][i+7] = t4;
B[y+4][i+0] = t5;B[y+4][i+1] = t6;B[y+4][i+2] = t7;B[y+4][i+3] = t8;
}
/*步骤三*/
for(x = i+4;x < i+8;x++){
t1 = A[x][j+4];t2 = A[x][j+5];t3 = A[x][j+6];t4 = A[x][j+7];
B[j+4][x] = t1;B[j+5][x] = t2;B[j+6][x] = t3;B[j+7][x] = t4;
}
}
}
}
else if((M == 61) && (N == 67)){ /* 61*67情况 */
for( i = 0;i < N;i+=16) {
for(j = 0;j < M;j+=16){
for(x = i;x < N && x < i+16;x++){
for(y = j;y < M && y < j+16;y++){
t1 = A[x][y];
B[y][x] = t1;
}
}
}
}
}
else{
for (i = 0; i < N; i++) { /* 普通情况 */
for (j = 0; j < M; j++) {
t1 = A[i][j];
B[j][i] = t1;
}
}
}
}
三.实验总结
总测评
将A,B两个部分的代码进行总测评如下,总得分为53分满分:
总结
本次cachelab包含两部分,一部分是编写一个高速缓存模拟器,另一部分是根据缓存的特性来优化矩阵转置函数.
A部分编写模拟缓存中,主要考察代码能力,除了要求能够实现解析命令行参数以及读取文件中的信息这两个功能以外,还需要对缓存的结构以及与缓存相关的各种参数的含义十分清晰,需要理解透彻什么情况下缓存会命中或不命中,以及不命中时何时该驱逐原缓存行,此外还需要掌握LRU替换策略的原理以及实现方法.
B部分优化矩阵转置中,主要考察对命中与不命中行为的运用,需要动脑筋使得原有缓存块在被覆盖掉之前尽可能多次被访问,同时在本实验中,也加深了对分块法提高命中率的理解和运用.
总之,通过本实验,我加深了对缓存这块知识点的理解和运用.
文末声明:
1.笔者处于学习阶段,记录这篇博客仅是分享笔者的学习ics的历程,其中将实验过程中自己十分疑惑的地方、难以理解的地方以及笔者认为需要注意的细节阐述得较为清楚,希望自己的理解能够帮助到大家,如有专业性上不正确的地方敬请读者们批评指正。
2.此lab绝非单凭本人之力完成参考了以下两篇博客,笔者认为二者各有如下优缺点:
参考博客[1]的 Part A 部分阐述得十分详细,其分析思路值得大家学习,但是其中某些代码细节做的不到位,以及某些功能函数设计得不够高效;其次Part B 部分阐述得过于简易,无助于读者们深入理解学习。
参考博客[2]的Part A 部分阐述得比较普通,细节上不如第一篇博客,但是其在Part B 部分阐述得十分中肯详细,细节以及原理都很到位,对基础知识不够熟练的读者而言比较友好,图文并茂,通俗易懂。
总之笔者帮大家汇总了二者各自的可读之处,当然读者们也可按自己想法选择性阅读。
最后,再次希望本文能够帮助到大家!