计组Lab4 模拟cache设计

Connor

在做这个实验之前,我们需要掌握cache的基础知识~你可能需要知道的有

  1. cache行,有效位,tag的概念和作用
  2. cache直接映射,组相联映射,全相联映射的概念
  3. LRU等cache替换原则

ps:访问cache的地址和访问主存的地址的数据是相同的,只是cache和主存解析这个地址的方式不同,如组相联cache的访问形式是tag,set,offset,而主存只会把这个地址拆成block(块号),offset的形式。

如果对cache的基础知识有些不了解~这里强推B站Beokayy_的视频,讲的非常细致!醍醐灌顶!!
带你彻底搞懂cache

Part 1 Cache模拟器实现

问ai就行(),如果扎实掌握cache基础知识的话其实不难理解((
其实是我写不动了。。

提示:
用静态二维数组而非动态数组可以少写很多行代码
输出无前导0的16进制数,可以直接用 %llx,会方便许多

在这里给一个AC代码做参考~,各位小心查重————

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include "cachelab.h"
#include<stdio.h>
#include<stdlib.h>
#include<getopt.h>
#include<string.h>

typedef struct{
int valid;
int time; //LRU
unsigned long long tag;
}Line;

Line cache[2048][15];
int hit = 0, miss = 0, eviction = 0, v = 0;

unsigned long long get_s_addr(unsigned long long addr, int b, int s){
unsigned long long x = ((unsigned long long)1 << (unsigned long long)s) -(unsigned long long)1;
addr >>= (unsigned long long)b;
addr &= x;
return addr;
}

void load(unsigned long long s_addr, unsigned long long tag, int E){

Line *set = cache[s_addr];
for(int i = 0; i < E; i++){
if(set[i].valid && set[i].tag == tag){
hit++;
if(v) printf(" hit");
int old_time = set[i].time;
for(int j = 0; j < E; j++){
if(set[j].valid && set[j].time < old_time){
set[j].time++;
}
}
set[i].time = 0;
return;
}
}
//MISS
miss++;
if(v) printf(" miss");
for(int i = 0; i < E; i++){
if(!set[i].valid){
set[i].valid = 1;
set[i].tag = tag;
for(int j = 0; j < E; j++){
if(set[j].valid && j != i){
set[j].time++;
}
}
set[i].time = 0;
return;
}
}

//eviction
eviction++;
if(v) printf(" eviction");
int lru_index = 0;
for(int i = 1; i < E; i++){
if(set[i].time > set[lru_index].time)
lru_index = i;
}

set[lru_index].tag = tag;
for(int i = 0; i < E; i++){
if(i != lru_index)
set[i].time++;
}
set[lru_index].time = 0;
}

int main(int argc, char *argv[])
{
int s = -1, E = -1, b = -1;
char *tfile = NULL;
int opt;

while((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1){
switch(opt){
case 'h': break;
case 'v': v = 1; break;
case 's': s = atoi(optarg); break;
case 'E': E = atoi(optarg); break;
case 'b': b = atoi(optarg); break;
case 't': tfile = optarg; break;
default: break;
}
}

char buff[1024], op;
int size;
unsigned long long addr;

FILE *fp = fopen(tfile, "r");

while(fgets(buff, 1023, fp) != NULL){

if(sscanf(buff, " %c %llx,%d", &op, &addr, &size) != 3) {
fprintf(stderr , "%s\n" , buff );
continue ;
}
unsigned long long s_addr = get_s_addr(addr, b, s);
unsigned long long tag = addr >>(unsigned long long) (b + s);
if(v){
printf("%c %llx,%d", op, addr, size);
}

if(op == 'I');//do nothing

if(op == 'L'){
load(s_addr, tag, E);
}
if(op == 'S'){
load(s_addr, tag, E);
}
if(op == 'M'){
load(s_addr, tag, E);
load(s_addr, tag, E);
}
if(v) printf("\n");
}

printSummary(hit, miss, eviction);
return 0;
}

接下来主要关注实验二

Part2 矩阵转置优化

这个实验让我们干什么?

首先我们有一个s = 4(一共2^4 = 16组),E = 1(每个组只有一行,相当于直接映射),b = 5(一行2^5 =32个字节,即8int)的cache

我们接下来需要实现一个矩阵转置函数,使得cache的miss率尽量小

为啥直接转不行啊

我们想到反转一个M行N列矩阵最直接的方法是

1
2
3
4
5
6
int i, j;
for(i = 0; i < M; i++){
for(j = 0; j < N; j++){
B[j][i] = A[i][j];
}
}

提交测评,发现16 * 16矩阵的miss次数达到了305次,而32 * 32矩阵的次数更是达到了惊人的1211次,和要求的次数差得甚远
image17.png

为什么会这样呢🤔

我们这里拿16 * 16的矩阵为例,看看这段代码是怎么运行的

代码的逻辑是,先把A的第1行放到B的第1列,再把A的第2行放到B的第2列,等等等。

A矩阵长这样(有点丑请见谅
image1.png
至于为什么我们把A矩阵画成这样这样,是因为cache的一行能放8个int,也就是说每次读到A[i][j]的时候,他会把A[i][j]所在第i行连着的8个元素一同装进cache,如存A[3][5]时会把A[3][0:8]的所有元素都装进cache。那我们就把8个画在一起吧~。

ps:A[0][0:8]表示A[0][0]~A[0][7],简写一下防止我累死

还记得我们的cache是直接映射的吗,这也就是说,每个A[i][0:8]A[i][9:16]映射到cache中的位置是固定的,我们这里不妨假设A[0][0:8]映射到了cache的第0行,那么也就是说,这些A矩阵在cache的映射关系如下
image2.png

蓝字表示,数组的八个元素映射到的cache的固定位置

类似的,B矩阵的映射关系同理。在这里,不失一般性地,我们也假设A和B两个矩阵在内存上是连续的,这样B[0][0:8]同样也会映射到cache的第0
image3.png

理解了映射关系,我们就可以分析刚才的那段代码了,首先,当i = 0, j = 0时,会先访问A[0][0],这样,他就会把A[0][0:8]全部扔到cache的第0行,miss次数变成了1。
image4.png
然而,不仅读取A数组需要把A装进cache,赋值B数组的时候也要把B装进cache!

我们会把A[0][0]赋值给B[0][0],这时,也需要把B[0][0:8]的内容也装进cache,还记得吗,B[0][0:8]也只能映射到cache的第0行,此时miss次数+1,而且原来A装进cache第0行的内容也被清理掉了

然后i = 0, j = 1,执行语句B[1][0] = A[0][1],同样,我们这时候要去cache中找有没有A[0][1],也就是寻找A[0][0:8],然而,就在刚刚,我们的A数组的这八个元素已经被清掉了。

这就导致cache的次数再次加1。我们要重新把A[0][0:8]放在cache的第一行。再然后,我们继续执行循环,把A[0][1:8]分别赋值在B[1:8][0]上,接下来,再把B数组的对应元素放在cache里,这时这个cache就长成这样了
image5.png
再然后,我们继续执行循环,要把A[0][9:16]赋值给B[9:16][0],就在这时,我们首先把A[0][9:16]放进cache,按照映射关系放在了cache的第1行。

再然后,再把B数组的第8行到第15行的前八个元素都放进cache里。
这时,发现B数组的前8行的元素就都被新8行的元素替换掉了
此时cache变成了这样。
image6.png
这样A[0][9:16]放进cache里,miss+1。B的八行替换,miss+8。

很好,这样第一行就替换完了。

随后i = 1,我们再次执行同样的操作。现在A[1][0:9]扔进cache里,按照映射关系扔进第二行,接下来,在把B[0][0:8] ~ B[7][0:8]都扔进cache里,替换了B的后九行的八个元素。cache又加8了。。。

等等,发现了什么😮,那这么说读到B[1][8:16]还有八次miss,B[2][0:8]还有8次miss······。那给每个B的元素赋值都有一次miss!

那岂不是单单给B赋值就需要16 * 16 = 256 次miss!这还没算读取A产生的miss,算了更大,不可能达到题目的<100次要求

那怎么办?

16 * 16矩阵分块策略

我们发现,上述做法的最大问题,就是B[0][i] ~ B[7][i]扔到cache的0,2,4,······14行之后,B[9][i] ~ B[15][i]会重新占据cache的0,2,4······14行,然后读B[0][i+1] ~ B[7][i+1]又会重新占据cache的0,2,4,······14行,导致cache疯狂miss

那怎么办?

其实只要我们先不读B[9][i] ~ B[15][i],在把B[0][i] ~ B[7][i]放到cache之后并且赋值之后,直接赋值B[0][i+1] ~ B[7][i+1],就不会产生多余的8次miss了!

也就是说,我们只需要把16 * 16的矩阵分成四个8 * 8矩阵,就可以避免大多数的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
int i, j, k, t0, t1, t2, t3, t4, t5, t6, t7;
    for (i = 0; i < N; i += 8){
        for (j = 0; j < M; j += 8){
             for (k = i; k < i + 8; k++){
                    t0 = A[k][j];
                    t1 = A[k][j+1];
                    t2 = A[k][j+2];
                    t3 = A[k][j+3];
                    t4 = A[k][j+4];
                    t5 = A[k][j+5];
                    t6 = A[k][j+6];
                    t7 = A[k][j+7];

                    B[j][k]   = t0;
                    B[j+1][k] = t1;
                    B[j+2][k] = t2;
                    B[j+3][k] = t3;
                    B[j+4][k] = t4;
                    B[j+5][k] = t5;
                    B[j+6][k] = t6;
                    B[j+7][k] = t7;
            }
        }
    }

image18.png
哇喔,就是这样,我们拿下了16 * 16矩阵的分数!

注意到,我上面用了t0~t7的变量,为什么要用这些而不直接把A的值赋给B呢
实际上这些变量都是寄存器,我们可以把A数组的值暂存到寄存器里,然后再赋给B

还记得上面写的这些内容吗

理解了上述映射的内容,我们就可以分析刚才的那段代码了,首先,当i = 0, j = 0时,会先访问A[0][0],这样,他就会把A[0][0:8]全部扔到cache的第0行,miss次数变成了1。

然而,不仅读取A数组需要把A装进cache,赋值B数组的时候也要把B装进cache!

我们会把A[0][0]赋值给B[0][0],这时,也需要把B[0][0:7]的内容也装进cache,还记得吗,B[0][0:7]也只能映射到cache的第0行,此时miss次数+1,而且原来A装进cache第0行的内容也被清理掉了!

然后i = 0, j = 1,执行语句B[1][0] = A[0][1],同样,我们这时候要去cache中找有没有A[0][1],也就是寻找A[0][0:8],但是,就在刚刚,我们的A数组的这八个元素已经被清掉了。

这就导致cache的次数再次加1。我们要重新把A[0][0:8]放在cache的第一行。再然后,我们继续执行循环,把A[0][1:8]分别赋值在B[1:8][0]上,接下来,再把B数组的对应元素放在cache里,这时这个cache就长成这样了

如果我们先把A[0][0:8]的值都放进8个寄存器里,这样就不需要再重新把A[0][0:8]重新再放到cache的第一行了,直接把寄存器的值赋给B就行了,这样又少了几次miss,不错不错😊

然而,我们32 * 32矩阵的miss次数还是在惊人的1155次,举例AC相差甚远。

怎会如此?

32 * 32矩阵分块策略

我们需要先分析为什么16 * 16的分块策略不适用于 32 * 32
实际上,我们把A和B两个矩阵的映射关系写清楚就好理解了
image7.png
同样,我们把A[0][0:8]先扔到cache里,再存到t0~t78个寄存器中,然后我们再以此把B放到cache中
B[0][0:8] ~ B[3][0:8]放在了cache的0,4,8,12行,嗯~4次miss
image8.png
接下来再放B[4][0:8] ~ B[7][0:8],发现,按照映射关系,他们会把B[0][0:8] ~ B[3][0:8]都挤出去。

再然后,把A[1][0:8]扔到cache里,再存到8个寄存器里。随后,为了给B[0][1] ~ B[3][1]赋值,我们又要把他们扔到cache的0,4,8,12行

这一幕似曾相识😮,最开始写的直接转换似乎就是出现了这个类似的问题才导致疯狂的miss。
那怎么办呢,难道要用更小的矩阵?用4 * 4的?

可是,cache的一行能存8个int,4 * 4分块是不是有些屈才?
实际上,4 * 4的分块方式会导致A矩阵和B矩阵都出现较多的miss(留给你思考。
image19.png
不过miss数也大大减少,我们向着胜利迈出了很大的一步!

其实说,我们有更好的复杂,只不过有亿点方法🤔

以下内容有些抽象🤔

我们这里以非对角线上的矩阵为例

我们稍微更改了一下图,这里我们以A[0][8:16]转置到B[8:16][0]为例,蓝字表示数组能映射到cache中的位置,那么初始状态下,矩阵和cache长这样

image9.png

接下来,我们先把A[0][9:16]装进cache,把的前四行装进cache,并且直接赋值,那么cache变成了这样

image10.png

橙色表示:转置之后结果正确的元素

但是!我们先不赋值B[12:16][0],这样会把B的前四行顶掉,太浪费了。但是!我们A的后4个元素也别闲着,我们把它转移到B[8:12][4]去!这样,我们就再也不用访问A[0][8:16]了,即
image11.png

B中红色表示已被转移但是顺序不对的元素~

上述操作,我们一共有A的miss1次,B的miss四次~

接下来,我们把A的前4行都进行这个操作,这样A的总miss次数达到了4次
image12.png
cache变成了这样~

接下来,我们把B[8][4:8]的值放进t0~t3的四个寄存器里,把A[4:8][0]放到t4~t7四个寄存器里,前一步不需要动cache,而后一步会把A的前4行扔出cache。A的前四行已经没用了,直接扔掉扔掉
image13.png

紫色表示被扔到寄存器里的值~

现在,我们的目的是,把t4~t7,t0~t3放进B的正确位置!我们刚刚把B[8][4:8]的元素都放进寄存器里了,这样的话,我们只需要先把t4~t7扔上去,就再也不需要把访问B[8][0:8]了!

所以我们先把t4~t7扔放到B[8][0:8],这一步没有导致miss,然后,再把t0~t3的值放到B数组的正确位置,即B[12][0:4],这一步我们把B的12行装进了cache~

image14.png

就像这样~,这样B[8]就排好了!

image15.png

ps:橙色表示排好了

现在我们把A的4到7行和B的12行都装进cache了,那就顺手把12行的后4个元素装进去吧,反正也不会增加miss(因为A[4][0:8]~A[8][0:8]一直在cache呢
image16.png
哇喔!这样,我们组装好了8 * 8 矩阵右上角的第一行,以及整个12行。

接下来,我们仿照刚才的操作,装好B第9行的右半部分和整个13行······就可以把整个8 * 8矩阵装好了!但是这些操作,都没有使得A矩阵miss,并且也只是让了B的第k+4行替换了B的第k行,每次也只会增加1次miss——

所以,以上操作已经很可以使得miss数尽量小了!

这是非对角线的情况,在对角线上,因为A可能和B的位置冲突,所以miss数会增加!但是我们也尽可能保证了低的miss数

下面是AC代码(小心查重——)

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
int i, j, m, t0, t1, t2, t3, t4, t5, t6, t7;
if (M == 16)
{
for (i = 0; i < N; i += 8)
for (j = 0; j < M; j += 8)
for (m = i; m < i + 8; m++)
{
t0 = A[m][j];
t1 = A[m][j+1];
t2 = A[m][j+2];
t3 = A[m][j+3];
t4 = A[m][j+4];
t5 = A[m][j+5];
t6 = A[m][j+6];
t7 = A[m][j+7];

B[j][m] = t0;
B[j+1][m] = t1;
B[j+2][m] = t2;
B[j+3][m] = t3;
B[j+4][m] = t4;
B[j+5][m] = t5;
B[j+6][m] = t6;
B[j+7][m] = t7;
}
}
else if (M == 32)
{
for (i = 0; i < 32; i += 8)
{
for (j = 0; j < 32; j += 8)
{
for (m = 0; m < 4; m++)
{
t0 = A[i+m][j];
t1 = A[i+m][j+1];
t2 = A[i+m][j+2];
t3 = A[i+m][j+3];
t4 = A[i+m][j+4];
t5 = A[i+m][j+5];
t6 = A[i+m][j+6];
t7 = A[i+m][j+7];

B[j][i+m] = t0;
B[j+1][i+m] = t1;
B[j+2][i+m] = t2;
B[j+3][i+m] = t3;


B[j][i+m+4] = t4;
B[j+1][i+m+4] = t5;
B[j+2][i+m+4] = t6;
B[j+3][i+m+4] = t7;
}

for (m = 0; m < 4; m++)
{

t0 = B[j+m][i+4];
t1 = B[j+m][i+5];
t2 = B[j+m][i+6];
t3 = B[j+m][i+7];


t4 = A[i+4][j+m];
t5 = A[i+5][j+m];
t6 = A[i+6][j+m];
t7 = A[i+7][j+m];

B[j+m][i+4] = t4;
B[j+m][i+5] = t5;
B[j+m][i+6] = t6;
B[j+m][i+7] = t7;


B[j+m+4][i] = t0;
B[j+m+4][i+1] = t1;
B[j+m+4][i+2] = t2;
B[j+m+4][i+3] = t3;


t4 = A[i+4][j+m+4];
t5 = A[i+5][j+m+4];
t6 = A[i+6][j+m+4];
t7 = A[i+7][j+m+4];

B[j+m+4][i+4] = t4;
B[j+m+4][i+5] = t5;
B[j+m+4][i+6] = t6;
B[j+m+4][i+7] = t7;
}
}
}
}

提交,成功AC

不过,这种分块策略不是最优的,还可以再思考一下怎么继续降低miss数从而拿到Excellent((

不过我不行了拿到分跑了跑了跑了——

关于上机

24级的Cache实验上机共有两题,第一题是矩阵分块策略(随便8x8分块或者12x12分块就能直接过

第二题是一个题干较长但实际并不复杂的Cache模拟器实现,只是把第一问的替换策略改了一下。题干较长,但不难理解,只要读懂题了就不会有问题——

总的来说难度并不大,上机前不用有太大压力——

下有一串神秘代码,可以输出cache参数为s, E, b是,矩阵行列为N, M时,直接分块转置情况下cache 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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int s = -1; // set bits(初始值-1表示未赋值)
int E = -1; // 每组block个数
int b = -1; // block bits
char *tracefile = NULL; // trace文件路径
int v_flag = 0;

int hits = 0 ;
int misses = 0 ;
int evictions = 0 ;

// 单次Cache访问的状态
typedef struct access_status {
int hit; // 1=命中,0=未命中
int miss; // 1=缺失,0=无
int eviction; // 1=替换,0=无
} access_status_t;

// 去掉十六进制地址的前导0
char* remove_hex_leading_zero(unsigned long long addr) {
char *hex_str = (char*)malloc(32 * sizeof(char));
if (hex_str == NULL) {
perror("malloc hex_str failed");
exit(EXIT_FAILURE);
}
// 先转为十六进制字符串(小写)
sprintf(hex_str, "%llx", addr);
// 找第一个非0字符
char *start = hex_str;
while (*start == '0' && *(start+1) != '\0') {
start++;
}
// 标准C替代strdup:先计算长度,再malloc+strcpy
int len = strlen(start);
char *res = (char*)malloc((len + 1) * sizeof(char)); // +1 存'\0'
if (res == NULL) {
perror("malloc res failed");
free(hex_str);
exit(EXIT_FAILURE);
}
strcpy(res, start); // 复制字符串
free(hex_str);
return res;
}

// 去掉十进制大小的前导0
char* remove_dec_leading_zero(int size) {
char *dec_str = (char*)malloc(16 * sizeof(char));
if (dec_str == NULL) {
perror("malloc dec_str failed");
exit(EXIT_FAILURE);
}
sprintf(dec_str, "%d", size);
// 找第一个非0字符
char *start = dec_str;
while (*start == '0' && *(start+1) != '\0') {
start++;
}
// 标准C替代strdup
int len = strlen(start);
char *res = (char*)malloc((len + 1) * sizeof(char));
if (res == NULL) {
perror("malloc res failed");
free(dec_str);
exit(EXIT_FAILURE);
}
strcpy(res, start);
free(dec_str);
return res;
}


typedef struct cache_line {
unsigned long long tag; // 标记位(地址高位)
int valid; // 有效位(1=有效,0=无效)
int lru_counter; // LRU计数器:数值越大,最近使用越近
} cache_line_t;

// Cache组(Set):包含E个行
typedef struct cache_set {
cache_line_t *lines; // 指向本组的行数组
} cache_set_t;

// Cache整体:包含S个组
typedef struct cache {
cache_set_t *sets; // 指向组数组
int current_lru; // 全局LRU计数(用于更新line的lru_counter)
} cache_t;

void printSummary(int hits, int misses, int evictions)
{
printf("hits:%d misses:%d evictions:%d\n", hits, misses, evictions);
}

cache_t* init_cache(int s, int E) {
int S = 1 << s; // 组的数量
cache_t *cache = (cache_t*)malloc(sizeof(cache_t));

// 分配组数组
cache->sets = (cache_set_t*)malloc(S * sizeof(cache_set_t));

// 为每个组分配行数组,并初始化行
for (int i = 0; i < S; i++) {
cache->sets[i].lines = (cache_line_t*)malloc(E * sizeof(cache_line_t));

// 初始化行:valid=0,tag=0,lru_counter=0
for (int j = 0; j < E; j++) {
cache->sets[i].lines[j].valid = 0;
cache->sets[i].lines[j].tag = 0;
cache->sets[i].lines[j].lru_counter = 0;
}
}

cache->current_lru = 0; // 全局LRU计数器初始化为0
return cache;
}

// 提取地址的set index
unsigned long long get_set_index(unsigned long long address, int s, int b) {
unsigned long long mask = (1 << s) - 1; // s位全1的掩码
return (address >> b) & mask;
}

// 提取地址的tag
unsigned long long get_tag(unsigned long long address, int s, int b) {
return address >> (b + s);
}

// 访问Cache(核心逻辑:命中/缺失/替换)
access_status_t access_cache(cache_t *cache, unsigned long long address) {
access_status_t status = {0, 0, 0}; // 初始化为无
unsigned long long set_idx = get_set_index(address, s, b);
unsigned long long tag = get_tag(address, s, b);
//当前在set这个组中,组中元素有E个
cache_set_t *set = &cache->sets[set_idx];
int empty_line_idx = -1;
int lru_line_idx = 0;
int max_lru = set->lines[0].lru_counter;

// 1. 遍历组内所有行,检查命中/找空闲行/找LRU行
for (int i = 0; i < E; i++) {
cache_line_t *line = &set->lines[i];

// 记录空闲行
if (line->valid == 0) {
empty_line_idx = i;
}


// 找LRU行(lru_counter最大)
if (line->lru_counter > max_lru) {
max_lru = line->lru_counter;
lru_line_idx = i;
}


// 检查命中(valid=1且tag匹配)
if (line->valid == 1 && line->tag == tag) {
status.hit = 1;
hits++;

// 更新LRU计数器(标记为最近使用)
for(int k=0;k<E;k++) {
if(set->lines[k].lru_counter <= line->lru_counter && k!=i) {
set->lines[k].lru_counter ++ ;
}
}
line->lru_counter = 0 ;


return status ;
}
}

status.miss = 1 ;
misses++;

// 2. 未命中:处理缺失/替换
if (!status.hit) {
// 2.1 有空闲行:直接占用
if (empty_line_idx != -1) {
cache_line_t *line = &set->lines[empty_line_idx];
line->valid = 1;
line->tag = tag;

for(int i=0;i<E;i++) {
if(i != empty_line_idx) set->lines[i].lru_counter ++ ;
}
line->lru_counter = 0;


}
// 2.2 无空闲行:LRU替换
else {
evictions++;
status.eviction = 1 ;
cache_line_t *line = &set->lines[lru_line_idx];
line->tag = tag;

for(int i=0;i<E;i++) {
if(i != lru_line_idx) set->lines[i].lru_counter ++ ;
}
line->lru_counter = 0;


}
}

return status ;
}

// 拼接状态字符串(hit/miss/eviction,按顺序)
void append_status_str(char *buf, access_status_t status) {
if (status.hit) {
strcat(buf, " hit");
} else if (status.miss) {
strcat(buf, " miss");
if (status.eviction) {
strcat(buf, " eviction");
}
}
}





// 模拟内存地址计算:假设矩阵按行优先存储
// addr = base + (row * width + col) * sizeof(int)
unsigned long long get_addr(unsigned long long base, int r, int c, int N) {
return base + (r * N + c) * 4;
}


void test_transpose(cache_t *cache, int N, int M , int b_size ) {
unsigned long long A_base = 0x20000;
unsigned long long B_base = 0x60000;

int i, j, ii, jj;
// 对于不规则矩阵,16x16 的分块通常是性能“甜点区”

// N = 60 (A的行), M = 68 (A的列)
for (i = 0; i < N; i += b_size) {
for (j = 0; j < M; j += b_size) {
// 遍历块内部
for (ii = i; ii < i + b_size && ii < N; ii++) {
for (jj = j; jj < j + b_size && jj < M; jj++) {

// 1. 读取 A[ii][jj]
// A 的宽度是 M (68)
unsigned long long addr_A = A_base + (ii * M + jj) * 4;
access_cache(cache, addr_A);

// 2. 写入 B[jj][ii]
// B 是转置后的,宽度是 N (60)
unsigned long long addr_B = B_base + (jj * N + ii) * 4;
access_cache(cache, addr_B);
}
}
}
}
}

int main() {
// 典型的 Cache Lab 参数: s=5, E=1, b=5 (512B 直接映射)
s = 5; E = 1; b = 5;
cache_t *cache = init_cache(s, E);

// 设置规模
int N = 60; // A的行
int M = 68; // A的列
printf("Testing Matrix Transpose (N=%d, M=%d, s=%d, E=%d, b=%d)...\n", N, M, s, E, b);
int MIN = (1<<30) ;
int index = -1 ;
for(int i=4;i<=N;i++) {
printf("b_size = %d\n" , i);
test_transpose(cache, N, M , i);
printSummary(hits, misses, evictions);

if(misses < MIN) {
MIN = misses;
index = i ;
}

free(cache) ;
cache = init_cache(s,E);
hits = 0 , misses = 0 , evictions = 0;
}

printf("i=%d\tMIN=%d\n",index,MIN);
return 0;
}
  • Title: 计组Lab4 模拟cache设计
  • Author: Connor
  • Created at : 2025-12-09 18:43:20
  • Updated at : 2026-07-28 02:53:18
  • Link: https://redefine.ohevan.com/2025/12/09/cache/
  • License: This work is licensed under CC BY-NC-SA 4.0.