Eagle233-Blog

[CSAPP] Lab4 - Cache Lab Report


Categories 课内 CSAPP
Tags

3.6k Words   |   18 Minutes

csim.c

实验思想:采用封装的思想,提高函数的复用性,有助于理解总体代码。

前置函数:定义在cachelab.h:

/* 
 * printSummary - This function provides a standard way for your cache
 * simulator * to display its final hit and miss statistics
 */ 
void printSummary(int hits,  /* number of  hits */
                  int misses, /* number of misses */
                  int evictions); /* number of evictions */

首先我们先构思出建立一个缓存需要哪些部分。这包括:

  • 缓存的基本数据结构
  • 从命令行获得缓存s E b和读写的t信息
  • 初始化缓存
  • 获取文件读写信息
  • 访问缓存
  • 增加LRU次数
  • 释放Cache

于是我们先搭建总体框架。其中函数的返回值和参数暂时不完善,我们之后按需求修改。

#include "cachelab.h"

uint64_t hits = 0;
uint64_t misses = 0;
uint64_t evictions = 0;

typedef struct { 
    int valid;
    uint64_t tag;
    uint64_t lru;
} Line;

typedef struct {
    Line *lines;
} Set;

typedef struct {
    Set *sets;
    uint64_t s;
    uint64_t E;
    uint64_t b;
} Cache;

void getCommandInfo(uint64_t s, uint64_t E, uint64_t b) {

}

Cache initCache() {

}

void accessCache(Cache *cache, uint64_t address) {

}

void updateLRU(Cache *cache) {

}

void getFileInfo(char *tracefile) {

}

void freeCache(Cache *cache) {

}

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

    printSummary(hits, misses, evictions);
    
}

接下来我们逐个分析。

getCommandInfo

这个函数的目的是为了读取命令行输入的指令,获得缓存s E b和读写的t信息。

Usage: ./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile>

我们可以使用getopt。这个头文件一般只在Linux下使用。getopt.h必须要包括,否则会报错,原因未知。

#include <unistd.h>
#include <getopt.h>
int getopt(int argc, char * const argv[], const char *optstring);

特殊变量:

  • optarg:当前选项的参数值(如 -b value 中的 value)
  • optind:下一个要处理的 argv 索引
  • opterr:设为 0 可禁止错误信息输出

optstring规则:

  • 字符:表示允许的选项(如 “ab:c”)
    • ‘a’:无参数
    • ‘b:’:需要参数
    • ‘c’:无参数

这个函数很明显比较适合使用在main函数中,所以我们把getCommandInfo的功能放在main函数中。

int main(int argc, char *argv[]) {
    uint64_t s;
    uint64_t E;
    uint64_t b;
    char tracefile[fileNameLength];

    char opt;
    while ((opt = getopt(argc, argv, "s:E:b:t:")) != -1) {
        switch (opt) {
            case 's':
                s = atoi(optarg);
                break;
            case 'E':
                E = atoi(optarg);
                break;
            case 'b':
                b = atoi(optarg);
                break;
            case 't':
                strcpy(tracefile, optarg);
                break;
        }
    }

    printSummary(hits, misses, evictions);
    
}

这样我们就完成了获取命令行指令的函数。

initCache

我们已经获得了s E b信息,我们可以开始初始化缓存了。我们需要给cache的s E b赋值,并且开辟内存空间,而且需要使每一路的valid tag lru都设置为0。

Cache initCache(uint64_t s, uint64_t E, uint64_t b) {
    int S = 1 << s;

    Cache cache;
    cache.s = s;
    cache.E = E;
    cache.b = b;

    cache.sets = (Set *)malloc(S * sizeof(Set));
    for (int i = 0; i < S; i++) {
        cache.sets[i].lines = (Line *)malloc(E * sizeof(Line));
        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 = 0;
        }
    }

    return cache;
}

在main函数中新增:

Cache cache;
cache = initCache(s, E, b);

getFileInfo

<操作类型> <地址>,<大小>
L 10,4 
S 18,4
L 20,4

我们只需要处理L(load) S(store) M(modify load/store),不需要理会L。其中M进行了两次的内存访问。

为了安全地从文件中读取uint64_t,可以如下操作:

#include <inttypes.h>
fscanf(fp, " %c %" SCNx64 ",&d", &opt, &address, %size) == 3

完整的函数如下:

void getFileInfo(char *tracefile, Cache *cache) {
    FILE *fp = fopen(tracefile, "r");
    if (fp == NULL) {
        return;
    }

    char opt;
    uint64_t address;
    int size;

    while (fscanf(fp, " %c %" SCNx64 ",%d", &opt, &address, &size) == 3) {
        switch (opt) {
            case 'L':
                accessCache(cache, address);
                break;
            case 'S':
                accessCache(cache, address);
                break;
            case 'M':
                accessCache(cache, address);
                accessCache(cache, address);
                break;
        }
        updateLRU(cache);
    }
}

