【CMU 15-213 CSAPP】详解cachelab——模拟缓存、编写缓存友好代码

前言

本文是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 深入理解计算机系统 课程视频

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

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地址addrsize用于 -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的次数。一共有三个关卡:

  1. 对 <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零分
  2. 对 <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零分
  3. 对 <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. 实验过程

实验环境有点小坑:

  1. test-trans运行超时:将实验文件夹从共享文件夹中移动到Linux单独的目录中
  2. 移动后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爽过

相关推荐
一丝晨光1 小时前
gcc 1.c和g++ 1.c编译阶段有什么区别?如何知道g++编译默认会定义_GNU_SOURCE?
c语言·开发语言·c++·gnu·clang·gcc·g++
爱吃生蚝的于勒3 小时前
C语言最简单的扫雷实现(解析加原码)
c语言·开发语言·学习·计算机网络·算法·游戏程序·关卡设计
hutaotaotao6 小时前
c语言用户不同命令调用不同函数实现
c语言·开发语言
lb36363636366 小时前
介绍一下位操作符(c基础)
c语言·知识点
vortex58 小时前
解决 VSCode 中 C/C++ 编码乱码问题的两种方法
c语言·c++·vscode
Xiao Fei Xiangζั͡ޓއއ8 小时前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法
子朔不言9 小时前
[ARM-2D 专题]6.脏矩形定义的宏使用技巧和分析
c语言·arm开发·arm2d·显控开发-新龙微
BT-BOX10 小时前
STM32仿真proteus位带操作和keil增加头文件C文件
c语言·stm32·proteus
醉颜凉10 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
五味香13 小时前
Linux学习,ip 命令
linux·服务器·c语言·开发语言·git·学习·tcp/ip