cachelab實驗

計算機系統原理實驗日誌

1、實驗內容linux

一、編寫一個C程序(csim.c,大約200-300行),用於模擬Cache的行爲。算法

二、已提供一個參考的cache模擬器(可執行文件csim-ref),目標是本身寫的ubuntu

csim和參考的csim-ref行爲一致,即二者模擬同一個訪存過程獲得的windows

cache 命中次數(hits)、不命中次數(misses)、替換次數(evicts)都相同。數組

 

2、相關知識緩存

1.高速緩存存儲器結構:數據結構

內存地址劃分以下:函數

   t=m-(s+b)工具

要訪問高速緩存存儲空間中的字節:測試

根據內存地址進行 組選擇,行匹配,字抽取;

存儲層次主要有四個問題

(1)映射規則:

直接映射:E=1

全相聯映射:S=1—>s=0

E-路組相聯映射:

(圖中E=2)

(2)查找算法

(3)替換算法

a. FIFO算法 -- 先進先出,易理解也易實現

b. OPT算法 — 基本不可能實現、通常用LRU算法替代

c. LRU算法

本次實驗要用到的是LRU算法,因此主要介紹LRU—最近最少使用替換策略:

cache的容量有限,當滿的時候須要犧牲行(驅逐某行),先遍歷當前組,判斷它是否容量已滿,若是未滿:將有效位不爲1的一行更新有效位爲1,同時更新標記位和LRU計數值,若是滿了,找出最小LRU所在的行做爲犧牲行。

(4)寫策略:

a.直寫+非寫分配

b.寫回+寫分配(適用於較低層次的緩存)

 

2. Linux下getopt()函數的簡單使用

  因爲本實驗中須要獲取命令行,因此須要用到unistd.h頭文件中的getopt:int getopt(int argc,char * const argv[ ],const char * optstring);

它用來分析命令行參數;

參數argc和argv分別表明參數個數和內容,跟main的命令行參數相同。

參數optstring爲選項字符串,告知getopt()能夠處理哪一個選項以及哪一個選項須要參數,若是選項字符串裏的字母后接着冒號「:」,則表示還有相關的參數,全域變量optarg 即會指向此額外參數;

它能夠返回選項字符(以整型值的形式),返回-1時即表明解析完成。

/*

關於選項字符串的詳細解釋以下:

"a:b:cd::e"即一個選項字符串。它對應到命令行就是-a ,-b ,-c ,-d, -e 。其中 冒號表示參數,一個冒號就表示這個選項後面必須帶有參數,沒有帶參數就會報錯,可是這個參數能夠和選項連在一塊兒寫,也能夠用空格隔開,好比

-a123 和-a 123(中間有空格)都表示123是-a的參數;

兩個冒號的就表示這個選項的參數是可選的,便可以有參數,也能夠沒有參數,但要注意有參數時,參數與選項之間不能有空格(有空格會報錯的哦),這一點和一個冒號時是有區別的,

選項後面爲兩個冒號,且沒有跟參數,則optarg = NULL,

有些選項是不用帶參數的,而這樣不帶參數的選項能夠寫在一塊兒。。

*/

 

3.fopen函數

  由於本次實驗中須要讀取trace文件,因此須要用到fopen函數來打開文件:

FILE * fopen(const char * path, const char * mode);

參數path字符串包含欲打開的文件路徑及文件名;

參數mode字符串則表明着打開文件的方式,包括 ’r’, ’w’, ’a’, ’r+’, ’rb+’ 等等;

文件順利打開後,指向該流的文件指針就會被返回。

若是文件打開失敗則返回NULL,並把錯誤代碼存在errno 中。

調用它的通常形式爲:

文件指針名 = fopen(文件名,使用文件方式);

其中,文件指針名 -- 被說明爲FILE 類型的指針變量;

文件名 -- 被打開文件的文件名,爲字符串常量或字符串數組;

使用文件方式 -- 指文件的類型和操做要求。

 

4.C庫函數 -- fscanf函數

  int fscanf(FILE *stream, const char *format, ...)

  它從流 stream 讀取格式化輸入,

第一個參數stream 是指向 FILE 對象的指針,該 FILE 對象標識了流;

第二個參數format 是字符串,

format 說明符形式爲 [=%[*][width][modifiers]type=]

