diff --git a/.gitignore b/.gitignore index 8a7b0136d824f8609f49564b9fb61c48ec728ab0..4a864ef687a3d0450986ab7500b71ba85fa18034 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,23 @@ -# Do not track local environment settings/solution. -.vscode/ +# Do not track local environment settings. +.env/ +.venv/ .idea/ .vs/ +.vscode/ +env/ +venv/ # Ignore compiled files. *.out .csim_results + + +# Do not track python cache files. +*.pyc + + +# Do not track local environment logs. +*.log +*.log.* diff --git a/documents/references.md b/documents/references.md index c7f621cd066dcf6e08f0535aeaf75cd797bdced9..418d61fde84a2106b9c0c2fd63184d671ab6736c 100644 --- a/documents/references.md +++ b/documents/references.md @@ -4,9 +4,30 @@ All references to external logic. Includes anything from stack overflow links to notes about logic from previous works. -## C -### All Printf Format Types -<https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm> +## Python +### Parsing Command Line Args with ArgParse +<https://docs.python.org/3/library/argparse.html> + +### Display Int as Binary +<https://stackoverflow.com/a/699891> + +### Exponents and Logarithms +<https://docs.python.org/3.8/library/math.html#power-and-logarithmic-functions> + +### Splitting a String on Regex Match +<https://stackoverflow.com/a/13209313> + +### Dynamically Creating Bit Masks +<https://stackoverflow.com/a/26303439> + +### Bitwise Operations +<https://www.tutorialspoint.com/python/bitwise_operators_example.htm> + +### Character to Int +<https://stackoverflow.com/a/704160> + +### Parse Hex Values +<https://stackoverflow.com/a/604413> ### Printing all Values in Argv <https://stackoverflow.com/a/35861090> @@ -43,6 +64,9 @@ similarly in that code is fed in one or more input files from a given directory. ### Getting Last Argument <https://www.cyberciti.biz/faq/linux-unix-bsd-apple-osx-bash-get-last-argument/> +### Getting Arg Count +<https://linuxconfig.org/bash-script-how-to-check-number-of-supplied-command-line-arguments> + ### Color Output <https://stackoverflow.com/a/5947802> diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..d18666dcb976102f01c72ccd7ee263e808173192 --- /dev/null +++ b/main.py @@ -0,0 +1,378 @@ +""" +Date: 02-28-20 +Class: CS 5541 +Assignment: Cache Simulator +Author: Brandon Rodriguez +Email: bfp5870@wmich.edu + + +Simulates system use of a cache for data storage. +Cache Settings Arguments: + * S: Number of sets in cache. + * E: Lines per set. + * B: Block size (number of bytes per block). + * s: Bit count to identify set. + * e: Bit count to identify line. + * b: Bit count to identify block. +""" + +# System Imports. +import argparse, math, re + +# User Imports. + + +class CacheSimulator(): + def __init__(self): + """ + Class initialization. + """ + self.s = None # Number of bits to identify set. + self.S = None # Number of sets in cache. + self.e = None # Number of bits to identify line. + self.E = None # Number of lines per set. + self.b = None # Number of bits to identify block byte. + self.B = None # Number of bytes in a block. + self.m = None # Total number of identifying bits in address. + self.M = None # Total addresses in cache. + + self.cache = None + self.access_log = None + self.hits = None + self.misses = None + self.evictions = None + self.verbose = False + + def main(self, s, E, b, file_name, verbose): + """ + Program main. + """ + if verbose: + self.verbose = True + + # Calculate total sizes of cache structures. + self.s = s + self.S = int(math.pow(2, self.s)) + self.E = E + self.e = int(math.log2(self.E)) + self.b = b + self.B = int(math.pow(2, self.b)) + self.m = self.E * self.S + # self.M = int(math.pow(2, self.m)) + self.M = int(math.pow(2, 64)) + + # Create cache object. + self.cache = [] + self.access_log = [] + for set in range(self.S): + self.cache.append([]) + self.access_log.append([]) + + # Create lines in set. + for line in range(self.E): + self.cache[len(self.cache) - 1].append({ + 'valid': False, + 'tag': 0, + 'block': [], + }) + + # Run simulation on cache, using file. + self.run_simulation(file_name) + + def run_simulation(self, file_name): + """ + Runs full simulation of cache, using given input file. + :param file_name: Name of file to read from. + """ + self.hits = 0 + self.misses = 0 + self.evictions = 0 + + # Open file to read in values. + with open(file_name) as file: + # Read in line. + for line in file: + if line.strip() != '': + self.handle_line(line) + + print('hits:{0} misses:{1} evictions:{2}'.format(self.hits, self.misses, self.evictions)) + + def handle_line(self, line, main_bool=True): + """ + Handles cache simulation for given line instruction. + :param line: Line to handle for. + :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one. + Note that only initial cache line read ins will print or update missed/skipped/evicted variables. + """ + split_line = re.split(r'[ ,\n]', line) + + # Check if instruction load. If so, we can skip line. + if split_line[0] == 'I': + # Found instruction load. Returning. + return + else: + line = line.strip() + + # Not an instruction load. Keep processing. Start by parsing line info. + parsed_data = {} + try: + # Try parsing as hex. + address = int(split_line[2], 16) + except ValueError: + # Invalid address. Attempt to correct bad values. + address = '' + for character in split_line[2]: + # Check if int value of character is greater than f. + if ord(character.lower()) > 102: + address += 'f' + else: + # Not a value less than "f". Attempt to_lower, I guess? + address += character.lower() + + # Attempt again. On error, assume invalid and skip line. + try: + address = int(address, 16) + except ValueError: + return + + parsed_data['address'] = address + + # Attempt to read size value. On error, assume invalid and skip line. + try: + parsed_data['value_size'] = int(split_line[3], 16) - 1 + except ValueError: + return + + # Get mask values. + block_mask = self.B - 1 + block_offset = self.b + set_mask = ((self.S << block_offset) - 1) ^ block_mask + set_offset = self.s + tag_mask = (self.M - 1) ^ set_mask ^ block_mask + + # Use masks to get address chunks. + parsed_data['block'] = address & block_mask + parsed_data['set'] = (address & set_mask) >> block_offset + parsed_data['tag'] = (address & tag_mask) >> (block_offset + set_offset) + + # Check which of 3 operations apply. + if split_line[1] == 'L': + # Is load. + self.process_line(line, parsed_data, main_bool) + + elif split_line[1] == 'S': + # Is store. + self.process_line(line, parsed_data, main_bool) + + elif split_line[1] == 'M': + # Load then store. + load_result = self.process_line(line, parsed_data, False) + store_result = self.process_line(line, parsed_data, False) + + # Check if this is an instruction directly from file. + if main_bool: + # Is instruction from file. Print data. + if load_result == 0 and store_result == 0: + if self.verbose: + print('{0} hit hit Decimal Format: {1}'.format(line, parsed_data['address'])) + self.hits += 2 + elif load_result == 1 and store_result == 0: + if self.verbose: + print('{0} miss hit Decimal Format: {1}'.format(line, parsed_data['address'])) + self.misses += 1 + self.hits += 1 + elif load_result == 2 and store_result == 0: + if self.verbose: + print('{0} miss eviction hit Decimal Format: {1}'.format(line, parsed_data['address'])) + self.misses += 1 + self.evictions += 1 + self.hits += 1 + + else: + # Unknown operation. + raise ValueError('Unknown operation "{0}"'.format(split_line[1])) + + def print_cache(self): + """ + Prints simulated cache in an easier to read format. + """ + print('\nCache:') + for set_index in range(len(self.cache)): + print(' Set {0}:'.format(set_index, self.cache[set_index])) + for line_index in range(len(self.cache[set_index])): + print(' Line {0}: {1}'.format(line_index, self.cache[set_index][line_index])) + print('') + + def process_line(self, line, parsed_data, main_bool): + """ + Handle store instruction. Works as described in class video. + :param line: Original line read in. + :param parsed_data: Data parsed from line. + :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one. + Note that only initial cache line read ins will print or update missed/skipped/evicted variables. + :return: 0 on hit | 1 on miss | 2 on eviction. + """ + set = parsed_data['set'] + + # Compare against the all entries in the cache for match. + for index in range(len(self.cache[set])): + # Check if match. + if self.cache[set][index]['valid'] is True and self.cache[set][index]['tag'] == parsed_data['tag']: + # Match found. + self.process_hit(line, parsed_data, index, main_bool) + return 0 + else: + # Not a match. Examine why. + if self.cache[set][index]['valid'] is False: + # Cache location was not set. Cold miss. We can just set and return. + self.process_miss(line, parsed_data, index, main_bool) + return 1 + + # If we made it this far, cache locations were set, but tags did not match. Conflict miss. + self.process_eviction(line, parsed_data, main_bool) + return 2 + + def process_hit(self, line, parsed_data, index, main_bool): + """ + Processes simulated cache for a hit. + :param line: Original line read in. + :param parsed_data: Data parsed from line. + :param index: Current index in set. + :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one. + Note that only initial cache line read ins will print or update missed/skipped/evicted variables. + """ + if main_bool: + # Is main cache file line. Update hits/misses/evictions. + self.hits += 1 + + # Print if verbose flag set. + if self.verbose: + print('{0} hit Decimal Format: {1}'.format(line, parsed_data['address'])) + + self.update_access_log(parsed_data['set'], index) + + def process_miss(self, line, parsed_data, index, main_bool): + """ + Processes simulated cache for a miss. + :param line: Original line read in. + :param parsed_data: Data parsed from line. + :param index: Current index in set. + :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one. + Note that only initial cache line read ins will print or update missed/skipped/evicted variables. + """ + if main_bool: + # Is main cache file line. Update hits/misses/evictions. + self.misses += 1 + + # Print if verbose flag set. + if self.verbose: + print('{0} miss Decimal Format: {1}'.format(line, parsed_data['address'])) + + # Pull out relevant data from passed dict. + address = parsed_data['address'] + value_size = parsed_data['value_size'] + set = parsed_data['set'] + tag = parsed_data['tag'] + + self.cache[set][index]['valid'] = True + self.cache[set][index]['tag'] = tag + + # Set block values that are "loaded" into memory. + lower_offset = int(address / self.B) * self.B + upper_offset = (int(address / self.B) + 1) * self.B + value_offset = address + value_size + self.cache[set][index]['block'] = [] + for i in range(self.B): + self.cache[set][index]['block'].append(lower_offset + i) + + self.update_access_log(set, index) + + # Check if size sets address outside of current cache block. + if value_offset > upper_offset: + # Size sets address outside of current cache block. Update additional blocks as needed. + # Start by getting our new size. + new_size = value_offset - upper_offset + if new_size < 0: + new_size = 0 + + # Pass our new line data. + new_line = ' L {0},{1}'.format(upper_offset, new_size) + self.handle_line(new_line, main_bool=False) + + def process_eviction(self, line, parsed_data, main_bool): + """ + Processes simulated cache for an eviction. + :param line: Original line read in. + :param parsed_data: Data parsed from line. + :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one. + Note that only initial cache line read ins will print or update missed/skipped/evicted variables. + """ + if main_bool: + # Is main cache file line. Update hits/misses/evictions. + self.misses += 1 + self.evictions += 1 + + # Print if verbose flag set. + if self.verbose: + print('{0} miss eviction Decimal Format: {1}'.format(line, parsed_data['address'])) + + # Pull out relevant data from passed dict. + address = parsed_data['address'] + value_size = parsed_data['value_size'] + set = parsed_data['set'] + tag = parsed_data['tag'] + + # Get LRU index from set. + index = self.access_log[set][0] + + self.cache[set][index]['tag'] = tag + + # Set block values that are "loaded" into memory. + lower_offset = int(address / self.B) * self.B + upper_offset = (int(address / self.B) + 1) * self.B + value_offset = address + value_size + self.cache[set][index]['block'] = [] + for i in range(self.B): + self.cache[set][index]['block'].append(lower_offset + i) + + self.update_access_log(set, index) + + # Check if size sets address outside of current cache block. + if value_offset >= upper_offset: + # Size sets address outside of current cache block. Update additional blocks as needed. + # Start by getting our new size. + new_size = value_offset - upper_offset + if new_size < 0: + new_size = 0 + + # Pass our new line data. + new_line = ' L {0},{1}'.format(upper_offset, new_size) + self.handle_line(new_line, main_bool=False) + + def update_access_log(self, set, index): + """ + Updates set's access log according to "Least Recently Used" ordering. + Farther from index 0 means more recently used. + :param set: Set to update access log of. + :param index: Index to update in set. + """ + # Check if index is currently in set's log. If so, remove. + if index in self.access_log[set]: + self.access_log[set].remove(index) + + # Add index to end of current set's log. That way, log is organized starting with "least used" first. + self.access_log[set].append(index) + + +if __name__ == '__main__': + # Define our argparser and get command line args. + parser = argparse.ArgumentParser(description='Cache Simulator') + parser.add_argument('-v', '-V', action='store_true', default=False, help='Flag for verbose mode.', dest='verbose') + parser.add_argument('-s', action='store', required=True, help='Total sets.', dest='set_count') + parser.add_argument('-E', action='store', required=True, help='Lines per set.', dest='line_count') + parser.add_argument('-b', action='store', required=True, help='Bytes per block.', dest='block_size') + parser.add_argument('-t', '-T', action='store', required=True, help='File to read in.', dest='file_name') + args = parser.parse_args() + + # If args parsed properly, run program main. + CacheSimulator().main(int(args.set_count), int(args.line_count), int(args.block_size), args.file_name, args.verbose) diff --git a/makefile b/makefile deleted file mode 100644 index bd2568aed1d28e23f307dd7dcf88390348d675ee..0000000000000000000000000000000000000000 --- a/makefile +++ /dev/null @@ -1,42 +0,0 @@ - -# Tell makefile to use commands defined here, if file with same name exists. -.PHONY: all compile run valgrind clean -# Set default if none is specified. -default: valgrind clean - - -# Fully build and execute project. -all: compile run clean - - -# Compile program. -compile: - gcc -Wall -Wpedantic -std=c99 *.c - - -# Compile and run program. -run: compile - ./a.out - - -# Compile program and run with valgrind (memory checking). -valgrind: compile - valgrind --leak-check=full ./a.out - - -# Clean up directory. -clean: - @rm -f *.o - @rm -f a.out - - -# Display help output. -help: - @echo "Default: Executes \"valgrind\", then \"clean\"." - @echo "" - @echo "Available Commands:" - @echo " all - Executes \"run\", then \"clean\"." - @echo " compile - Compiles program to \"a.out\"." - @echo " run - Compiles program to \"a.out\". Then run." - @echo " valgrind - Compiles program to \"a.out\". Then run with memory error checking." - @echo " clean - Remove temporary/compiled files from directory." diff --git a/readme.md b/readme.md index bf76f33ac138453332093b0903e55e124667cfd7..1b7e97086034a555729778a3eb120a2772564e40 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,8 @@ -# C - Cache Simulator +# Python - Cache Simulator ## Language -C +Python ## Description @@ -11,8 +11,8 @@ A project to simulate system use of a cache for data storage. ## Running Program Open a terminal and cd to project root.<br> -For a list of available makefile commands, run `make help`.<br> -In a terminal, run `make <command_here>`. +Use the `run.sh` bash script to execute file.<br> +For example, type `./run.sh -v -s 2 -E 1 -b 4 -t ./wmucachelab/traces/dave.trace` ## References diff --git a/run.sh b/run.sh index a1b3aad1dfa91f1ee3fb9f3392b7138a116d9182..69730dec3a4bd074539cc5e5b849c2f5f16d6033 100755 --- a/run.sh +++ b/run.sh @@ -23,12 +23,13 @@ cd "$(dirname "$0")" function main () { - # Check if first arg was provided. - if [[ $1 != "" ]] + # Check that required number of args were provided. + if [[ $# -lt 8 ]] then - execute_single_file $@ + # Not enough arguments. Display help. + display_help else - execute_all_files $@ + execute_single_file $@ fi echo "" @@ -57,26 +58,26 @@ function execute_single_file () { then file=$return_value - echo -e "${color_blue}Found input file \"$file\". Attempting to compile C code.${color_reset}" - - # Attempt to compile C code. - make compile + echo -e "${color_blue}Found input file \"$file\".${color_reset}" # Run input file on example cache simulator file. echo "" echo -e "${color_blue}Running input file on \"wmucachelab/csim-ref\". Runtime args:${color_reset}" - echo -e "${color_blue} -v -s 4 -E 1 -b 4 -t $file${color_reset}" - ./wmucachelab/csim-ref -v -s 4 -E 1 -b 4 -t $file + echo -e "${color_blue} $@${color_reset}" + ./wmucachelab/csim-ref $@ +# ./wmucachelab/csim-ref -v -s 1 -E 2 -b 1 -t $file # Run input file on compiled C code. echo "" - echo -e "${color_blue}Running input file on compiled C code. Runtime args:${color_reset}" - echo -e "${color_blue} -v -s 4 -E 1 -b 4 -t $file${color_reset}" - valgrind --leak-check=full ./a.out -v -s 4 -E 1 -b 4 -t $file + echo -e "${color_blue}Running input file on Python code. Runtime args:${color_reset}" + echo -e "${color_blue} $@${color_reset}" +# python3 main.py $@ + valgrind --leak-check=full ./a.out $@ # Clean up directory. make clean + else echo -e "${color_red}Failed to determine absolute path for input file.${color_reset}" echo "Terminating script." @@ -93,15 +94,6 @@ function execute_single_file () { } -### - # Runs all cache simulation files. - ## -function execute_all_files () { - echo -e "${color_red}Not yet implemented.${color_reset}" - exit 1 -} - - ### # Gets absolute path of provided file/directory. ## @@ -117,4 +109,23 @@ function get_absolute_path () { } +function display_help () { + echo "" + echo "Cache Simulator" + echo "Description:" + echo " Runs two cache simulator programs." + echo " First one is a premade, \"correct\" simulator with proper results." + echo " The second one is this custom Python implementation." + echo " Run one after another to compare results." + echo "" + echo "Options:" + echo " -s <#> Number of set bits for cache." + echo " -E <#> Number of lines per set for cache." + echo " -b <#> Number of block offset bits for cache." + echo " -t <file> Input file to use as trace." + echo " -v Optional verbose flag." + echo "" +} + + main $@ diff --git a/wmucachelab/traces/book_pg660_nice.trace b/wmucachelab/traces/book_pg660_nice.trace new file mode 100644 index 0000000000000000000000000000000000000000..8e5202aac4c2f19b35f1237a3b142a80259d2128 --- /dev/null +++ b/wmucachelab/traces/book_pg660_nice.trace @@ -0,0 +1,16 @@ + S 0,4 + S 30,4 + S 4,4 + S 34,4 + S 8,4 + S 38,4 + S C,4 + S 3C,4 + S 10,4 + S 40,4 + S 14,4 + S 44,4 + S 18,4 + S 48,4 + S 1C,4 + S 4C,4 diff --git a/wmucachelab/traces/book_pg660_thrashing.trace b/wmucachelab/traces/book_pg660_thrashing.trace new file mode 100644 index 0000000000000000000000000000000000000000..fd2fc91edd973c885a74f69a3cdbfdf5188e4719 --- /dev/null +++ b/wmucachelab/traces/book_pg660_thrashing.trace @@ -0,0 +1,16 @@ + S 0,4 + S 20,4 + S 4,4 + S 24,4 + S 8,4 + S 28,4 + S C,4 + S 2C,4 + S 16,4 + S 30,4 + S 14,4 + S 34,4 + S 18,4 + S 38,4 + S 1C,4 + S 3C,4 diff --git a/wmucachelab/traces/class_video_example.trace b/wmucachelab/traces/class_video_example.trace new file mode 100644 index 0000000000000000000000000000000000000000..bc0c62d3bef5c3f6ff9a613341f928c1ef4dccb9 --- /dev/null +++ b/wmucachelab/traces/class_video_example.trace @@ -0,0 +1,5 @@ + S 0,1 + S 1,1 + S 7,1 + S 8,1 + S 0,1