Skip to content

Latest commit

 

History

History
353 lines (319 loc) · 12.6 KB

cachelab.md

File metadata and controls

353 lines (319 loc) · 12.6 KB

Cache Lab 笔记

这个 lab 分为 Part A 和 Part B ,Part A 要求根据 traces 目录下的 trace 文件,写一个模拟缓存的程序,仅需要修改 csim.c ;Part B 要求优化矩阵转置函数,减少缓存不命中数,仅需修改 trans.c,具体要求认真阅读文档。
参考资料:
马天猫的CS学习之旅
blocking

Part A

csim.c 中写个缓存模拟器,要求没有警告和错误,实现和参考的模拟器一样的功能,实现 Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t <file> 。这里的 -t 的参数是 trace 文件,trace 文件包含四种与缓存相关的操作, “I” 是一个指令加载,不会访问模拟的缓存, “L” 是一个数据加载,会访问一次缓存, “S” 是数据存储,和加载一样会访问一次缓存, “M” 是数据修改,相当于一次加载和一次缓存,也就是相当于两次加载。因而可以通过实现加载函数来处理这些操作。
可以使用一个结构数组 cache[] 来表示缓存,包含 s 个组,每组 E 行,结构数组的每一项都包含一个 tag ,一个有效位和一个访问计数。首先可以使用 getopt 函数实现命令行参数的处理,具体 man 3 getopt ,然后读取 trace 文件,通过 fgets 函数读取每一行,分析出每一行的操作,保存每行的 address 和 size 两个数值,根据 address 和 size 得到偏置位 offset 和 组号 setindex, 标记位 tag,然后按照操作类型调用 Load() 函数。 Load 函数根据所得到组号 setindex , 标记位 tag, 与 cache 数组中的块比较,如果存在该 tag 块就 hit ,如果不命中就 miss ,然后在 cache 数组中添加该 tag ;如果 cache 行满了,就选择一行置换,这里要求使用 LRU (least-recently used) 置换方法。
这里还实现了 -h-v 参数,和参考的模拟器有点不同,但不影响结果,代码如下:

#include "cachelab.h"
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* 用宏表示的数组的索引,m 行 n 列 */
#define IDX(m, n, E) m *E + n
#define MAXSIZE 30
char input[MAXSIZE]; /* 保存每行的字符串 */
int hit_count = 0, miss_count = 0, eviction_count = 0;
int debug = 0; /* 参数 v 的标记*/

/* 一个缓存行的结构 */
struct sCache {
  int vaild; /* 有效位 */
  int tag;   /* 标记位 */
  int count; /* 最近访问的计数 */
};
typedef struct sCache Cache;

/* Cache last_eviction; */

/* 将 16 进制转为 10 进制数 */
int hextodec(char c);
/* 反转二进制的 b 位,可以不需要 */
/* int converse(int n, int b); */
/* 缓存加载 */
void Load(int count, unsigned int setindex, unsigned int tag,
          unsigned int offset, unsigned int size, double s_pow, unsigned int E,
          double b_pow, Cache *cache);

