因为在上的计算机系统基础课程讲到这儿了顺便还有个实验,就总结一下高速缓存和内存交互的过程。
何为高速缓存?
高速缓存,简称cache。
我们知道,常用的32位计算机上的内存条可以存储000…000-111…1111也就是2^32大小的键值对,key为32位的地址,value为32位的值。当cpu通过MMU想要访问内存某个地址的时候,主存就会返回相应地址的值。
内存中的数据通常是以块为单位存储的,一个块是什么呢?一个块的大小若为4bytes,那么就刚好占了某一个32位地址的32位值。
如果cpu直接从内存中读取值,这个速度会很慢很慢,于是就有聪明的B想出了高速缓存(Cache)的办法:在cpu到内存中读取数据过程之间增设一个cache数组,这个cache数组负责从内存中读取数据块然后返回给cpu。
那么为什么要有这么多余的一步呢?直接从内存中读取不香吗?原因有2点:
- 高速缓存从硬件上来说速度快于直接从内存中取
- cpu某一时刻只能执行一个进程,这个进程对内存的需求是很局限的,你把这个进程需要的数据块都缓存好,那读的时候就很快很方便了。
高速缓存如何实现
在说实现之前,首先要说明一下内存地址的构成和高速缓存行的构成。
内存地址 :Tag + 组号 + 块内字节偏移
高速缓存行(Cache line):有效位 + tag + 块数据
so,cache是如何从内存中取读取数据的呢?
高速缓存的单位是cache line,可以分为n组,这里就拿4组来举例说明,如果有64个cache line,总共分为4组,那么每组也就有16个cache line。对于内存中的块来说,第n块只能存在n mod 4 组内。
当cpu想要取一个内存地址的数据时,会现在高速缓存中第n组找,这个n就对应主存地址的组号,条件是 有效位=1,cache line 的tag = 内存地址的tag,如果满足条件就说明找到了,那么就根据块内数据的字节偏移返还所需要的字节。
直接一说可能很懵逼,举个简单的例子:
假设内存地址有10位,cache lines被分为4组,一共16个cache line,也就是一组有4个line,内存中一个块大小为4bytes
那么,如何知道内存地址的10位中tag占几位?组号占几位?块内字节偏移占几位呢?首先总共有4组,那么也就是说,我用2^2也就是00-11也就是2位就可以表示组数了,而一个块共有4个bytes,若想要表示出第n个字节,也只需要00-11也就是2个bit就可以表示出第几个字节了,所以tag位有10-2-2=6位。
还有个问题,为什么主存的地址 是 tag在最前,组号在中间,块内字节偏移在最后呢? 如果把内存中的所有数据块按照每行4个拍成二维数组,那么每一行就对应tag,每一列就对应组号,块号 = tag*4+组号 。 而确定了哪一块,最后才能确定块的哪一个字节。
了解了主存的地址的细节由来,那么就可以继续了解cache读取内存的细节了:
还是上面的例子,当cpu想取地址为 0000081111 的字节时,首先 tag = 000008 , 组号 = 3, 块内字节偏移 = 3 。根据上面谈到的算法过程:首先cache先定位到第3组,也就是cache数组中cache[12]-cache[15](第三组的四个cache line中去)。 查找这4个cache line有没有满足flag = 1 并且tag = 内存的tag。
若有则表示这个cache line的32位数据块就存着所需要的主存地址的数据块,接着再取这个数据块的第3个字节返还给cpu。
若无,那么cache需要做的就是寻找这一个组的牺牲的cache line来保存所需要查找的主存块。当然了,这个替换策略有许多种,这里介绍LRU(least recently use)算法实现的替换策略:
需要在cache line中添加一个计数器count
规则为:
代码:
#include<iostream>
#include<cstring>
#include<stdlib.h>
using namespace std;
struct node{
char Valid='0'; //有效位 令'1'有效,'0'无效
char CacheTag[6]; //标识位(组内条目偏移)
char CacheData[32]; //数据块
int count; //用于LRU统计次数
}cache[16]; //16 lines
char replaceItem[32];
//用来避免直接给char[]用字符串赋值末尾的自动补'\0'的情况
void assign(char a[],string x){
for(int i=0;i<x.size();i++){
a[i]=x[i];
}
}
//用于把一个char[]赋值给另一个char[]
void strCopy(char source[],char destination[],int length){
for(int i=0;i<length;i++){
destination[i]=source[i];
}
}
//若找到时刷新方法
void fresh1(int begin,int end,int vis){
int visCount = cache[vis].count;
for(int i=begin;i<end;i++){
if(i!=vis&&cache[i].count<visCount&&cache[i].Valid=='1'){
cache[i].count++;
}
}
cache[vis].count=0;
// cout<<"run fresh1!~"<<endl;
}
//若未找到时的刷新方法
void fresh2(int begin,int end,int i){
cache[i].count=0;
for(int j=begin;j<end;j++){
if(j!=i&&cache[j].Valid=='1'){
cache[j].count++;
}
}
}
//判断组内是否还有空的cache line
int judgeIsFull(int begin,int end){
for(int i=begin;i<end;i++){
//未命中且该组未满
if(cache[i].Valid=='0'){
cout<<"未命中且该组未满,放入cache["<<i<<"]"<<endl;
cache[i].Valid = '1';
fresh2(begin,end,i);
return i;
}
}
for(int i=begin;i<end;i++){
//未命中且该组已满,i被淘汰替换~
if(cache[i].Valid=='1'&&cache[i].count==3){
cout<<"未命中且该组已满,替换cache["<<i<<"]"<<endl;
fresh2(begin,end,i);
return i;
}
}
return -1;
}
char* Cache(char binary[]){
char *result = (char*)malloc(32*sizeof(char));
int groupIndex = ((binary[6]-'0')<<1)+binary[7]-'0';
//search group's cache line
int begin = groupIndex<<2,end = (groupIndex+1)<<2;
for(int i=begin;i<end;i++){
node* tmpCache = new node;
tmpCache = &cache[i];
int flag1 = tmpCache->Valid-'0';
int flag2 = 1;
char* CacheTag = tmpCache->CacheTag;
for(int j=0;j<6;j++){
if(CacheTag[j]!=binary[j]){
flag2 = 0;
break;
}
}
//如果找到了,则返回
if(flag1&flag2){
result = &tmpCache->CacheData[0];
fresh1(begin,end,i);
cout<<"find it!"<<endl;
return result;
}
}
//if not found,then replace as LRU
cout<< "not found!~";
int newGroupIndex = judgeIsFull(begin,end);
//replace CacheTag
strCopy(binary,cache[newGroupIndex].CacheTag,6);
//replace CacheData
strCopy(replaceItem,cache[newGroupIndex].CacheData,32);
return cache[newGroupIndex].CacheData;
}
void printStr(char* x,int size){
for(int i=0;i<size;i++){
cout<<x[i];
}
}
int main(){
assign(replaceItem,"10001000100010001111111111111111");
char binary[10];
string x;
while(1){
printf("请输入10位内存,输入exit退出\n");
cin>>x;
if(x.size()!=10) break;
assign(binary,x);
char* result = Cache(binary);
cout<<"binary = ";
printStr(binary,10);
cout<<endl<<"result = ";
printStr(result,32);
cout<<endl;
// 测试每次运行之后第0组每个cache line的count
// for(int i=0;i<4;i++){
// printf("cache[%d].count = %d,",i,cache[i].count);
// }
// cout<<endl;
}
/*
测试数据:
binary1 = 0000010000 //第0组第0个
binary2 = 0000010000 //第0组第0个
binary3 = 0000100000 //第0组第1个
binary4 = 0000110000 //第0组第2个
binary5 = 0001000000 //第0组第3个
binary6 = 0001010000 //第0组第4个,将替换掉第0个
*/
return 0;
}
结果: