CacheLab是CSAPP的第五个实验,这里考察了缓存Cache机制和缓存优化。
严格来讲,我觉得A Part相比于B Part确实简单很多,自己完成了B Part时,走了很多弯路,也给自己限制了太多条条框框,虽然最后也能拿到满分,但还是距离标准答案有些差距。
Lab
A Part
A Part考察对于缓存Cache的理解。事实上Cache可以看成一种利用程序局部性,从而利用两次硬件上hash的实现,从而保存频繁使用的数据。
指令格式
所需读取的指令格式为:[space]operation address,size。
具体来说,就是I、L、S、M四种指令,其中I不需要进行处理。
通过对traces中测试用例的观察,以及测试时所使用的用例(s/E/b的配置),我们可以确定输入地址address不会与s的数值冲突。
根据输入的指令,我们可以建立一个简单的struct来保存一条指令所有的信息,方便调取。
//内存追踪所需的结构体
struct track
{
unsigned long addr;
int size;
char ins;
};
基本框架
就如文档所说,我们需要建立一个简单的框架,来应对各种的参数输入,采用文档所说的getopt标准库。
并且解决一些必须的函数,如参数获取、字符串分割、16进制转10进制。
主要要点如下:
- getopt的参数需设置为:“s: E: b: t: v”,s/E/B/t后带着:意味着这些参数需要有具体的参数值,v则是一个类似模式启用的参数,不需要具体的数值(最后事实上也没实现v,因为这个部分确实比较简单,不需要实现v来debug)。
- 一行一行读取文件时,注意换行符,这里采用了fscanf+fgetc来进行处理,其中fgetc就是用于去除换行符。
#include "cachelab.h"
#include <getopt.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#define MAX_LEN 1024 //每一行最大1024 Bytes
//使用getopt所需要的4个全局变量
extern char* optarg; //用来保存选项的参数
extern int optind; //用来记录下一个检索位置
extern int opterr; //用来表示是否将错误信息输出到stderr
extern int optopt; //用来表示不再选项字符串optstring的选项
//内存追踪所需的结构体
struct track
{
unsigned long addr;
int size;
char ins;
};
//hex地址转为long
unsigned long htol(const char *s)
{
unsigned long n = 0;
for(int i=0;(s[i]>='0'&&s[i]<='9')||(s[i]>='a'&&s[i]<='f');++i)
{
n*=16;
if(s[i]>='0'&&s[i]<='9')
n+=(s[i]-'0');
else
n+=(10+s[i]-'a');
}
return n;
}
//string转为结构体track
struct track string2track(char* curline)
{
struct track result;
char* sub_str;
const char split_token[3]=" ,";
//读取第1个分割的子串——指令类型
sub_str=strtok(curline,split_token);
result.ins = *sub_str;
//读取第2个分割的子串——地址
sub_str=strtok(NULL,split_token);
result.addr = htol(sub_str);
//读取第3个分割的子串——size
sub_str=strtok(NULL,split_token);
result.size = atoi(sub_str);
return result;
}
//main函数
int main(int argc, char *argv[])
{
int opt; //opt接受getopt的结果,判断是否终止
int s; //组索引位数量,s=log2(S)
int E; //每组的行数,E=1代表着直接映射cache
int b; //块偏移位数,b=log2(B)
bool v_mode = 0; //记录是否需要详细v-mode
char* tracename=NULL; //tracename
FILE* tracefile=NULL; //tracefile
char curline[MAX_LEN]; //缓存每一行的内容
struct track content; //每一行内存追踪所需的
/*--------------1. 配置缓存------------------*/
//:代表着-s,-E,-b,-t后需要接收参数,-v不需要
//读取配置
while ((opt = getopt(argc, argv, "s:E:b:t:v")) != -1)
{
switch (opt)
{
case 's':
s = atoi(optarg);
break;
case 'E':
E = atoi(optarg);
break;
case 'b':
b = atoi(optarg);
break;
case 'v':
v_mode = 1;
break;
case 't':
tracename = argv[optind-1];
break;
default:
fprintf(stderr, "Usage: %s [-t nsecs] [-n] name\n",argv[0]);
exit(EXIT_FAILURE);
}
}
//读取文件
tracefile = fopen(tracename,"r");
//读取失败,exit
if(tracefile==NULL)
{
perror(tracename);
exit(EXIT_FAILURE);
}
//此处省略缓存配置
/*--------------2. 逐行处理------------------*/
while(fscanf(tracefile,"%[^\n]",curline)!=EOF) //读一行到curline中
{
fgetc(tracefile); //跳过换行符
content = string2track(curline); //把curline内容保存到content中
//.....
}
//M相比于L,就是多了一次hit
if(content.ins=='M')
++hit;
++cmd_count;
}
return 0;
exit(EXIT_SUCCESS);
}
Cache配置
高速缓存结构关键概念如下:
- 有S个组,利用地址中间s位作为组索引[hash],进行查询。
- 每个组,有E个行,用来缓存数据
- 每一行,都有1个有效位(记录是否启动);t个标记位保存着地址高位t位,结合组索引s,共同确定一组数据的唯一性;B个字节的存储空间,保存着地址高位为ts的所有数据,用于offset。
其中S=2^s ,B=2^b,因此我们根据输入的参数,就可以模拟一个高速缓存结构。
最后就是LRU策略的实现,事实上可以使用双向链表来管理这一策略,但本文直接采用数组进行保存上一次调用得时间来处理。
具体的实现如下,其中有效位valid_bit采用bool数组;tag采用unsigned long数组,方便之后的移位操作;B采用int数组;last_time数组用来实现last_time。
//缓存模拟
int S=pow(2,s); //组数
int B=pow(2,b); //块大小,一个缓存块B个字节
int t=64-s-b; //标记位数
//初始化valid_bit与tag_bit
//last_time[i][j]标记着i组j行的上一次使用时间
bool** valid_arr=(bool**)malloc(sizeof(bool*)*S);
unsigned long** tag_arr=(unsigned long**)malloc(sizeof(unsigned long*)*S);
int** last_time=(int**)malloc(sizeof(int*)*S);
for(i=0;i<S;++i)
{
valid_arr[i] = (bool*)malloc(sizeof(bool)*E);
tag_arr[i] = (unsigned long*)malloc(sizeof(unsigned long)*E);
last_time[i] = (int*)malloc(sizeof(int)*E);
}
for(i=0;i<S;++i)
{
for(j=0;j<E;++j)
{
valid_arr[i][j]=false;
tag_arr[i][j]=0;
last_time[i][j]=-1;
}
}
Cache模拟
通过仔细的分析,我们在本文讨论的高速缓存结构下,可以确认以下的结论:
- 指令加载I不需要处理
- 加载数据L和保存数据S,所处理的逻辑是一致的,都是从Cache中寻找是否已有缓存的该数据
- 修改数据M,其处理逻辑与L几乎一致,唯一的区别是多一次hit。因为M就是在进行了L之后,在必然命中的情况下进行了一次S
因此,我们是事实上需要仔细讨论的就是L的情况,具体如下:
- 若是命中,即即组S中存在着某一行E,其有效位valid_bit为true,且tag_bit与地址中间的标记位一致。此时hit数+1,更新最新使用时间last_time
- 不命中分两种情况,存在空行,将数据放入空行中,即有效位valid_bit置一,标记位tag更改为地址的标记位,更新最新使用时间last_time,miss数+1
- 不存在空行,遍历last_time,寻找LRU的行,进行驱逐,即标记位tag更改为地址的标记位,更新最新使用时间last_time,miss数+1,evic数+1。
至此,我们就解决了A Part,正如我所说,S的逻辑与L一致,而M就是多了一次hit,代码与结果如下:
#include "cachelab.h"
#include <getopt.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#define MAX_LEN 1024 //每一行最大1024 Bytes
//使用getopt所需要的4个全局变量
extern char* optarg; //用来保存选项的参数
extern int optind; //用来记录下一个检索位置
extern int opterr; //用来表示是否将错误信息输出到stderr
extern int optopt; //用来表示不再选项字符串optstring的选项
//内存追踪所需的结构体
struct track
{
unsigned long addr;
int size;
char ins;
};
//hex地址转为long
unsigned long htol(const char *s)
{
unsigned long n = 0;
for(int i=0;(s[i]>='0'&&s[i]<='9')||(s[i]>='a'&&s[i]<='f');++i)
{
n*=16;
if(s[i]>='0'&&s[i]<='9')
n+=(s[i]-'0');
else
n+=(10+s[i]-'a');
}
return n;
}
//string转为结构体track
struct track string2track(char* curline)
{
struct track result;
char* sub_str;
const char split_token[3]=" ,";
//读取第1个分割的子串——指令类型
sub_str=strtok(curline,split_token);
result.ins = *sub_str;
//读取第2个分割的子串——地址
sub_str=strtok(NULL,split_token);
result.addr = htol(sub_str);
//读取第3个分割的子串——size
sub_str=strtok(NULL,split_token);
result.size = atoi(sub_str);
return result;
}
//main函数
int main(int argc, char *argv[])
{
int opt; //opt接受getopt的结果,判断是否终止
int s; //组索引位数量,s=log2(S)
int E; //每组的行数,E=1代表着直接映射cache
int b; //块偏移位数,b=log2(B)
bool v_mode = 0; //记录是否需要详细v-mode
char* tracename=NULL; //tracename
FILE* tracefile=NULL; //tracefile
char curline[MAX_LEN]; //缓存每一行的内容
struct track content; //每一行内存追踪所需的所有信息
int hit=0; //命中数
int miss=0; //不命中数
int evic=0; //逐出数
int i,j;
int cmd_count=0; //记录命令数
bool found; //标记是否被缓存
bool space; //标记是否有空行
int index; //用于寻找LRU
/*--------------1. 配置缓存------------------*/
//:代表着-s,-E,-b,-t后需要接收参数,-v不需要
//读取配置
while ((opt = getopt(argc, argv, "s:E:b:t:v")) != -1)
{
switch (opt)
{
case 's':
s = atoi(optarg);
break;
case 'E':
E = atoi(optarg);
break;
case 'b':
b = atoi(optarg);
break;
case 'v':
v_mode = 1;
break;
case 't':
tracename = argv[optind-1];
break;
default:
fprintf(stderr, "Usage: %s [-t nsecs] [-n] name\n",argv[0]);
exit(EXIT_FAILURE);
}
}
//读取文件
tracefile = fopen(tracename,"r");
//读取失败,exit
if(tracefile==NULL)
{
perror(tracename);
exit(EXIT_FAILURE);
}
//缓存模拟
int S=pow(2,s); //组数
int B=pow(2,b); //块大小,一个缓存块B个字节
int t=64-s-b; //标记位数
//初始化valid_bit与tag_bit
//last_time[i][j]标记着i组j行的上一次使用时间
bool** valid_arr=(bool**)malloc(sizeof(bool*)*S);
unsigned long** tag_arr=(unsigned long**)malloc(sizeof(unsigned long*)*S);
int** last_time=(int**)malloc(sizeof(int*)*S);
for(i=0;i<S;++i)
{
valid_arr[i] = (bool*)malloc(sizeof(bool)*E);
tag_arr[i] = (unsigned long*)malloc(sizeof(unsigned long)*E);
last_time[i] = (int*)malloc(sizeof(int)*E);
}
for(i=0;i<S;++i)
{
for(j=0;j<E;++j)
{
valid_arr[i][j]=false;
tag_arr[i][j]=0;
last_time[i][j]=-1;
}
}
/*--------------2. 逐行处理------------------*/
while(fscanf(tracefile,"%[^\n]",curline)!=EOF) //读一行到curline中
{
fgetc(tracefile); //跳过换行符
content = string2track(curline); //把curline内容保存到content中
//指令加载不需要处理,直接跳过
if(content.ins=='I')
continue;
//L、M、S处理
//L==S,而M就是比L多了一次hit
//LRU机制
//1 有空行,填到空行内
//2 无空行,填到【上一次使用】最【久】的缓存内
//先处理地址,获得组索引s_idx和标记tag
int s_idx=(int)((content.addr<<(t))>>(64-s));
unsigned long tag=(content.addr>>(s+b));
//过一遍 s_idx组,查看是否已被缓存
found=false; //标记是否已被缓存
for(j=0;j<E;++j)
{
//在缓存内
if(valid_arr[s_idx][j]==true && tag_arr[s_idx][j]==tag)
{
++hit;
found=true;
last_time[s_idx][j]=cmd_count; //更新使用时间
break;
}
}
//未被缓存
if(!found)
{
++miss; //未缓存意味着miss
space=false; //标记是否有空行
//过一遍组s_idx,看是否有空行
for(j=0;j<E;++j)
{
//有空行
if(valid_arr[s_idx][j]==false)
{
//缓存
valid_arr[s_idx][j]=true; //有效位更改,不再是空行
tag_arr[s_idx][j]=tag;
last_time[s_idx][j]=cmd_count;
space=true;
break;
}
}
//无空行
if(!space)
{
++evic; //需要驱逐
//寻找最久远的行
index=0;
for(j=1;j<E;++j)
{
if(last_time[s_idx][j]<last_time[s_idx][index])
index=j;
}
//找到LRU,进行缓存
tag_arr[s_idx][index]=tag;
last_time[s_idx][index]=cmd_count;
}
}
//M相比于L,就是多了一次hit
if(content.ins=='M')
++hit;
++cmd_count;
}
printSummary(hit, miss, evic);
//释放内存,防止泄露
for(i=0;i<S;++i)
{
free(valid_arr[i]);
free(tag_arr[i]);
free(last_time[i]);
}
free(valid_arr);
free(tag_arr);
free(last_time);
return 0;
exit(EXIT_SUCCESS);
}
结果:
usr@ub2004:~/csapplab/cachelab/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
B Part[标准]
这里先记录最标准的答案,并讲述相关的要点,之后会叙述个人绕弯路的解决方案,虽然最后也满分了,但对于64x64的优化远不如标准答案。
B Part需要优化矩阵转置trans,使其缓存命中率提高。
要点
根据调用结果与A、B的地址,我们发现:
- A、B在下标相同时,其组索引一致,又由于E=1,即直接映射缓存,所以对角线的L+S必然会导致冲突不命中。
- b=5,意味着1行保存32个字节,即8个int,说明一行每整8个int一组,保存在一个缓存组(一组只有一行)
- 绝大多数的miss,来源于冲突不命中,需要解决抖动问题。
根据文档建议,我们需要采用block分块技术。
32x32
根据上述的分析,我们发现一组缓存可以保存数组一行8个int,在转置之后意味着8行,8个缓存。
在32x32的情况下,矩阵一行32个保存在4个缓存组中,又S=5,共有32个缓存组,则缓存器最大可以保存8x32个int。
稍加分析,我们就可以确定应该采用8x8作为block,因为每超过8行,就会出现冲突不命中,而转置后,大于8列就意味着大于8行,所以必然是8x8作为一个block。
确立了block之后,我们利用局部变量将结果保存在寄存器中,减少对Cache的冲突(这也是我走了弯路的原因)。
代码如下:
void transpose32(int M,int N,int A[N][M],int B[M][N])
{
int i,j,k;
int t1,t2,t3,t4,t5,t6,t7,t8; //保存到寄存器内
for(i=0;i<32;i+=8)
{
for(j=0;j<32;j+=8)
{
for(k=0;k<8;++k)
{
//保存到寄存器中,这里7次hit,1次miss
t1=A[i+k][j];
t2=A[i+k][j+1];
t3=A[i+k][j+2];
t4=A[i+k][j+3];
t5=A[i+k][j+4];
t6=A[i+k][j+5];
t7=A[i+k][j+6];
t8=A[i+k][j+7];
//加载到B中
B[j][i+k]=t1;
B[j+1][i+k]=t2;
B[j+2][i+k]=t3;
B[j+3][i+k]=t4;
B[j+4][i+k]=t5;
B[j+5][i+k]=t6;
B[j+6][i+k]=t7;
B[j+7][i+k]=t8;
}
}
}
}
- 这里对于A矩阵,只有每一次进入新的block中,会有在每一行的第一个元素上,有1次miss,共有128次miss
- 而对于B矩阵,除了每一次新的block的所造成的与A矩阵一致的miss外,还有对角线必然导致的冲突不命中。
usr@ub2004:~/csapplab/cachelab/cachelab-handout$ ./test-trans -M 32 -N 32
Function 0 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:1766, misses:287, evictions:255
Function 1 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 1 (Simple row-wise scan transpose): hits:870, misses:1183, evictions:1151
Summary for official submission (func 0): correctness=1 misses=287
TEST_TRANS_RESULTS=1:287
64x64
相比于32x32,这里难度要大很多,因为此时每4行就会发生冲突不命中,但本身由于一行能保存8列的int,但从A矩阵出发,可以采用4x8作为block,但一旦考虑转置,就只能是4x4作为最基本的单元。
但一旦采用4x4,我们就浪费了一行保存8个int的性质,对A中同一行8个int,在对于B转置的结果下,只能一次写入4个,就意味着写的时候必然会导致Cache的变动,大大增加了miss率。
最终我们依然采用8x8作为一个基础的block,但实际处理上,可以对4个4x4的cell进行处理,从上到下,从左到右,A和B的4个cell,分别命名为:A11、A12、A21、A22。最终所采用的处理流程:
- A11->B11,A12->B12,这里用了8组缓存,A4组,B4组,在非对角线的block中,这些缓存相互独立,不会冲突。
- A21->寄存器(4个局部变量)->B12,B12->寄存器(4个局部变量)->B21,这里利用了A21必然需要用到B12的原因,将原来的A12保存在B12,省略了A矩阵不必要的缓存切换。这里发生的冲突,主要在于调用了B12后又调用了B21。
- A22->B22,因为上一步访问了A21,这里甚至没有切换A的缓存
void transpose64(int M,int N,int A[N][M],int B[M][N])
{
int i,j,k;
int t1,t2,t3,t4,t5,t6,t7,t8; //保存到寄存器内
for(i=0;i<64;i+=8)
{
for(j=0;j<64;j+=8)
{
//A11,A12 -> B11,B12
for(k=0;k<4;++k)
{
t1=A[i+k][j]; t2=A[i+k][j+1];
t3=A[i+k][j+2]; t4=A[i+k][j+3];
t5=A[i+k][j+4]; t6=A[i+k][j+5];
t7=A[i+k][j+6]; t8=A[i+k][j+7];
B[j][i+k]=t1; B[j+1][i+k]=t2; //翻转,A11一行 -> B11一列
B[j+2][i+k]=t3; B[j+3][i+k]=t4;
B[j][i+k+4]=t5; B[j+1][i+k+4]=t6; //翻转,A12一行 -> B12一列
B[j+2][i+k+4]=t7; B[j+3][i+k+4]=t8;
}
//A21 -> B12 && B12 -> B21
for(k=0;k<4;++k)
{
t1=A[i+4][j+k]; t2=A[i+5][j+k]; //A21一列 元素
t3=A[i+6][j+k]; t4=A[i+7][j+k];
t5=B[j+k][i+4]; t6=B[j+k][i+5]; //B12一行元素,即A12一列元素
t7=B[j+k][i+6]; t8=B[j+k][i+7];
B[j+k][i+4]=t1; B[j+k][i+5]=t2; //A21 -> B12
B[j+k][i+6]=t3; B[j+k][i+7]=t4;
B[j+k+4][i]=t5; B[j+k+4][i+1]=t6; //B12 -> B21 即 A12 -> B21,有B12到B21的平移移动
B[j+k+4][i+2]=t7;B[j+k+4][i+3]=t8;
}
//A22 -> B22
for(k=4;k<8;++k)
{
t1=A[i+k][j+4]; t2=A[i+k][j+5]; //A22一行元素
t3=A[i+k][j+6]; t4=A[i+k][j+7];
B[j+4][i+k]=t1; B[j+5][i+k]=t2; //A22 -> B22
B[j+6][i+k]=t3; B[j+7][i+k]=t4;
}
}
}
}
usr@ub2004:~/csapplab/cachelab/cachelab-handout$ ./test-trans -M 64 -N 64
Function 0 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:9066, misses:1179, evictions:1147
Function 1 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 1 (Simple row-wise scan transpose): hits:3474, misses:4723, evictions:4691
Summary for official submission (func 0): correctness=1 misses=1179
TEST_TRANS_RESULTS=1:1179
61x67
因为互不对称,且61和67都不是8的倍数,所以没有什么太精妙的办法,就不断尝试block的大小尝试。
好在这个的限度很高,在元素数少于64x64的情况下,其miss的容忍度却高了一伴。
最后尝试之后,17可以达到最小的miss率,当然16、18、19等也是可以满分的。
void transpose61(int M,int N,int A[N][M],int B[M][N])
{
int i,j;
const int block_len=17;
int x,y;
for(i=0;i<N;i+=block_len)
for(j=0;j<M;j+=block_len)
for(x=i;x<N&&x<i+block_len;++x)
for(y=j;y<M&&y<j+block_len;++y)
B[y][x]=A[x][y];
}
B Part[个人绕弯路的思路]
我个人实现的思路有点问题,所以这里主要是记录一下自己的想法,和如何一步步优化到满分的。
且因为思路太过粗暴,代码过于丑陋,就不放上来献丑了。
最后我总结了一下自己思路进弯路的原因
- 文档建议不能采用12个以上的局部变量,我按照一般的规范将形参也看成局部变量,所以没有办法豪放地一口气使用8个t局部变量来保存一行的结果,加上必须得的循环变量i,j等,最后包括循环变量在内也就用了7-8个局部变量。
- 过分专注于对角线block,虽然后来在专注对角线block的时候也反应过来,可以采用临时存储的思路去处理一般的block。
32x32
最终我的结果是289,也是满分。
个人思路如下:
- 一样是8x8的block,但对block进行分类对角线block和一般block,思路受限于局部变量太少。
- 对于32x32而言,AB中一般block互不干扰,所以可以直接采用B[j][i]=A[i][j]进行处理,对于32x32中的16个8x8个block,共有12个block可以这样处理,接下来就剩下4个对角线block。
- 直接一行一行遍历(再次受限于局部变量),其冲突不命中十分严重,这里采用了其他空闲的寄存器作为跳板,即将A_Block_1 -> B_Block_3 -> B_Block_1,利用这思路可以处理Block 1 2 3,最后只剩下最后一个block。
- 最后一个block处理时,就直接遍历,因为所剩余的miss完全充足。
64x64
为了优化这个真的是要了老命,没有足够的局部变量的情况下,真的费了好大劲来讲miss降到了1300以下,最后结果1280+。
同样是将block分为一般block和对角线block,但不能直接套用32x32的思路,因为64x64矩阵中的block会经常出现冲突。
以下是分析过程:
- 按照11-12-21-22的A遍历顺序复制到B的11-21-12-22,A中的miss有11和21共8次,而B中的miss有11、21、12、22共16次,1个一般block就有24次miss,这是无法容忍的,因为单单一般block就有56个,这里就有1344次miss了。
- 显然,对于B的缓存不够友好,稍加修改,将A的顺序改为11-21-22-12,B的顺序改为11-12-22-21,这里一共20次miss,已经大幅优化了,但一般block依然有1120次miss,而对角线block显然更加困难,所以还需要优化。
- 利用自己画地为牢的、仅有的4个局部变量,我在A的11-21之间保存下12中的1行数据(4个),从而在之后遍历到12时,剩下了一次对于cache的访问,从而将miss降到19,这样一般block就用了1064个miss。
- 接下来考虑对角线block,借用32x32的经验,这里采用的A_block_1_1112 -> B_block_2_1112, A_block_1_2122 -> B_block_3_2122,处理了前6个对角线block,这里每一个对角线block的处理都用了接下来的2个block的空间,从而解决了6/8的问题。这里的block,每个只有20次miss,到此为止用了1184个miss。
//前6个block for(i=0;i<6;++i) { //前4行存到下一个block中 for(ii=8*i;ii<8*i+4;++ii) for(jj=8*i;jj<8*i+8;++jj) B[8+ii][8+jj]=A[ii][jj]; //后4行存到下下个block中 for(ii=8*i+4;ii<8*i+8;++ii) for(jj=8*i;jj<8*i+8;++jj) B[16+ii][16+jj]=A[ii][jj]; for(ii=8*i;ii<8*i+8;++ii) { for(jj=8*i;jj<8*i+4;++jj) B[ii][jj]=B[8+jj][8+ii]; for(jj=8*i+4;jj<8*i+8;++jj) B[ii][jj]=B[16+jj][16+ii]; } }
- 第7个block,分为前4行与后4行,分次保存在B_block_8中。
//倒数第二个block i=6; //前4行存到下一个block的前4行 for(ii=8*i;ii<8*i+4;++ii) for(jj=8*i;jj<8*i+8;++jj) B[8+ii][8+jj]=A[ii][jj]; for(ii=8*i;ii<8*i+8;++ii) { for(jj=8*i;jj<8*i+4;++jj) B[ii][jj]=B[8+jj][8+ii]; } //后4行存到下一个block的前4行 for(ii=8*i+4;ii<8*i+8;++ii) for(jj=8*i;jj<8*i+8;++jj) B[8+ii][8+jj]=A[ii][jj]; for(ii=8*i;ii<8*i+8;++ii) { for(jj=8*i+4;jj<8*i+8;++jj) B[ii][jj]=B[8+jj][8+ii]; }
- 第8个block,则是分情况讨论,其中A11和A22,采用从下到上,不超过对角线的两轮遍历遍历一个cell,即第一轮处理对角线+右上角,第二轮处理左下角。
- 而A12和A21,利用6个局部变量,保存左下角的6个元素,之后类似A11和A22,进行从下到上的顺序访问(最小化冲突不命中率),一行一行地填充B21和B12。
虽然经过这么费劲的操作,但最终的miss数为1280+,也就勉强低于1300,只能说理解题意很重要,整个过程中,束手束脚于所能保存的局部变量太少(4-6个),在后期考虑着各种花哨的优化细节时,事实上也隐隐感觉到了正规的做法,但还是觉得不应该使用那么多局部变量。
总结
第6章Cache是读的最快的一章,花了一天,从早上看到晚上,就全看完了,感觉比起之前要简单一些。
lab方面,A Part整理清楚框架后,不到一个小时就解决了,A Part花了半天;而B Part顺着自己的思路走,半天处理了32x32,而又花了大半天处理64x64,然后将64x64做完后就觉得不对劲,只好百度,之后就是顺着正规思路再写一遍,总共一个lab花了2-3天。
至此,CSAPP第一部分的前6章已经全部看完了,之后还有一半左右需要努力,不过估计后续会看的更快,更仔细的还得去OSTEP和计算机网络里更仔细的学习。
希望自己五一假期期间可以看完CSAPP,并且已经有种感觉,应该回头重新看一遍,复习一下了,好在已经在B站找到一些总结视频了,可以路上碎片时间看一看。
——2023.4.24