在main函数加上:

getFileInfo(tracefile, &cache);

接下来我们需要完成accessCache和updateLRU,后者较为简单,我们从易到难。

updateLRU

实现很简单,每次进行了一次操作都需要更新valid位置不为0的LRU,以便于我们找到使用最不频繁的路进行替换。

void updateLRU(Cache *cache) {
    int S = 1 << cache->s;
    int E = cache->E;

    for (int i = 0; i < S; i++) {
        for (int j = 0; j < E; j++) {
            if (cache->sets[i].lines[j].valid) {
                cache->sets[i].lines[j].lru++;
            }
        }
    }
}

accessCache

这个函数比较复杂。首先我们需要处理传入的地址,区分出tag index block_offset,只不过对于这个实验block_offset可以不考虑。之后访问相对应的缓存路,分为三类情况:

  • 命中了:比对tag并且确认valid不为0。再把缓存的lru设置为0,表示其刚刚被访问了,hits++。
  • 未命中且有空的路:加载到空的路中,tag更新,valid = 1,misses++。
  • 未命中且没有空的路:加载到lru最大的路中,tag更新,valid = 1,lru = 0,evictions++,misses++。
void accessCache(Cache *cache, uint64_t address) {
    uint64_t s = cache->s;
    uint64_t b = cache->b;

    uint64_t tag = address >> (s + b);

    uint64_t mask = UINT64_MAX;
    mask >>= 64 - (s + b);
    uint64_t temp = address & mask;

    uint64_t index = temp >> b;

    int isEmpty = 0;
    for (int i = 0; i < cache->E; i++) {
        if (cache->sets[index].lines[i].valid && cache->sets[index].lines[i].tag == tag) {
            cache->sets[index].lines[i].lru = 0;
            hits++;
            return;
        }

        if (cache->sets[index].lines[i].valid == 0) {
            isEmpty = 1;
        }
    }

    if (isEmpty) {
        for (int i = 0; i < cache->E; i++) {
            if (cache->sets[index].lines[i].valid == 0) {
                cache->sets[index].lines[i].valid = 1;
                cache->sets[index].lines[i].tag = tag;
                misses++;
                return;
            }
        }
    } else {
        uint64_t MaxLRU = 0;
        for (int i = 0; i < cache->E; i++) {
            MaxLRU = cache->sets[index].lines[i].lru > MaxLRU ? cache->sets[index].lines[i].lru : MaxLRU;
        }

        for (int i = 0; i < cache->E; i++) {
            if (MaxLRU == cache->sets[index].lines[i].lru) {
                cache->sets[index].lines[i].lru = 0;
                cache->sets[index].lines[i].tag = tag;
                misses++;
                evictions++;
                return;
            }
        }
    }
}

freeCache

释放内存即可,逐层释放。

void freeCache(Cache *cache) {
    int S = 1 << cache->s;
    for (int i = 0; i < S; i++) {
        free(cache->sets[i].lines);
    }
    free(cache->sets);
}

在main函数加上:

freeCache(&cache);

完整代码

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

#define fileNameLength 1000

uint64_t hits = 0;
uint64_t misses = 0;
uint64_t evictions = 0;

typedef struct { 
    int valid;
    uint64_t tag;
    uint64_t lru;
} Line;

typedef struct {
    Line *lines;
} Set;

typedef struct {
    Set *sets;
    uint64_t s;
    uint64_t E;
    uint64_t b;
} Cache;

Cache initCache(uint64_t s, uint64_t E, uint64_t b) {
    int S = 1 << s;

    Cache cache;
    cache.s = s;
    cache.E = E;
    cache.b = b;

    cache.sets = (Set *)malloc(S * sizeof(Set));
    for (int i = 0; i < S; i++) {
        cache.sets[i].lines = (Line *)malloc(E * sizeof(Line));
        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 = 0;
        }
    }

    return cache;
}

void accessCache(Cache *cache, uint64_t address) {
    uint64_t s = cache->s;
    uint64_t b = cache->b;

    uint64_t tag = address >> (s + b);

    uint64_t mask = UINT64_MAX;
    mask >>= 64 - (s + b);
    uint64_t temp = address & mask;

    uint64_t index = temp >> b;

    int isEmpty = 0;
    for (int i = 0; i < cache->E; i++) {
        if (cache->sets[index].lines[i].valid && cache->sets[index].lines[i].tag == tag) {
            cache->sets[index].lines[i].lru = 0;
            hits++;
            return;
        }

        if (cache->sets[index].lines[i].valid == 0) {
            isEmpty = 1;
        }
    }

    if (isEmpty) {
        for (int i = 0; i < cache->E; i++) {
            if (cache->sets[index].lines[i].valid == 0) {
                cache->sets[index].lines[i].valid = 1;
                cache->sets[index].lines[i].tag = tag;
                misses++;
                return;
            }
        }
    } else {
        uint64_t MaxLRU = 0;
        for (int i = 0; i < cache->E; i++) {
            MaxLRU = cache->sets[index].lines[i].lru > MaxLRU ? cache->sets[index].lines[i].lru : MaxLRU;
        }

        for (int i = 0; i < cache->E; i++) {
            if (MaxLRU == cache->sets[index].lines[i].lru) {
                cache->sets[index].lines[i].lru = 0;
                cache->sets[index].lines[i].tag = tag;
                misses++;
                evictions++;
                return;
            }
        }
    }
}

