commit 5c81b63fe807b9821bd68c43211a292ccd9b55d8 Author: Tyler Perkins Date: Mon Aug 23 13:40:34 2021 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4febf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +#ignore all .o files +src/**/*.o + +#ignore all swp files +**/*.swp + +#ignore all executables, but still keep the bin folder +bin/* +!bin/.gitkeep diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..53db516 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +############################################################################### +# Tyler Perkins +# 7-21-21 +# Makefile +# + +CC = g++ + +FLAGS = -pipe + +CFLAGS = -Wall +CFLAGS += -Ofast +#CFLAGS += -g +CFLAGS += -pg + +LIBRARIES = + +SRC = $(shell find . -name '*.cpp') +HPP = $(shell find . -name '*.hpp') +OBJ = $(subst .cpp,.o,$(SRC)) +BIN = ./bin + +TARGET = lrucache-test.out + +all : $(BIN)/$(TARGET) + +$(BIN)/$(TARGET) : $(OBJ) + @echo LD $(TARGET) + @$(CC) $(FLAGS) $(CFLAGS) -o $(BIN)/$(TARGET) $(OBJ) $(LIBRARIES) + +.cpp.o : + @echo CC $< + @$(CC) $(FLAGS) $(CFLAGS) $(LIBRARIES) -c $< -o $@ -D_LRU_DEBUG_ + +clean : + find . -type f -name '*.o' -delete + rm -rf $(TARGET) performance.prof gmon.out + +sanity : $(BIN)/$(TARGET) + $(BIN)/$(TARGET) + +profile: sanity + gprof $(BIN)/$(TARGET) > performance.prof diff --git a/README.md b/README.md new file mode 100644 index 0000000..116c087 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +LRUCache +======== + +This is my implementation of a LRU (Least Recently Used) Cache. +The cache will store values and keep track of what was accessed most recently. +This is great to help reduce the amount of repetitive malloc calls, for +example. + +This implementation differs from most in that: + +1. It is associate +2. Get always returns a value + +The big use for me was get() creating values. If a value is not found in the +cache, it creates the value T(Key) and returns that. This does not have to be +used, however, if you only use get() on values you are positive have been put() +into the cache. + +To use this in a project, just copy lru.hpp into your own projects source tree. +The namespace it uses is clortox + +Run tests +========= + +You can run the test suite with the following: + +```bash +make sanity + +# Relies on gprof +make profile +``` + +Interface +========= + +Constructors +------------ + +``` +LRUCache() +LRUCache(size_t max_size) +``` + +Defaults size of cache to 50 + +Utility +------- + +``` +void clear() +``` + +Clears out the contents of the cache + +``` +bool empty() const +``` + +Returns true if the cache is empty + +``` +bool full() const +``` + +Returns true if the cache is full + +``` +size_t getMaxCacheSize() const +``` + +Returns the cache size, passed as `max_size` when constructing + +Accessors +-------- + +``` +T& get(const Key&) +T& operator[](const Key&) +T& operator[](Key&) +``` + +Returns T associated with Key. If none exists, creates T(key), inserts it into +the cache, and returns a reference to it + +``` +void put(const Key&, const T&) +``` + +Places (Key, T) into the cache as the most recent item + +``` +bool erase(const Key&) +``` + +Erases element associated with Key. Use this if you know that item is not +likely to accessed soon and would like to delete ahead of time + +``` +void pop_back() +``` + +Removes oldest element from the cache. Can be used, however is more often used +internally diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/lru/lru.hpp b/src/lru/lru.hpp new file mode 100644 index 0000000..aa4c1df --- /dev/null +++ b/src/lru/lru.hpp @@ -0,0 +1,272 @@ +/////////////////////////////////////////////////////////////////////////////// +// Tyler Perkins +// 8-23-21 +// LRU cache implementation +// + +#ifndef _LRUCACHE_HPP_ +#define _LRUCACHE_HPP_ + +#include +#include +#include + +#include + +namespace clortox { + constexpr size_t __default_max_cache_size = 50; + + + template + struct value_iterator { + typename std::list::iterator it; + T value; + }; + + template> + class LRUCache{ + public: + LRUCache(); + LRUCache(size_t); + ~LRUCache(); + LRUCache(const LRUCache&); + LRUCache& operator=(const LRUCache&); + + void swap(LRUCache&); + + //utility + void clear (); + inline bool empty () const; + inline bool full () const; + inline size_t getMaxCacheSize() const; + + //accessors functions + T& get (const Key&); + inline T& operator[](const Key&); + inline T& operator[](Key&); + void put (const Key&, const T&); + bool erase (const Key&); + void pop_back (); + +#ifdef _LRU_DEBUG_ + bool wasHit = false; +#endif + + private: + //list + std::list _list; + //hashmap + std::unordered_map, Hash> _hash_map; + + size_t _max_cache_size; + }; +} + +/////////////////////////////////////////////////////////////////////////////// +// Implementation ///////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////// +// Default Ctor +template +clortox::LRUCache::LRUCache() + : LRUCache(__default_max_cache_size) {}; + +template +clortox::LRUCache::LRUCache( + size_t max_size) : _max_cache_size(max_size){ + + _hash_map.reserve(max_size); +} + +/////////////////////////////////////// +// Dector +template +clortox::LRUCache::~LRUCache(){ + while(!empty()){ + pop_back(); + } +} + +/////////////////////////////////////// +// Copy ctor +template +clortox::LRUCache::LRUCache(const LRUCache& rhs){ + _max_cache_size = rhs._max_cache_size; + _hash_map = rhs._hash_map; + _list = rhs._list; +} + +/////////////////////////////////////// +// Assignment operator +template +clortox::LRUCache& clortox::LRUCache< + Key, T, Hash>::operator=(const LRUCache& rhs){ + + if(this != &rhs){ + _max_cache_size = rhs._max_cache_size; + _hash_map = rhs._hash_map; + _list = rhs._list; + } + return *this; +} + +/////////////////////////////////////// +// Swap function +template +void clortox::LRUCache::swap(LRUCache& rhs){ + decltype(_hash_map) tmphashmap = rhs._hash_map; + rhs._hash_map = _hash_map; + _hash_map = tmphashmap; + + decltype(_list) tmplist = rhs._list; + rhs._list = _list; + _list = tmplist; + + decltype(_max_cache_size) tmpmax = rhs._max_cache_size; + rhs._max_cache_size = _max_cache_size; + _max_cache_size = tmpmax; +} + +/////////////////////////////////////// +// Clears out all elements from cache +// This should only be called if all +// elements in the cache are not likely +// to be used again +template +void clortox::LRUCache::clear(){ + for(auto it = _hash_map.begin(); it != _hash_map.end();){ + _list.erase(it->second.it); + it = _hash_map.erase(it); + } +} + +/////////////////////////////////////// +// Returns true if the cache is empty +template +bool clortox::LRUCache::empty() const{ + return _list.empty(); +} + +/////////////////////////////////////// +// Check if cache is full +template +bool clortox::LRUCache::full() const{ + return _list.size() == _max_cache_size; +} + + +/////////////////////////////////////// +// Returns the max cache size +template +size_t clortox::LRUCache::getMaxCacheSize() const{ + return _max_cache_size; +} + +/////////////////////////////////////////////////////////////////////////////// +// Accessor functions ///////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////// +// get element from cache. If it does +// not exist, create it +template +T& clortox::LRUCache::get(const Key& key){ + if(_hash_map.find(key) != _hash_map.end()){ //cache hit +#ifdef _LRU_DEBUG_ + wasHit = true; +#endif + //update list + _list.erase(_hash_map[key].it); + _list.push_front(key); + _hash_map[key].it = _list.begin(); + + return _hash_map[key].value; + } else { //cache miss +#ifdef _LRU_DEBUG_ + wasHit = false; +#endif + //if cache is full, remove oldest element + if(_list.size() == _max_cache_size){ + _hash_map.erase(_list.back()); + _list.pop_back(); + } + + //add new element + _list.push_front(key); + + _hash_map[key] = { .it = _list.begin(), .value = T(key) }; + return _hash_map[key].value; + } +} + +/////////////////////////////////////// +// Index operator returns value with +// assoc key. If it does not exist, we +// create it +template +T& clortox::LRUCache::operator[](const Key& key){ + return get(key); +} +template +T& clortox::LRUCache::operator[](Key& key){ + return get(key); +} + +/////////////////////////////////////// +// Puts the key value pair into the +// cache +template +void clortox::LRUCache::put(const Key& key, const T& value){ + if(_hash_map.find(key) != _hash_map.end()){ //cache hit +#ifdef _LRU_DEBUG_ + wasHit = true; +#endif + //update list + _list.erase(_hash_map[key].it); + _list.push_front(key); + _hash_map[key].it = _list.begin(); + + _hash_map[key].value = value; + } else { //cache hit, insert new element +#ifdef _LRU_DEBUG_ + wasHit = false; +#endif + //if cache is full, remove oldest element + if(_list.size() == _max_cache_size){ + _hash_map.erase(_list.back()); + _list.pop_back(); + } + + //add new element + _list.push_front(key); + _hash_map[key] = { .it = _list.begin(), .value = value }; + } +} + +/////////////////////////////////////// +// Remove element from cache. Only do +// this if positive it will not be +// referenced again +template +bool clortox::LRUCache::erase(const Key& key){ + if(_hash_map.find(key) != _hash_map.end()){ //cache hit + //remove element + _list.erase(_hash_map[key].it); + _hash_map.erase(key); + return true; + } //not in cache, return false + return false; +} + +/////////////////////////////////////// +// Remove oldest element +template +void clortox::LRUCache::pop_back(){ + if(!_list.empty()){ + _hash_map.erase(_list.back()); + _list.pop_back(); + } +} + +#endif //!_LRUCACHE_HPP_ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c8afa11 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,172 @@ +/////////////////////////////////////////////////////////////////////////////// +// Tyler Perkins +// 8-23-21 +// + +#include "lru/lru.hpp" +#include +#include +#include +#include +#include + +using clortox::LRUCache; + +class arr{ +public: + arr() { ptr = nullptr; }; + arr(int size){ + ptr = new int[size]; + for(int i = 0; i < size; ++i) + ptr[i] = i; + } + ~arr(){ delete[] ptr; }; + int get(int index) const { return ptr[index]; }; + +private: + int* ptr; +}; + +int main(int argc, char** argv){ + std::cout << "===== LRUCache Tests =====" << std::endl; + + std::cout << "Testing if cannonical..." << std::endl; + { + LRUCache cache; + assert(cache.getMaxCacheSize() == clortox::__default_max_cache_size); + assert(cache.empty()); + + LRUCache ccache(25); + assert(ccache.getMaxCacheSize() == 25); + + LRUCache dcache(ccache); + assert(dcache.getMaxCacheSize() == 25); + + ccache = cache; + assert(ccache.getMaxCacheSize() == clortox::__default_max_cache_size); + + ccache.swap(dcache); + assert(ccache.getMaxCacheSize() == 25); + assert(dcache.getMaxCacheSize() == clortox::__default_max_cache_size); + } + std::cout << "Testing if cannonical passed!" << std::endl; + + std::cout << "Testing put functions..." << std::endl; + { + LRUCache cache(5); + cache.put(1, 'a'); + cache.put(26, 'z'); + cache.put(10, 'j'); + cache.put(3, 'b'); + + assert(cache.get(26) == 'z'); + cache.put(10, 'j'); + assert(cache.get(10) == 'j'); + cache.put(2, 'b'); + assert(cache.get(2) == 'b'); + + cache.put(1, 'a'); + assert(cache.get(1) == 'a'); + + } + std::cout << "Testing put functions passed!" << std::endl; + + std::cout << "Testing get functions with custom types..." << std::endl; + { + LRUCache cache(3); + assert(cache.get(5).get(3) == 3); + assert(cache.get(6).get(5) == 5); + assert(cache.get(10).get(7) == 7); + //cache hit + assert(cache.get(6).get(5) == 5); + + //drop last element of cache + assert(cache.get(2).get(1) == 1); + } + std::cout << "Testing get functions with custom types passed!" << std::endl; + + std::cout << "Testing operator[] with custom types..." << std::endl; + { + LRUCache cache(3); + assert(cache[5].get(3) == 3); + assert(cache[6].get(5) == 5); + assert(cache[10].get(7) == 7); + //cache hit + assert(cache[6].get(5) == 5); + + //drop last element of cache + assert(cache[2].get(1) == 1); + } + std::cout << "Testing operator[] with custom passed!" << std::endl; + + std::cout << "Testing erase function..." << std::endl; + { + LRUCache cache(5); + cache.put(1, 'a'); + cache.put(2, 'b'); + cache.put(3, 'c'); + cache.put(4, 'd'); + cache.put(5, 'e'); + + assert(cache.full() == true); + + assert(cache.erase(26) == false); + + assert(cache.erase(1) == true); + + assert(cache.full() == false); + } + std::cout << "Testing erase function passed!" << std::endl; + + std::cout << "Testing pop_back..." << std::endl; + { + LRUCache cache(5); + cache.put(1, 'a'); + cache.put(2, 'b'); + cache.put(3, 'c'); + cache.put(4, 'd'); + cache.put(5, 'e'); + + assert(cache.full() == true); + + cache.pop_back(); + + assert(cache.full() == false); + + } + std::cout << "Testing pop_back passed!" << std::endl; + + std::cout << "Performance testing..." << std::endl; + { + srand(time(NULL)); + size_t hits = 0; + + std::cout << "Cache size : 1000\n"; + std::cout << "Range of elements : 10000\n"; + std::cout << "Iterations : 1000000\n"; + + LRUCache cache(1000); + + auto start = std::chrono::high_resolution_clock::now(); + + for(size_t i = 0; i < 1000000; ++i){ + cache.get(rand() % 10000 + 1); +#ifdef _LRU_DEBUG_ + if(cache.wasHit) + hits++; +#endif + } + + auto end = std::chrono::high_resolution_clock::now(); + std::cout << "Running time: " << + std::chrono::duration_cast(end-start).count() + << " milliseconds" << std::endl; +#ifdef _LRU_DEBUG_ + std::cout << "Cache hits: " << hits << std::endl; +#endif + + } + std::cout << "Performance testing finished!" << std::endl; + + std::cout << "===== LRUCache Tests =====" << std::endl; +}