├── .gitattributes ├── .gitignore ├── README.md └── cpu-cache-simulator ├── cache.py ├── line.py ├── memory.py ├── simulator.py └── util.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # ========================= 61 | # Operating System Files 62 | # ========================= 63 | 64 | # OSX 65 | # ========================= 66 | 67 | .DS_Store 68 | .AppleDouble 69 | .LSOverride 70 | 71 | # Thumbnails 72 | ._* 73 | 74 | # Files that might appear in the root of a volume 75 | .DocumentRevisions-V100 76 | .fseventsd 77 | .Spotlight-V100 78 | .TemporaryItems 79 | .Trashes 80 | .VolumeIcon.icns 81 | 82 | # Directories potentially created on remote AFP share 83 | .AppleDB 84 | .AppleDesktop 85 | Network Trash Folder 86 | Temporary Items 87 | .apdisk 88 | 89 | # Windows 90 | # ========================= 91 | 92 | # Windows image file caches 93 | Thumbs.db 94 | ehthumbs.db 95 | 96 | # Folder config file 97 | Desktop.ini 98 | 99 | # Recycle Bin used on file shares 100 | $RECYCLE.BIN/ 101 | 102 | # Windows Installer files 103 | *.cab 104 | *.msi 105 | *.msm 106 | *.msp 107 | 108 | # Windows shortcuts 109 | *.lnk 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CPU Cache Simulator 2 | 3 | This is a simulator for a CPU cache that I wrote for a college course. It's 4 | meant to demonstrate some of the different 5 | [replacement](https://en.wikipedia.org/wiki/CPU_cache#Replacement_policies), 6 | [write](https://en.wikipedia.org/wiki/CPU_cache#Write_policies), and [mapping 7 | policies](https://en.wikipedia.org/wiki/CPU_cache#Associativity) that CPUs can 8 | implement. 9 | 10 | 11 | To run the CPU cache simulator: 12 | 13 | simulator.py [-h] MEMORY CACHE BLOCK MAPPING REPLACE WRITE 14 | 15 | 16 | Once you start the simulator, you can enter commands to modify and read from the 17 | memory (which is randomized on initilization), and therefore indirectly modify 18 | the cache. You can also print the contents of the memory and cache, as well as 19 | view statistics about the cache's performance. 20 | 21 | The arguments and commands, along with their descriptions, are listed below. 22 | 23 | 24 | ### Arguments 25 | 26 | **MEMORY** - size of main memory in 2^N bytes 27 | 28 | **CACHE** - size of the cache in 2^N bytes 29 | 30 | **BLOCK** - size of a block of memory in 2^N bytes 31 | 32 | **MAPPING** - mapping policy for cache in 2^N ways 33 | * 0 - direct mapping 34 | * \>0 - 2^N way associative 35 | 36 | **REPLACE** - replacement policy for cache 37 | * LRU - least recently used 38 | * LFU - least frequently used 39 | * FIFO - first-in, first-out 40 | * RAND - random 41 | 42 | **WRITE** - write policy for cache 43 | * WB - write back 44 | * WT - write through 45 | 46 | 47 | ### Commands 48 | 49 | **read** ADDRESS - read byte from memory 50 | 51 | **write** ADDRESS BYTE - write random byte to memory 52 | 53 | **randread** AMOUNT - read byte from random location in memory AMOUNT times 54 | 55 | **randwrite** AMOUNT - write random byte to random location in memory AMOUNT 56 | times 57 | 58 | **printcache** START LENGTH - print LENGTH lines of cache from START 59 | 60 | **printmem** START LENGTH - print LENGTH blocks of memory from START 61 | 62 | **stats** - print out hits, misses, and hit/miss ratio 63 | 64 | **quit** - quit the simulator 65 | 66 | ## Example 67 | 68 | Here is an example run: 69 | 70 | python simulator.py 10 7 3 2 LRU WT 71 | 72 | This creates a simulation with 2^10 bytes of memory, 2^7 bytes of cache, uses 73 | 8-way (2^3) associate mapping, least-recently used replacement policy, and 74 | write-through write policy. 75 | -------------------------------------------------------------------------------- /cpu-cache-simulator/cache.py: -------------------------------------------------------------------------------- 1 | import random 2 | import util 3 | from math import log 4 | from line import Line 5 | 6 | 7 | class Cache: 8 | 9 | """Class representing a processor's main cache.""" 10 | 11 | # Replacement policies 12 | LRU = "LRU" 13 | LFU = "LFU" 14 | FIFO = "FIFO" 15 | RAND = "RAND" 16 | 17 | # Mapping policies 18 | WRITE_BACK = "WB" 19 | WRITE_THROUGH = "WT" 20 | 21 | def __init__(self, size, mem_size, block_size, mapping_pol, replace_pol, 22 | write_pol): 23 | self._lines = [Line(block_size) for i in range(size // block_size)] 24 | 25 | self._mapping_pol = mapping_pol # Mapping policy 26 | self._replace_pol = replace_pol # Replacement policy 27 | self._write_pol = write_pol # Write policy 28 | 29 | self._size = size # Cache size 30 | self._mem_size = mem_size # Memory size 31 | self._block_size = block_size # Block size 32 | 33 | # Bit offset of cache line tag 34 | self._tag_shift = int(log(self._size // self._mapping_pol, 2)) 35 | # Bit offset of cache line set 36 | self._set_shift = int(log(self._block_size, 2)) 37 | 38 | def read(self, address): 39 | """Read a block of memory from the cache. 40 | 41 | :param int address: memory address for data to read from cache 42 | :return: block of memory read from the cache (None if cache miss) 43 | """ 44 | tag = self._get_tag(address) # Tag of cache line 45 | set = self._get_set(address) # Set of cache lines 46 | line = None 47 | 48 | # Search for cache line within set 49 | for candidate in set: 50 | if candidate.tag == tag and candidate.valid: 51 | line = candidate 52 | break 53 | 54 | # Update use bits of cache line 55 | if line: 56 | if (self._replace_pol == Cache.LRU or 57 | self._replace_pol == Cache.LFU): 58 | self._update_use(line, set) 59 | 60 | return line.data if line else line 61 | 62 | def load(self, address, data): 63 | """Load a block of memory into the cache. 64 | 65 | :param int address: memory address for data to load to cache 66 | :param list data: block of memory to load into cache 67 | :return: tuple containing victim address and data (None if no victim) 68 | """ 69 | tag = self._get_tag(address) # Tag of cache line 70 | set = self._get_set(address) # Set of cache lines 71 | victim_info = None 72 | 73 | # Select the victim 74 | if (self._replace_pol == Cache.LRU or 75 | self._replace_pol == Cache.LFU or 76 | self._replace_pol == Cache.FIFO): 77 | victim = set[0] 78 | 79 | for index in range(len(set)): 80 | if set[index].use < victim.use: 81 | victim = set[index] 82 | 83 | victim.use = 0 84 | 85 | if self._replace_pol == Cache.FIFO: 86 | self._update_use(victim, set) 87 | elif self._replace_pol == Cache.RAND: 88 | index = random.randint(0, self._mapping_pol - 1) 89 | victim = set[index] 90 | 91 | # Store victim info if modified 92 | if victim.modified: 93 | victim_info = (index, victim.data) 94 | 95 | # Replace victim 96 | victim.modified = 0 97 | victim.valid = 1 98 | victim.tag = tag 99 | victim.data = data 100 | 101 | return victim_info 102 | 103 | def write(self, address, byte): 104 | """Write a byte to cache. 105 | 106 | :param int address: memory address for data to write to cache 107 | :param int byte: byte of data to write to cache 108 | :return: boolean indicating whether data was written to cache 109 | """ 110 | tag = self._get_tag(address) # Tag of cache line 111 | set = self._get_set(address) # Set of cache lines 112 | line = None 113 | 114 | # Search for cache line within set 115 | for candidate in set: 116 | if candidate.tag == tag and candidate.valid: 117 | line = candidate 118 | break 119 | 120 | # Update data of cache line 121 | if line: 122 | line.data[self.get_offset(address)] = byte 123 | line.modified = 1 124 | 125 | if (self._replace_pol == Cache.LRU or 126 | self._replace_pol == Cache.LFU): 127 | self._update_use(line, set) 128 | 129 | return True if line else False 130 | 131 | def print_section(self, start, amount): 132 | """Print a section of the cache. 133 | 134 | :param int start: start address to print from 135 | :param int amount: amount of lines to print 136 | """ 137 | line_len = len(str(self._size // self._block_size - 1)) 138 | use_len = max([len(str(i.use)) for i in self._lines]) 139 | tag_len = int(log(self._mapping_pol * self._mem_size // self._size, 2)) 140 | address_len = int(log(self._mem_size, 2)) 141 | 142 | if start < 0 or (start + amount) > (self._size // self._block_size): 143 | raise IndexError 144 | 145 | print("\n" + " " * line_len + " " * use_len + " U M V T" + 146 | " " * tag_len + "") 147 | 148 | for i in range(start, start + amount): 149 | print(util.dec_str(i, line_len) + ": " + 150 | util.dec_str(self._lines[i].use, use_len) + " " + 151 | util.bin_str(self._lines[i].modified, 1) + " " + 152 | util.bin_str(self._lines[i].valid, 1) + " " + 153 | util.bin_str(self._lines[i].tag, tag_len) + " <" + 154 | " ".join([util.hex_str(i, 2) for i in self._lines[i].data]) + " @ " + 155 | util.bin_str(self.get_physical_address(i), address_len) + ">") 156 | print() 157 | 158 | def get_physical_address(self, index): 159 | """Get the physical address of the cache line at index. 160 | 161 | :param int index: index of cache line to get physical address of 162 | :return: physical address of cache line 163 | """ 164 | set_num = index // self._mapping_pol 165 | 166 | return ((self._lines[index].tag << self._tag_shift) + 167 | (set_num << self._set_shift)) 168 | 169 | def get_offset(self, address): 170 | """Get the offset from within a set from a physical address. 171 | 172 | :param int address: memory address to get offset from 173 | """ 174 | return address & (self._block_size - 1) 175 | 176 | def _get_tag(self, address): 177 | """Get the cache line tag from a physical address. 178 | 179 | :param int address: memory address to get tag from 180 | """ 181 | return address >> self._tag_shift 182 | 183 | def _get_set(self, address): 184 | """Get a set of cache lines from a physical address. 185 | 186 | :param int address: memory address to get set from 187 | """ 188 | set_mask = (self._size // (self._block_size * self._mapping_pol)) - 1 189 | set_num = (address >> self._set_shift) & set_mask 190 | index = set_num * self._mapping_pol 191 | return self._lines[index:index + self._mapping_pol] 192 | 193 | def _update_use(self, line, set): 194 | """Update the use bits of a cache line. 195 | 196 | :param line line: cache line to update use bits of 197 | """ 198 | if (self._replace_pol == Cache.LRU or 199 | self._replace_pol == Cache.FIFO): 200 | use = line.use 201 | 202 | if line.use < self._mapping_pol: 203 | line.use = self._mapping_pol 204 | for other in set: 205 | if other is not line and other.use > use: 206 | other.use -= 1 207 | elif self._replace_pol == Cache.LFU: 208 | line.use += 1 209 | -------------------------------------------------------------------------------- /cpu-cache-simulator/line.py: -------------------------------------------------------------------------------- 1 | class Line: 2 | 3 | """Class representing a line within a processor's main cache.""" 4 | 5 | def __init__(self, size): 6 | self.use = 0 7 | self.modified = 0 8 | self.valid = 0 9 | self.tag = 0 10 | self.data = [0] * size 11 | -------------------------------------------------------------------------------- /cpu-cache-simulator/memory.py: -------------------------------------------------------------------------------- 1 | import util 2 | 3 | 4 | class Memory: 5 | 6 | """Class representing main memory as an array of bytes.""" 7 | 8 | def __init__(self, size, block_size): 9 | """Initialize main memory with a set number of bytes and block size.""" 10 | self._size = size # Memory size 11 | self._block_size = block_size # Block size 12 | self._data = [util.rand_byte() for i in range(size)] 13 | 14 | def print_section(self, start, amount): 15 | """Print a section of main memory. 16 | 17 | :param int start: start address to print from 18 | :param int amount: amount of blocks to print 19 | """ 20 | address_len = len(str(self._size - 1)) 21 | start = start - (start % self._block_size) 22 | amount *= self._block_size 23 | 24 | if start < 0 or (start + amount) > self._size: 25 | raise IndexError 26 | 27 | print() 28 | for i in range(start, start + amount, self._block_size): 29 | print(util.dec_str(i, address_len) + ": " + 30 | " ".join([util.hex_str(i, 2) for i in self.get_block(i)])) 31 | print() 32 | 33 | def get_block(self, address): 34 | """Get the block of main memory (of size self._block_size) that contains 35 | the byte at address. 36 | 37 | :param int address: address of byte within block of memory 38 | :return: block from main memory 39 | """ 40 | start = address - (address % self._block_size) # Start address 41 | end = start + self._block_size # End address 42 | 43 | if start < 0 or end > self._size: 44 | raise IndexError 45 | 46 | return self._data[start:end] 47 | 48 | def set_block(self, address, data): 49 | """Set the block of main memory (of size self._block_size) that contains 50 | the byte at address. 51 | 52 | :param int address: address of byte within block of memory 53 | :param list data: bytes to set as block of memory 54 | """ 55 | start = address - (address % self._block_size) # Start address 56 | end = start + self._block_size # End address 57 | 58 | if start < 0 or end > self._size: 59 | raise IndexError 60 | 61 | self._data[start:end] = data 62 | -------------------------------------------------------------------------------- /cpu-cache-simulator/simulator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import random 3 | import util 4 | from cache import Cache 5 | from memory import Memory 6 | 7 | 8 | def read(address, memory, cache): 9 | """Read a byte from cache.""" 10 | cache_block = cache.read(address) 11 | 12 | if cache_block: 13 | global hits 14 | hits += 1 15 | else: 16 | block = memory.get_block(address) 17 | victim_info = cache.load(address, block) 18 | cache_block = cache.read(address) 19 | 20 | global misses 21 | misses += 1 22 | 23 | # Write victim line's block to memory if replaced 24 | if victim_info: 25 | memory.set_block(victim_info[0], victim_info[1]) 26 | 27 | return cache_block[cache.get_offset(address)] 28 | 29 | 30 | def write(address, byte, memory, cache): 31 | """Write a byte to cache.""" 32 | written = cache.write(address, byte) 33 | 34 | if written: 35 | global hits 36 | hits += 1 37 | else: 38 | global misses 39 | misses += 1 40 | 41 | if args.WRITE == Cache.WRITE_THROUGH: 42 | # Write block to memory 43 | block = memory.get_block(address) 44 | block[cache.get_offset(address)] = byte 45 | memory.set_block(address, block) 46 | elif args.WRITE == Cache.WRITE_BACK: 47 | if not written: 48 | # Write block to cache 49 | block = memory.get_block(address) 50 | cache.load(address, block) 51 | cache.write(address, byte) 52 | 53 | 54 | replacement_policies = ["LRU", "LFU", "FIFO", "RAND"] 55 | write_policies = ["WB", "WT"] 56 | 57 | parser = argparse.ArgumentParser(description="Simulate the cache of a CPU.") 58 | 59 | parser.add_argument("MEMORY", metavar="MEMORY", type=int, 60 | help="Size of main memory in 2^N bytes") 61 | parser.add_argument("CACHE", metavar="CACHE", type=int, 62 | help="Size of the cache in 2^N bytes") 63 | parser.add_argument("BLOCK", metavar="BLOCK", type=int, 64 | help="Size of a block of memory in 2^N bytes") 65 | parser.add_argument("MAPPING", metavar="MAPPING", type=int, 66 | help="Mapping policy for cache in 2^N ways") 67 | parser.add_argument("REPLACE", metavar="REPLACE", choices=replacement_policies, 68 | help="Replacement policy for cache {"+", ".join(replacement_policies)+"}") 69 | parser.add_argument("WRITE", metavar="WRITE", choices=write_policies, 70 | help="Write policy for cache {"+", ".join(write_policies)+"}") 71 | 72 | args = parser.parse_args() 73 | 74 | mem_size = 2 ** args.MEMORY 75 | cache_size = 2 ** args.CACHE 76 | block_size = 2 ** args.BLOCK 77 | mapping = 2 ** args.MAPPING 78 | 79 | hits = 0 80 | misses = 0 81 | 82 | memory = Memory(mem_size, block_size) 83 | cache = Cache(cache_size, mem_size, block_size, 84 | mapping, args.REPLACE, args.WRITE) 85 | 86 | mapping_str = "2^{0}-way associative".format(args.MAPPING) 87 | print("\nMemory size: " + str(mem_size) + 88 | " bytes (" + str(mem_size // block_size) + " blocks)") 89 | print("Cache size: " + str(cache_size) + 90 | " bytes (" + str(cache_size // block_size) + " lines)") 91 | print("Block size: " + str(block_size) + " bytes") 92 | print("Mapping policy: " + ("direct" if mapping == 1 else mapping_str) + "\n") 93 | 94 | command = None 95 | 96 | while (command != "quit"): 97 | operation = input("> ") 98 | operation = operation.split() 99 | 100 | try: 101 | command = operation[0] 102 | params = operation[1:] 103 | 104 | if command == "read" and len(params) == 1: 105 | address = int(params[0]) 106 | byte = read(address, memory, cache) 107 | 108 | print("\nByte 0x" + util.hex_str(byte, 2) + " read from " + 109 | util.bin_str(address, args.MEMORY) + "\n") 110 | 111 | elif command == "write" and len(params) == 2: 112 | address = int(params[0]) 113 | byte = int(params[1]) 114 | 115 | write(address, byte, memory, cache) 116 | 117 | print("\nByte 0x" + util.hex_str(byte, 2) + " written to " + 118 | util.bin_str(address, args.MEMORY) + "\n") 119 | 120 | elif command == "randread" and len(params) == 1: 121 | amount = int(params[0]) 122 | 123 | for i in range(amount): 124 | address = random.randint(0, mem_size - 1) 125 | read(address, memory, cache) 126 | 127 | print("\n" + str(amount) + " bytes read from memory\n") 128 | 129 | elif command == "randwrite" and len(params) == 1: 130 | amount = int(params[0]) 131 | 132 | for i in range(amount): 133 | address = random.randint(0, mem_size - 1) 134 | byte = util.rand_byte() 135 | write(address, byte, memory, cache) 136 | 137 | print("\n" + str(amount) + " bytes written to memory\n") 138 | 139 | elif command == "printcache" and len(params) == 2: 140 | start = int(params[0]) 141 | amount = int(params[1]) 142 | 143 | cache.print_section(start, amount) 144 | 145 | elif command == "printmem" and len(params) == 2: 146 | start = int(params[0]) 147 | amount = int(params[1]) 148 | 149 | memory.print_section(start, amount) 150 | 151 | elif command == "stats" and len(params) == 0: 152 | ratio = (hits / ((hits + misses) if misses else 1)) * 100 153 | 154 | print("\nHits: {0} | Misses: {1}".format(hits, misses)) 155 | print("Hit/Miss Ratio: {0:.2f}%".format(ratio) + "\n") 156 | 157 | elif command != "quit": 158 | print("\nERROR: invalid command\n") 159 | 160 | except IndexError: 161 | print("\nERROR: out of bounds\n") 162 | except: 163 | print("\nERROR: incorrect syntax\n") 164 | -------------------------------------------------------------------------------- /cpu-cache-simulator/util.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | def rand_byte(): 5 | """Get a random byte. 6 | 7 | :return: random byte (integer from 0 - 255) 8 | """ 9 | return random.randint(0, 0xFF) 10 | 11 | 12 | def dec_str(integer, width): 13 | """Get decimal formatted string representation of an integer. 14 | 15 | :param int byte: integer to be converted to decimal string 16 | :return: decimal string representation of integer 17 | """ 18 | return "{0:0>{1}}".format(integer, width) 19 | 20 | 21 | def bin_str(integer, width): 22 | """Get binary formatted string representation of an integer. 23 | 24 | :param int byte: integer to be converted to binary string 25 | :return: binary string representation of integer 26 | """ 27 | return "{0:0>{1}b}".format(integer, width) 28 | 29 | 30 | def hex_str(integer, width): 31 | """Get hexadecimal formatted string representation of an integer. 32 | 33 | :param int byte: integer to be converted to hexadecimal string 34 | :return: hexadecimal string representation of integer 35 | """ 36 | return "{0:0>{1}X}".format(integer, width) 37 | --------------------------------------------------------------------------------