void updateLRU(Cache *cache) {
    int S = 1 << cache->s;
    int E = cache->E;

    for (int i = 0; i < S; i++) {
        for (int j = 0; j < E; j++) {
            if (cache->sets[i].lines[j].valid) {
                cache->sets[i].lines[j].lru++;
            }
        }
    }
}

void getFileInfo(char *tracefile, Cache *cache) {
    FILE *fp = fopen(tracefile, "r");
    if (fp == NULL) {
        return;
    }

    char opt;
    uint64_t address;
    int size;

    while (fscanf(fp, " %c %" SCNx64 ",%d", &opt, &address, &size) == 3) {
        switch (opt) {
            case 'L':
                accessCache(cache, address);
                break;
            case 'S':
                accessCache(cache, address);
                break;
            case 'M':
                accessCache(cache, address);
                accessCache(cache, address);
                break;
        }
        updateLRU(cache);
    }
}

void freeCache(Cache *cache) {
    int S = 1 << cache->s;
    for (int i = 0; i < S; i++) {
        free(cache->sets[i].lines);
    }
    free(cache->sets);
}

int main(int argc, char *argv[]) {
    uint64_t s;
    uint64_t E;
    uint64_t b;
    char tracefile[fileNameLength];

    char opt;
    while ((opt = getopt(argc, argv, "s:E:b:t:")) != -1) {
        switch (opt) {
            case 's':
                s = atoi(optarg);
                break;
            case 'E':
                E = atoi(optarg);
                break;
            case 'b':
                b = atoi(optarg);
                break;
            case 't':
                strcpy(tracefile, optarg);
                break;
        }
    }

    Cache cache;
    cache = initCache(s, E, b);

    getFileInfo(tracefile, &cache);

    freeCache(&cache);

    printSummary(hits, misses, evictions);
}

跑分截图

csim.c.png


trans.c

实验思路:先分块后优化

根据实验指导书,我们知道:s = 5, E = 1, b = 5,我们重点观察b = 5,因为这意味着我们的缓存一个组(全相联)最多可以缓存32bits == 4bytes,刚好是8个int。

我们在tracegen.c中发现:

/* Markers used to bound trace regions of interest */
volatile char MARKER_START, MARKER_END;

static int A[256][256];
static int B[256][256];
static int M;
static int N;

矩阵定义的大小是256*256 == 65536个int,刚好是缓存大小的整数倍。这意味着我们原始的转置函数:

/* 
 * trans - A simple baseline transpose function, not optimized for the cache.
 */
char trans_desc[] = "Simple row-wise scan transpose";
void trans(int M, int N, int A[N][M], int B[M][N])
{
    int i, j, tmp;

    for (i = 0; i < N; i++) {
        for (j = 0; j < M; j++) {
            tmp = A[i][j];
            B[j][i] = tmp;
        }
    }    

}

会使得矩阵A B重复使用同一块缓存的同一块区域,造成抖动极大。我们要尽可能减少冲突不命中和容量不命中,冷不命中是无法避免的。

我们的缓存共有32组,每组可以缓存8个整型,接下来分别分析题目要求的三种情况。

32 × 32

这个矩阵的每一行都需要4组(32/8)缓存,缓存一共可以容纳8行。我们可以使用8 × 8分块的方式来转置。原因如下:

  • 空间局部性好:每次访问都刚好读取八个元素进入缓存。

  • 避免冲突未命中:由于每个分块距离都较远,不容易发生冲突未命中。

可以写出如下的代码:

char transpose_submit_desc[] = "Transpose submission";
void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
    if (M == 32) {
        for (int k = 0; k < M; k += 8) {
            for (int l = 0; l < M; l += 8) {
                for (int i = k; i < k + 8; i++) {
                    for (int j = l; j < l + 8; j++) {
                        B[j][i] = A[i][j];
                    }
                }
            }
        }
    }
}

得到如下结果:

./test-trans -M 32 -N 32

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:1709, misses:344, evictions:312

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:869, misses:1184, evictions:1152

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

TEST_TRANS_RESULTS=1:344