int main(int argc, char *argv[]) {
  const char *str = "Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t "
                    "<file>\nOptions:\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 -s 4 -E 1 -b 4 -t "
                    "traces/yi.trace\n linux>  ./csim -v -s 8 -E 2 "
                    "-b 4 -t traces/yi.trace\n ";
  int opt = 0;                      /* 保存参数 */
  unsigned int s = 0, E = 0, b = 0; /* 组的位数 每组行数 和 块数目的位数 */
  double s_pow = 0, b_pow = 0; /* 组数 块数 */
  char *t = "";                /* trace 文件 */
  /*last_eviction.offset = 0;
    last_eviction.tag = -1;
    last_eviction.count = 0;*/

  /* getopt: 每次检查一个命令行参数 */
  while ((opt = getopt(argc, argv, "hvs:E:-b:-t:")) != -1) {
    switch (opt) {
    case 's':
      s = atoi(optarg);
      s_pow = 1 << s; /* 组数 */
      break;
    case 'E':
      E = atoi(optarg); /* 每组行数 */
      break;
    case 'b':
      b = atoi(optarg);
      b_pow = 1 << b; /* 每行块数 */
      break;
    case 't':
      t = optarg; /* trace 文件 */
      break;
    case 'v':
      debug = 1; /* v 标记 */
      break;
    case 'h':
      printf("%s", str); /* help 信息 */
      return 0;
      break;
    default: /* '?' */
      fprintf(stderr, "Usage: %s [-hv] -s <num> -E <num> -b <num> -t <file>\n",
              argv[0]);
      exit(EXIT_FAILURE);
    }
  }

  Cache *cache = (Cache *)malloc(16 * s_pow * E); /* 表示缓存的结构数组 */
  /* bug:
   Cache *cache = (Cache *)malloc(sizeof(Cache) * s * E); */
  for (int i = 0; i < s_pow * E; i++) { /* 初始化 */
    cache[i].vaild = 0;
    cache[i].tag = 0;
    cache[i].count = 0;
  }
  FILE *fp = fopen(t, "r"); /* 打开 trace 文件 */
  int count = 0;            /* 可以当作时间的计数,用于每次访问缓存时更新缓存行的计数 */

  /* 分析 trace 文件的每一行 */
  while (fgets(input, MAXSIZE, fp)) {
    int op = 0; /* 需要访问缓存的次数 */
    unsigned int offset = 0, tag = 0,
                 setindex = 0; /* 缓存行的块索引,tag 标记,组号 */
    char c;
    int cflag = 0;                      /* 是否有逗号的标记 */
    unsigned int address = 0, size = 0; /* 访问缓存的地址和大小 */
    count++;                            /* 计数 */

    for (int i = 0; (c = input[i]) && (c != '\n'); i++) {
      if (c == ' ') { /* 跳过空格 */
        continue;
      } else if (c == 'I') {
        op = 0; /* I 时不访问缓存 */
      } else if (c == 'L') {
        op = 1; /* L 时访问缓存一次 */
      } else if (c == 'S') {
        op = 1; /* S 时访问缓存一次 */
      } else if (c == 'M') {
        op = 2; /* M 时访问缓存两次 */
      } else if (c == ',') {
        cflag = 1; /* 有逗号 */
      } else {
        if (cflag) {          /* 是否有逗号? */
          size = hextodec(c); /* 有逗号时接下来的字符为 size */
        } else {
          address =
              16 * address + hextodec(c); /* 无逗号时接下来的字符为 address */
        }
      }
    }

    /* 从 address 取出 offset */
    for (int i = 0; i < b; i++) {
      offset = offset * 2 + address % 2;
      address >>= 1;
    }
    /* offset = converse(offset, b); */
    /* 从 address 取出 setindex */
    for (int i = 0; i < s; i++) {
      setindex = setindex * 2 + address % 2;
      address >>= 1;
    }
    // setindex = converse(setindex, s);
    /* 从 address 取出 tag */
    tag = address;

    /* 根据次数访问缓存 */
    if (debug && op != 0) {
      printf("\n%s", input);
    }
    if (op == 1) {
      Load(count, setindex, tag, offset, size, s_pow, E, b_pow, cache);
    }
    /* 为 M 时访问两次缓存,第一次调用加载函数,第二次直接 hit */
    if (op == 2) {
      Load(count, setindex, tag, offset, size, s_pow, E, b_pow, cache);
      hit_count++;
      if (debug) {
        printf(" hit");
      }
    }
    /*
    if (debug) {
      printf("%d %d %d\n", tag, setindex, offset);
    }
    */
  }

  free(cache);
  fclose(fp);
  // optind 记录处理的参数总数
  if (optind > argc) {
    fprintf(stderr, "Expected argument after options\n");
    exit(EXIT_FAILURE);
  }

  if (debug) {
    printf("\n");
  }
  printSummary(hit_count, miss_count, eviction_count);
  return 0;
}

/* 将 16 进制转为 10 进制数 */
int hextodec(char c) {
  if (c >= '0' && c <= '9') {
    return c - '0';
  }
  if (c >= 'A' && c <= 'F') {
    return c - 'A' + 10;
  }
  if (c >= 'a' && c <= 'f') {
    return c - 'a' + 10;
  }
  return 0;
}

/* 反转二进制的 b 位,可以不需要
int converse(int n, int b) {
  int res = 0;
  while (b--) {
    res = res * 2 + n % 2;
    n >>= 1;
  }
  return res;
}
*/

