本次 lab, cachelab.
0. 说明 ·
从这个 lab 开始,终于开始编写一些高级语言的代码了,而不像之前要去分析汇编。但是这并不意味这题目就简单了,实际上,cachelab 耗费了我 2 天多的时间。ok,言归正传,这个 lab 的目的是什么呢?
cachelab 帮助我们理解计算机存储体系中的重要组成部分–cache。 理解 cache 是如何组织的,如何工作的,又是如何影响我们的程序的性能的。
cache 站立于整个存储体系的上端(低于寄存器),其重要性不言而喻了。
题目说明:
cachelab 只有 2 个题目。
写一个 cache 工作的模拟器,给一段内存访问的 trace file,根据 trace file 仿真,得到这段 trace file 的 hit,miss,eviction 数。
编写 cache 友好的代码,具体是给 3 种给定大小的矩阵 A,求 A 的转置,每种大小都要 miss 要求。
具体题目请参照 cachelab 的 write up。
1. 第一部分–cache 工作方式仿真 ·
做完这个题目,可以让人理解 cache 的组织,工作方式,替换策略等。** 先说一下题目,** 本题主要可以分为以下几个部分:
trace file
题目会给出一些 trace files, trace file 的格式如下:
1 2 3 4 I 0400d7d4,8 M 0421c7f0,4 L 04f6b868,8 S 7ff0005c8,8
语法为:
1 [space]operation address,size
The operation field denotes the type of memory access: “I” denotes an instruction load, “L” a data load, “S” a data store, and “M” a data modify (i.e., a data load followed by a data store). There is never a space before each “I”. There is always a space before each “M”, “L”, and “S”. The address field specifies a 64-bit hexadecimal memory address. The size field specifies the number of bytes accessed by the operation.
值得注意的是,我们只关注 data ,也就是 M,L,S 开头的指令,不关注 I 开头的指令。解析时,可根据开头是否有空格来解析。
trace file 就是我们要编写的代码的输入源,我们要做的就是根据这些 trace file 来仿真。
cache 的组织方式
题目要求,最终的程序可以接受一些参数,改变 cache 的组织方式。 如:
1 ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace
代表:
set 的 bit 位数为: 4
E=1,即一个 set 中只有一个 cacheline
b=4,即一个 cache line 的 block 位数为 4
trace file 的路径为 traces/yi.trace
具体可使用的语法为:
1 2 3 4 5 6 7 Usage: ./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile> • -h: Optional help flag that prints usage info • -v: Optional verbose flag that displays trace info • -s <s>: Number of set index bits (S = 2^s is the number of sets) • -E <E>: Associativity (number of lines per set) • -b <b>: Number of block bits (B = 2^b is the block size) • -t <tracefile>: Name of the valgrind trace to replay
cacheline 的替换策略,采用 LRU
ok,知道这些了,题目还给了我们一个标准答案,csim-ref, 我们自己最终的程序名为 csim. 最终只要 csim 的输入输出和 csim-ref 相同即可。
写程序之前,一定要分析好再写,所以先来分析下我们该怎么做:
需要哪些背景知识:
cache 的组织方式:
这张图,给出了 cache 的组织方式以及 cacheline 的 read 方式。
cache 的组织方式非常直观,主要参数为 S, E, B. 这 3 个参数也决定了 cache 的相联关系:
E=1, 直接映射
E!=1,S!=1, E 路 S 组相联
S=1, 全相联映射。(内存和 Disk 就是这种映射)
cacheline 的 read 分为以下几步:
根绝 set bit,决定 set 索引
比较该组的所有 line,是否有匹配的 tag。如有,并且 valid 有效,则 hit,并根据 b 决定数据偏移。否则,第 3 步。
load 该 cacheline 到 cache 中,如果该组还有空块(valid=0), 则加载到空块中,如果没有空块,则根据替换策略进行替换。
总结下 read (load) 操作:
hit, 能在当前 cache 中找到 cacheline
miss,不能在 cahce 中找到 cacheline,但是该组中有空 cacheline。
eviction,不能再 cache 中找到 cacheline,且该组中没有空 cacheline,需要根据替换策略进行替换。
现在在看下 write (store) 操作:
cachelab 采用的的是 write-back+write-allocate 方式。所以,一旦我们的 store miss 了,需要将该块 load 到 cache 中。分析可以下 3 中情况:
hit, 能在当前 cache 中找到 cacheline
miss,不能在 cahce 中找到 cacheline,但是该组中有空 cacheline。
eviction,不能在 cache 中找到 cacheline,且该组中没有空 cacheline,需要根据替换策略进行替换。
最后看下 modify 操作:
modify 结合了 load 和 store 操作,分析可得以下 3 种情况:
hit (load) + hit (store), load 命中,store 也命中
miss (load)+hit (store), load miss,但是当前 cache set 中有空块,直接 load 到 cache 即可,后续 store 命中。
miss (load)+eviction (load)+hit (store),cache 种没有该 cacheline,且该组没有空块,最后 store 可命中
额外啰嗦一段:
cacheline 和 block 非常容易搞混,因为一会说将某个数据块 load 到 cache,一会又说将某个 cacheline load 到 cache 中。
其实通常我们说将 xx 数据块 load 到 cache 中,这个数据块是包围在一个 cacheline 中的,cacheline 除了这个数据块以外,还会包含一些元数据,如 tag,valid,以及用于实现替换策略的辅助位等。
简单来说,cacheline 包含 block, 通常说的 cacheline 大小,都是说的 cacheline 内部的 block 的大小。如 32 字节,64 字节等,都是说的 cacheline 内部的 block 的大小为 32,64 字节
好了,分析到这里,基本上把整个流程走了一遍。剩下一些与主题无关代码,包括参数解析,trace file 解析,可自行看以下代码(代码比较长,因为我加了详细注释,同时为了规范,加了一些 “无用” 代码,但看着应该没有压力):
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 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 #include "cachelab.h" #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <getopt.h> #include <ctype.h> #include <string.h> #define FILE_NAME_LEN 100 #define OS_PTR_LEN 64 typedef unsigned long long address64_t ; #define DEBUG_HIT(verbose) do{if (verbose) printf("hit\n" );}while(0) #define DEBUG_MISS(verbose) do{if (verbose) printf("miss\n" );}while(0) #define DEBUG_EVICT(verbose) do{if (verbose) printf("evict\n" );}while(0) typedef struct param { int v; int s; int E; int b; char t[FILE_NAME_LEN]; } param; typedef struct cache_line { int valid; int tag; int age; } cache_line; typedef enum op_mode { L = 0 , M = 1 , S = 2 } op_mode; typedef struct trace_item { op_mode mode; address64_t addr; } trace_item; typedef struct set { cache_line *lines; } set ; typedef struct cache { set *sets; } cache; char *usage = "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" ; unsigned int hits, miss, evicts;int parse_input_params (int argc, char *argv[], param *p) { int opt; while ((opt = getopt(argc, argv, "hvs:E:b:t:" )) != -1 ) { switch (opt) { case 'v' : p->v = 1 ; break ; case 's' : p->s = atoi(optarg); break ; case 'E' : p->E = atoi(optarg); break ; case 'b' : p->b = atoi(optarg); break ; case 't' : sprintf (p->t, "%s" , optarg); break ; case 'h' : case '?' : default : printf ("%s\n" , usage); return -1 ; } } return 0 ; } op_mode parse_op_mode (char c_mode) { switch (c_mode) { case 'L' : return L; case 'M' : return M; case 'S' : return S; default : perror("can't get op mode: invalid char!" ); exit (-1 ); } } size_t parse_trace_file (trace_item **trace_item_ptr, size_t *trace_item_num, const char *path) { FILE *file = NULL ; char *line = NULL ; trace_item *trace_items = *trace_item_ptr; size_t len = 0 ; if (!(file = fopen(path, "r" ))) { perror("trace file not exit" ); return -1 ; } int i = 0 ; while (getline(&line, &len, file) != -1 ) { if (line[0 ] == 'I' ) continue ; if (*trace_item_num <= i) { int new_num = *trace_item_num; do { new_num *= 2 ; } while (new_num <= i); trace_items = realloc (trace_items, new_num * sizeof (trace_item)); if (!trace_items) { printf ("realloc memory for trace item failed\n" ); exit (-1 ); } *trace_item_ptr = trace_items; memset (trace_items + (*trace_item_num), 0 , (new_num - *trace_item_num) * sizeof (trace_item)); *trace_item_num = (*trace_item_num) << 1 ; } trace_items[i].mode = parse_op_mode(line[1 ]); char addr[OS_PTR_LEN + 1 ]; int j = 3 ; int offset = 0 ; while (!isdigit (line[j]) && isspace (line[j])) ++j; offset = j; while (line[j] != ',' ) { addr[j - offset] = line[j]; j++; } addr[j - offset] = '\0' ; trace_items[i].addr = strtol(addr, NULL , 16 ); i++; } *trace_item_num = i; *trace_item_ptr = (trace_item *) realloc (*trace_item_ptr, sizeof (trace_item) * (*trace_item_num)); fclose(file); return 0 ; } void init_cache (cache *sys_cache, param *p) { const int S = 1 << p->s; const int E = p->E; set *sets = (set *) malloc (sizeof (set ) * S); for (size_t i = 0 ; i < S; i++) { cache_line *cls = (cache_line *) malloc (sizeof (cache_line) * E); memset (cls, 0 , sizeof (cache_line) * E); sets[i].lines = cls; } sys_cache->sets = sets; } void check_if_hit (set *cache_set, param *p, int tag, size_t *cl_idx) { const int E = p->E; for (size_t i = 0 ; i < E; i++) { if (!cache_set->lines[i].valid) continue ; if (cache_set->lines[i].tag == tag) { *cl_idx = i; return ; } } *cl_idx = -1 ; } void check_has_empty_block (set *cache_set, param *p, size_t *cl_idx) { const int E = p->E; for (size_t i = 0 ; i < E; i++) { if (!cache_set->lines[i].valid) { *cl_idx = i; return ; } } *cl_idx = -1 ; } void find_evict_cache_line (set *cache_set, param *p, size_t *cl_idx) { const int E = p->E; int min_age_idx = 0 ; for (size_t i = 1 ; i < E; i++) { if (cache_set->lines[min_age_idx].age > cache_set->lines[i].age) { min_age_idx = i; } } *cl_idx = min_age_idx; } void do_load (set *cache_set, param *p, int tag, int age) { const int verbose = p->v; size_t cl_idx; check_if_hit(cache_set, p, tag, &cl_idx); if (cl_idx != -1 ) { hits++; cache_line *cl = &cache_set->lines[cl_idx]; cl->tag = tag; cl->age = age; DEBUG_HIT(verbose); return ; } check_has_empty_block(cache_set, p, &cl_idx); if (cl_idx != -1 ) { miss++; cache_line *cl = &cache_set->lines[cl_idx]; cl->valid = 1 ; cl->tag = tag; cl->age = age; DEBUG_MISS(verbose); return ; } find_evict_cache_line(cache_set, p, &cl_idx); if (cl_idx != -1 ) { miss++; DEBUG_MISS(verbose); cache_line *cl = &cache_set->lines[cl_idx]; cl->tag = tag; cl->age = age; evicts++; DEBUG_EVICT(verbose); return ; } } void do_store (set *cache_set, param *p, int tag, int age) { const int verbose = p->v; size_t cl_idx; check_if_hit(cache_set, p, tag, &cl_idx); if (cl_idx != -1 ) { hits++; cache_set->lines[cl_idx].age = age; DEBUG_HIT(verbose); return ; } check_has_empty_block(cache_set, p, &cl_idx); if (cl_idx != -1 ) { miss++; cache_set->lines[cl_idx].valid = 1 ; cache_set->lines[cl_idx].tag = tag; cache_set->lines[cl_idx].age = age; DEBUG_MISS(verbose); return ; } find_evict_cache_line(cache_set, p, &cl_idx); if (cl_idx != -1 ) { miss++; DEBUG_MISS(verbose); cache_set->lines[cl_idx].tag = tag; cache_set->lines[cl_idx].age = age; evicts++; DEBUG_HIT(verbose); return ; } } void do_modify (set *cache_set, param *p, int tag, int age) { const int verbose = p->v; size_t cl_idx = -1 ; check_if_hit(cache_set, p, tag, &cl_idx); if (cl_idx != -1 ) { hits += 2 ; cache_set->lines[cl_idx].age = age; DEBUG_HIT(verbose); DEBUG_HIT(verbose); return ; } check_has_empty_block(cache_set, p, &cl_idx); if (cl_idx != -1 ) { miss++; DEBUG_MISS(verbose); cache_set->lines[cl_idx].valid = 1 ; cache_set->lines[cl_idx].tag = tag; cache_set->lines[cl_idx].age = age; hits++; DEBUG_HIT(verbose); return ; } find_evict_cache_line(cache_set, p, &cl_idx); if (cl_idx != -1 ) { miss++; DEBUG_MISS(verbose); DEBUG_EVICT(verbose); evicts++; cache_set->lines[cl_idx].tag = tag; cache_set->lines[cl_idx].age = age; hits++; DEBUG_HIT(verbose); return ; } } void simulate (cache *sys_cache, param *p, trace_item *trace_items, const unsigned int item_num) { const int s = p->s; const int b = p->b; const int tag_bit = OS_PTR_LEN - s - b; address64_t set_mask = 1 ; for (size_t i = 0 ; i < s - 1 ; i++) { set_mask = set_mask << 1 | 1 ; } address64_t tag_mask = 1 ; for (size_t i = 0 ; i < tag_bit - 1 ; i++) { tag_mask = tag_mask << 1 | 1 ; } for (size_t i = 0 ; i < item_num; i++) { trace_item item = trace_items[i]; address64_t addr = item.addr; op_mode mode = item.mode; int set_idx = addr >> b & set_mask; int tag = addr >> (b + s); switch (mode) { case L: do_load(&sys_cache->sets[set_idx], p, tag, i); break ; case M: do_modify(&sys_cache->sets[set_idx], p, tag, i); break ; case S: do_store(&sys_cache->sets[set_idx], p, tag, i); break ; } } } int main (int argc, char *argv[]) { param sys_param; size_t trace_item_len = 10 ; trace_item *trace_items = (trace_item *) malloc (trace_item_len * sizeof (trace_item)); cache sys_cache; int ret; ret = parse_input_params(argc, argv, &sys_param); if (ret) { printf ("parse input params failed\n" ); printf ("%s\n" , usage); return -1 ; } ret = parse_trace_file(&trace_items, &trace_item_len, sys_param.t); if (ret) { printf ("parse trace file failed" ); return -1 ; } init_cache(&sys_cache, &sys_param); simulate(&sys_cache, &sys_param, trace_items, trace_item_len); printSummary(hits, miss, evicts); return 0 ; }
2. 第二部分–编写 cache 友好代码 ·
本题能加强学生对 cache 的认识,编写 cahce 友好的代码。原题很简单,就是给一个矩阵 A,求其转置。但是有一些额外说明,具体请读 writeup。下面是本题的答案要求:
m,代表 miss 次数。
本题核心点:
加强理解直接相联映射
理解 blocking(分块)机制带来的时间局部性提升
重点:blocking 机制 ,一定要理解 blocking,请参照: http://csapp.cs.cmu.edu/public/waside/waside-blocking.pdf
在正式解题前,先说下系统的一些参数,最重要的就是 cache 的组织方式了:
s=5, S=32, 即 32 组
E=1, 即每组一个 cacheline
b=5,B=32, 即一个 cacheline 的 block 大小为 32 字节(后面简称 cacheline 大小为 32 字节,实际说的是 cacheline 内部的 block 大小)
blocking 机制运用到矩阵转置来,即将 A 分成多个行条带,A 行条带扫描。B 分为多个块,每个块由多个条带组成,每个 B 条带按照列扫描。如图:
1. 32x32·
一个 cacheline 32 字节, 一个 int 4 字节, 则一个 cacheline 可以放 8 个 int, 矩阵为 32x32, 则矩阵一行需要 4 个 cacheline。
现在的问题是如何确定条带的长度。
我们知道 cpu 一次 read,都会 load 一个 cache line 到 cache 中。一个 cacheline 是 32 字节。如何将条带设置低于 32 字节,比如 12 字节,那么 32 字节中会有 20 字节无法利用,浪费了一半的 cache。如果大于 32 字节,那扫描一个条带就会触发至少 2 次 load。那是不是设置成 32 字节就好了?也不是,我们还要考虑 A,B 的 load,store 交替过程中,会造成 cacheline 的 overlap(冲突不命中)。为了让你理解这个概念,先看以下面这道题:
image-20200729145230952
image-20200729145241517
现在回到我们的题目,我们想要 cacheline 的利用率高,又不想发生太多 cacheline 的冲突不命中。观察到在 32x32 的矩阵中,** 每 8 行重复一个 cache 空间。** 如果一个条带为 32 字节,即 8 个矩阵元素,刚好对应了一个 cache line 大小。所以我们可以设置, A 的行扫描条带为 8 个元素, B 的列扫描条带为 8 个元素,那一个块的宽度呢?显然一个 cacheline 是 32 字节,刚好也是 8 个元素,宽度为 8 肯定利用率高。
最终决定的参数:
A 行条带 8 元素
B 中一个块的大小为 8x8
另外,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 void trans32_v1 (int A[32 ][32 ], int B[32 ][32 ]) { #define LEN 32 #define BSIZE 8 int i, j, k,q; for (i = 0 ; i < LEN; i += BSIZE) { for (j = 0 ; j < LEN; j += BSIZE) { for (k = j; k < j + BSIZE; k++) { for (q = 0 ; q < BSIZE; q++) { B[i + q][k] = A[k][i + q]; } } } } #undef LEN #undef BSIZE }
这样写,miss 数是 300 多,而满分要求 miss 数 < 300. 继续分析,什么导致了多余的 miss?
cachelab 的 writeup 中,有这样一句话:
对角线? 是的,对角线上的元素,会发生冲突不命中问题。究其根本,在于我们对 A 进行了反复读。如何解决?把 A 的数据放到寄存器就行了。再次回到 write u 中:
题目要求我们最多不能定义 12 个局部变量,上述代码仅仅 4 个。我们还有 8 个变量没用,最终的代码:
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 char trans32_v1_desc[] = "trans32_v1_desc" ;void trans32_v1 (int A[32 ][32 ], int B[32 ][32 ]) { #define LEN 32 #define BSIZE 8 int i, j, k; int t0, t1, t2, t3, t4, t5, t6, t7; for (i = 0 ; i < LEN; i += BSIZE) { for (j = 0 ; j < LEN; j += BSIZE) { for (k = j; k < j + BSIZE; k++) { t0 = A[k][i + 0 ]; t1 = A[k][i + 1 ]; t2 = A[k][i + 2 ]; t3 = A[k][i + 3 ]; t4 = A[k][i + 4 ]; t5 = A[k][i + 5 ]; t6 = A[k][i + 6 ]; t7 = A[k][i + 7 ]; B[i + 0 ][k] = t0; B[i + 1 ][k] = t1; B[i + 2 ][k] = t2; B[i + 3 ][k] = t3; B[i + 4 ][k] = t4; B[i + 5 ][k] = t5; B[i + 6 ][k] = t6; B[i + 7 ][k] = t7; } } } #undef LEN #undef BSIZE }
最终的 miss 数为 287 ,在代码的注释部分,给出了 miss 的详细计算方式。
2. 61x67·
64x64 的最后来说,先把简单的说了来。
61 和 67 看着不规则比较难,但是题目要求很松,只要低于 2000miss 即可。
依然是上面的算法,更改 BSIZE, 经测试 BSIZE=16 就能满足要求。
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 void trans6761_v1 (int A[67 ][61 ], int B[61 ][67 ]) { #define M 67 #define N 61 #define BSIZE 16 int i, j, k, q; for (i = 0 ; i < N; i += BSIZE) { for (j = 0 ; j < M; j += BSIZE) { for (k = j; k < j + BSIZE; k++) { for (q = 0 ; q < BSIZE; q++) { if (k < M && i + q < N) { B[i + q][k] = A[k][i + q]; } else { break ; } } } } } #undef N #undef M #undef BSIZE }
最终的 miss 为 1817
3. 64x64·
64x64 则难很多了,如果依然照搬上面的算法,最终的 miss 应该是 1800 多,离满分 1300 还差很远。来分析下原因:
如果 BSIZE 依然取 8 会发生什么问题?
上图给出了此时的矩阵内部 cacheline 分布,有个很严重的问题在于一个 cache 空间,在矩阵中仅 4 行就重复了。那如果条带继续为 8, B 列向扫描一个条带,后 4 个元素就会替换前 4 个元素所在的 cacheline。造成过多冲突不命中 。
那,如果将 BSIZE 设置为 4 呢?
实际上,采用同样的算法,BSIZE=4, 的确是最优的了。但是设置为 4 又会带来什么缺点?cacheline 的利用率仅有一半 。 一个 cacheline 32 字节,可装载 8 个元素,但实际只是用了 4 个元素。
那,如何保住利用率的同时,减少冲突不命中?
要保住利用率,BSIZE 应该是 8 的整数倍。但是冲突不命中如何解决?
以 BSIZE=8 为例:
A 条带长度为 8,我们将一个 A 条带的前 4 列正常填入 B 的前 4 行,而 **A 条带的后 4 列填入到其他地方,** 再等待某个时机,将这些临时填充的数据归还到正确位置即可。示意图:
绿色代表正常填入区间。
灰色代表本应该填入,但是不填入,转而填入到红色区域。
红色区域代表临时填充区间。
接着,在某个时机:
几个问题:
为什么红色区域要横着填?
因为要考虑 cache 友好性,如果竖着填,那将跨越多个 cacheline。
为什么要选择第 0 行,第 4 5 6 7 列 作为 第 0 列,第 4 5 6 7 行的临时区间?
这只是个示意图,实际上我选择了 3 种方案,最终方案不是这样映射的,贴这个图是为了方便理解。
某个时机?具体是什么时机?
将要填写第 0 行,第 4,5,6,7 列数据时,首先先移动数据到正确的位置(第 0 列,第 4,5,6,7 行),然后才填写第 0 行,第 4,5,6,7 列。
最靠右的块如何处理?它已经没有 右边的空间 作为缓冲了。
单独处理。具体之后会说。
最终方案 ·
最终我选择方案为: BSIZE=8,临时填充区间示意图如下:
为什么会这样选?如果像之前示意图那样选,依然会存在很多冲突映射,甚至不如不做映射。仔细分析了下 trace file, 手动模拟了 cache line 的 load store 过程,选择的这样的临时填充映射。
最后再说说扫描到最后一个 8x8 的方格时如何处理 ,由于最后的方格右边已经没有空间做缓冲,那先考虑一个简单的,直接对 A 和 B 矩阵的最后 8x8 方格做一一映射。显然后 4x8 会把和前 4x8 会产生冲突不命中。
这样做的 miss 数应该是 1500 多。虽然比最开始的算法 1800 多,还是好不少,但依然没法满分。没办法,继续考虑。
既然是后 4x8 和前 4x8 冲突了,那把两次赋值过程分开不就好了吗?
是的,基于这样的思想,我改了代码,嗯,不错,这次跑的结果为 1190 。总算挤近 1300 了。
别忘了,依然要解决重复加载 A 条带带来的冲突不命中问题(即,多定义局部变量)。
最终代码:
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 void trans64_v1 (int A[64 ][64 ], int B[64 ][64 ]) { #define LEN 64 int i, j, k; int t0, t1, t2, t3, t4, t5, t6, t7; for (i = 0 ; i < LEN; i += 8 ) { for (j = 0 ; j < LEN; j += 8 ) { for (k = j; k < j + 8 ; k++) { t0 = A[k][i + 0 ]; t1 = A[k][i + 1 ]; t2 = A[k][i + 2 ]; t3 = A[k][i + 3 ]; t4 = A[k][i + 4 ]; t5 = A[k][i + 5 ]; t6 = A[k][i + 6 ]; t7 = A[k][i + 7 ]; if (j == 0 ) { B[i + 0 ][k] = t0; B[i + 1 ][k] = t1; B[i + 2 ][k] = t2; B[i + 3 ][k] = t3; B[i + k % 4 ][k - k % 4 + 8 ] = t4; B[i + k % 4 ][k - k % 4 + 9 ] = t5; B[i + k % 4 ][k - k % 4 + 10 ] = t6; B[i + k % 4 ][k - k % 4 + 11 ] = t7; } else { if (k == j) { for (size_t a = i; a < i + 4 ; a++) { for (size_t b = j; b < j + 4 ; b++) { B[b + i - j + 4 ][a - i + j - 8 ] = B[a][b]; B[b + i - j + 4 ][a - i + j - 4 ] = B[a][b + 4 ]; } } } if (j != LEN - 8 ) { B[i + 0 ][k] = t0; B[i + 1 ][k] = t1; B[i + 2 ][k] = t2; B[i + 3 ][k] = t3; B[i + k % 4 ][k - k % 4 + 8 ] = t4; B[i + k % 4 ][k - k % 4 + 9 ] = t5; B[i + k % 4 ][k - k % 4 + 10 ] = t6; B[i + k % 4 ][k - k % 4 + 11 ] = t7; } else { B[i + 0 ][k] = t0; B[i + 1 ][k] = t1; B[i + 2 ][k] = t2; B[i + 3 ][k] = t3; } } } } for (size_t a = 56 ; a < 64 ; a++) { t4 = A[a][i + 4 ]; t5 = A[a][i + 5 ]; t6 = A[a][i + 6 ]; t7 = A[a][i + 7 ]; B[i + 4 ][a] = t4; B[i + 5 ][a] = t5; B[i + 6 ][a] = t6; B[i + 7 ][a] = t7; } } #undef LEN }
如果你眼尖,会发现一个问题,那就是局部变量用了 13 个。好吧,确实是,but,我们是可以解决的:
1 2 3 4 5 6 7 8 9 for (size_t a = i; a < i + 4 ; a++){ for (size_t b = j; b < j + 4 ; b++) { B[b + i - j + 4 ][a - i + j - 8 ] = B[a][b]; B[b + i - j + 4 ][a - i + j - 4 ] = B[a][b + 4 ]; } }
把这个循环展开即可。
3. 总结 ·
呼,加上今天的文章,cachelab 一共花了 3 天的时间,不过是完全值得的,能够深入理解经常碰到的 cacheline 的本质,理清了很多概念上的东西。 cache 友好的代码确实很难想也很难写,以我目前的能力,写代码时还考虑不到这么底层,考虑一些时间、空间局部性就快极限了,每一行代码都去想 cache line 实在耗费很多精力,不得不说,还有很多路要走啊。
最近也要忙项目了,下一个 lab 不知道什么时候再做。但一定要把 csapp 啃完!