离答案要求的300并不遥远。我们想到实验允许我们使用12个局部变量,因此可以进行优化。我们注意到,当我们的分块矩阵在对角线上的时候,时常会发生冲突不命中。我们可以用局部变量保存冲突的元素,保证对角线上的分块矩阵不容易冲突。由于我们在循环之中已经用了4个变量,因此我们还能使用8个变量进行优化。

这样处理之后,由于我们预先访问了分块矩阵一行的元素,这里只会开销一个miss。之后每一行访问矩阵B,各自出现8次misses,这样就会有(1 + 8) * 8 = 72次misses,相比于之前的一定是大幅减小,因为之前的方法要交替访问A B,misses的次数要多得多。

char transpose_submit_desc[] = "Transpose submission";
void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
    int i, j;
    if (M == 32) {
        for (int k = 0; k < M; k += 8) {
            for (int l = 0; l < M; l += 8) {
                if (k == l) {
                    for (i = k; i < k + 8; i++) {
                        int temp1 = A[i][l];
                        int temp2 = A[i][l + 1];
                        int temp3 = A[i][l + 2];
                        int temp4 = A[i][l + 3];
                        int temp5 = A[i][l + 4];
                        int temp6 = A[i][l + 5];
                        int temp7 = A[i][l + 6];
                        int temp8 = A[i][l + 7];

                        B[l][i] = temp1;
                        B[l + 1][i] = temp2;
                        B[l + 2][i] = temp3;
                        B[l + 3][i] = temp4;
                        B[l + 4][i] = temp5;
                        B[l + 5][i] = temp6;
                        B[l + 6][i] = temp7;
                        B[l + 7][i] = temp8;
                    }
                } else {
                    for (i = k; i < k + 8; i++) {
                        for (j = l; j < l + 8; j++) {
                            B[j][i] = A[i][j];
                        }
                    }
                }
            }
        }
    }
}
./test-trans -M 32 -N 32

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:1765, misses:288, evictions:256

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:869, misses:1184, evictions:1152

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

TEST_TRANS_RESULTS=1:288

64 × 64

// TODO

61 × 67

这一题对miss的要求很低,因此我们可以大胆分一个比较大的块。经过测试,选取 16 × 16。

要注意矩阵A是N行M列,不要搞混行列。

if (M == 61) {
    for (int i = 0; i < M; i += 16) {
        for (int j = 0; j < N; j += 16) {
            for (int k = i; k < i + 16 && k < M; k++) {
                for (int l = j; l < j + 16 && l < N; l++) {
                    B[l][k] = A[k][l];
                }
            }
        }
    }
}
eagle233@Eagle233-X16:~/cachelab-handout$ ./test-trans -M 61 -N 67

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:6186, misses:1993, evictions:1961

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:3755, misses:4424, evictions:4392

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

TEST_TRANS_RESULTS=1:1993

虽然有点含糊,但是通过要求还是很简单的。

完整代码

void transpose_submit(int M, int N, int A[N][M], int B[M][N]) {
    if (M == 32) {
        int i, j, k, l;
        int temp1, temp2, temp3, temp4, temp5, temp6, temp7, temp8;
        for (k = 0; k < M; k += 8) {
            for (l = 0; l < M; l += 8) {
                if (k == l) {
                    for (i = k; i < k + 8; i++) {
                        temp1 = A[i][l];
                        temp2 = A[i][l + 1];
                        temp3 = A[i][l + 2];
                        temp4 = A[i][l + 3];
                        temp5 = A[i][l + 4];
                        temp6 = A[i][l + 5];
                        temp7 = A[i][l + 6];
                        temp8 = A[i][l + 7];

                        B[l][i] = temp1;
                        B[l + 1][i] = temp2;
                        B[l + 2][i] = temp3;
                        B[l + 3][i] = temp4;
                        B[l + 4][i] = temp5;
                        B[l + 5][i] = temp6;
                        B[l + 6][i] = temp7;
                        B[l + 7][i] = temp8;
                    }
                } else {
                    for (i = k; i < k + 8; i++) {
                        for (j = l; j < l + 8; j++) {
                            B[j][i] = A[i][j];
                        }
                    }
                }
            }
        }
    }

    if (M == 64) {
        // TODO
    }    

    if (M == 61) {
        for (int i = 0; i < N; i += 16) {
            for (int j = 0; j < M; j += 16) {
                for (int k = i; k < i + 16 && k < N; k++) {
                    for (int l = j; l < j + 16 && l < M; l++) {
                        B[l][k] = A[k][l];
                    }
                }
            }
        }
    }
}

两个文件的跑分截图

// TODO



Page views: Loading...  ·  Visitors: Loading...
Except where otherwise noted, original content on this site is dedicated to the public domain under CC0 1.0.
Powered by Hexo & Theme mdsuper
沪ICP备2026040813号
Search