附加參數---根據不一樣的 format 字符串,函數可能須要一系列的附加參數,每一個參數包含了一個要被插入的值,替換了format參數中指定的每一個 % 標籤。參數的個數應與 % 標籤的個數相同。

若是成功,該函數返回成功匹配和賦值的個數。

若是到達文件末尾或發生讀錯誤,則返回 EOF。

總而言之,它按照你定義的格式讀取輸入並賦值到你對應的附加參數中,而後返回讀取輸入的個數。

 

5.動態分配函數malloc與calloc

  malloc()和calloc()的功能:

在內存的動態存儲區中分配n個長度爲size的連續空間,

函數返回一個指向分配起始地址的指針。

區別:

calloc在動態分配完內存後,自動初始化該內存空間爲零,

而malloc不初始化,裏邊數據是隨機的垃圾數據。

 

3、實驗原理

注意事項:

a.因爲模擬器必須適應不一樣的s, E, b,因此數據結構必須動態申請,

注意初始化。

b.測試數據中以「I」開頭的行是對指令緩存(i-cache)進行讀寫,咱們編寫

的是數據緩存(d-cache),這些行直接忽略。

c.此次實驗假設內存所有對齊,即數據不會跨越block,因此測試數據裏面的

數據大小也能夠忽略。

 

能夠根據老師所給提示逐步分析:

一、如何從命令行中解析s、E、b、t(trace)參數?(getopt函數)

   根據相關知識中所介紹關於getopt函數的知識,咱們能很快的解決這個問題。用法以下:

  

  

在switch語句中具體實現相應選項及參數對應的功能便可。

 

二、如何構建一個cache?

   實驗只要求咱們測試hit/miss/eviction的次數,並無實際的數據存儲,

因此咱們不用實現line中的block部分,只需關注是否命中和替換,不關注

數據從內存加載到cache block的哪一個位置,

因此cache只須要是一個二維數組cache[S][E]的形式(與b無關),

因爲S、E的值是不肯定的,因此咱們能夠考慮採用指針去實現此二維數組:

三、如何從訪存trace文件中讀取每條訪存操做?

   天然是靈活運用相關知識中介紹的fscanf函數啦~

   訪存操做的格式以下:

  

   忽略以i開頭的指令緩存操做及每條訪存操做的訪問字節數:

  

四、如何從訪存操做中的起始地址參數中提取標記位t、組索引s、塊偏移b?

   已給提示是移位。

   咱們不須要知道塊大小,須要塊偏移是爲了求t,t = m-b-s,實際上將地址

做爲二進制串時,只須要將實際地址右移(b+s)便可獲得t的值;

而s,將實際地址右移b位後,低s位即它的值,爲了獲取這低s位,咱們

將1左移s位再減1,可獲得2^s-1,將它與右移b位後的實際地址相與便可

獲得其低s位,以下:

  

 

五、cache模擬器採用哪一種放置策略和替換策略?

   放置策略:組內放哪兒  -- 放置於組內第一個空閒行;

替換策略:LRU,與csim-ref保持一致;

總體思想是爲每一行設置一個計數器,將空閒行的計數器值置爲0,

其他行每使用一次加一,新放置的行的計數器值則設爲其他行的最大值+1,

每次須要替換時就找到組內計數器值最小的(若是值相同,則是按行序最小的)

一行進行替換。

根據以上分析,其實實現Cache模擬的大體設計思路已經出來了,

以後就是考慮具體實現啦~

 

 

 

 

4、實驗步驟

1. 準備過程

(1)把cachelab-handout.tar拷貝到linux下再解壓縮

(不要在windows下解壓)

可直接將壓縮包從windows桌面下拖到ubuntu中,

也可經過郵箱發送在ubuntu中下載;

命令即 tar xvf cachelab-handout.tar

(2)安裝valgrind工具

>sudo su

>apt-get install valgrind

安裝完成後輸入以下命令可查看每次訪存操做(可選):

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

 

至此,工具準備已經完成,具體要怎麼進行實驗,咱們能夠看到壓縮包中有readme,其中 ,因此咱們去查看這兩個文件,發現

而cachelab.c中即這四個函數的實現,

在partA中,咱們只須要printSummary這個打印結果的函數便可。

那麼partA部分其餘的函數就所有自由發揮了。

 

2.具體設計

(1)數據結構的設計