/* 缓存加载 */
void Load(int count, unsigned int setindex, unsigned int tag,
          unsigned int offset, unsigned int size, double s_pow, unsigned int E,
          double b_pow, Cache *cache) {

  /* 根据所得到组号 set , 标记位 tag, 与 cache 数组中的 tag 比较,如果存在该 tag
   * 的缓存行就 hit */
  for (int i = 0; i < E; i++) {
    if (cache[IDX(setindex, i, E)].vaild &&
        tag == cache[IDX(setindex, i, E)].tag) {
      /* bug:
      // cache[IDX(setindex, i, E)].count = 1;
      // cache[IDX(setindex, i, E)].count = 1;
      // if (tag == last_eviction.tag) {
      //  cache[IDX(setindex, i, E)].count = last_eviction.count + count;
      //} else {*/
      cache[IDX(setindex, i, E)].count = count;
      //}
      hit_count++;
      if (debug) {
        printf(" hit");
      }
      return;
    }
  }

  /* 缓存不命中 选择一个空闲的缓存行保存 tag */
  miss_count++;
  if (debug) {
    printf(" miss");
  }
  for (int i = 0; i < E; i++) {
    if (!cache[IDX(setindex, i, E)].vaild) {
      cache[IDX(setindex, i, E)].tag = tag;
      cache[IDX(setindex, i, E)].count = count;
      cache[IDX(setindex, i, E)].vaild = 1;
      return;
    }
  }

  /* 缓存行已满,应淘汰一行,这里要求使用 LRU 算法,通过循环找出最早访问的那块*/
  int mix_index = 0, mix_count = 1000000000;
  for (int i = 0; i < E; i++) {
    if (cache[IDX(setindex, i, E)].count < mix_count) {
      mix_count = cache[IDX(setindex, i, E)].count;
      mix_index = i;
    }
  }

  eviction_count++;
  if (debug) {
    printf(" eviction");
  }

  /*last_eviction.offset = cache[IDX(setindex, mix_index, E)].valid;
   last_eviction.tag = cache[IDX(setindex, mix_index, E)].tag;
   last_eviction.count = cache[IDX(setindex, mix_index, E)].count;*/
  cache[IDX(setindex, mix_index, E)].tag = tag;
  cache[IDX(setindex, mix_index, E)].count = count;
  cache[IDX(setindex, mix_index, E)].vaild = 1;

  return;
}

使用 unix> make && ./test-csimunix> make && ./driver.py 测试得到 Part A 27分满分,测试全部通过。(但这里有一个问题,如果访问缓存的地址超出了缓存行的范围怎么办,这个 lab 不做要求)

Part B

Part B 要求优化矩阵转置函数,减少缓存 miss 情况,将 miss 数降至可得分的值,仅需修改 trans.c 。所给的 Cache 包含 32 组,每组 1 行,每行 32 块,共 1024 字节。需要安装 valgrind 工具,使用 unix> make && ./test-transunix> make && ./driver.py 测试,这里将使用如下三个矩阵作为测试得分点:

  • 32 × 32: 得满分要求 miss < 300
  • 64 × 64: 得满分要求 miss < 1300
  • 61 × 67: 得满分要求 miss < 2000

做法采用分块的方法,参见网络旁注 blocking ,通过分块可以提高时间局部性和空间局部性。
对于 32×32 的矩阵,直接分块 8×8 ,注意对角线上的块 A、B 缓存时会发生冲突。在分块内进行转置。对于 61×67 的不规则的矩阵,由于对 miss 要求不高,尝试分块 16×16 可以得到满分。对于 64×64 的矩阵,分块 4×4 ,miss 降到 1699 ,未能达到满分,正在做。代码如下:

void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
  int i, j, ii, jj, a1, a2, a3, a4, a5, a6, a7, a0;
  if (M == 32) {

    for (i = 0; i < N; i += 8) {
      for (j = 0; j < M; j += 8) {
        for (ii = i; ii < i + 8; ii++) {
          jj = j;
          a0 = A[ii][jj];
          a1 = A[ii][jj + 1];
          a2 = A[ii][jj + 2];
          a3 = A[ii][jj + 3];
          a4 = A[ii][jj + 4];
          a5 = A[ii][jj + 5];
          a6 = A[ii][jj + 6];
          a7 = A[ii][jj + 7];
          B[jj][ii] = a0;
          B[jj + 1][ii] = a1;
          B[jj + 2][ii] = a2;
          B[jj + 3][ii] = a3;
          B[jj + 4][ii] = a4;
          B[jj + 5][ii] = a5;
          B[jj + 6][ii] = a6;
          B[jj + 7][ii] = a7;
        }
      }
    }
  } else if (M == 64) {
    for (i = 0; i < N; i += 4) {
      for (j = 0; j < M; j += 4) {
        for (ii = i; ii < i + 4; ii++) {
          jj = j;
          a0 = A[ii][jj];
          a1 = A[ii][jj + 1];
          a2 = A[ii][jj + 2];
          a3 = A[ii][jj + 3];
          B[jj][ii] = a0;
          B[jj + 1][ii] = a1;
          B[jj + 2][ii] = a2;
          B[jj + 3][ii] = a3;
        }
      }
    }
  } else {
    for (i = 0; i < N; i += 16) {
      for (j = 0; j < M; j += 16) {
        for (ii = i; ii < i + 16 && ii < N; ii++) {
          for (jj = j; jj < j + 16 && jj < M; jj++) {
            a0 = A[ii][jj];
            B[jj][ii] = a0;
          }
        }
      }
    }
  }
}