这个lab在新版被用来替代Performance Lab,内容有所扩充。第一部分的缓存模拟器有助于你加深对Cache的原理的理解。第二部分的优化也有了详细的评分机制,而且也并不简单,其中64*64的矩阵更是有挑战性。

Cache Lab 简介

相比Performance Lab的标题 Code OptimizationCache Lab的标题是 Understanding Cache Memories,更侧重于让学生去理解Cache具体的原理。

Part A实现一个缓存模拟器要求你透彻理解缓存的寻址方式,替换方式等等,Part B则是让你根据缓存的原理来优化程序,减少缓存的不命中来加快运行速度。

不过有一点要注意的是,在Cache Lab中将读取和写入视为一样的操作,即他们占用的时间一致,我们只需要考虑miss和hit的次数即可。但是在实际的计算机中,写入的时间大致是读取的时间的三倍,这也是我们应该记住的。

文件说明

github地址:Cache Lab

  • csim.c:实现缓存模拟器的文件
  • trans.c:实现矩阵转置的文件
  • csim-ref:标准的缓存模拟器
  • csim:由你实现的模拟器可执行程序
  • tracegen:测试你的矩阵转置是否正确,并给出错误信息
  • test-trans:测试你的矩阵转置优化的如何,并给出评分
  • driver.py:自动进行测试

在每一次更新之后,首先用make生成文件,之后用相应的test跑分即可。

Part A:Writing a Cache Simulator 实现一个缓存模拟器

至于缓存的具体细节这里我就不讲了,具体的内容基本参见CSAPP-3e第六章的6.3,6.4节

讲义上首先给我们提供了一个程序示例

linux> valgrind --log-fd=1 --tool=lackey -v --trace-mem=yes ls -l

执行,我们可以看到如下面这样的输出:

I  04ead900,3
I  04ead903,3
I  04ead906,5
I  04ead838,3
I  04ead83b,3
I  04ead83e,5
 L 1ffefff968,8
I  04ead843,3
I  04ead846,3
I  04ead849,5
 L 1ffefff960,8
I  04ead84e,3
I  04ead851,3
......

这样的trace文件中记载着每一次对内存的操作,前面的字母代表操作类型,统一的格式是:

[空格][操作类型][空格][内存地址][逗号][大小]

其中如果第一个不是空格而是I,则代表加载,没有实际意义。

操作类型有以下三种:

  • L:读取,从内存中读取
  • S:存储,向内存中存储
  • M:修改,这涉及一次读取,一次存储操作

然后实验给我们提供了一个程序csim-ref,我们要做的就是写出一个和它功能一样的程序。

这里首先分析一下应该怎么去入手:

  1. 首先要获取命令行参数,这里使用getopt()函数即可
  2. 要设计好Cache的结构,我们这里使用二维数组来表示Cache,其中数组的元素是一个如下的结构体。
    • typedef struct {
      unsigned long int valid;
      unsigned long int tag;
      } cache_line;
      

      这里只有Cache结构里面的两个字段,是因为我们现在只需要考虑hit,miss,eviction的次数,所以不需要考虑后面的block部分

  3. 获得index和tag,地址最后的b位是块偏移,往前s位是Cache的组的index,剩下最前面的部分是tag,Cache首先根据index确定是哪个组,然后在对应的组里寻找对应tag的部分是否存在。
  4. 实验中要求的替换策略是LRU,即替换掉最老的哪个,我在一开始设置了一个时间变量,给每个Cache里面的元素加入一个时间戳,但是在后面仔细分析我的代码后发现,Cache的每个组里按照地址顺序存储的就是由先至后加入Cache的内容,于是可以去掉这个时间戳,改为按照地址的顺序去存储新加入的内容,在hit之后马上把hit的内容移动到Cache最后,发生eviction之后直接删掉组里第一个元素,后面的全部向前移动即可。

其他的就没什么好说的了,具体看我的代码实现吧。我个人感觉我自己卡的地方就在LRU上,开始自己多加的时间戳并没有什么卵用。

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

// Author: Yue Pan
// Replacement Policy: LRU

enum {
    TYPE_MODIFY,
    TYPE_LOAD,
    TYPE_SAVE
};

enum {
    HIT,
    MISS,
    EVICTION
};

typedef struct {
    int visible;
    int set_num;
    int entry;
} cache_size;

typedef struct {
    unsigned long int valid;
    unsigned long int tag;
} cache_line;