對於每一行,咱們須要一個有效位,一個標記位,一個計數器(用於LRU算法),

因此能夠設計一個結構體:

對於cache的構建,前面已經提到過了,cache可看成二維數組cache[S][E],

採用指針去實現:

而後在解析命令行獲得參數S、E的值後再動態分配並初始化內存,由於

每一行都應該初始化爲0,因此能夠採用calloc函數:

根據S(set的數目)申請一個數組,該數組元素是lines的入口的指針;

接着循環S次,每次申請E個line,讓剛申請的指針數組的元素指向它們。

整體數據結構以下:

內存的分配及初始化以下:

(2)解析命令行的邏輯設計

   經過getopt函數獲取命令行,逐個獲取每一個選項並做出相應操做:

當選項爲h或輸入不符合要求時,咱們中斷並打印詳細命令用法以提示;

當選項爲v時,咱們設置verbose標識爲true,即說明須要輸出每條訪問

操做的詳細信息,在以後分析每條訪存操做的過程當中就能夠經過此標識

去判斷是否輸出對應信息;

   當選項爲s,E,b時,咱們要判斷其輸入參數的值是否在1-64之間(s可爲0),

若是不是,咱們進行中斷並打印詳細命令用法以提示,

若是是,記錄其值(不輸入)。

 

(3)LRU算法的具體實現

   其設計思路在實驗原理的第5個問題中已經說明,就再也不羅嗦了,實現以下:

(4) 判斷每條訪存操做的結果的過程

從訪存地址獲取其所在組,在此組中逐行對比有效位與標誌位,

若是有標誌位相同且有效位爲1的行,則命中,設置hit_flag爲true,

並更新此行的LRU計數器值,hit++;

若是沒有,miss++,尋找LRU值最小的行做爲替換行,更新其LRU計數器值,

若是此行有效位爲1,表明此行不爲空閒行,發生了替換,evication++,

不然要對此空閒行有效位置1;

 

(5)讀取tracefile並統計其中訪存操做獲得的命中、不命中、替換次數

   在實驗原理中已經詳述瞭如何從訪存trace文件中讀取每條訪存操做及

如何從訪存操做中的起始地址參數中提取標記位t、組索引s、塊偏移b,

而且已經實現瞭如何判斷每條具體訪存操做的結果,

還須要考慮的是從tracefile中獲取每條訪存操做後如何實現其相應邏輯,

若是訪存操做爲load/store,只須要進行一次對此訪問操做的判斷便可;

若是訪存操做爲Modify,至關於進行一次load再對此地址進行一次store,

因此會引發兩次訪存操做,須要進行兩次判斷,

其結果能夠爲兩次緩存命中, 或 一次未命中(可能包含替換)、一次命中;

 

(6)打印命令詳細用法

   這個就沒什麼好介紹的了,只是爲了減小代碼重複因此單獨作了一個函數:

(7)釋放內存

    在c中若是進行了內存分配,必然要釋放內存,因此不要忘了free:

   

 

3.操做步驟

  完成csim.c代碼設計與編寫後,便可進行測試

(1)輸入make clean後輸入make命令編譯csim.c,生成可執行文件csim

(2)輸入./csim –v –s <num> –E <num> –b <num> -t <tracesfile>

查看每次訪存cache是否命中和替換的詳細信息(可選)

(3)輸入./test-csim運行cache模擬器的評價程序,查看得分是否爲27分,

 27分(滿分)則csim.c正確,低於27分則需修改csim.c。

(修改時可用相同的sEbt參數分別運行csim和csim-ref,查看每次訪存的

詳細信息是否一致)

5、實驗結果

   實驗結果以下:

     

   

6、遇到的問題

  問題1:一個不算問題的問題:

  我實現的時候考慮了s爲0即全相聯的狀況,可是.csim-ref中好像是不能

出現s爲0的輸入的:

這個其實就把這的小於0改成<=0便可:

 

  問題2:我想讓s,b,E三個選項做爲可選選項,也就是說不輸入這三個選項及參數時默認其爲0,1,1

可是會出現以下錯誤:

可是若是直接輸入這些參數又是正確的:

想到參數可輸入可不輸入時選項字符串應該爲

又嘗試了一下:

並且此次連直接輸入這些參數也是錯誤的:

因此感受問題應該出在getopt函數上,可是還沒找到解決辦法。

 

