cachelab
cachelab
概览
本实验分为个部分,part a是用C语言构建一个缓存模拟器,part b是调优矩阵转置。
本次实验需要修改的文件是csim.c
和trans.c
。
Part A:Writing a Cache Simulator
前置知识
了解这些知识,会让Part A的旅程更加通顺,如果基础不错,可以跳过此节。
LRU
LRU(Least Recently Used)替换策略是一种缓存管理策略,用于决定在缓存已满的情况下,应该将哪一个缓存块替换出去。LRU 策略的核心思想是:最近最少使用的块是最不可能在近期再次被访问的,因此应该首先被替换。
实现LRU策略的方法(使用链表)
双向链表:维护一个双向链表,头部是最近使用的块,尾部是最久未使用的块访问更新:每次访问某个块时,将其移动到链表头部替换:当需要替换的时候,从链表尾部移除块
LRU 的基本思想是:
- 追踪使用时间:每当缓存行被访问时,更新它的访问时间或计数器。这样就可以知道哪些缓存行最近被使用过。
- 选择替换行:当缓存需要替换某一行时,选择那个最长时间没有被访问过的缓存行,即“最少使用的”行进行替换。
实现细节
last_access_time
:每个缓存行都有一个last_access_time
字段来记录上次访问的时间(或者是一个计数器)。每次缓存行被访问时,这个时间都会被更新。time
:在CacheSet
中维护一个time
变量作为全局时间计数器。在访问或插入缓存行时,增加这个计数器的值。- 选择 LRU 行:在插入新数据时,遍历当前缓存集合中的所有行,选择
last_access_time
最小的行进行替换。如果某行的valid
字段为0
(即无效行),直接替换它。
缓存
缓存结构:(S, E, B, m)
- 主存地址的最大数量:M = 2 ^ m
- 缓存集合:S = 2 ^ S
- 块(
Block
)容量(bytes):B = 2 ^ b
🍊 这里理解Maximum number of unique memory addresses
(主存地址的最大数量):
如果能映射到缓存的主存地址,是从0x10
到0x50
,那么主存地址的最大数量为M = 0x50 -0x10 = 0x40(hex) = 64(dec),对应的二进制表示为0b0100 0000
,需m = 7。(存疑todo📌)
缓存容量(bytes):C = S x E x B
getopt
getopt
是一个用于解析命令行参数的函数(from ChatGPT)。
getopt
函数的输入参数包括:
int argc
:命令行参数的数量。通常是main
函数的第一个参数。char *const argv[]
:命令行参数的数组。通常是main
函数的第二个参数。const char *optstring
:一个字符串,用于指定可接受的选项。每个选项字符后面如果跟一个冒号(:
),表示该选项需要一个参数。
getopt
的输出包括:
int
:getopt
返回解析到的选项字符。如果所有选项解析完毕,返回 -1。optarg
:一个全局变量,指向当前选项的参数值(如果有)。optind
:一个全局变量,表示下一个要处理的命令行参数的索引。
示例代码:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> /** * Function to parse command line arguments */ void parse_args(int argc, char *argv[]) { int opt; while ((opt = getopt(argc, argv, "a:b:c")) != -1) { switch (opt) { case 'a': printf("Option a with value %s\n", optarg); break; case 'b': printf("Option b with value %s\n", optarg); break; case 'c': printf("Option c\n"); break; default: fprintf(stderr, "Usage: %s -a value -b value -c\n", argv[0]); exit(EXIT_FAILURE); } } // Print remaining command line arguments for (int i = optind; i < argc; i++) { printf("Remaining argument: %s\n", argv[i]); } } int main(int argc, char *argv[]) { parse_args(argc, argv); // Other code to run the program printf("Program is running...\n"); return 0; }
解释:
opt = getopt(argc, argv, "a:b:c")
:getopt
解析命令行参数。"a:b:c"
:表示程序接受三个选项:-a
和-b
需要参数,-c
不需要参数。
switch (opt)
:根据解析到的选项执行不同的代码。case 'a'
:处理-a
选项,并输出其参数值optarg
。case 'b'
:处理-b
选项,并输出其参数值optarg
。case 'c'
:处理-c
选项。default
:处理无效选项并输出使用说明。
for (int i = optind; i < argc; i++)
:输出剩余的命令行参数。
假设程序名为 parse_args
,运行以下命令:
./parse_args -a value1 -b value2 -c arg1 arg2
输出:
Option a with value value1 Option b with value value2 Option c Remaining argument: arg1 Remaining argument: arg2 Program is running...
getopt
函数用于解析命令行参数,它通过返回解析到的选项字符来驱动一个 switch
语句,执行相应的选项处理逻辑。optarg
和 optind
是两个全局变量,分别用于存储当前选项的参数值和下一个要处理的命令行参数的索引。
memcpy
memcpy
是 C 标准库中的一个函数,用于从一个内存位置复制数据到另一个内存位置。其定义在 <string.h>
头文件中。函数原型如下:
void *memcpy(void *dest, const void *src, size_t n);
dest
: 目标内存地址,复制数据到该地址。src
: 源内存地址,从该地址复制数据。n
: 复制的字节数。
使用示例:
#include <stdio.h> #include <string.h> int main() { char src[10] = "abcdef"; char dest[10]; memcpy(dest, src, 6); printf("Source: %s\n", src); printf("Destination: %s\n", dest); return 0; }
sscanf
sscanf
是 C 语言中的一个标准库函数,用于从字符串中读取格式化的数据。它的功能类似于 scanf
,但 sscanf
从给定的字符串中读取数据,而不是从标准输入(如键盘)读取。
函数原型
int sscanf(const char *str, const char *format, ...);
参数
str
:指向包含要读取数据的字符串的指针。format
:一个格式化字符串,指定如何解释输入字符串的内容。格式化字符串的格式与scanf
函数相同。...
:一个或多个指向变量的指针,用于存储从字符串中读取的数据。
返回值
sscanf
返回成功读取的数据项的数量。如果发生错误或没有成功读取任何数据项,返回值可能是 0
或 EOF
。
任务
修改csim.c
来实现一个缓存模拟器。这个模拟器使用tracefile
作为模拟访问内存的输入文件,输出对应四种操作的命中、丢失和替换次数。且输出结果要跟上述提到的参考模拟器的表现相同。
实验对于Part A的编程要求是:
- 要分配任意大小的数据空间,以便于支持任意
s
、E
和b
大小的缓存。应该用函数malloc
- 模拟器需要排除指令加载(instruction load)这个参数,所以在解析参数的时候,需要过滤掉这个选项。
- 评分函数是
printSummary(hit_count, miss_count, eviction_count)
- 加入可选参数
-v
来打印对应的缓存命中、丢失和替换细节。 - 建议(
肯定)使用函数getopt
来解析命令行参数。 - 数据修改操作其实相当于先加载数据然后再向相同的位置存储数据。所以
M
操作可能导致两次缓存命中或者一次丢失,一次命中加上一次的替换。 - 默认读取内存数据的时候,读取的块是自动对齐的。这样读取数据的时候,永不会碰到块边界,所以可以忽略size
Part A评分是27分。
自动打分软件是test-csim
,它会检测我们实现的模拟器在执行参考的tracefile
时的正确性,确保打分前,已经编译好了我们的模拟器。
打分软件的使用:
linux> make linux> ./test-csim
执行后,会自动给出每个测试案例的分数、缓存的参数(s,E,b)、实现的模拟器和参考模拟器之间的结果对比以及输入的参考文件。
思路
写一下大概要做的事情:
- 解析输入参数
create_cache()
:按照参数s、E、b创建具有LRU
策略的缓存- 缓存块
cache_block_t
结构体 - 缓存
cache_t
结构体 - 缓存加载替换策略
- 缓存块
convert_tracefile_to_mem_operations()
:转换参考文件作为缓存的模拟操作
现在的疑惑是:
- 我从文件中得到了例如从地址
0x10
读取4个字节的地址,那么我怎么才能将这四个字节的数据存储到我写的缓存中呢? - 最近最少使用原则的具体实现是什么?
实现缓存系统
结构:
- 缓存块(
CacheBlock
):包含一个指向以字节为单位的指针(char *data
)的结构体 - 缓存行(
CacheLine
):包含有效位(valid
)、标签位(tag
)和缓存块(CacheBlock
)的结构体 - 缓存集合(
CacheSet
):包含一个指向缓存行数组(CacheLinel
)的指针和缓存的行数(num_lines
) - 缓存(
Cache
):包含一个指向缓存集合(CacheSet
)数组的指针,缓存集合数量(num_sets
)和每个缓存块的大小(block_size
)
方法:
- 初始化缓存(
init_cache
):输入一个指向缓存的指针,缓存集合数、缓存行数以及缓存块大小,初始化一个缓存空间,且将指针指向这个缓存空间 - 查找缓存行(
find_cache_line
):输入缓存集合索引、缓存行数索引,如果对应的缓存行有效,则返回对应的缓存块;否则返回null - 字选择(
access_cache_word
):输入缓存块数据的首字节偏移,获取对应的字节数据(char* data) - 行替换(
insert_cache_line
):输入缓存集合索引、缓存行数索引以及要缓存的数据(char *data),按照LRU规则将数据保存到对应的缓存块中。且对应的缓存行的有效位设置为1,标签位设置为缓存行数索引。
现在支持LRU替换策略的缓存系统已经完成,下一步要做的是从内存中读取数据块的操作。
模拟内存
🍋到这里我们就做的很棒了,回顾之前的代码,你已经成功模拟了一个缓存系统!
所以,给自己点掌声吧👏
本节的主要内容是实现从内存中读取数据块的操作。但是别担心,这次的代码并不需要完成一个内存系统,而是简单地将要读取的内存数据,存储在已有的缓存系统中。
现在内存读取和修改的格式如下:
operation address, size M 0421c7f0, 4 L 04f6b868, 8 S 7ff0005c8, 8
L
:数据加载(data load)S
:数据存储(data store)M
:数据修改(data modify),即先加载数据然后存储数据
举例M 0421c7f0,4
,修改地址为0x0421c7f0
出的4个字节的数据。
首先我们要判断0421c7f0
处的数据是否存在于缓存:
- 如果存在,那么缓存命中,并进行读操作,然后修改完毕后,再写回。
- 如果不存在,那么缓存丢失,需要先将地址
0421c7f0
处4个字节的数据读取出来,然后将数据存储到对应的缓存中。缓存完内存数据后,随后进行读操作,修改完毕后,再写回缓存。
那么里面就有几个问题需要确认:
- 如何判断
0421c7f0
是否存在于缓存体系中?内存地址到缓存地址的映射函数 - 如果读取的数据块大小大于一个缓存块(block_size)的话,怎么才能依次保存内存块到缓存中?
将所需解决问题进一步抽象,可以总结为以下两点:
- 地址映射:内存地址映射到缓存地址
- 缓存命中判断:根据有效位和标签确认指定内存地址数据是否存在于缓存中
首先解决地址映射问题。
在缓存系统中(上述的Cache
结构),要想找到一个数据,需要提供标签tag
、索引index
和块内偏移offset
。而我们的系统中,块内偏移的位数是由缓存的块大小block_size
决定,索引index
的位数由缓存集合的num_sets
决定。
内存位数划分如下:
tag_bits | set_bits | block_bits |
---|
假设我们缓存参数如下:
Parameter | Description |
---|---|
set_bits = log2(num_sets) | Number of set index bits |
block_bits = log2(block_size) | Number of block offset bits |
tag_bits = m - (set_bits + block_bits) | Number of tag bits |
这样当一个二进制的内存地址address
过来的时候:
- 首先获取它的集合偏移
index
:index = address/(2 ^ block_bits) % set_bits
- 然后确认对应的行的
tag
:tag = address / (2 ^ (block_bits + tag_bits))
- 最后获取其在块中的偏移
offset
:offset = address % block_bits
前面两个将除法转换为右移操作的时候,可以简化操作:
- 集合偏移
index
:``index = (address >> block_bits) % set_bits` - 获取
tag
:tag = address >> (block_bits + tag_bits)
假设我们有如下缓存参数:
S
:缓存集合数--num_sets
E
:每个集合中的缓存行数--num_lines_per_set
B
:缓存块大小(以字节为单位)--block_size
在此情况下,映射内存地址到缓存地址的步骤如下:
int get_set_index(Cache *cache, int address) { return (address / cache->block_size) % cache->num_sets; } int get_tag(Cache *cache, int address) { return address / (cache->block_size * cache->num_sets); } int get_offset(int address, int block_size) { return address % block_size; }
而且重要的是,需考虑处理缓存块大小大于一个缓存块的情况。
明天写吧,今天放假!累死了~
🌞好的,现在已经写完了内存地址转换为缓存地址的方法和结构体
// Structure to represent the partition typedef struct { int index; int tag; int offset; }AddressPartition; AddressPartition get_address_partition(Cache *cache, int address);
根据内存操作缓存的代码实现如下:
缓存丢失进程process_to_handle_miss()
:
- 描述:缓存丢失的应对策略。搜寻缓存集合,若不存在此标签的缓存行时,缓存丢失。此时调用该进程,将对应的内存块,放到对应的缓存行中
// Convert memory address to cache parameters AddressPartition get_address_partition(Cache *cache, int address) { AddressPartition cache_params; cache_params.index = (address / cache->block_size) % cache->num_sets; cache_params.tag = address / (cache->block_size * cache->num_sets); cache_params.offset = address % cache->block_size; return cache_params; } // Function to handle a cache hit void process_cache_hit(Cache *cache, CacheLine *line, char operation, int offset, int size) { if (operation == 'M' || operation == 'S') { // Write back to cache char *data = access_cache_word(line, offset); memset(data, 0, size); // Example modification } } // Function to handle a cache miss void process_cache_miss(Cache *cache, char operation, AddressPartition cache_params) { CacheBlock memory_block; memory_block.data_size = cache->block_size; memory_block.data = (char *) malloc(cache->block_size * sizeof(char)); // Simulate reading from memory (e.g., initialize with zeros) memset(memory_block.data, 0, cache->block_size); insert_cache_line(cache, cache_params.index, cache_params.tag, memory_block); free(memory_block.data); if (operation == 'M') { CacheLine *line = find_cache_line(cache, cache_params.index, cache_params.tag); } } // Function to handle memory operation
解析文件内容
现在关于缓存系统和从特定内存地址操作特定数量的数据方法已经完成,下一步就是解析输入参数,然后模拟缓存操作。而对于输入的参数,可以从cachelab
有已经写好的缓存模拟器来确定。
这个参考模拟器在选择替换哪个缓存行的时候,遵循LRU (least-recently used)
最近最少使用替换原则。
参考缓存模拟器使用方法如下:
Usage: ./csim-ref [-hv] -s <s> -E <E> -b <b> -t <tracefile> • -h: Optional help flag that prints usage info • -v: Optional verbose flag that displays trace info • -s <s>: Number of set index bits (S = 2s is the number of sets) • -E <E>: Associativity (number of lines per set) • -b <b>: Number of block bits (B = 2b is the block size) • -t <tracefile>: Name of the valgrind trace to replay
输入的 tracefile
的格式是[space]operation address, size
,operation 有 4 种:`
I
:指令加载(instruction load)L
:数据加载(data load)S
:数据存储(data store)M
:数据修改(data modify),即先加载数据然后存储数据
而/trace
目录下包含了用于评估缓存模拟器正确性的一系列参考跟踪的文件tracefile
。
分析下函数的输入输出:
- 可选的帮助信息:h
- 可选的冗余信息:v
- 必填的缓存信息:s E b
- 必填的操作文件信息:t
要想实现将特定的 tracefile
作为内存操作的来源,需要实现一个读取文件的函数MemoryOperation * convert_tracefile_to_memory_operation(const char *filename)
:
- 输入:tracefile
- 输出:包含内存操作的集合,每个集合中包含了十六进制的地址、操作以及数据大小
要想实现这个函数,需要:
-
定义数据结构:定义表示缓存操作的结构体。
-
读取文件内容:逐行读取文件中的数据。
-
解析每行数据:根据文件中的格式解析操作类型、地址和数据大小。
-
存储转换后的数据:将解析后的数据存储到一个结构体数组或链表中。
首先定义一个内存操作的结构体MemoryOperation
// Structure for memory operation typedef struct { size_t address; // Memory address char operation; // Operation type size_t data_size; // Data size } MemoryOperation;
然后涉及到读取文件的操作,补充下文件读取和提取数据的知识:
FILE
FILE *file = fopen(filename, "r"); //以只读模式打开文件,并将文件指针存储在 file 变量中 if (file == NULL) { perror("Error opening file"); return 1; }
perror
perror
函数会输出指定的错误前缀消息和对应的标准错误描述。
它会打印出传入的字符串参数,后跟一个冒号、空格和对应的错误消息(基于全局变量 errno
的值)。
上面的文件如果读取为空,则调用 perror
输出 "Error opening file: " 以及具体的错误信息(例如 "No such file or directory")
解析每行数据的话,实现一个解析行参数的函数int parse_line(const char *line, MemoryOperation *operation);
。它接受一行文本并返回一个 指向MemoryOperation
结构体的指针,并且根据参数返回是否解析成功。
这里使用sscanf
函数从给定的字符串中读取数据。
// int parse_line(const char *line, MemoryOperation *operation) { char op; unsigned int address; size_t size; // 使用 sscanf 解析行内容 if (sscanf(line, " %c %u,%zu", &op, &address, &size) != 3) { return 0; // 解析失败 } operation->operation = op; operation->address = address; operation->size = size; return 1; // 解析成功 }
这个函数的构思是:
- 输入一行字符串,输出解析后指向内存操作结构体指针
- 如果解析成功,则返回1,并且将字符串中包含的内存操作属性,存放到指针值指向的结构体中;如果解析失败,则返回解析错误具体原因并且终止继续解析。
这里讨论同一解析函数不同实现的优劣:
MemoryOperation *parse_line(const char *line)和int parse_line(const char *line, MemoryOperation *operation)的优劣对比
方法一:返回
MemoryOperation
指针
MemoryOperation *parse_line(const char *line); 优势:
- 简洁的接口:调用者不需要提前分配
MemoryOperation
结构体,只需处理返回的指针。- 清晰的失败处理:可以通过返回
NULL
明确表示解析失败。劣势:
- 内存管理复杂:函数内部需要动态分配内存,调用者必须负责释放返回的指针,增加了内存泄漏的风险。
- 性能开销:每次调用都需要进行内存分配和释放,可能会有性能开销。
方法二:通过参数返回
MemoryOperation
int parse_line(const char *line, MemoryOperation *operation); 优势:
- 内存管理简单:调用者负责分配
MemoryOperation
结构体,内存管理更为清晰,不容易引起内存泄漏。- 性能更高:避免了每次调用时的内存分配和释放,性能开销较小。
劣势:
- 调用方式稍微复杂:调用者需要提前分配
MemoryOperation
结构体,并传递指针给函数。总结
- 方法一 更适合需要简洁接口和明确失败处理的场景,但内存管理复杂,性能开销较大。
- 方法二 更适合需要高性能和简单内存管理的场景,但调用方式稍复杂,需要调用者提前分配结构体。
解析行完毕后,将返回的内存操作整合为一个结构体数组。
下一步就是解析命令行参数了,我们需要按照输入的参数来确定缓存的大小和输入的文件。
解析命令行
要用到getopt
,一个用于解析命令行参数的函数。
最终得分:21,有两个多联合的缓存没得分,没找到原因,就这样吧。
未来如果还有时间,试着修复一下代码。🍋
贴一下文件:
csim.c
#include <stdio.h> #include "cachelab.h" #include "cache/cache.h" #include <unistd.h> #include <string.h> #include <stdlib.h> #include <getopt.h> #include <limits.h> int main(int argc, char *argv[]) { char opt; int s,E,b; int verbose = 0; char *filename = (char*) malloc (128 * sizeof(char)); while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) { switch (opt) { case 'h': print_help(); exit(0); case 'v': verbose = 1; break; case 's': s = atoi(optarg); break; case 'E': E = atoi(optarg); break; case 'b': b = atoi(optarg); break; case 't': strcpy(filename, optarg); break; default: print_help(); exit(0); } } int num_sets = 1 << s; int num_lines_per_set = E; int block_size = 1 << b; Cache cache; init_cache(&cache, num_sets, num_lines_per_set, block_size); MemoryOperation *operations; int num_operations = convert_tracefile_to_memory_operation(filename, &operations); if (num_operations > 0) { for (int i = 0; i < num_operations; i++) { if (verbose) { printf("%c %zx,%zu ", operations[i].instruction, operations[i].address, operations[i].data_size); } handle_memory_operation(&cache, operations[i], verbose); if (verbose) printf("\n"); } free(operations); free_cache(&cache); printSummary(cache.cache_log.hits, cache.cache_log.misses, cache.cache_log.evictions); } else { print_help(); } return 0; }
cache.h
#include <stddef.h> // Enumeration for memory operation types typedef enum { LOAD, STORE, MODIFY, UNKNOWN }OperationType; // Structure for memeory operation typedef struct { size_t address; //Memory address char instruction; //Insrtuction type size_t data_size; //Data size } MemoryOperation; // Structure to store cache statistics typedef struct { int hits; int misses; int evictions; } CacheStatistics; // Structure to represent a cache block which contains a pointer to data typedef struct { char *data; int data_size; }CacheBlock; // Structure to represent a cache line typedef struct { int valid; int tag; CacheBlock block; unsigned long last_access_time; // Time of last access for LRU }CacheLine; // Structure to represent a cache set typedef struct { CacheLine *lines; // Array of cache lines in this set int line_nums; // Number of cache lines in the set unsigned long time; // Time counter for LRU }CacheSet; // Structure to represent the entire cache typedef struct { CacheSet *sets; int num_sets; int num_lines_per_set; int block_size; CacheStatistics cache_log; }Cache; // Structure to represent the partition typedef struct { int index; int tag; int offset; }AddressPartition; // Function declarations void print_help(); void init_cache(Cache *cache, int num_sets, int num_lines_per_set, int block_size); CacheLine* find_cache_line(Cache *cache, int set_index, int tag); char* access_cache_word(CacheLine *line, size_t offset); void copy_block(CacheBlock memory_block, CacheBlock cache_block); void insert_cache_line(Cache *cache, int set_index, int tag, CacheBlock memory_block, int verbose); void free_cache(Cache *cache); AddressPartition get_address_partition(Cache *cache, size_t address); void handle_memory_operation(Cache *cache, MemoryOperation operation, int verbose); void handle_memory_operation_without_size(Cache *cache, MemoryOperation operation, int verbose); int parse_line(const char *line, MemoryOperation *operation); int convert_tracefile_to_memory_operation(const char *filename, MemoryOperation **operations);
cache.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "cache.h" #include <limits.h> void print_help() { printf("** A Cache Simulator by Deconx\n"); printf("Usage: ./csim-ref [-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("-b <num> Number of block offset bits.\n"); printf("-t <file> Trace file.\n\n\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"); } /* Initializes the cache with the specified number of sets, lines per set, and block size. (S)num_sets: Number of sets. (E)num_lines_per_set: Number of lines per set. (B)block_size: Block size(bytes) */ void init_cache(Cache *cache, int num_sets, int num_lines_per_set, int block_size){ //initialize the parameters of cache cache->num_sets = num_sets; cache->num_lines_per_set = num_lines_per_set; cache->block_size = block_size; cache->sets = (CacheSet *)malloc(num_sets * sizeof(CacheSet)); cache->cache_log.hits = 0; cache->cache_log.misses = 0; cache->cache_log.evictions = 0; //organize the cache set for(int i = 0; i < num_sets; i++){ cache->sets[i].line_nums = num_lines_per_set; cache->sets[i].lines = (CacheLine *) malloc(num_lines_per_set * sizeof(CacheLine)); cache->sets[i].time = 0; //LRU time //organize the cache line for(int j = 0; j < num_lines_per_set; j++){ cache->sets[i].lines[j].valid = 0; // Initialize valid bit to 0 cache->sets[i].lines[j].tag = 0; // Initialize tag to 0 cache->sets[i].lines[j].block.data = (char *)malloc(block_size * sizeof(char)); cache->sets[i].lines[j].block.data_size = block_size; } } } /* Determine if a copy of the word w is stored in one of the cache lines contained in set i A copy of w is contained in the line if and only if the valid bit is set and the tag in the cache line matches the tag in the address of w. */ CacheLine* find_cache_line(Cache *cache, int set_index, int tag){ // Set selection CacheSet target_set = cache->sets[set_index]; // Line mactching for (int i = 0; i < target_set.line_nums; i++){ if(target_set.lines[i].valid && target_set.lines[i].tag == tag){ target_set.lines[i].last_access_time = ++target_set.time; //Update access time cache->cache_log.hits ++; // Cache hit return (target_set.lines + i); } } //Cache miss cache->cache_log.misses++; return NULL; } /* Read data from the cache block at a specific byte offset. size_t offset: The first byte in the desired word. In the example, the block offset bits of (0b100) indicate that the copy of w starts at byte 4 in the block. */ char* access_cache_word(CacheLine *line, size_t offset){ if (line->valid) { return (line->block.data + offset); } return NULL; } //Copy memory block to cache block //TODO:Swap the order of two parameters void copy_block(CacheBlock memory_block, CacheBlock cache_block){ if(memory_block.data_size == cache_block.data_size){ int block_size = cache_block.data_size; memcpy(cache_block.data, memory_block.data, block_size); } } /* Insert the new block in one of the cache lines of the set indicated by the set index bits. */ void insert_cache_line(Cache *cache, int set_index, int tag, CacheBlock memory_block, int verbose){ //Set selection CacheSet target_set = cache->sets[set_index]; unsigned long min_time = ULONG_MAX; int lru_index = 0; // Index of the least recently used cache line for (int i = 0; i < target_set.line_nums; i++){ // If a cache line is invalid, choose it for replacement if (!target_set.lines[i].valid) { lru_index = i; break; } // Otherwise, find the cache line with the samllest last access time if (target_set.lines[i].last_access_time < min_time){ min_time = target_set.lines[i].last_access_time; lru_index = i; } } // Cache evictions:If the selected line is valid, we are evicting it. if (target_set.lines[lru_index].valid) { if (verbose) { printf("eviction "); } cache->cache_log.evictions++; } // Replace the LRU cache line with the new memory block CacheLine *line = &target_set.lines[lru_index]; line->valid = 1; line->tag = tag; //copy_block(memory_block, line->block); line->last_access_time = ++target_set.time; } //Free all allocated memory for the cache void free_cache(Cache *cache){ for (int i = 0; i < cache->num_sets; i++){ for (int j = 0; j < cache->num_lines_per_set; j++){ free(cache->sets[i].lines[j].block.data); // Free data memory } free(cache->sets[i].lines); // Free array of cache lines } free(cache->sets); // Free array of cache sets } /*---------------------- Memory operation part -------------------------------------------*/ // Convert memory address to cache parameters AddressPartition get_address_partition(Cache *cache, size_t address) { AddressPartition cache_params; cache_params.index = (address / cache->block_size) % cache->num_sets; cache_params.tag = address / (cache->block_size * cache->num_sets); cache_params.offset = address % cache->block_size; return cache_params; } // Get the operation type OperationType get_operation_type(const char operation_char) { if (operation_char == 'L') { return LOAD; } else if (operation_char == 'S') { return STORE; } else if (operation_char == 'M') { return MODIFY; } else { return UNKNOWN; } } /* Assume that memory accesses are aligned properly, such that a single memory access never crosses block boundaries. */ void handle_memory_operation_without_size(Cache *cache, MemoryOperation operation, int verbose) { int block_size = cache->block_size; AddressPartition cache_params = get_address_partition(cache, operation.address); // Check if the block is in the cache CacheLine *line = find_cache_line(cache, cache_params.index, cache_params.tag); // Assume the memory block is aligned properly CacheBlock memory_block; memory_block.data_size = block_size; memory_block.data = (char *) malloc(block_size * sizeof(char)); if (line && verbose) { printf("hit "); } else { if (verbose) printf("miss "); // Miss: first insert the block to cache insert_cache_line(cache, cache_params.index, cache_params.tag, memory_block, verbose); } if (operation.instruction == 'M') { CacheLine *line_twice = find_cache_line(cache, cache_params.index, cache_params.tag); if (line_twice && verbose) printf("hit "); } } int find_specific_tag_line(CacheSet *set, int tag) { CacheLine *lines = set->lines; int line_nums = set->line_nums; for (int i = 0; i < line_nums; i++) { if(lines[i].valid && lines[i].tag == tag) { return i; } } return -1; } int find_empty_line(CacheSet *set){ CacheLine *lines = set->lines; int line_nums = set->line_nums; for (int i = 0; i < line_nums; i++) { if(!lines[i].valid) { return i; } } return -1; } int find_eviction_line(CacheSet *set) { CacheLine *lines = set->lines; int line_nums = set->line_nums; unsigned long min_time = ULONG_MAX; int lru_index = -1; for (int i = 0; i < line_nums; i++) { if(lines[i].last_access_time < min_time) { min_time = lines[i].last_access_time; lru_index = i; } } return lru_index; } void update_cache_line(Cache *cache, int index, int tag, int verbose) { int line_index = find_empty_line(&cache->sets[index]); if (line_index == -1) { if (verbose) { printf("eviction "); } cache->cache_log.evictions++; line_index = find_eviction_line(&cache->sets[index]); } cache->sets[index].lines[line_index].valid = 1; cache->sets[index].lines[line_index].tag = tag; cache->sets[index].lines[line_index].last_access_time = ++cache->sets[index].time; } void handle_cache_operation(Cache *cache, int index, int tag, int verbose) { int line_index = find_specific_tag_line(&cache->sets[index], tag); // Miss if (line_index == -1) { if(verbose) printf("miss "); // MISS OPt cache->cache_log.misses++; update_cache_line(cache, index, tag, verbose); } else { if (verbose) printf("hit "); cache->cache_log.hits++; } } void handle_memory_operation(Cache *cache, MemoryOperation operation, int verbose) { AddressPartition cache_params = get_address_partition(cache, operation.address); int index = cache_params.index; int tag = cache_params.tag; if (operation.instruction == 'M') { handle_cache_operation(cache, index, tag, verbose); handle_cache_operation(cache, index, tag, verbose); } else { handle_cache_operation(cache, index, tag, verbose); } } /* Parse a single line of input to extract a memory operation The input line is expected to be in the format: "OPERATION ADDRESS,SIZE", where: - OPERATION is a single character ('L', 'S', or 'M') - ADDRESS is an unsigned integer represented in hexadecimal format - SIZE is a size_t value representing the size of the memory operation Example input line: "L 10,1" */ int parse_line(const char *line, MemoryOperation *operation) { char verbose; char op; size_t address; size_t size; // Use sscanf to parse the line content, expecting the address in hexadecimal format if (sscanf(line, "%c%c %zx,%zu", &verbose, &op, &address, &size) != 4) { return 0; } // Omit I instruction if (verbose == 'I') return 0; // Store the parsed data in the provided MemoryOperation structure operation->address = address; operation->instruction = op; operation->data_size = size; return 1; // Parsing successful } /* Read a trace file and converts each line to MemoryOperation. @param filename The name of the trace file to read @param operations Pointer to a pointer to store the dynamically allocated array of MemoryOperation @retuen The number of operations read and stored in the array, or -1 if an error occured. */ int convert_tracefile_to_memory_operation(const char *filename, MemoryOperation **operations) { FILE *file = fopen(filename, "r"); if (!file) { return -1; } // Allocate an inital array to store MemoryOperation size_t capacity = 10; *operations = (MemoryOperation *) malloc (capacity * sizeof(MemoryOperation)); if(!*operations) { perror("Memory allocation error"); fclose(file); return -1; } char line[256]; size_t count = 0; while (fgets(line, sizeof(line), file)) { if (count >= capacity) { // Resize the array if the capacity is exceeded capacity *= 2; MemoryOperation *temp = (MemoryOperation *)realloc(*operations, capacity * sizeof(MemoryOperation)); if (!temp) { perror("Memory reallocation error"); free(*operations); fclose(file); return -1; } *operations = temp; } MemoryOperation operation; if (parse_line(line, &operation)) { (*operations)[count++] = operation; } } fclose(file); return count; }
测试代码test_cache.c
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include "cache.h" #include <string.h> void test_init_cache() { Cache cache; int num_sets = 2; int num_lines_per_set = 4; int block_size = 8; // Initialize the cache init_cache(&cache, num_sets, num_lines_per_set, block_size); // Check the parameters of cache assert(cache.num_sets == num_sets); assert(cache.num_lines_per_set == num_lines_per_set); assert(cache.block_size == block_size); // Check each set and line for (int i = 0; i < num_sets; i++) { assert(cache.sets[i].line_nums == num_lines_per_set); for (int j = 0; j < num_lines_per_set; j++){ assert(cache.sets[i].lines[j].valid == 0); assert(cache.sets[i].lines[j].tag == 0); assert(cache.sets[i].lines[j].block.data != NULL); assert(cache.sets[i].lines[j].block.data_size == block_size); } } //Free the allocated memory for test free_cache(&cache); printf("Init_cache() tests passed!\n"); } void test_copy_block(){ int block_size = 8; CacheBlock memory_block; memory_block.data_size = block_size; memory_block.data = (char *) malloc(block_size * sizeof(char)); assert(memory_block.data != NULL); strcpy(memory_block.data, "imgod!"); assert(strcmp(memory_block.data, "imgod!") == 0); CacheBlock cache_block; cache_block.data_size = block_size; cache_block.data = (char *) malloc(block_size * sizeof(char)); copy_block(memory_block, cache_block); assert(strcmp(cache_block.data, "imgod!") == 0); free(cache_block.data); free(memory_block.data); printf("copy_block tests passed!\n"); } void test_insert_cache() { Cache cache; int num_sets = 2; int num_lines_per_set = 4; int block_size = 8; // Initialize the cache init_cache(&cache, num_sets, num_lines_per_set, block_size); // Initialize the memory block data and insert to the cache CacheBlock memory_block; memory_block.data_size = block_size; memory_block.data = (char *) malloc(block_size * sizeof(char)); assert(memory_block.data != NULL); strcpy(memory_block.data, "imgod!"); assert(strcmp(memory_block.data, "imgod!") == 0); insert_cache_line(&cache, 1, 102, memory_block, 1); assert(cache.sets[1].lines[0].valid == 1); assert(cache.sets[1].lines[0].tag == 102); printf("insert_cache_line() tests passed!\n"); free_cache(&cache); free(memory_block.data); } void read_and_write() { Cache cache; int num_sets = 2; int num_lines_per_set = 4; int block_size = 8; // Initialize the cache init_cache(&cache, num_sets, num_lines_per_set, block_size); // Initialize the memory block data and insert to the cache CacheBlock memory_block; memory_block.data_size = block_size; memory_block.data = (char *) malloc(block_size * sizeof(char)); assert(memory_block.data != NULL); strcpy(memory_block.data, "imgod!"); assert(strcmp(memory_block.data, "imgod!") == 0); insert_cache_line(&cache, 1, 102, memory_block, 1); // Find the data from the cache CacheLine *target_line = find_cache_line(&cache, 1, 102); assert(target_line != NULL); assert(target_line->valid == 1); assert(target_line->tag == 102); assert(target_line->block.data != NULL); char *cache_data = access_cache_word(target_line, 1); assert(strcmp(cache_data, "mgod!") == 0); free_cache(&cache); free(memory_block.data); printf("All read and write tests passed!\n"); } void test_address_partition() { Cache cache; int num_sets = 4; int num_lines_per_set = 1; int block_size = 2; // Initialize the cache init_cache(&cache, num_sets, num_lines_per_set, block_size); int address = 15; AddressPartition cache_params = get_address_partition(&cache, address); assert(cache_params.index == 3); assert(cache_params.tag == 1); assert(cache_params.offset == 1); printf("All address partition tests passed!\n"); } void test_memory_operation() { Cache cache; int num_sets = 4; int num_lines_per_set = 1; int block_size = 16; // Initialize the cache printf("All memory operation tests passed!\n"); } void test_insert_cacheline() { Cache cache; int num_sets = 4; int num_lines_per_set = 1; int block_size = 16; // Initialize the cache init_cache(&cache, num_sets, num_lines_per_set, block_size); //Create a memory block for testing CacheBlock memory_block; memory_block.data_size = block_size; memory_block.data = (char *) malloc(block_size * sizeof(char)); strcpy(memory_block.data, "block1"); // Insert the first cache line insert_cache_line(&cache, 0, 1, memory_block, 1); // Insert the second cache line strcpy(memory_block.data, "block2"); insert_cache_line(&cache, 1, 2, memory_block, 1); // Insert the third cache line(should cause eviction in the first set) strcpy(memory_block.data, "block3"); insert_cache_line(&cache, 0, 3, memory_block, 1); strcpy(memory_block.data, "block4"); insert_cache_line(&cache, 0, 4, memory_block, 1); assert(cache.cache_log.evictions == 2); free(memory_block.data); free_cache(&cache); printf("All insert tests ok!\n"); } void test_parse_line() { const char *line = " o ff4005b6,5"; MemoryOperation operation; if (parse_line(line, &operation)) { printf("Operation: %c, Address: %lx, Size: %zu \n", operation.instruction, operation.address, operation.data_size); } else { printf("Error parsing line!\n"); } } void print_cache_behavior(int hits, int misses, int evictions) { for(int i = 0; i < hits; i++) { printf("hit "); } } void test_convert_tracefile_to_opreation() { Cache cache; int num_sets = 4; int num_lines_per_set = 1; int block_size = 4; // Initialize the cache init_cache(&cache, num_sets, num_lines_per_set, block_size); const char *filename = "/home/crx/study/csapp/cachelab-handout/traces/yi3.trace"; MemoryOperation *operations; int num_operations = convert_tracefile_to_memory_operation(filename, &operations); if (num_operations > 0) { for (int i = 0; i < num_operations; i++) { //printf("Operation: %c, Address: %lx, Size: %zu\n", operations[i].instruction, operations[i].address, operations[i].data_size); printf("%c %zx,%zu,", operations[i].instruction, operations[i].address, operations[i].data_size); int per_hits = cache.cache_log.hits; int per_misses = cache.cache_log.misses; int per_evictions = cache.cache_log.evictions; handle_memory_operation_without_size(&cache, operations[i], 1); printf("\n"); } printf("Hits: %d, Misses: %d, Evictions: %d\n", cache.cache_log.hits, cache.cache_log.misses, cache.cache_log.evictions); free(operations); free_cache(&cache); } else { printf("Failed to convert trace file to memory operations\n"); } } int main(){ /* test_init_cache(); test_copy_block(); test_insert_cache(); read_and_write(); test_address_partition(); */ //test_memory_operation(); //test_insert_cacheline(); //test_parse_line(); //printf("unsigned int is %ld size\n", sizeof(size_t)); test_convert_tracefile_to_opreation(); return 0; }
ok,结束了。
时间2024年8月5日18:12:13
本文作者:上山砍大树
本文链接:https://www.cnblogs.com/shangshankandashu/articles/18316398
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步