[笔记]计算机基础 6 CSAPP Lab5-CacheLab

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。最终所采用的处理流程:

  1. A11->B11,A12->B12,这里用了8组缓存,A4组,B4组,在非对角线的block中,这些缓存相互独立,不会冲突。
  2. A21->寄存器(4个局部变量)->B12,B12->寄存器(4个局部变量)->B21,这里利用了A21必然需要用到B12的原因,将原来的A12保存在B12,省略了A矩阵不必要的缓存切换。这里发生的冲突,主要在于调用了B12后又调用了B21。
  3. 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会经常出现冲突。
以下是分析过程:

  1. 按照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了。
  2. 显然,对于B的缓存不够友好,稍加修改,将A的顺序改为11-21-22-12B的顺序改为11-12-22-21,这里一共20次miss,已经大幅优化了,但一般block依然有1120次miss,而对角线block显然更加困难,所以还需要优化。
  3. 利用自己画地为牢的、仅有的4个局部变量,我在A的11-21之间保存下12中的1行数据(4个),从而在之后遍历到12时,剩下了一次对于cache的访问,从而将miss降到19,这样一般block就用了1064个miss。
  4. 接下来考虑对角线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];
         }
     }
    
  5. 第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];
    }
    
  6. 第8个block,则是分情况讨论,其中A11和A22,采用从下到上,不超过对角线的两轮遍历遍历一个cell,即第一轮处理对角线+右上角第二轮处理左下角
  7. 而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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值