7、實驗心得體會

 

在最後把整個代碼貼一下:

#include "cachelab.h"
#include <stdio.h>	/* fopen freopen perror */
#include <stdint.h>	/* uintN_t */
#include <unistd.h> /* getopt */
#include <getopt.h> /* getopt -std=c99 POSIX macros defined in <features.h> prevents <unistd.h> from including <getopt.h>*/
#include <stdlib.h> /* atol exit*/
#include <errno.h>  /* errno */

#define false 0
#define true 1

typedef struct
{
    _Bool valid;    				// 有效位 
    uint64_t tag;   				// 標記位 
    uint64_t time_counter;  		// LRU計數器,替換此值最小的行 
}line;
typedef line *entry_of_lines;
typedef entry_of_lines *entry_of_sets;

typedef struct
{
    int hit;						// 記錄命中次數 
    int miss;						// 記錄未命中次數
    int eviction;					// 記錄替換次數 
}result;

entry_of_sets InitializeCache(uint64_t S, uint64_t E);

result HitMissEviction(entry_of_lines search_line, result Result, uint64_t E, uint64_t tag, _Bool verbose);

result ReadAndTest(FILE *tracefile, entry_of_sets cache, uint64_t S, uint64_t E, uint64_t s, uint64_t b, _Bool verbose);

void RealseMemory(entry_of_sets cache, uint64_t S, uint64_t E);

