Wordsearch Solver
lru_cache.hpp
1 #ifndef UTILITY_LRU_CACHE_HPP
2 #define UTILITY_LRU_CACHE_HPP
3 
4 #include <fmt/format.h>
5 #include <fmt/ostream.h>
6 #include <fmt/ranges.h>
7 
8 #include <cassert>
9 #include <cstddef>
10 #include <functional>
11 #include <list>
12 #include <ostream>
13 #include <type_traits>
14 #include <unordered_map>
15 #include <utility>
16 
17 namespace utility {
18 
20 template <class A, class B> struct ValueIteratorPair {
21  A value;
22  B iter;
23 };
24 
41 template <class KeyType, class ValueType, class Hash = std::hash<KeyType>>
42 class LruCache {
43 
44 public:
45  using Key = KeyType;
46  using Value = ValueType;
47 
56  const Value* lookup(const Key& key) {
57  // fmt::print("Looking up {}\n", key);
58  auto it = cache_.find(key);
59  if (it == cache_.end()) {
60  // fmt::print("lookup did not find {}\n", key);
61  ++misses_;
62  return {};
63  }
64 
65  ++hits_;
66  auto& cached_it = it->second.iter;
67  if (cached_it != used_.begin()) {
68  // fmt::print("Setting {} to front of used_ sz: {}\n", key, used_.size());
69  used_.erase(cached_it);
70  used_.push_front(&it->first);
71  cached_it = used_.begin();
72  }
73 
74  // fmt::print("Returning cached {}\n", it->second.first);
75  return &it->second.value;
76  }
77 
84  const Value& insert(const Key& k, const Value& v) {
85  return this->insert_impl(k, v);
86  }
88  const Value& insert(Key&& k, const Value& v) {
89  return this->insert_impl(std::move(k), v);
90  }
92  const Value& insert(const Key& k, Value&& v) {
93  return this->insert_impl(k, std::move(v));
94  }
96  const Value& insert(Key&& k, Value&& v) {
97  return this->insert_impl(std::move(k), std::move(v));
98  }
99 
101  void erase(const Key& k) {
102  // Won't work with move only types, but not a problem
103  const auto it = cache_.find(k);
104  if (it == cache_.end()) {
105  return;
106  }
107  assert(it != cache_.end() && "Trying to erase from cache non-existant key");
108  used_.erase(it->second.second);
109  cache_.erase(it);
110  }
111 
112 private:
113  // Yes the perfect forwarding insert_impl avoids the duplication of the above.
114  // But the above gives nicer errors. Still unsure how much I like it
115 
116  template <class K, class V> const Value& insert_impl(K&& key, V&& val) {
117  const auto it = cache_.find(key);
118  if (it != cache_.end()) {
119  assert(val == it->second.value &&
120  "We assume a key always maps to the same val");
121  // fmt::print("Insert doing nothing, already contains key {}\n", key);
122  // Assumes a key always maps to the same val
123  return it->second.value;
124  }
125 
126  if (used_.size() >= capacity_) {
127  assert(capacity_ > 0);
128  const auto* remove_key = used_.back();
129  // fmt::print("Removing key {}\n", *remove_key);
130  cache_.erase(*remove_key);
131  used_.pop_back();
132  }
133 
134  // Double yuck. The map element needs to store an iterator to the list
135  // element. The list element needs to store a pointer to the key element in
136  // the map. So insert the key and value into the map. This way we can take
137  // the address of the key to put onto the front of the list. Then that that
138  // new list iterator to the front of the list, and set the map's iterator to
139  // that.
140  // using T = typename Map::mapped_type;
141  // auto&& vall = std::move(val);
142  // auto it2 = ListIteratorType{};
143  // T t{std::move(vall), {}};
144  auto [i, b] = cache_.emplace(
145  std::forward<K>(key),
146  typename Map::mapped_type{std::forward<V>(val), ListIteratorType{}});
147  // auto [i, b] = cache_.emplace(key, val, nullptr});
148  assert(b);
149  static_cast<void>(b);
150  used_.push_front(&i->first);
151  i->second.iter = used_.begin();
152  return i->second.value;
153  }
154 
155  using List = std::list<const Key*>;
156  using ListIteratorType = typename List::iterator;
157  using Map =
158  std::unordered_map<Key, ValueIteratorPair<Value, ListIteratorType>, Hash,
159  std::equal_to<void>>;
160 
161 public:
162  bool empty() const { return cache_.empty(); }
163  std::size_t size() const { return cache_.size(); }
164  std::size_t capacity() const { return capacity_; }
165  void clear() {
166  used_.clear();
167  cache_.clear();
168  }
169 
170  LruCache(LruCache&& cache) = default;
171 
172  // https://stackoverflow.com/a/5695855/8594193
173  friend void swap(LruCache& c0, LruCache& c1) {
174  // fmt::print("I'm swapped!\n");
175  using std::swap;
176  swap(c0.used_, c1.used_);
177  swap(c0.cache_, c1.cache_);
178  swap(c0.capacity_, c1.capacity_);
179  swap(c0.print_hitrate_on_destruction_, c1.print_hitrate_on_destruction_);
180  swap(c0.hits_, c1.hits_);
181  swap(c0.misses_, c1.misses_);
182  }
183 
190  LruCache(const LruCache& other)
191  : capacity_(other.capacity_),
192  print_hitrate_on_destruction_(other.print_hitrate_on_destruction_),
193  hits_(other.hits_), misses_(other.misses_) {
194 
195  static_assert(
196  std::is_copy_constructible_v<Key>,
197  "Copy constructor for LruCache disabled as Key is not copyable");
198  static_assert(
199  std::is_copy_constructible_v<Value>,
200  "Copy constructor for LruCache disabled as Value is not copyable");
201 
202  // fmt::print("Copy cons\n");
203  cache_.reserve(other.size());
204  // Insert in reverse so that our LRU order matches the original
205  for (auto it = other.used_.rbegin(); it != other.used_.rend(); ++it) {
206  const auto& other_k = **it;
207  // Can't use other.lookup as not const (would change lru order)
208  const auto& other_v = other.cache_.at(other_k).value;
209  this->insert(other_k, other_v);
210  }
211  assert(other.used_.size() == used_.size());
212  auto other_it = other.used_.begin();
213  for ([[maybe_unused]] const auto* key : used_) {
214  assert(key != *other_it++ && "Pointers should not be the same");
215  assert(*key == **other_it++ && "Keys should be identical after copy");
216  }
217  assert(used_.size() == cache_.size());
218  assert(capacity() == other.capacity());
219  }
220 
221  LruCache& operator=(LruCache other) {
222  // fmt::print("Copy assignment op called\n");
223  swap(*this, other);
224  return *this;
225  }
226 
227  // See https://en.cppreference.com/w/cpp/container/unordered_map
228  // We can use a default constructor for std::unordered_map here as "References
229  // and pointers to either key or data stored in the container are only
230  // invalidated by erasing that element, even when the corresponding iterator
231  // is invalidated."
232  LruCache() = default;
233 
234  explicit LruCache(const std::size_t capacity) : capacity_(capacity) {
235  cache_.reserve(capacity);
236  }
237 
238  explicit LruCache(const std::size_t capacity,
239  const bool print_hitrate_on_destruction)
240  : capacity_(capacity),
241  print_hitrate_on_destruction_(print_hitrate_on_destruction) {
242  cache_.reserve(capacity);
243  }
244 
245  ~LruCache() {
246  if (hits_ > 0 || misses_ > 0) {
247  fmt::print("LruCache destructor summary: Hits {} / misses {} - {}%\n",
248  hits_, misses_,
249  static_cast<double>(hits_) / static_cast<double>(misses_) *
250  100.0);
251  }
252  }
253 
254 private:
255  template <class K, class V>
256  friend std::ostream& operator<<(std::ostream& os,
257  const LruCache<K, V>& cache);
258 
259  List used_ = {};
260  Map cache_ = {};
261  std::size_t capacity_ = 64;
262 
263  bool print_hitrate_on_destruction_ = false;
264  std::size_t hits_ = 0;
265  std::size_t misses_ = 0;
266 };
267 
268 template <class K, class V>
269 std::ostream& operator<<(std::ostream& os, const LruCache<K, V>& cache) {
270  fmt::memory_buffer b{};
271  for (const auto& [k, v] : cache.cache_) {
272  fmt::format_to(b, "{}: {}, ", k, v.value);
273  }
274  if (b.size() > 2) {
275  b.resize(b.size() - 2);
276  }
277  return os << fmt::to_string(b);
278 }
279 
280 } // namespace utility
281 
282 #endif // UTILITY_LRU_CACHE_HPP
Quick and dirty attempt at implementing an LRU cache.
Definition: lru_cache.hpp:42
const Value & insert(Key &&k, Value &&v)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition: lru_cache.hpp:96
void erase(const Key &k)
Remove a key k from the cache, if it exists.
Definition: lru_cache.hpp:101
LruCache(const LruCache &other)
Copy constructor.
Definition: lru_cache.hpp:190
const Value & insert(const Key &k, Value &&v)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition: lru_cache.hpp:92
const Value & insert(const Key &k, const Value &v)
Insert a key k and a value v.
Definition: lru_cache.hpp:84
const Value & insert(Key &&k, const Value &v)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition: lru_cache.hpp:88
const Value * lookup(const Key &key)
Lookup a value given a key.
Definition: lru_cache.hpp:56
Utility functions.
Definition: flat_char_value_map.hpp:13
std::pair neater replacement with appropriate names
Definition: lru_cache.hpp:20