typedef struct {
    char op;
    unsigned long int addr;
    unsigned long int index;
    unsigned long int tag;
    unsigned int mem_size;
} operation;

typedef struct {
    int hits;
    int misses;
    int evictions;
} summary;

void Caching(cache_line *cache, operation an_op, cache_size c_size, summary *my_summary) {

    int index;
    int type;
    int result;
    unsigned long int temp_rag;

    // get type
    switch (an_op.op) {
        case 'M':
            type = TYPE_MODIFY;
            break;
        case 'L':
            type = TYPE_LOAD;
            break;
        case 'S':
            type = TYPE_SAVE;
            break;
    }
    cache_line *pcache = cache + an_op.index * c_size.entry;

    // get result type
    result = EVICTION;
    for (index = 0; index < c_size.entry; index++) {
        if ((pcache + index)->valid == 0) {
            result = MISS;
            break;
        }
        if ((pcache + index)->tag == an_op.tag) {
            result = HIT;
            break;
        }
    }

    // LRU policy
    switch (result) {
        // if HIT, just refrash the time
        case HIT:
            temp_rag = (pcache + index)->tag;
            for ( ; index < c_size.entry - 1 && (pcache + index + 1)->valid == 1; index++) {
                (pcache + index)->tag = (pcache + index + 1)->tag;
            }
            (pcache + index)->tag = temp_rag;
            my_summary->hits++;
            break;
        // if MISS, add it to the cache
        case MISS:
            my_summary->misses++;
            (pcache + index)->valid = 1;
            (pcache + index)->tag = an_op.tag;
            break;
        // if EVICTION, delete the line who have minimum time and add it to the cache
        case EVICTION:
            my_summary->misses++;
            my_summary->evictions++;
            for (index = 0; index < c_size.entry - 1; index++) {
                (pcache + index)->tag = (pcache + index + 1)->tag;
            }
            (pcache + index)->tag = an_op.tag;
            break;
    }

    // the write in type modify is sure to hit
    if (type == TYPE_MODIFY)
        my_summary->hits++;
        
    // print type ( Only visible = 1 )
    if (c_size.visible == 1) {
        switch (type) {
            case TYPE_MODIFY:
                printf("M ");
                break;
            case TYPE_LOAD:
                printf("L ");
                break;
            case TYPE_SAVE:
                printf("S ");
                break;
        }
        printf("%lx,%d ", an_op.addr, an_op.mem_size);
        switch (result) {
            case HIT:
                printf("hit");
                break;
            case MISS:
                printf("miss");
                break;
            case EVICTION:
                printf("miss eviction");
                break;
        }
        if (type == TYPE_MODIFY)
            printf(" hit");
        printf("\n");
    }
}


void print_help_info(void) {
    // Print the help inforation of this program
    printf("Usage: ./csim [-hv] -s <num> -E <num> -b <num> -t <file>\n");
    printf("Options:\n");
    printf("  -h         Print this help message.\n");
    printf("  -v         Optional verbose flag.\n");
    printf("  -s <num>   Number of set index bits.\n");
    printf("  -E <num>   Number of lines per set.\n");
    printf("  -b <num>   Number of block offset bits.\n");
    printf("  -t <file>  Trace file.\n");
    printf("\n");
    printf("Examples:\n");
    printf("  linux>  ./csim -s 4 -E 1 -b 4 -t traces/yi.trace\n");
    printf("  linux>  ./csim -v -s 8 -E 2 -b 4 -t traces/yi.trace\n");
}

