本項目大體上就是要求用C\C++來模擬cpu對cache的訪問,然后統計hits、misses和eviction的次數。其實並沒有想象中的那么難,感覺完全可以當成一道acm里面的大模擬題。。下面就對這個題目涉及到的一些知識點做下總結:
(一)linux命令行處理
由於題目要求是在linux下以附加命令參數的方式來執行程序的,所以要對命令行的參數進行一下處理。這里要用到getopt函數,函數原型為:
int getopt(int argc,char** argv,const char* opstring);
我們都知道linux的命令參數分長參數和短參數,而這個函數是用來且只能用來處理短參數的,函數的返回值即為當前調用它所讀取到的那個參數(int對應其ASCII碼值),其中的opstring是一個短參數集合的字符串,形如:
const char* optstring = "hvs:t:b:E:";
其中每一個字母后面如果加一個冒號代表其必須帶有一個附加的參數,不帶就是必須沒有附加的參數,而如果有兩個冒號則是可帶可不帶參數,還有一點很重要,這些參數也就是字母的順序是無所謂前后的,只要在這個字符串里就可以。
這個函數的調用要引用<getopt.h>庫,該庫還為我們提供了幾個比較用幫助的全局變量,比較常見的有:optarg、opterror、optind,分別表示當前解析的參數的附加參數、解析是否出錯(存在不能識別的opt即不在我們的optstring中,或者該加附帶參數的opt沒加不該加的加了)從而打印錯誤信息、下一個要解析的參數的index。我們可以根據自己的需要來利用並且手動的更改這些變量。
比如opterror=1的如果出錯程序就會打印錯誤信息而當其等於0的時候程序就不會打印錯誤信息。我們通過optarg來獲取每次解析參數所得到的附加參數,以字符串的形式返回!
關於命令行就總結這些,還有長參數的解析函數getopt_long(),詳情可以去查看linux man page: http://linux.die.net/man/3/getopt_long
實現代碼:
char* trace_file; const char* optstring = "hvs:E:b:t"; char opt; //deal with the short-option while((opt=getopt(argc,argv,optstring))!=-1) { switch (opt) { case 'h': printusage(argv); break; case 'v': flag = 1; break; case 's': state.s = atoi(optarg); break; case 'E': state.E = atoi(optarg); break; case 'b': state.b = atoi(optarg); break; case 't': trace_file = optarg; break; default : printusage(argv); break; } }
(二)Cache的初始化
這部分其實就是通過編程語言的形式來“模擬”出cache的簡單模型,即S=2^s個分組(set),每一組有E行,每行有B=2^b個存儲單元,還有tag標記位和valid有效位,具體的圖片如下圖:
(圖片來源:CSAPP.2E P305)
第一看這張圖的時候一直看不懂,后來明白了上面得灰色部分是cache,而最下面哪一行長條是內存的一個地址,這個原理就是我們對內存的地址按照給定的參數s,E,b(其實還有個m,不過這題默認m=64)來划分成不同的塊,從而借此來在cache中查找其是否存在,如果存在就是一個hit,直接在cache中提取從而節省訪存的時間,反之就要將其載入cache然后在提取。
至於語言上的實現,就是設置幾個結構體然后用malloc分配空間,或者用3維數組來實現應該也可以(不過數據量太大應該就不行了)
代碼實現:
Cache init(int s,int E,int b) { int i,j; int S = 1<<s; int B = 1<<b; Cache cache_t; Set set_t; Line line_t; cache_t.s = (Set*)malloc(sizeof(Set)*S); for(i = 0;i < S;i++) { set_t.l = (Line*)malloc(sizeof(Line)*E); cache_t.s[i] = set_t; for(j = 0;j < E;j++) { line_t.block = (int*)malloc(sizeof(int)*B); cache_t.s[i].l[j] = line_t; cache_t.s[i].l[j].flag = 0; cache_t.s[i].l[j].tag = 0; cache_t.s[i].l[j].used_time = 0; } } return cache_t; }
(三)核心部分:對內存的訪問
關於從trace文件中獲取的數據,每條有兩個要注意,一是開頭的命令字母,這點cmu在一開始的提示文檔里也給我們指出來了:
注意這個‘M’是相當於兩次訪存!
第二個就是提供的address了,其實我們每次更新cache只要將address給傳入到state_fresh函數就可以,然后根據給定的b、E、s來確定miss和hit的情況:
void state_fresh(Cache* cache,State* state,int ad,int cflag) { int i,j; Set set_t; Line line_t; int cnt = getbi(ad); int m = 0; int set = 0,tag = 0; //get set for(i = cnt-state->b;i > cnt-state->b-state->s;i--) set += bi[i]*(1<<(m++)); //get tag m = 0; for(i = cnt-state->b-state->s;i > 0;i--) tag += bi[i]*(1<<(m++));
首先對於給定的address我們確定他如果在cache中則他必須在的set的序號和他所固有的tag值,計算出這兩個值后剩下的就是在cache中的相應位置去查找即可:
首先給出hit的情況:
//search in cache set_t = cache->s[set]; for(i = 0;i < state->E;i++) { line_t = set_t.l[i]; if(line_t.flag==1 && line_t.tag==tag) { state->hit++; cache->s[set].l[i].used_time++; if(cflag) printf("hit\n"); return; } }
如果set匹配,行匹配(flag=1且tag匹配),則標記位hit做好相應的處理工作,這題的關鍵點就是miss的情況,因為我們要分別討論其是否要進行evict以及如何進行evict。
還是先給出有空位的情況:
//miss-could be flag = 0 or tag not equal state->miss++; //get the max used number int* used_num = (int*)malloc(2*sizeof(int)); int Min = getnum(&set_t,state,used_num); //1.search for empty seats for(i = 0;i < state->E;i++) { line_t = set_t.l[i]; if(line_t.flag == 0) { cache->s[set].l[i].flag = 1; cache->s[set].l[i].tag = tag; cache->s[set].l[i].used_time = used_num[1]+1; if(cflag) printf("miss\n"); return; } }
getnum函數就是找到當前ad所必須在的set的所有行的最小值和最大值,分別存儲在used_num[0]和used_num[1]中,最小值很好理解就是為了稍后的找不到空行時替換用的,而最大值則是為了將miss的ad載入cache時標記其優先級用的。
LRU(Least-Recently used)算法就是在evict時主要用到的,這里只是給出了他的低級實現,簡單的記錄一下優先級,更全的大家可以參考:http://flychao88.iteye.com/blog/1977653 里面從低級到高級的LRU都有講解,還有java的實現
//2.evict if(cflag) { if(cache->s[set].l[Min].flag==1) printf("miss "); else printf("miss\n"); } if(cache->s[set].l[Min].flag==1) { state->evict++; if(cflag) printf("eviction\n"); } cache->s[set].l[Min].flag = 1; cache->s[set].l[Min].tag = tag; cache->s[set].l[i].used_time = used_num[1]+1; free(used_num);