Initial Commit

This commit is contained in:
Tyler Perkins 2021-08-23 13:40:34 -04:00
commit 5c81b63fe8
6 changed files with 600 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -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

43
Makefile Normal file
View File

@ -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

104
README.md Normal file
View File

@ -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

0
bin/.gitkeep Normal file
View File

272
src/lru/lru.hpp Normal file
View File

@ -0,0 +1,272 @@
///////////////////////////////////////////////////////////////////////////////
// Tyler Perkins
// 8-23-21
// LRU cache implementation
//
#ifndef _LRUCACHE_HPP_
#define _LRUCACHE_HPP_
#include <unordered_map>
#include <list>
#include <cstddef>
#include <iostream>
namespace clortox {
constexpr size_t __default_max_cache_size = 50;
template<class Key, class T>
struct value_iterator {
typename std::list<Key>::iterator it;
T value;
};
template<class Key, class T, class Hash = std::hash<Key>>
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<Key> _list;
//hashmap
std::unordered_map<Key, value_iterator<Key, T>, Hash> _hash_map;
size_t _max_cache_size;
};
}
///////////////////////////////////////////////////////////////////////////////
// Implementation /////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////
// Default Ctor
template<class Key, class T, class Hash>
clortox::LRUCache<Key, T, Hash>::LRUCache()
: LRUCache(__default_max_cache_size) {};
template<class Key, class T, class Hash>
clortox::LRUCache<Key, T, Hash>::LRUCache(
size_t max_size) : _max_cache_size(max_size){
_hash_map.reserve(max_size);
}
///////////////////////////////////////
// Dector
template<class Key, class T, class Hash>
clortox::LRUCache<Key, T, Hash>::~LRUCache(){
while(!empty()){
pop_back();
}
}
///////////////////////////////////////
// Copy ctor
template<class Key, class T, class Hash>
clortox::LRUCache<Key, T, Hash>::LRUCache(const LRUCache& rhs){
_max_cache_size = rhs._max_cache_size;
_hash_map = rhs._hash_map;
_list = rhs._list;
}
///////////////////////////////////////
// Assignment operator
template<class Key, class T, class Hash>
clortox::LRUCache<Key, T, Hash>& 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<class Key, class T, class Hash>
void clortox::LRUCache<Key, T, Hash>::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<class Key, class T, class Hash>
void clortox::LRUCache<Key, T, Hash>::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<class Key, class T, class Hash>
bool clortox::LRUCache<Key, T, Hash>::empty() const{
return _list.empty();
}
///////////////////////////////////////
// Check if cache is full
template<class Key, class T, class Hash>
bool clortox::LRUCache<Key, T, Hash>::full() const{
return _list.size() == _max_cache_size;
}
///////////////////////////////////////
// Returns the max cache size
template<class Key, class T, class Hash>
size_t clortox::LRUCache<Key, T, Hash>::getMaxCacheSize() const{
return _max_cache_size;
}
///////////////////////////////////////////////////////////////////////////////
// Accessor functions /////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////
// get element from cache. If it does
// not exist, create it
template<class Key, class T, class Hash>
T& clortox::LRUCache<Key, T, Hash>::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<class Key, class T, class Hash>
T& clortox::LRUCache<Key, T, Hash>::operator[](const Key& key){
return get(key);
}
template<class Key, class T, class Hash>
T& clortox::LRUCache<Key, T, Hash>::operator[](Key& key){
return get(key);
}
///////////////////////////////////////
// Puts the key value pair into the
// cache
template<class Key, class T, class Hash>
void clortox::LRUCache<Key, T, Hash>::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<class Key, class T, class Hash>
bool clortox::LRUCache<Key, T, Hash>::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<class Key, class T, class Hash>
void clortox::LRUCache<Key, T, Hash>::pop_back(){
if(!_list.empty()){
_hash_map.erase(_list.back());
_list.pop_back();
}
}
#endif //!_LRUCACHE_HPP_

172
src/main.cpp Normal file
View File

@ -0,0 +1,172 @@
///////////////////////////////////////////////////////////////////////////////
// Tyler Perkins
// 8-23-21
//
#include "lru/lru.hpp"
#include <iostream>
#include <cassert>
#include <stdlib.h>
#include <time.h>
#include <chrono>
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<int, char> cache;
assert(cache.getMaxCacheSize() == clortox::__default_max_cache_size);
assert(cache.empty());
LRUCache<int, char> ccache(25);
assert(ccache.getMaxCacheSize() == 25);
LRUCache<int, char> 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<int, char> 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<int, arr> 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<int, arr> 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<int, char> 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<int, char> 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<int, arr> 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<std::chrono::milliseconds>(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;
}