/* 當選項爲 h,或輸入錯誤的命令(格式錯誤或用法錯誤)時,打印命令詳細用法 */ 
void printHelpMessage()
{
	//這些提示信息是按照.csim-ref的提示信息編寫的
	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("-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 * const argv[])
{
    result Result = {0, 0, 0};
    const char *command_options = "hvs:E:b:t:";

    FILE* tracefile = NULL;

    entry_of_sets cache = NULL;

	_Bool verbose = false;	/* flag whether switch to verbose mode, zero for default */
	uint64_t s = 0;	/* number of sets ndex's bits */
	uint64_t b = 0;	/* number of blocks index's bits */
	uint64_t S = 0; /* number of sets */
	uint64_t E = 0;	/* number of lines */

	char ch;    /* command options */

	while((ch = getopt(argc, argv, command_options)) != -1)
	{
		switch(ch)
		{
			case 'h':
			{
				printHelpMessage(); 
				exit(EXIT_SUCCESS);
			}

			case 'v':
			{
				verbose = true;
				break;
			}

			case 's':
			{

				if (atol(optarg) <= 0)	/* We assume that there are at least two sets */
				{
					printHelpMessage(); 
					exit(EXIT_FAILURE);
				}
				s = atol(optarg);
				S = 1 << s;
				break;
			}

			case 'E':
			{
				if (atol(optarg) <= 0)
				{
					printHelpMessage(); 
					exit(EXIT_FAILURE);
				}
				E = atol(optarg);
				break;
			}

			case 'b':
			{
				if (atol(optarg) <= 0)	/* We assume that there are at least two sets */
				{
					printHelpMessage(); 
					exit(EXIT_FAILURE);
				}
				b = atol(optarg);
				break;
			}

			case 't':
			{
				if ((tracefile = fopen(optarg, "r")) == NULL)
				{
					perror("Failed to open tracefile");
					exit(EXIT_FAILURE);
				}
				break;
			}

			default:
			{
				printHelpMessage(); 
				exit(EXIT_FAILURE);
			}
		}
	}

    if (s == 0 || b ==0 || E == 0 || tracefile == NULL)
    {
        printHelpMessage(); 
        exit(EXIT_FAILURE);
    }

    cache = InitializeCache(S, E);
    Result = ReadAndTest(tracefile, cache, S, E, s, b, verbose);
    RealseMemory(cache, S, E);   /* Don't forget this in C/C++, and do not double release which causes security problem */
    //printf("hits:%d misses:%d evictions:%d\n", Result.hit, Result.miss, Result.eviction);
    printSummary(Result.hit, Result.miss, Result.eviction);
	return 0;
}

/* 初始化緩存 */ 
entry_of_sets InitializeCache(uint64_t S, uint64_t E)
{
    entry_of_sets cache;

    //使用 calloc而非 malloc,由於咱們須要初始化爲0 

    if ((cache = calloc(S, sizeof(entry_of_lines))) == NULL) 
    {
        perror("Failed to calloc entry_of_sets");
        exit(EXIT_FAILURE);
    }

    for(int i = 0; i < S; ++i) 
    {
        if ((cache[i] = calloc(E, sizeof(line))) == NULL)
        {
            perror("Failed to calloc line in sets");
        }
    }

    return cache;
}

/* 判斷每條訪存操做是 hit or miss or miss+evication */ 
result HitMissEviction(entry_of_lines search_line, result Result, uint64_t E, uint64_t tag, _Bool verbose)
{
    uint64_t oldest_time = UINT64_MAX;							//用於尋找 LRU計數器值最小的行 
    uint64_t youngest_time = 0;									//用於尋找 LRU計數器值最大的行 以 更新替換進入緩存的行的LRU值 
    uint64_t oldest_block = UINT64_MAX;							//記錄 LRU計數器值最小的行號	
    _Bool hit_flag = false;

    for (uint64_t i = 0; i < E; ++ i)
    {
        if (search_line[i].tag == tag && search_line[i].valid) 	//命中 
        {
            if (verbose)    printf("hit\n");
            hit_flag = true;
            ++Result.hit;
            ++search_line[i].time_counter; 						//更新計數器值 
            break;
        }
    }

    if (!hit_flag)  											//未命中
    {
        if (verbose)    printf("miss");
        ++Result.miss;
        uint64_t i;
        for (i = 0; i < E; ++i)    								//尋找LRU值最小的行做爲替換行(空閒行的值最小)
        {
            if (search_line[i].time_counter < oldest_time)
            {
                oldest_time = search_line[i].time_counter;
                oldest_block = i;
            }
            if (search_line[i].time_counter > youngest_time)    //尋找LRU值最大的行以更新 新進入緩存的行的計數器值 
            {
                youngest_time = search_line[i].time_counter;
            }
        }

        search_line[oldest_block].time_counter = youngest_time + 1;
        search_line[oldest_block].tag = tag;

        if (search_line[oldest_block].valid) 					//假如替換行以前有效位爲 1,那麼發生了替換 
        {
            if (verbose)    printf(" and eviction\n");
            ++Result.eviction;
        }
        else
        {
            if (verbose)    printf("\n");
            search_line[oldest_block].valid = true;
        }
    }
    return Result;
}

/* 計算結果 (若verbose爲真,打印每條訪存操做詳細結果) */ 
result ReadAndTest(FILE *tracefile, entry_of_sets cache, uint64_t S, uint64_t E, uint64_t s, uint64_t b, _Bool verbose)
{
    result Result = {0, 0, 0};
    char ch;
    uint64_t address;
    //讀取tracefile中每條訪存操做,獲取指令及地址,原本可直接忽略讀取字節數,但爲了打印訪存操做,須要記錄 
    //地址是十六進制數,因此採用 %lx 而非 %lu
    while((fscanf(tracefile, " %c %lx%*[^\n]", &ch, &address)) == 2) 
    {
        if (ch == 'I')
        {
            continue;
        }
        else
        {
            uint64_t set_index_mask = (1 << s) - 1;
            uint64_t set_index = (address >> b) & set_index_mask;
            uint64_t tag = (address >> b) >> s;
            entry_of_lines search_line = cache[set_index];

			// load / store 操做只會引發一次訪存操做
            if (ch == 'L' || ch == 'S')
            {
                if (verbose)    printf("%c %lx ", ch, address);
                Result = HitMissEviction(search_line, Result, E, tag, verbose);
            }

			/* data modify操做會進行一次 load再對此地址進行一次 store,因此會引發兩次訪存操做 
               結果能夠爲 兩次緩存命中, 或 一次未命中(可能包含替換)、一次命中. */
            else if (ch == 'M')
            {
                if (verbose)    printf("%c %lx ", ch, address);
                Result = HitMissEviction(search_line, Result, E, tag, verbose);
                Result = HitMissEviction(search_line, Result, E, tag, verbose);
            }

            else
            {
                continue;
            }
        }
    }
    return Result;
}

/* 釋放內存 */ 
void RealseMemory(entry_of_sets cache, uint64_t S, uint64_t E)
{
    for (uint64_t i = 0; i < S; ++i)
    {
        free(cache[i]);
    }
    free(cache);
}