Release Date: | Wednesday, April 17. |
Acceptance Deadline: | Friday, April 19, 11:59 pm. |
Due Date: | Sunday, April 28, 11:59 pm. |
Collaboration Policy: | Level 1 |
Group Policy: | Pair-optional (you may work in a group of 2 if you wish) |
In this lab, you will write a C program simulating the behavior of a hardware cache on real-world memory usage traces. Writing and testing your simulator will help you understand the different types of caches designs and the impact that cache memories can have on the performance of your programs.
Your cache simulator will simulate an arbitrary hardware cache (also called a 'cache memory'), as defined by the three-value model discussed in class: S (the number of cache sets), E (the number of cache lines per set), and B (the number of bytes in each data block). Your program will simulate the behavior of the specified cache on a trace file, which consists of a series of memory accesses that your program will replay in simulation. The output of your simulator will be three values: the number of cache hits, the number of cache misses, and the number of block evictions performed.
Note that your simulator will not actually cache or otherwise store any real data from memory; instead, you will simply be replaying the series of memory accesses and tallying which accesses would result in hits/misses/evictions if the simulated cache were an actual hardware cache.
To help you test your simulator, you are provided with a reference simulator implementation as well as a driver program that will automatically test your simulator by comparing its results against the reference simulator. Details on the trace files and the reference simulator are provided below.
The simulator operates on memory trace files as input, which are files generated by valgrind
that describe a series of memory accesses. Below is an example of a trace file that specifies a sequence of four memory operations (one per line):
I 0400d7d4,8 M 0421c7f0,4 L 04f6b868,8 S 7ff0005c8,8
The general format of each line of a trace file is as follows:
[space][operation] [address],[size]
The space field is either a space or nothing, depending on the operation type (as detailed below). The operation field indicates the type of memory operation, which is one of the following:
There is never a space before an "I" operation, while there is always a space before each "M", "L", or "S" operation.
The address field specifies a 64-bit memory address written in hex. Finally, the size field specifies the number of bytes accessed by the operation.
For example, in the first line of the example trace file above, there is no leading space (since the operation is an instruction load), and the operation is reading an 8-byte value (instruction) located at memory address 0x0400d7d4
.
You are provided with a set of preexisting trace files within the traces
subdirectory of your lab repository. You can use these reference trace files to test your cache simulator.
In addition to the included traces, you can use valgrind
to generate your own memory traces in this format, like so:
$ valgrind --log-fd=1 --tool=lackey -v --trace-mem=yes [some-cmd]
The above example will run the program [some-cmd]
and dump a trace of its memory accesses to the terminal. To save the output in a file, just redirect to a file by appending something like > mynewtrace.trace
to the end of the valgrind
command. The actual command [some-cmd]
could be any program you like with any arguments, e.g., you could use ls -l
to get a memory trace from the ls
program.
The simulator program takes the following command-line arguments:
-h
: Flag that prints usage info-v
: Flag that turns on extra (verbose) output-s <s>
: Number of set index bits (small-s)-E <E>
: Associativity (number of lines per set)-b <b>
: Number of block offset bits (small-b)-t <tracefile>
: Name of the trace file to replayWhen run, the simulator replays the memory operations listed in the specified trace file against the specified cache (using the cache values specified by the command line arguments). On completion, the simulator outputs a single line, which specifies the number of (simulated) cache hits, misses, and evictions.
For example, here is a sample run of the reference simulator (with arguments specified in any order):
$ ./cachesim-ref -s 4 -E 1 -b 4 -t traces/t2.trace hits:4 misses:5 evictions:3
Adding the verbose flag -v
will print extra information about each memory access in the trace, such as shown below:
$ ./cachesim-ref -s 4 -E 1 -b 4 -t traces/t2.trace -v L 10,1 miss M 20,1 miss hit L 22,1 hit S 18,1 hit L 110,1 miss eviction L 210,1 miss eviction M 12,1 miss eviction hit hits:4 misses:5 evictions:3
You are provided with a skeleton implementation of your own simulator that provides the same interface and output format as specified above (minus the additional verbose output). However, the skeleton implementation does not actually do any cache simulation, and simply outputs 0 for all three cache statistics. Your job will be to complete your own cache simulator so that it produces the same results as the reference simulator on any given trace file for a particular cache configuration.
In order to match the reference simulator, you must adhere to the following specifications while designing your cache simulator. Follow each of these instructions carefully, as each one of them has the potential to completely change your cache's behavior if not followed.
s
, E
, and b
. This means that you will need to allocate storage for your data structures using malloc
.valgrind
traces.Your lab files contained in your repository consist of the following:
cachesim.c
: Your cache simulator program. This is the only file you should modify.cachesim-ref
: The executable reference simulator.Makefile
: Used to build your program.traces/
: Directory of reference trace files for testing.test-cachesim
: Executable program to automatically test your simulator against the reference simulator.To test your cache simulator against all of the reference trace files and output an auto-generated correctness score, compile your simulator by running make
and then execute the test-cachesim
program. With a complete and correct cache simulator, this will result in the following output:
$ ./test-cachesim Your simulator Reference simulator Points (s,E,b) Hits Misses Evicts Hits Misses Evicts 3 (1,1,1) 9 8 6 9 8 6 traces/t1.trace 3 (4,2,4) 4 5 2 4 5 2 traces/t2.trace 3 (2,1,4) 2 3 1 2 3 1 traces/t3.trace 3 (2,1,3) 167 71 67 167 71 67 traces/t4.trace 3 (2,2,3) 201 37 29 201 37 29 traces/t4.trace 3 (2,4,3) 212 26 10 212 26 10 traces/t4.trace 3 (5,1,5) 231 7 0 231 7 0 traces/t4.trace 6 (5,1,5) 265189 21775 21743 265189 21775 21743 traces/t5.trace 27 Simulator summary: scored 27 of 27 points
Here are some general and specific tips for working on your cache simulator.
The basic outline of your simulator should be (1) create your (initially empty) cache data structure, and then (2) replay the operations specified in the trace file against your cache, updating your cache appropriately as you go and logging hits, misses, and evictions. Remember that your cache does not actually store any data, so you are really just tracking cache metadata without any actual data blocks involved.
The primary new feature of C that you'll be using in this lab is struct
types. Refer to the x86 structures slides for a refresher on the basics of defining and using structs. In the specific context of this lab, start by deciding what metadata you'll need to maintain for each cache line and define a struct
for that purpose (i.e., a struct
representing a single cache line). You may then wish to do the same thing for a single cache set and/or the cache itself. Remember that struct types can contain other struct types as fields (a struct type is no different from any other type, e.g., int
; it's simply likely to be a larger number of bytes).
The appropriate type to represent a memory address read from a trace file (i.e., an unsigned 64-bit value) is unsigned long long
, since a regular long
is often just a 32-bit value (implementation dependent). You may wish to use a typedef
to avoid repeatedly typing this type name. Remember that a typedef
is simply a way to give some existing type an alias. For example, if you wanted to define a new type alias memaddr_t
, you could use a typedef
as follows:
typedef unsigned long long memaddr_t;
The starter code in cachesim.c
includes a number of predefined global variables as well as several helper functions. You should not modify the included helper functions, but you will need to add your own to avoid writing the entire simulator inside main
. You may also wish to add additional global variables (but as always, only variables that actually need to be global should be declared as such).
Warning: If you write any helper functions that return pointers, watch out for this general pitfall of C programs: never return the address of a local variable from a function, since after the function returns, the address will now point to stack memory in the old (deallocated) stack frame. For example, the following is valid C code but is unsafe:
int* foo() { int x = 5; return &x; // danger - memory deallocated on return! }
This lab may be the first time you have read files in C, so you may not be familiar with the standard functions for doing so. Files can be opened using the fopen
function and closed using the fclose
function.
Once opened, the easiest way to read a single line from an opened file is using the fgets
function. An important principle when working with files is that open files have a file pointer, which is basically a piece of internal state tracking where you are in reading or writing the file. When you first open a file, the file pointer normally starts at the beginning of the file (file position 0). Functions that read or write data (e.g., fgets
) advance the internal file pointer. Hence, calling fgets
(or any other I/O function) multiple times will yield different results as the file pointer advances.
A particularly useful function for parsing out the fields contained within a line is sscanf
, which works like scanf
but reads input from a given string rather than from user input. Here is an sscanf tutorial. Note that if you are trying to extract a memory address in the trace file format into an unsigned 64-bit number (as suggested above), then you should use the format specifier %llx
in your sscanf
format string (this specifier says to read a 64-bit unsigned value given in hex format).
Remember that valgrind
does not put a space in front of "I" lines as it does for "M", "L", and "S", which should be helpful in identifying (and ignoring) these lines.
Each data load (L) or store (S) operation can cause at most one cache miss. However, the data modify operation (M) is treated as a load followed by a store to the same address. Thus, an M operation can result in two cache hits, or a miss and a hit plus a possible eviction.
The verbose flag -v
provides an easy way to build in additional debugging output to your program without breaking the mandated output format of the program (which is just the single output line at the very end). Your program can produce as much verbose output (in whatever format) you wish. A basic example of producing verbose output is given in the starter code of cachesim.c
. While you are not required to implement any particular verbose output, adding verbose output that roughly follows the reference simulator is a good idea and should make debugging your simulator easier.
Do your initial debugging on the small traces (particularly t2.trace
and t3.trace
). These traces are smaller than t1.trace
and will likely be easier to debug.
To run the simulator within GDB, launch the program within gdb
, then pass the command line arguments when starting the program using run
, e.g.:
$ gdb cachesim (gdb) run -s 4 -E 1 -b 4 -t traces/t2.trace
As in Lab 2, be sure to run valgrind
on your cache simulator to check for many kinds of memory errors (even if your program appears to be working).
The starter code uses the standard C function getopt
to help in parsing command-line arguments, which is much easier (and more flexible) than trying to manually parse arguments. While you do not need to write this part of the code yourself, it is a good idea to have a basic idea of what is going on (both to understand the starter code and for general C knowledge, since this is the standard way that most C programs handle command-line arguments). Refer to this part of the starter code (inside main
) while reviewing the paragraph below.
The basic idea is that getopt
is given a string that specifies all of the possible command-line arguments, some of which may take associated values (e.g., the -t
and -s
arguments), and some of which may just be boolean flags (e.g., the -h
and -v
flags). The string passed to getopt
specifies the former type of argument by including a colon :
after the associated character (e.g., t:
). The specific usage of getopt
in cachesim.c
(i.e., a switch
statement wrapped in a loop) is highly idiomatic and should be mostly self-explanatory. One detail that may not be apparent is that optarg
is a predefined global variable (declared in the C library); it is (re)set by each invocation of getopt
to be the argument value associated with the argument currently being parsed.
As usual, you should begin by initializing your GitHub repo (and forming a group, if applicable) via the link posted to Slack. You can then clone the repository to the class server to begin working. You are responsible for completing cachesim.c
, but should not create or modify any other file.
If you are working in a group, it is recommended to go through Part 3 of the Git tutorial, which covers some specific topics applicable to collaboration (most significant of which is handling merge conflicts). To minimize the chance of Git-related collaboration difficulties, observe best practices: always git pull
at the start of a working session, and always git commit
and git push
at the end of a working session. You should also review the course policies on group work.
Your final submission will consist of your committed and pushed cachesim.c
file at the time of the due date. Remember to submit your individual group reports to me if you worked in a group.
Your simulator will be graded on program correctness, design, and style. Remember that the test-cachesim
program will check your simulator for correctness only; nothing else!
You can (and should) consult the Coding Design & Style Guide for tips on design and style issues. Please ask if you have any questions about what constitutes good program design and/or style that are not covered by the guide.