C++¶
Least recently used cache (LRU cache)¶
Python provides a decorator to wrap a function with a memoizing callable that
saves up to the maxsize
most recent calls:
The Functional Programming in C++1 book provides an example of general unbounded function cache
(similar to Python's @functools.cache
) implemented using std::map
(\(\mathcal{O}(\log n)\))
insertion and retrieval with operator[]
).
Since insertion and retrieval of elements (using operator[]
) is \(\mathcal{O}(\log n)\) for std::map
,
the implementation in is not the most efficient.
Additionally, std::map
does not retain the order of insertion which is necessary to implement LRU cache.
An LRU cache can be implemented efficiently using two data structure:
- a
std::deque
(ordered, with \(\mathcal{O}(1)\) insertion or removal of elements at the end or beginning), and - a
std::unordered_map
(average \(\mathcal{O}(1)\) insertion and removal).
The std::deque
keeps track of the elements added to cache,
and allows to easily and efficiently evict old elements when the cache is full and a new element needs to be stored.
std::unordered_map
provides an efficient mapping between the function arguments (stored as keys)
and the position of the corresponding cached value (stored as values).
#include <deque>
#include <tuple>
#include <utility>
#include <unordered_map>
#include <optional>
template <typename Result, typename... Args>
auto lru_cache(Result (*f)(Args...), std::optional<size_t> cache_size = std::nullopt) {
using cache_t = std::deque<std::pair<Args..., Result>>;
cache_t cache; // (1)!
std::unordered_map<Args..., typename cache_t::iterator> cache_map; // (2)!
return [f, cache, cache_map, cache_size](Args... args) mutable -> Result { // (3)!
auto cached_map_it = cache_map.find(args...);
if(cached_map_it == cache_map.end()){ // (4)!
if(cache_size && cache.size() == *cache_size){ // (5)!
// Remove oldest element from the cache
cache_map.erase(cache.front().first); // (6)!
cache.pop_front(); // (7)!
}
// Compute new element, add it to the cache, and return it
const auto result = f(args...);
cache.emplace_back(std::make_pair(args..., result)); // (8)!
cache_map.emplace(args..., cache.end() - 1); // (9)!
return result;
}
else{ // (10)!
// Update cached element position in the cache and return it
auto cached_it = cached_map_it->second; // (11)!
cache.push_back(*cached_it); // (12)!
cache.erase(cached_it); // (13)!
cached_map_it->second = cache.end() - 1; // (14)!
return cache.back().second;
}
};
}
std::deque
stores a pair of arguments and the result of the function, and represents the cache.std::unordered_map
maps arguments to elements in the cache (std::deque
) and provides \(\mathcal{O}(1)\) access to the cache.- The lambda is
mutable
in order to modify the cache and the cache map. - The arguments are not found in the cache.
- The cache is full.
- Remove element from
cache_map
corresponding to the arguments of the oldest element in the cache (at the front of the queue). - Remove the oldest element from the cache.
- Add pair of results and arguments to the cache.
- Add the arguments and the iterator to the last element in the cache to
cache_map
. Thestd::deque::end
iterator points after the last element in thestd::deque
. - The arguments are found in the cache.
std::unordered_map::find
returns an iterator to the element in the map. An element ofstd::unordered_map
is astd::pair
with the key and the value. The value is an iterator to the element in thestd::deque
.cached_map_it->second
is the iterator to the element in thestd::deque
(the cache).- Add the requested cahed element at the back of the queue, it is now the most recently used element.
- Remove the cached element from the current position in the queue, since it is now in the back.
- Update the iterator in the
std::unordered_map
to point to the new position of the cached element, which is at the back of the queue.std::deque::end
points after the last element in thestd::deque
.
The lru_cache
function returns a lambda that wraps the function f
and implements the LRU cache.
It can be used as follows:
-
Functional Programming in C++, Ivan Čukić, Manning Publications Co., ISBN 9781617293818. ↩