在 cache.c
中实现如下函数:
1.根据读的要求设置了tag , valid_bit , dirty_bit; 根据回的要求设置了dirty_bit。
2.根据主存地址划分:
typedef struct
{
bool dirty_bit:1; //脏位
bool data[64]; //数据位:每块大小为2^6,即64字节
uint32_t tag:3; //标记
bool valid_bit:1; //有效位
}cache_line;
cache_line *Cache;
void init_cache(int total_size_width, int associativity_width) {
int i;
int cache_line_num = exp2(total_size_width - associativity_width);
Cache = malloc(cache_line_num * sizeof(cache_line ));
for(i = 0 ; i < cache_line_num ; i++)
{
Cache[i].valid_bit = 0;
Cache[i].dirty_bit = 0;
}
}
1.总思路:
分为三个部分:主存地址进来
若命中:
(1)按主存地址先访问cache,如果命中,返回数据;
若不命中:
(2)在cache中找空闲行,即判断valid_bit是否为0,如果为0,则将此cache行作为新的读入的数据存放的地方;
(3)如果该cache组是全部满的,即所有的valid_bit为1,那么随便取一行,此时先判断脏位是否为1,为1的话就将当前的cache行先写回主存,再则将此cache行作为新的读入的数据存放的地方;
2.分情况说明:
uint32_t cache_read(uintptr_t addr) {
try_increase(1); //访问cache次数加一
uint32_t ret_data;
addr = addr & 0x7FFF; //主存地址划分
uint32_t mem_blocks_num , mem_tag , cache_group_num , block_offset;
block_offset = addr & 0x3C;
cache_group_num = (addr >> 6) & 0x3F;
mem_tag = (addr >> 12) & 0x7;
mem_blocks_num = (addr >> 6) & 0x1FF;
uint32_t paddr;
int i , j , flag;
int cache_group_start = cache_group_num * 4; //情况一:在当前cache中查找
for(i = cache_group_start ; i < cache_group_start + 4 ; i++)
{
if(Cache[i].tag == mem_tag && Cache[i].valid_bit == 1)
{
hit_increase(1);
break;
}
}
if(i < cache_group_start + 4) //如果命中
{
ret_data = Cache[i].data[block_offset] + (Cache[i].data[block_offset + 1] << 8) + (Cache[i].data[block_offset + 2] << 16) + (Cache[i].data[block_offset + 3] << 24);//将该数据返回
}
else //如果不命中,则分为寻找空闲行和随即替换两种情况
{
for(j = cache_group_start ; j < cache_group_start + 4 ; j++)//寻找空闲行
{
if(Cache[j].valid_bit == 0)
break;
}
if(j < cache_group_start + 4 ) //情况二,不命中但找到空闲行
{
mem_read(mem_blocks_num , Cache[j].data);//先将数据读入
Cache[j].tag = mem_tag;
Cache[j].valid_bit = 1;
ret_data = Cache[j].data[block_offset] + (Cache[j].data[block_offset + 1] << 8) + (Cache[j].data[block_offset + 2] << 16) + (Cache[j].data[block_offset + 3] << 24);
}
else //情况三:不命中且此组cache行全满,考虑随机替换
{
flag = rand()%4 + cache_group_start; //mod4找到一个0~3的任意数
if(Cache[flag].dirty_bit == 1) //若脏位为1,先写回
{
paddr = ( Cache[flag].tag << 6 ) | cache_group_num ;
mem_write(paddr , Cache[flag].data);
}
mem_read(mem_blocks_num , Cache[flag].data); //读入主存块
Cache[flag].tag = mem_tag;
Cache[flag].valid_bit = 1;
ret_data = Cache[flag].data[block_offset] + (Cache[flag].data[block_offset + 1] << 8) + (Cache[flag].data[block_offset + 2] << 16) + (Cache[flag].data[block_offset + 3] << 24);
}
}
return ret_data;
}
1.总思路:
分为三个部分:主存地址进来,和读的函数是一样的。
若命中:
(1)按主存地址先访问cache,如果命中,则将掩码对应的偏移处的值修改;
若不命中:
(2)在cache中找空闲行,即判断valid_bit是否为0,如果为0,则将此cache行作为新的读入数据的地方,并将掩码对应的偏移处的值修改;
(3)如果该cache组是全部满的,即所有的valid_bit为1,那么随便取一行,此时先判断脏位是否为1,为1的话就将当前的cache行先写回主存,再则将此cache行作为新的读入的数据存放的地方 , 并将掩码对应的偏移处的值修改;
2.分情况说明:
这里调用到了一个cache_alter()函数,作用是设置掩码对应的偏移处的值修改:
在这里说明一下,因为那个mem_read()和mem_write()函数的第二个参数,即数据区的参数都是uint8_t的,所有设置数据区的时候都是按一个一个字节来存,但这样就造成了很不方便的地方,每次将数据返回时要把四个字节拼在一起,每次写完了之后还要将每个字节分散开写回到cache的数据区,真的很不方便。
在对比uncache_read的时候那个*p来了这么一个秒操作,( Data & (~wmask) ) | (data &wmask)这个也是仿照着它写的,就是掩码的地方修改,其它非掩码的字节不变。原来还傻傻地把掩码分情况讨论了。
最后改完之后,再分散到cache数据区的每个字节。
void cache_alter(int i , uint32_t block_offset , uint32_t wmask , uint32_t data)
{ //写cache行操作
uint32_t Data;
Data = Cache[i].data[block_offset] + (Cache[i].data[block_offset + 1] << 8) + (Cache[i].data[block_offset + 2] << 16) + (Cache[i].data[block_offset + 3] << 24);
Data = ( Data & (~wmask) ) | (data &wmask);
Cache[i].data[block_offset+3] = (Data>>24) & 0xff;
Cache[i].data[block_offset+2] = (Data>>16) & 0xff;
Cache[i].data[block_offset+1] = (Data>>8) & 0xff;
Cache[i].data[block_offset] = Data & 0xff;
}
void cache_write(uintptr_t addr, uint32_t data, uint32_t wmask) {
try_increase(1);
addr = addr & 0x7FFF; //划分主存地址
uint32_t mem_blocks_num , mem_tag , cache_group_num , block_offset;
block_offset = addr & 0x3C;
cache_group_num = (addr >> 6) & 0x3F;
mem_tag = (addr >> 12) & 0x7;
mem_blocks_num = (addr >> 6) & 0x1FF;
uint32_t paddr;
int i , j ,flag;
int cache_group_start = cache_group_num * 4;//第一种情况:在cache中寻找
for(i = cache_group_start ; i < cache_group_start + 4; i++)
{
if(Cache[i].tag == mem_tag && Cache[i].valid_bit == 1)
{
hit_increase(1);
break;
}
}
if(i < cache_group_start + 4) //如果写命中
{
cache_alter(i , block_offset , wmask ,data); //修改cache行的对应块内偏移的数据
Cache[i].dirty_bit = 1;
}
else //如果写不命中
{
for(j = cache_group_start ; j < cache_group_start + 4 ; j++)//找空闲行
{
if(Cache[j].valid_bit == 0)
break;
}
if(j < cache_group_start + 4) //如果有空闲行
{
mem_read(mem_blocks_num , Cache[j].data);//先读入一块新的主存块
cache_alter(j , block_offset , wmask , data);//修改cache行的对应块内偏移的数据
Cache[j].tag = mem_tag; //设置标志位和其它
Cache[j].valid_bit = 1;
Cache[j].dirty_bit = 1;
}
else //如果没有空闲行,则采用随机替换
{
flag = rand()%4 + cache_group_start;
if(Cache[flag].dirty_bit == 1) //如果脏位为1,需先写回主存
{
paddr = (Cache[flag].tag << 6 ) | cache_group_num;//拼接写回主存的主存块号
mem_write(paddr , Cache[flag].data);//写回对应的主存块
}
mem_read(mem_blocks_num , Cache[flag].data); //先读入新的主存块
cache_alter(flag , block_offset , wmask , data);//修改cache行的对应块内偏移的数据
Cache[flag].tag = mem_tag;//设置标志位和其它
Cache[flag].valid_bit = 1;
Cache[flag].dirty_bit = 1;
}
}
}
想一想, 为什么编译器为变量分配存储空间的时候一般都会对齐? 访问一个没有对齐的存储空间会经历怎么样的过
程?
因为对齐的话更有利于访存的进行;比如int类型的数是按四字节对齐的,如果没有对齐,它是紧凑排列的话每次访存都要从头开始一个一个依次查询,时间消耗变大。就好像链表和数组,链表是没有对齐的,每次访存都要从链表头开始;而数组只要知道它的首地址和访存第几个,就可以计算出访存地址。
这可太多了:
这是有点隐晦的错误了,就是在写了之后就会给报错,在随机替换的时候错了,那这就是写回的时候有问题,写错地方了,改了之后就是把旧的主存块号拼出来,用它。
原来被绕进去了,是应该先写回主存块呢,还是先写进cache行,再写回主存块呢?为什么会这样,那原来那块如果改过怎么办?于是明白了应该先写回再装入新的。
这就太坑了,因为mem_read和write函数入口参数类型的误导,致使我们做了很多单字节转四字节和四字节转单字节的操作,好烦心好累,明明可以强制类型转化,最后才发现。
这就是写回的时候有问题,写错地方了,改了之后就是把旧的主存块号拼出来,用它。
原来被绕进去了,是应该先写回主存块呢,还是先写进cache行,再写回主存块呢?为什么会这样,那原来那块如果改过怎么办?于是明白了应该先写回再装入新的。
这就太坑了,因为mem_read和write函数入口参数类型的误导,致使我们做了很多单字节转四字节和四字节转单字节的操作,好烦心好累,明明可以强制类型转化,最后才发现。
爽。