CS520
Spring 2013
Programming Assignment 6
Due Sunday May 5


The goal of this assignment is to build a memory system simulator.

A memory system includes both a main memory and one or more memory caches. The memory consists of a sequence of 32-bit words, with each word having a 32-bit address. The memory words are addressed starting with zero.

The length of memory and the number of caches are specified when the memory system is initialized.

Each cache is an array of sets. Each set contains an array of lines. Each line contains a block plus control information. Each block is an array of words.

The caches for a memory system are all identical and are configured when the memory system is initialized. The configuration includes specifying the number of cache sets, the number of cache lines per set, and the number of words per cache block.

Each cache is numbered, beginning with zero. A cache number must be passed to the read/write functions to identify which cache is to be used to access memory.

All caches are write-back caches and utilize LRU replacement.

A memory system contains locks (mutexes) to protect shared data structures when there are multiple caches, because each cache will be accessed by a separate CPU (thread).

The configuration of the memory system has the following constraints:

The public interface to the simulator is the following six functions:

void *initializeMemorySystem(unsigned int memoryLength, unsigned int numberOfCaches, unsigned int wordsPerBlock, unsigned int numberOfSets, unsigned int linesPerSet, unsigned int numberOfLocks);
int readInt(void *handle, unsigned int cache, unsigned int address);
float readFloat(void *handle, unsigned int cache, unsigned int address);
void writeInt(void *h, unsigned int cache, unsigned int address, int value);
void writeFloat(void *h, unsigned int cache, unsigned int address, float value);
void printStatistics(void *h);

initializeMemorySystem initializes a memory system, configured according to its six parameters. It returns a "handle" for reading or writing to memory utilizing the caches. If the memory system initialization fails, then NULL is returned. (However, if malloc fails during the initialization, treat this as a fatal error: print an error message to stderr and call exit with -1.) Multiple memory systems can be in operation at the same time.

readInt and readFloat read the word at the specified address from the memory system denoted by the specified handle, utilizing the specified cache. The cache will first be checked to see if the word is present in the cache. If so, the word is read from the cache. If the word is not in the cache, then the block containing the word will be first loaded into the cache, and then the desired word will be accessed from the cache.

writeInt and writeFloat write the specified value into the word at the specified address of the memory system denoted by the specified handle, utilizing the specified cache. The cache will first be checked to see if the word to be updated is present in the cache. If so, the value is written to the word in the cache. If the word is not in the cache, then the block containing the word will be first loaded into the cache, and then the value will be written to the desired word.

If the set where a block is to be loaded is full, then the least-recently-used block in the set is chosen to be replaced. This is done by recording a timestamp in a cache line when a block is read into the cache line, and also each time the cache line is subsequently accessed. This timestamp is simply a running count of the number of read/write requests that have been made to a particular cache. The cache line with the lowest timestamp is chosen for replacement.

The write-back strategy should be implemented using a "dirty" bit that tracks whether a block has been modified since it was loaded into the cache. When the block is replaced, if its dirty bit is set, then it should be written back to the memory. If the dirty bit is not set, then the block can be overwritten without writing it back to memory.

If a bad address or a bad cache number is passed to a read or a write function, print an error message to stderr and call exit with -1. You do not need to do any validation on the handle, however.

The printStatistics function prints to stdout, for each cache in a memory system, the total number of reads issued via the cache, the number of reads that hit in the cache, the total number of writes issued via the cache, and the number of writes that hit in that cache.

Here is an example of how the printStatistics output should look:

Cache 0:
  total number of reads: 256
  reads that hit in cache: 192
  total number of writes: 256
  writes that hit in cache: 192
Cache 1:
  total number of reads: 256
  reads that hit in cache: 0
  total number of writes: 0
  writes that hit in cache: 0

Multiple caches are supported in order to simulate a memory system interacting with multiple CPUs, each with its own cache. In the simulation, the multiple CPUs will be represented by multiple POSIX threads making concurrent requests on the memory system. To provide consistency of the memory system across the multiple caches, a central directory should be maintained that all threads will consult when performing a memory read or write.

The directory will contain an entry for each block that is currently located in at least one of the caches. An entry will indicate whether the block is shared (read-only) by a set of caches, or if the block is owned by one cache.

There are a number of cases to consider:

You should properly synchronize your code so that updates to the caches and the directory are done safely. Synchronization should be done at the level of the set. Each set should be mapped to a mutex by taking the set number modulo the number of locks. Before reading or writing a word at a given address, lock the mutex for the set number of the address. This will ensure that any related directory entries or cache lines will not be modified by another CPU (thread) while the read/write is being completed.

Think carefully about the dirty bit when ownership of a block is being transferred or when a block is moving from being owned to being shared. When a block is moving from being owned to being shared, first write the block back to memory if its dirty bit is set. (One complication, if you do not want a thread to be updating a cache it does not own, is that the dirty bit will now be set for the block in the original cache. However, one solution would be to ignore the dirty bit for blocks that are not owned.) When a block is moving from being shared to being owned, the dirty bit should not be set, since the block has been in read-only mode.

A related tricky situation is to properly handle the invalidation of lines when the status of a block moves from shared to owned. If multiple caches have a copy of the block and then a write is performed to the block, the copies of the block in caches where the write is not performed are now out of date. If the status of the block later moves back to shared, you must be careful that the out-of-date copies are not mistakenly accessed. One solution would be to have the writer go into the other caches and mark the lines as invalid which contain the out-of-date copies.

A read or write is considered to hit in a cache, if no read or write needs to be performed to memory. That is, an operation hits in a cache even if the containing block needs to be first copied from another cache.

Your program will be graded primarily by testing it for correct functionality. In addition, however, you may lose points if your program is not properly structured and documented. Decompose sub-problems appropriately into functions and do incremental testing. Leave your debugging output in your code, but disabled, when you do your final assignment submission.

Supporting direct-mapped caches will be worth 60 points. Supporting set-associative caches will be worth 20 points. Supporting multiple caches utilized by separate threads (one thread per cache) will be worth 20 points.

Your programs will be graded using agate.cs.unh.edu so be sure to test in that environment.

Be sure you disable any debugging output before you submit your assignment.

You should submit all the source code for your assignment in one file called memSystem.c.

Your programs should be submitted for grading from agate.cs.unh.edu. To turn in this assignment, type:
~cs520/bin/submit prog6 memSystem.c

Submissions can be checked by typing:
~cs520/bin/scheck prog6

This assignment is due Sunday May 5. The standard late policy concerning late submissions will be in effect. See the course overview webpage.

Remember: as always you are expected to do your own work on this assignment. Copying code from another student or from sites on the internet is explicitly forbidden!


Last modified on May 2, 2013.

Comments and questions should be directed to hatcher@unh.edu