int main(int argc, char **argv) {

    // get options
    char opt;
    int help = 0, visible = 0, set_power = 0, set_num = 0, entry = 0, block_power = 0, trace = 0;
    char *filepath;
    while((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
        switch (opt) {
            case 'h':
                help = 1;
                break;
            case 'v':
                visible = 1;
                break;
            case 's':
                set_power = atoi(optarg);
                set_num = 1 << set_power;
                break;
            case 'E':
                entry = atoi(optarg);
                break;
            case 'b':
                block_power = atoi(optarg);
                break;
            case 't':
                trace = 1;
                filepath = optarg;
                break;
            default:
                help = 1;
                break;
        }
    }

    // Print help info
    if (help == 1) {
        print_help_info();
        exit(0);
    }
    if (set_power == 0 || entry == 0 || block_power == 0 || trace == 0) {
        printf("Arguments Error!\n\n");
        print_help_info();
        exit(1);
    }

    // Build cache
    summary my_summary;
    cache_size c_size;
    int index;
    my_summary.hits = my_summary.misses = my_summary.evictions = 0;
    c_size.visible = visible, c_size.set_num = set_num, c_size.entry = entry;
    cache_line *cache = (cache_line *)malloc(sizeof(cache_line) * set_num * entry);
    if (cache == NULL) {
        printf("Memory Error!\n");
        exit(1);
    }
    for (index = 0; index < set_num * entry; index++) {
        (cache + index)->valid = 0;
        (cache + index)->tag = 0xffffffff;
    }

    // Read file
    FILE *trace_file = fopen(filepath, "r");
    operation an_op;
    char line[80];
    char *pline = NULL;
    while (fgets(line, 80, trace_file) != NULL) {
        pline = line;
        if (*pline++ == 'I')
            continue;
        sscanf(pline, "%c %lx,%u", &an_op.op, &an_op.addr, &an_op.mem_size);
        an_op.index = (an_op.addr >> block_power) & ~(~0 << set_power);
        an_op.tag = an_op.addr >> (block_power + set_power);
        Caching(cache, an_op, c_size, &my_summary);
    }
    free(cache);

    // Print answer
    printSummary(my_summary.hits, my_summary.misses, my_summary.evictions);
    return 0;
}

测试的结果如下:

                        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:Optimizing Matrix Transpose 优化矩阵转置

第二部分要求我们优化对不同规格的矩阵的转置操作。 这里有三个矩阵,他们的要求分别如下:

  • 32 * 32: 8 points if m < 300, 0 points if m > 600
  • 64 × 64: 8 points if m < 1300, 0 points if m > 2000
  • 61 × 67: 10 points if m < 2000, 0 points if m > 3000

然后实验给出的Cache的参数如下:

  • 是一个直接映射高速缓存,等于说 E=1
  • 32字节的Block size,等于说 b=5
  • 有32个set,等于说 s=5

一个int是4个字节,缓存一共有32 * 32个字节,等于说可以装下256个整数。

此外,题目还要求我们使用不超过12个变量。

32 * 32

对于32 * 32的矩阵来说,一次可以装下8行的值,于是我们尝试用8 * 8的分块来处理,增加缓存命中。

测试的结果如下:

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

轻易的就达到了题目要求。【事实上,在后面的题目中我们可以知道这里是可以更进一步的优化的,不过我们已经满分了】

我的代码如下:

if (M == 32 && N == 32) {
    int i, j, k, l, t1, t2, t3, t4, t5, t6, t7, t8;
    for (i = 0; i < N; i += 8) {
        for (j = 0; j < M; j += 8) {
            for (k = i; k < i + 8; k++) {
                for (l = j; l < j + 8; l += 8) {
                    t1 = A[k][l];
                    t2 = A[k][l + 1];
                    t3 = A[k][l + 2];
                    t4 = A[k][l + 3];
                    t5 = A[k][l + 4];
                    t6 = A[k][l + 5];
                    t7 = A[k][l + 6];
                    t8 = A[k][l + 7];
                    B[l][k] = t1;
                    B[l + 1][k] = t2;
                    B[l + 2][k] = t3;
                    B[l + 3][k] = t4;
                    B[l + 4][k] = t5;
                    B[l + 5][k] = t6;
                    B[l + 6][k] = t7;
                    B[l + 7][k] = t8;
                }
            }
        }
    }
}

64 *64

这里和之前相比,区别就是现在我们的缓存只能存储下四行了,无法使用8*8分块进行优化。

那么我们改一下,使用4*4分块试试?

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:6498, misses:1699, evictions:1667

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=1699

TEST_TRANS_RESULTS=1:1699

miss:1699次,距离满分还差得远。那么问题出现在哪里?很显然4*4的分块是没有很好的利用缓存的,造成了87.5%的缓存浪费。而题目要求我们使用不超过12个变量,我们在遍历的过程中需要使用4个变量,那么还有8个变量可以供我们使用,此外在矩阵B中也有相当大的空位够我们拿来存储,于是我自己又写了一个版本。

int i, j, k, l, t1, t2, t3, t4, t5, t6, t7, t8;
        for (i = 0; i < M; i += 8) {
            for (j = 0; j < N; j += 8) {

                for (k = i; k < i + 4; k++) {
                    for (l = j; l < j + 4; l += 4) {
                        t1 = A[k][l];
                        t2 = A[k][l + 1];
                        t3 = A[k][l + 2];
                        t4 = A[k][l + 3];
                        B[l][k] = t1;
                        B[l + 1][k] = t2;
                        B[l + 2][k] = t3;
                        B[l + 3][k] = t4;
                    }
                }

                t1 = A[i][j + 4];
                t2 = A[i][j + 5];
                t3 = A[i][j + 6];
                t4 = A[i][j + 7];

                t5 = A[i + 1][j + 4];
                t6 = A[i + 1][j + 5];
                t7 = A[i + 1][j + 6];
                t8 = A[i + 1][j + 7];
 
                for (k = i + 4; k < i + 6; k++) {
                    for (l = j + 2; l < j + 4; l++) {
                        B[l][k] = A[k][l];                        
                    }
                }

                for (k = i + 2; k < i + 4; k++) {
                    for (l = j + 4; l < j + 6; l++) {
                        B[l][k] = A[k][l];
                    }
                }

                B[j + 4][i + 4] = A[i + 2][j + 6];
                B[j + 4][i + 5] = A[i + 2][j + 7];
                B[j + 5][i + 4] = A[i + 3][j + 6];
                B[j + 5][i + 5] = A[i + 3][j + 7];

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

                B[j + 4][i + 1] = t5;
                B[j + 5][i + 1] = t6;
                B[j + 6][i + 1] = t7;
                B[j + 7][i + 1] = t8;

                B[j + 6][i + 2] = B[j + 4][i + 4];
                B[j + 7][i + 2] = B[j + 4][i + 5];
                B[j + 6][i + 3] = B[j + 5][i + 4];
                B[j + 7][i + 3] = B[j + 5][i + 5];

                t1 = A[i + 4][j];
                t2 = A[i + 5][j];
                t3 = A[i + 6][j];
                t4 = A[i + 7][j];

                t5 = A[i + 4][j + 1];
                t6 = A[i + 5][j + 1];
                t7 = A[i + 6][j + 1];
                t8 = A[i + 7][j + 1];

                B[j + 4][i + 4] = A[i + 6][j + 2];
                B[j + 4][i + 5] = A[i + 7][j + 2];
                B[j + 5][i + 4] = A[i + 6][j + 3];
                B[j + 5][i + 5] = A[i + 7][j + 3];

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

                B[j + 1][i + 4] = t5;
                B[j + 1][i + 5] = t6;
                B[j + 1][i + 6] = t7;
                B[j + 1][i + 7] = t8;

                B[j + 2][i + 6] = B[j + 4][i + 4];
                B[j + 2][i + 7] = B[j + 4][i + 5];
                B[j + 3][i + 6] = B[j + 5][i + 4];
                B[j + 3][i + 7] = B[j + 5][i + 5];

                for (k = i + 4; k < i + 8; k++) {
                    for (l = j + 4; l < j + 8; l += 4) {
                        t1 = A[k][l];
                        t2 = A[k][l + 1];
                        t3 = A[k][l + 2];
                        t4 = A[k][l + 3];
                        B[l][k] = t1;
                        B[l + 1][k] = t2;
                        B[l + 2][k] = t3;
                        B[l + 3][k] = t4;
                    }
                }

            }
        }

但是跑出来的结果却令我非常惊讶,miss是1691,几乎没有什么改进。再一想,我缓存的位置不对,对于A和B两个矩阵应该分上下两个部分来处理,因为一次Cache可以刚好把矩阵的上一半或是下一半放进去。但是在尝试后,我的miss仍然大于1300。

最后在网上看到了一个思路,打开了我的想法,在经过一番改进之后,跑出了更好的1171次。

想法首先没错,我们首先对A和B的上半部分操作,这样除了第一次以外缓存全部命中。我们把B的上半部分填满,不能浪费。

  1. 首先B的左上角当然可以正确转置。
  2. 而B的右上角也填上A的右上角中的数。这里放上去的也是转置过的,我们最后只需要把这部分平移到B的左下角就好。
  3. 整个过程按照A和B的行来遍历操作。

现在,B的左上已经Ok,而右上则需要平移到左下。接着,我们处理右上和左下。

  1. 首先用四个变量存储A的左下角的一列。
  2. 再用四个变量存储B的右上角的一行。
  3. 把四个变量存储的A的左下角的一列移动到B右上角的一行
  4. 把四个变量存储的B的右上角的一行平移到B的右下角的一行

经过上面的操作,矩阵B的左下,右上下,左上就都完成了。

最后,我们直接对右下角转置即可。

这样跑出来,我们的结果是miss:1179,已经满分了。

但是后来看到一个解答,把最后对右下角的转置移动到了第二大步骤的最后,更加减少了miss,原因是第二大步骤本来就是在对下半部分操作,顺带处理这一行当然不会出现miss,就放在里面按列处理了。

最终的代码如下:

int i, j, k, l, t1, t2, t3, t4, t5, t6, t7, t8;
    for (i = 0; i < N; i += 8) {
        for (j = 0; j < M; j += 8) {
            for (k = i; k < i + 4; k++) {
                t1 = A[k][j];
                t2 = A[k][j + 1];
                t3 = A[k][j + 2];
                t4 = A[k][j + 3];
                t5 = A[k][j + 4];
                t6 = A[k][j + 5];
                t7 = A[k][j + 6];
                t8 = A[k][j + 7];

                B[j][k] = t1;
                B[j + 1][k] = t2;
                B[j + 2][k] = t3;
                B[j + 3][k] = t4;
                B[j][k + 4] = t5;
                B[j + 1][k + 4] = t6;
                B[j + 2][k + 4] = t7;
                B[j + 3][k + 4] = t8;
            }
            for (l = j + 4; l < j + 8; l++) {

                t5 = A[i + 4][l - 4];
                t6 = A[i + 5][l - 4];
                t7 = A[i + 6][l - 4];
                t8 = A[i + 7][l - 4];

                t1 = B[l - 4][i + 4];
                t2 = B[l - 4][i + 5];
                t3 = B[l - 4][i + 6];
                t4 = B[l - 4][i + 7];

                B[l - 4][i + 4] = t5;
                B[l - 4][i + 5] = t6;
                B[l - 4][i + 6] = t7;
                B[l - 4][i + 7] = t8;

                B[l][i] = t1;
                B[l][i + 1] = t2;
                B[l][i + 2] = t3;
                B[l][i + 3] = t4;

                B[l][i + 4] = A[i + 4][l];
                B[l][i + 5] = A[i + 5][l];
                B[l][i + 6] = A[i + 6][l];
                B[l][i + 7] = A[i + 7][l];
            }
        }
    }

最后的跑分结果如下:

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:9074, misses:1171, evictions:1139

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=1171

TEST_TRANS_RESULTS=1:1171

61 * 67

第三部分就很松了,只要miss在2000以内就行,我们直接简单分块操作。这里我设置了一个size表示分块的大小,我从4一直测试到30,最后发现23的时候miss是最小的。

【事实上,这里随便利用多的变量搞点循环展开还能优化很多,我就不再搞了】

我的代码如下:

else if (M == 61 && N == 67) {
    int i, j, k, l, t;
    int size = 23;
    for (i = 0; i < N; i += size) {
        for (j = 0;  j < M; j += size) {
            for (k = i; k < i + size && k < N; k++) {
                for (l = j; l < j + size && l < M; l++) {
                    t = A[k][l];
                    B[l][k] = t;
                }
            }
        }
    }
}

测试的结果如下:

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:6251, misses:1928, evictions:1896

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:3756, misses:4423, evictions:4391

Summary for official submission (func 0): correctness=1 misses=1928

TEST_TRANS_RESULTS=1:1928

总结

Cache Lab整体有难度,但又确实有好方法,是一个非常好的实验。第一部分要求你透彻理解Cache的原理,否则bug一堆还要查资料到底是哪里的问题。(当时扫了一下书就来做真是踩了大坑,于是回去认真读)第二部分的64*64很有趣,也很不好想,利用B本身来作为缓存的这个想法是非常好的。完成整个Lab之后我才觉得对于高速缓存算是过关了,彻底理解了很多东西。

最后跑分如下:

Part A: Testing cache simulator
Running ./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


Part B: Testing transpose function
Running ./test-trans -M 32 -N 32
Running ./test-trans -M 64 -N 64
Running ./test-trans -M 61 -N 67

Cache Lab summary:
                        Points   Max pts      Misses
Csim correctness          27.0        27
Trans perf 32x32           8.0         8         287
Trans perf 64x64           8.0         8        1171
Trans perf 61x67          10.0        10        1928
          Total points    53.0        53

满分,很舒服。

下一章节开始向系统的上一层进发了,Shell Lab和Malloc Lab都是建立在操作系统之上的。