From bb31b4484ddd52246e8d7429378aa20ce3db23e5 Mon Sep 17 00:00:00 2001 From: William Dussault Date: Thu, 21 May 2026 16:27:37 -0400 Subject: [PATCH 1/3] Make all template methods inline at the place of declaration --- include/cachemere/cache.h | 1478 ++++++----------- include/cachemere/composite_key.h | 2 +- include/cachemere/policy/constraint_count.h | 107 +- include/cachemere/policy/constraint_memory.h | 139 +- .../cachemere/policy/detail/bloom_filter.h | 102 +- .../policy/detail/counting_bloom_filter.h | 179 +- include/cachemere/policy/detail/hash_mixer.h | 25 +- include/cachemere/policy/eviction_gdsf.h | 136 +- include/cachemere/policy/eviction_lru.h | 129 +- .../cachemere/policy/eviction_segmented_lru.h | 245 ++- include/cachemere/policy/insertion_always.h | 29 +- include/cachemere/policy/insertion_tinylfu.h | 113 +- 12 files changed, 998 insertions(+), 1686 deletions(-) diff --git a/include/cachemere/cache.h b/include/cachemere/cache.h index f0b1f9b..cd4d8c6 100644 --- a/include/cachemere/cache.h +++ b/include/cachemere/cache.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -77,7 +76,17 @@ class Cache /// @brief Simple constructor. /// @param args Arguments to forward to the constraint policy constructor. - template Cache(Args... args); + template + Cache(Args... args) + : m_insertion_policy(std::make_unique()), + m_eviction_policy(std::make_unique()), + m_constraint_policy(std::make_unique(std::forward(args)...)), + m_mutex{}, + m_data{}, + m_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size), + m_byte_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size) + { + } /// @brief Constructor to initialize the cache with a set of items. /// @details Will insert items in order in the cache as long as the constraint is satisfied. @@ -85,37 +94,68 @@ class Cache /// @param args Tuple of arguments to forward to the constraint policy constructor. /// @warning Items that are imported from the collection are moved out of the container and left /// in an unspecified state. The container itself can be reused after clearing it. - template Cache(C& collection, std::tuple args); + template + Cache(C& collection, std::tuple args) + : m_insertion_policy(std::make_unique()), + m_eviction_policy(std::make_unique()), + m_constraint_policy(std::move( + std::apply([](auto&&... params) { return std::make_unique(std::forward(params)...); }, std::move(args)))), + m_mutex{}, + m_data{}, + m_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size), + m_byte_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size) + { + import(collection); + } /// @brief Check whether a given key is stored in the cache. /// @tparam KeyView The type of the key used for retrieving items. /// @param key The key whose presence to test. /// @return Whether the key is in cache. - template bool contains(const KeyView& key) const; + template bool contains(const KeyView& key) const + { + const LockGuard guard{lock()}; + return m_data.find(key) != m_data.end(); + } /// @brief Find a given key in cache returning the associated value when it exists. /// @tparam KeyView The type of the key used for retrieving items. /// @param key The key to lookup. /// @return The value if `key` is in cache, `std::nullopt` otherwise. - template std::optional find(const KeyView& key) const; + template std::optional find(const KeyView& key) const + { + const LockGuard guard{lock()}; + return find_locked(key); + } /// @brief Try to find a given key in cache without blocking for the cache lock. /// @tparam KeyView The type of the key used for retrieving items. /// @param key The key to lookup. /// @return A result indicating whether the lock was acquired and, if so, whether the key was found. - template TryFindResult try_find(const KeyView& key) const; + template TryFindResult try_find(const KeyView& key) const + { + const LockGuard guard{try_lock()}; + if (!guard.owns_lock()) { + return TryFindResult{false, std::nullopt}; + } + return TryFindResult{true, find_locked(key)}; + } /// @brief Try to find a given key in cache, waiting for up to the provided timeout to acquire the lock. /// @tparam KeyView The type of the key used for retrieving items. /// @param key The key to lookup. /// @param timeout The maximum amount of time to wait for the cache lock. /// @return A result indicating whether the lock was acquired and, if so, whether the key was found. - template, int> = 0> - TryFindResult try_find(const KeyView& key, const std::chrono::duration& timeout) const; + template + TryFindResult try_find(const KeyView& key, const std::chrono::duration& timeout) const + requires(detail::HasTimedLockingV) + { + const LockGuard guard{try_lock(timeout)}; + if (!guard.owns_lock()) { + return TryFindResult{false, std::nullopt}; + } + return TryFindResult{true, find_locked(key)}; + } /// @brief Find a given key in cache returning the associated value when it exists, or inserting a new value if it doesn't. /// @tparam KeyView The type of the key used for retrieving items. @@ -123,7 +163,18 @@ class Cache /// @param key The key to lookup. /// @param fn The factory function to generate the value if the key is not in cache. /// @return The value associated with the key. - template Fn> Value find_or_insert(const KeyView& key, Fn&& fn); + template Fn> Value find_or_insert(const KeyView& key, Fn&& fn) + { + const LockGuard guard{lock()}; + + if (const auto found = find_locked(key); found.has_value()) { + return *found; + } + + Value value = fn(key); + insert_locked(Key(key), value); + return value; + } /// @brief Copy the cache contents in the provided container. /// @details The container should conform to either of the STL's interfaces for associative @@ -132,7 +183,27 @@ class Cache /// If the provided container has `size()` and `reserve()` methods, `collect_into` will reserve /// the appropriate amount of space in the container before inserting. /// @param container The container in which to insert the items. - template C> void collect_into(C& container) const; + template C> void collect_into(C& container) const + { + using namespace detail; + + const LockGuard guard{lock()}; + + // Reserve space if the container has a reserve() method and a size method(). + if constexpr (traits::ReservableContainer) { + container.reserve(container.size() + m_data.size()); + } + + // Copy the cache contents to the container. + for (const auto& [key, cached_item] : m_data) { + // Use emplace_back if container is a sequence container, or emplace if container is an associative container. + if constexpr (traits::SequenceContainer) { + container.emplace_back(key, cached_item.m_value); + } else { + container.emplace(key, cached_item.m_value); + } + } + } /// @brief Insert a key/value pair in the cache. /// @details If the key is new, the key/value pair will be inserted. @@ -140,110 +211,297 @@ class Cache /// @param key The key to associate with the value. /// @param value The value to store. /// @return Whether the item was inserted in cache. - bool insert(Key key, Value value); + bool insert(Key key, Value value) + { + const LockGuard guard{lock()}; + return insert_locked(std::move(key), std::move(value)); + } /// @brief Try to insert a key/value pair in the cache without blocking for the cache lock. /// @param key The key to associate with the value. /// @param value The value to store. /// @return A result indicating whether the lock was acquired and, if so, whether the item was inserted. - TryInsertResult try_insert(Key key, Value value); + TryInsertResult try_insert(Key key, Value value) + { + const LockGuard guard{try_lock()}; + if (!guard.owns_lock()) { + return TryInsertResult{false, false}; + } + return TryInsertResult{true, insert_locked(std::move(key), std::move(value))}; + } /// @brief Try to insert a key/value pair in the cache, waiting for up to the provided timeout to acquire the lock. /// @param key The key to associate with the value. /// @param value The value to store. /// @param timeout The maximum amount of time to wait for the cache lock. /// @return A result indicating whether the lock was acquired and, if so, whether the item was inserted. - template, int> = 0> - TryInsertResult try_insert(Key key, Value value, const std::chrono::duration& timeout); + template + TryInsertResult try_insert(Key key, Value value, const std::chrono::duration& timeout) + requires(detail::HasTimedLockingV) + { + const LockGuard guard{try_lock(timeout)}; + if (!guard.owns_lock()) { + return TryInsertResult{false, false}; + } + return TryInsertResult{true, insert_locked(std::move(key), std::move(value))}; + } /// @brief Remove a key and its value from the cache. /// @details If the key is not present in cache, no operation is taken. /// @param key The key to remove from the cache. /// @return Whether the key was present in cache. - bool remove(const Key& key); + bool remove(const Key& key) + { + const LockGuard guard{lock()}; + auto key_and_item = m_data.find(key); + if (key_and_item != m_data.end()) { + remove(std::move(key_and_item)); + return true; + } + return false; + } /// @brief Clears the cache contents. - void clear(); + void clear() + { + const LockGuard guard{lock()}; + + m_data.clear(); + + m_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); + m_byte_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); + + m_insertion_policy->clear(); + m_eviction_policy->clear(); + m_constraint_policy->clear(); + } /// @brief Retain all objects matching a predicate. /// @details Removes all items for which `predicate_fn` returns false. /// @param predicate_fn The predicate function. /// @tparam P The type of the predicate function. /// The predicate should have the signature `bool fn(const Key& key, const Value& value)`. - template void retain(P predicate_fn); + template void retain(P predicate_fn) + { + const LockGuard guard{lock()}; + for (auto it = m_data.begin(); it != m_data.end();) { + const CacheItem& item = it->second; + + if (!predicate_fn(it->first, item.m_value)) { + remove(it++); + } else { + ++it; + } + } + } /// @brief Apply a function to all objects in cache. /// @param unary_function The function to be applied to all items in cache. /// The function should have the signature `void fn(const Key& key, const Value& value)`. - template void for_each(F unary_function); + template void for_each(F unary_function) + { + const LockGuard guard{lock()}; + for (const auto& [key, value] : m_data) { + unary_function(key, value.m_value); + } + } /// @brief Swaps the current cache with another cache of the same type. /// @details Calls `std::terminate` if the cache runs in thread-safe mode and an exception is thrown while locking its mutex. /// @param other The cache to swap this instance with. - void swap(CacheType& other) noexcept; + void swap(CacheType& other) noexcept + { + try { + // Acquire both cache locks. + std::pair guards{lock_pair(other)}; + + using std::swap; + + swap(m_statistics_window_size, other.m_statistics_window_size); + + swap(m_insertion_policy, other.m_insertion_policy); + swap(m_eviction_policy, other.m_eviction_policy); + swap(m_constraint_policy, other.m_constraint_policy); + + swap(m_data, other.m_data); + + swap(m_hit_rate_acc, other.m_hit_rate_acc); + swap(m_byte_hit_rate_acc, other.m_byte_hit_rate_acc); + } catch (const std::system_error& e) { + // The only exception that can sensibly be thrown in the above block is a `system_error` when acquiring the mutexes of both caches (if the caches + // are running in thread-safe mode). + // + // According to the [reference for std mutexes](https://en.cppreference.com/w/cpp/named_req/Mutex), this exception can be thrown for one of two + // reasons: + // - If the calling thread does not have the required privileges. + // - If the implementation determines that acquiring the lock would lead to a deadlock. + // + // Since both those errors are unrecoverable, we'll validate that the error code is one of the two we expect, and we'll terminate. + assert(e.code() == std::errc::operation_not_permitted || e.code() == std::errc::resource_deadlock_would_occur); + std::terminate(); + } catch (...) { + // Even though this is technically not possible in the current state of the code, we'll catch any leftover exception to satisfy clang-tidy. + std::terminate(); + } + } /// @brief Get the number of items currently stored in the cache. /// @warning This method acquires a mutual exclusion lock to secure the item count. /// Calling this excessively while the cache is under contention will be detrimental to performance. /// @return How many items are in cache. - [[nodiscard]] size_t number_of_items() const; + [[nodiscard]] size_t number_of_items() const + { + const LockGuard guard{lock()}; + return m_data.size(); + } /// @brief Update the cache constraint. /// @details Forwards the update to the constraint and evicts items from the cache until the /// constraint is satisfied. /// @param args Arguments to forward to the `Constraint::update()` . - template void update_constraint(Args... args); + template void update_constraint(Args... args) + { + const LockGuard guard{lock()}; + m_constraint_policy->update(std::forward(args)...); + + while (!m_constraint_policy->is_satisfied()) { + auto eviction_ticket = m_eviction_policy->pop_victim(); + auto key_and_item = m_data.find(eviction_ticket.key()); + + // If this trips, the eviction policy tried to evict an item not in cache: the eviction policy and the cache are out of sync. + assert(key_and_item != m_data.end()); + + remove_popped_victim(key_and_item); + } + + assert(m_constraint_policy->is_satisfied()); + } /// @brief Get a reference to the insertion policy used by the cache. - [[nodiscard]] MyInsertionPolicy& insertion_policy(); + [[nodiscard]] MyInsertionPolicy& insertion_policy() + { + return *m_insertion_policy; + } /// @brief Get a const reference to the insertion policy used by the cache. - [[nodiscard]] const MyInsertionPolicy& insertion_policy() const; + [[nodiscard]] const MyInsertionPolicy& insertion_policy() const + { + return *m_insertion_policy; + } /// @brief Get a reference to the eviction policy used by the cache. - [[nodiscard]] MyEvictionPolicy& eviction_policy(); + [[nodiscard]] MyEvictionPolicy& eviction_policy() + { + return *m_eviction_policy; + } /// @brief Get a const reference to the eviction policy used by the cache. - [[nodiscard]] const MyEvictionPolicy& eviction_policy() const; + [[nodiscard]] const MyEvictionPolicy& eviction_policy() const + { + return *m_eviction_policy; + } /// @brief Get a reference to the constraint policy used by the cache. - [[nodiscard]] MyConstraintPolicy& constraint_policy(); + [[nodiscard]] MyConstraintPolicy& constraint_policy() + { + return *m_constraint_policy; + } /// @brief Get a const reference to the constraint policy used by the cache. - [[nodiscard]] const MyConstraintPolicy& constraint_policy() const; + [[nodiscard]] const MyConstraintPolicy& constraint_policy() const + { + return *m_constraint_policy; + } /// @brief Compute and return the running hit rate of the cache. /// @details The hit rate is computed using a sliding window determined by the sliding window /// size passed to the constructor. /// @return The hit rate, as a fraction. - [[nodiscard]] double hit_rate() const; + [[nodiscard]] double hit_rate() const + { + return boost::accumulators::rolling_mean(m_hit_rate_acc); + } /// @brief Compute and return the running byte hit rate of the cache, in bytes. /// @details The byte hit rate represents the average amount of data saved by cache accesses. /// This is a very useful metric in applications where item load times scale /// linearly with item size, for instance in a web server. /// @return The byte hit rate, in bytes. - [[nodiscard]] double byte_hit_rate() const; + [[nodiscard]] double byte_hit_rate() const + { + return boost::accumulators::rolling_mean(m_byte_hit_rate_acc); + } /// @brief Get the size of the sliding window used for computing statistics. /// @return The size of the statistics sliding window. - [[nodiscard]] uint32_t statistics_window_size() const; + [[nodiscard]] uint32_t statistics_window_size() const + { + return m_statistics_window_size; + } /// @brief Set the size of the sliding window used for computing statistics. /// @warning This will reset the access log, so cache accesses made prior to calling this will not be /// counted in the statistics. /// @param window_size The desired statistics window size. - void statistics_window_size(uint32_t window_size); + void statistics_window_size(uint32_t window_size) + { + m_statistics_window_size = window_size; + + m_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); + m_byte_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); + } protected: - LockGuard lock() const; - LockGuard try_lock() const; - LockGuard lock(std::defer_lock_t defer_lock_tag) const; - template, int> = 0> - LockGuard try_lock(const std::chrono::duration& timeout) const; - std::pair lock_pair(CacheType& other) const; + LockGuard lock() const + { + return LockGuard{m_mutex}; + } + + LockGuard try_lock() const + { + return LockGuard{m_mutex, std::try_to_lock}; + } + + LockGuard lock(std::defer_lock_t defer_lock_tag) const + { + return LockGuard{m_mutex, defer_lock_tag}; + } + + template + LockGuard try_lock(const std::chrono::duration& timeout) const + requires(detail::HasTimedLockingV) + { + LockGuard guard{m_mutex, std::defer_lock}; + [[maybe_unused]] const bool locked = guard.try_lock_for(timeout); + return guard; + } - template void import(C& collection); + std::pair lock_pair(CacheType& other) const + { + LockGuard my_guard = lock(std::defer_lock); + LockGuard other_guard = other.lock(std::defer_lock); + + std::lock(my_guard, other_guard); + + return std::make_pair(std::move(my_guard), std::move(other_guard)); + } + + template void import(C& collection) + { + const LockGuard guard{lock()}; + + for (auto& [key, value] : collection) { + const auto key_size = static_cast(m_measure_key(key)); + const auto value_size = static_cast(m_measure_value(value)); + CacheItem item{key_size, std::move(value), value_size}; + + if (!m_constraint_policy->prepare_insert(key, item).is_satisfied()) { + return; + } + + insert_or_update(std::move(key), std::move(item)); + } + } private: using CacheItem = Item; @@ -275,1027 +533,223 @@ class Cache mutable MeanAccumulator m_hit_rate_acc; mutable MeanAccumulator m_byte_hit_rate_acc; - template std::optional find_locked(const KeyView& key) const; - bool insert_locked(Key key, Value value); - template bool collect_evictions(const Key& candidate_key, ConstraintTicket& ticket); - bool check_insert(const Key& candidate_key, const CacheItem& item); - bool check_replace(const Key& candidate_key, const CacheItem& old_item, const CacheItem& new_item); - - void insert_or_update(Key&& key, CacheItem&& value); - void remove(DataMapIt it); - void remove_popped_victim(DataMapIt it); - - void on_insert(const Key& key, const CacheItem& item) const; - void on_update(const Key& key, const CacheItem& old_item, const CacheItem& new_item) const; - void on_cache_hit(const Key& key, const CacheItem& item) const; - template void on_cache_miss(const KeyView& key) const; - void on_evict(const Key& key, const CacheItem& item) const; -}; + template std::optional find_locked(const KeyView& key) const + { + const auto key_and_item = m_data.find(key); + if (key_and_item != m_data.end()) { + on_cache_hit(key_and_item->first, key_and_item->second); + return key_and_item->second.m_value; + } -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void swap(Cache& lhs, Cache& rhs) noexcept; + on_cache_miss(key); + return std::nullopt; + } -template class InsertionPolicy, - template class EvictionPolicy, - template class ConstraintPolicy, - typename MeasureValue, - typename MeasureKey, - typename KeyHash, - LockingStrategy Locking> -template -Cache::Cache(Args... args) - : m_insertion_policy(std::make_unique()), - m_eviction_policy(std::make_unique()), - m_constraint_policy(std::make_unique(std::forward(args)...)), - m_mutex{}, - m_data{}, - m_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size), - m_byte_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size) -{ -} + bool insert_locked(Key key, Value value) + { + const auto key_size = static_cast(m_measure_key(key)); + const auto value_size = static_cast(m_measure_value(value)); -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -Cache::Cache(Coll& collection, std::tuple args) - : m_insertion_policy(std::make_unique()), - m_eviction_policy(std::make_unique()), - m_constraint_policy(std::move( - std::apply([](auto&&... params) { return std::make_unique(std::forward(params)...); }, std::move(args)))), - m_mutex{}, - m_data{}, - m_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size), - m_byte_hit_rate_acc(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size) -{ - import(collection); -} + CacheItem new_item{key_size, std::move(value), value_size}; -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -inline bool Cache::contains(const KeyView& key) const -{ - LockGuard guard(lock()); - return m_data.find(key) != m_data.end(); -} + auto it = m_data.find(key); + if (it != m_data.end()) { + if (check_replace(key, it->second, new_item)) { + // We call insert_or_update because we might have evicted the original key to make room for this one. + insert_or_update(std::move(key), std::move(new_item)); + return true; + } + } else { + if (check_insert(key, new_item)) { + const auto it_and_ok = m_data.insert_or_assign(std::move(key), std::move(new_item)); + assert(it_and_ok.second); -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -std::optional Cache::find_locked(const KeyView& key) const -{ - auto key_and_item = m_data.find(key); - if (key_and_item != m_data.end()) { - on_cache_hit(key_and_item->first, key_and_item->second); - return key_and_item->second.m_value; - } + on_insert(it_and_ok.first->first, it_and_ok.first->second); + return true; + } + } - on_cache_miss(key); - return std::nullopt; -} + return false; + } -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -std::optional Cache::find(const KeyView& key) const -{ - LockGuard guard(lock()); - return find_locked(key); -} + template bool collect_evictions(const Key& candidate_key, ConstraintTicket& ticket) + { + std::vector> keys_to_evict; -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -auto Cache::try_find(const KeyView& key) const -> TryFindResult -{ - LockGuard guard(try_lock()); - if (!guard.owns_lock()) { - return TryFindResult{false, std::nullopt}; - } + while (!ticket.is_satisfied()) { + auto eviction_ticket = m_eviction_policy->pop_victim(); + auto key_and_item = m_data.find(eviction_ticket.key()); - return TryFindResult{true, find_locked(key)}; -} + // If this trips, the eviction policy recommended the eviction of a key not in cache: the eviction policy and the + // cache are out of sync. + assert(key_and_item != m_data.end()); -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template, int>> -auto Cache::try_find(const KeyView& key, const std::chrono::duration& timeout) const -> TryFindResult -{ - LockGuard guard(try_lock(timeout)); - if (!guard.owns_lock()) { - return TryFindResult{false, std::nullopt}; - } + if (!m_insertion_policy->should_replace(key_and_item->first, candidate_key)) { + m_eviction_policy->rollback(std::move(eviction_ticket)); + for (auto it = keys_to_evict.rbegin(); it != keys_to_evict.rend(); ++it) { + m_eviction_policy->rollback(std::move(it->first)); + } + return false; + } - return TryFindResult{true, find_locked(key)}; -} + ticket.register_eviction(key_and_item->first, key_and_item->second); + keys_to_evict.emplace_back(std::move(eviction_ticket), key_and_item); + } -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template Fn> -V Cache::find_or_insert(const KeyView& key, Fn&& fn) -{ - LockGuard guard(lock()); + for (auto& [_, key_and_item] : keys_to_evict) { + remove_popped_victim(key_and_item); + } - if (const auto found = find_locked(key); found.has_value()) { - return *found; + return true; } - V value = fn(key); - insert_locked(K(key), value); - return value; -} + bool check_insert(const Key& candidate_key, const CacheItem& item) + { + auto insertion_ticket = m_constraint_policy->prepare_insert(candidate_key, item); -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template Container> -void Cache::collect_into(Container& container) const -{ - using namespace detail; + if (insertion_ticket.is_satisfied()) { + // The constraint is already satisfied, so we just check the insertion policy. + return m_insertion_policy->should_add(candidate_key); + } - LockGuard guard(lock()); + if (!insertion_ticket.is_satisfiable()) { + return false; // item can't be inserted even if we evict everything. + } + + return collect_evictions(candidate_key, insertion_ticket); + } - // Reserve space if the container has a reserve() method and a size method(). - if constexpr (traits::ReservableContainer) { - container.reserve(container.size() + m_data.size()); + bool check_replace(const Key& candidate_key, const CacheItem& old_item, const CacheItem& new_item) + { + auto replacement_ticket = m_constraint_policy->prepare_replace(candidate_key, old_item, new_item); + if (replacement_ticket.is_satisfied()) { + return true; + } + if (!replacement_ticket.is_satisfiable()) { + return false; + } + return collect_evictions(candidate_key, replacement_ticket); } - // Copy the cache contents to the container. - for (const auto& [key, cached_item] : m_data) { - // Use emplace_back if container is a sequence container, or emplace if container is an associative container. - if constexpr (traits::SequenceContainer) { - container.emplace_back(key, cached_item.m_value); + void insert_or_update(Key&& key, CacheItem&& item) + { + const auto key_and_item = m_data.find(key); + if (key_and_item != m_data.end()) { + using std::swap; + swap(key_and_item->second, item); + on_update(key_and_item->first, item, key_and_item->second); } else { - container.emplace(key, cached_item.m_value); + // Insert. + const auto it_and_ok = m_data.insert_or_assign(std::move(key), std::move(item)); + assert(it_and_ok.second); + on_insert(it_and_ok.first->first, it_and_ok.first->second); } } -} -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -bool Cache::insert_locked(K key, V value) -{ - const auto key_size = static_cast(m_measure_key(key)); - const auto value_size = static_cast(m_measure_value(value)); + void remove(DataMapIt it) + { + on_evict(it->first, it->second); + m_data.erase(it); + } - CacheItem new_item{key_size, std::move(value), value_size}; + void remove_popped_victim(DataMapIt it) + { + if constexpr (detail::traits::event::HasOnEvict) { + m_insertion_policy->on_evict(it->first, it->second); + } + if constexpr (detail::traits::event::HasOnEvict) { + m_constraint_policy->on_evict(it->first, it->second); + } + m_data.erase(it); + } - auto it = m_data.find(key); - if (it != m_data.end()) { - if (check_replace(key, it->second, new_item)) { - // We call insert_or_update because we might have evicted the original key to make room for this one. - insert_or_update(std::move(key), std::move(new_item)); - return true; + void on_insert(const Key& key, const CacheItem& item) const + { + // Call event handler iif the method is defined in the policy. + if constexpr (detail::traits::event::HasOnInsert) { + m_insertion_policy->on_insert(key, item); } - } else { - if (check_insert(key, new_item)) { - const auto it_and_ok = m_data.insert_or_assign(std::move(key), std::move(new_item)); - assert(it_and_ok.second); - on_insert(it_and_ok.first->first, it_and_ok.first->second); - return true; + if constexpr (detail::traits::event::HasOnInsert) { + m_eviction_policy->on_insert(key, item); + } + + if constexpr (detail::traits::event::HasOnInsert) { + m_constraint_policy->on_insert(key, item); } } - return false; -} + void on_update(const Key& key, const CacheItem& old_item, const CacheItem& new_item) const + { + // Call event handler iif the method is defined in the policy. + if constexpr (detail::traits::event::HasOnUpdate) { + m_insertion_policy->on_update(key, old_item, new_item); + } -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -bool Cache::insert(K key, V value) -{ - LockGuard guard(lock()); - return insert_locked(std::move(key), std::move(value)); -} + if constexpr (detail::traits::event::HasOnUpdate) { + m_eviction_policy->on_update(key, old_item, new_item); + } -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -auto Cache::try_insert(K key, V value) -> TryInsertResult -{ - LockGuard guard(try_lock()); - if (!guard.owns_lock()) { - return TryInsertResult{false, false}; + if constexpr (detail::traits::event::HasOnUpdate) { + m_constraint_policy->on_update(key, old_item, new_item); + } } - return TryInsertResult{true, insert_locked(std::move(key), std::move(value))}; -} + void on_cache_hit(const Key& key, const CacheItem& item) const + { + // Update the cache hit rate accumulators. + m_hit_rate_acc(1); + m_byte_hit_rate_acc(static_cast(item.m_value_size)); -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template, int>> -auto Cache::try_insert(K key, V value, const std::chrono::duration& timeout) -> TryInsertResult -{ - LockGuard guard(try_lock(timeout)); - if (!guard.owns_lock()) { - return TryInsertResult{false, false}; - } - - return TryInsertResult{true, insert_locked(std::move(key), std::move(value))}; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -bool Cache::remove(const K& key) -{ - LockGuard guard(lock()); - - auto key_and_item = m_data.find(key); - if (key_and_item != m_data.end()) { - remove(key_and_item); - return true; - } - return false; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::clear() -{ - LockGuard guard(lock()); - - m_data.clear(); - - m_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); - m_byte_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); - - m_insertion_policy->clear(); - m_eviction_policy->clear(); - m_constraint_policy->clear(); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -void Cache::retain(P predicate_fn) -{ - LockGuard guard(lock()); - - for (auto it = m_data.begin(); it != m_data.end();) { - const CacheItem& item = it->second; - - if (!predicate_fn(it->first, item.m_value)) { - remove(it++); - } else { - ++it; + // Call event handler iif the method is defined in the policy. + if constexpr (detail::traits::event::HasOnCacheHit) { + m_insertion_policy->on_cache_hit(key, item); } - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -void Cache::for_each(F unary_function) -{ - LockGuard guard(lock()); - for (const auto& [key, value] : m_data) { - unary_function(key, value.m_value); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::swap(CacheType& other) noexcept -{ - try { - // Acquire both cache locks. - std::pair guards{lock_pair(other)}; - - using std::swap; - - swap(m_statistics_window_size, other.m_statistics_window_size); - - swap(m_insertion_policy, other.m_insertion_policy); - swap(m_eviction_policy, other.m_eviction_policy); - swap(m_constraint_policy, other.m_constraint_policy); - - swap(m_data, other.m_data); - - swap(m_hit_rate_acc, other.m_hit_rate_acc); - swap(m_byte_hit_rate_acc, other.m_byte_hit_rate_acc); - } catch (const std::system_error& e) { - // The only exception that can sensibly be thrown in the above block is a `system_error` when acquiring the mutexes of both caches (if the caches - // are running in thread-safe mode). - // - // According to the [reference for std mutexes](https://en.cppreference.com/w/cpp/named_req/Mutex), this exception can be thrown for one of two - // reasons: - // - If the calling thread does not have the required privileges. - // - If the implementation determines that acquiring the lock would lead to a deadlock. - // - // Since both those errors are unrecoverable, we'll validate that the error code is one of the two we expect, and we'll terminate. - assert(e.code() == std::errc::operation_not_permitted || e.code() == std::errc::resource_deadlock_would_occur); - std::terminate(); - } catch (...) { - // Even though this is technically not possible in the current state of the code, we'll catch any leftover exception to satisfy clang-tidy. - std::terminate(); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline size_t Cache::number_of_items() const -{ - LockGuard guard(lock()); - return m_data.size(); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -void Cache::update_constraint(Args... args) -{ - LockGuard guard(lock()); - m_constraint_policy->update(std::forward(args)...); - - while (!m_constraint_policy->is_satisfied()) { - auto eviction_ticket = m_eviction_policy->pop_victim(); - auto key_and_item = m_data.find(eviction_ticket.key()); - - // If this trips, the eviction policy tried to evict an item not in cache: the eviction policy and the cache are out of sync. - assert(key_and_item != m_data.end()); - - remove_popped_victim(key_and_item); - } - - assert(m_constraint_policy->is_satisfied()); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline auto Cache::insertion_policy() -> MyInsertionPolicy& -{ - return *m_insertion_policy; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline auto Cache::insertion_policy() const -> const MyInsertionPolicy& -{ - return *m_insertion_policy; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline auto Cache::eviction_policy() -> MyEvictionPolicy& -{ - return *m_eviction_policy; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline auto Cache::eviction_policy() const -> const MyEvictionPolicy& -{ - return *m_eviction_policy; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline auto Cache::constraint_policy() -> MyConstraintPolicy& -{ - return *m_constraint_policy; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline auto Cache::constraint_policy() const -> const MyConstraintPolicy& -{ - return *m_constraint_policy; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline double Cache::hit_rate() const -{ - return boost::accumulators::rolling_mean(m_hit_rate_acc); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline double Cache::byte_hit_rate() const -{ - return boost::accumulators::rolling_mean(m_byte_hit_rate_acc); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline uint32_t Cache::statistics_window_size() const -{ - return m_statistics_window_size; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -inline void Cache::statistics_window_size(uint32_t window_size) -{ - m_statistics_window_size = window_size; - - m_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); - m_byte_hit_rate_acc = MeanAccumulator(boost::accumulators::tag::rolling_window::window_size = m_statistics_window_size); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -auto Cache::lock() const -> LockGuard -{ - LockGuard guard{m_mutex}; - return guard; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -auto Cache::try_lock() const -> LockGuard -{ - LockGuard guard{m_mutex, std::try_to_lock}; - return guard; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -auto Cache::lock([[maybe_unused]] std::defer_lock_t defer_lock_tag) const -> LockGuard -{ - LockGuard guard{m_mutex, defer_lock_tag}; - return guard; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template, int>> -auto Cache::try_lock(const std::chrono::duration& timeout) const -> LockGuard -{ - LockGuard guard{m_mutex, std::defer_lock}; - [[maybe_unused]] const bool locked = guard.try_lock_for(timeout); - return guard; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -auto Cache::lock_pair(CacheType& other) const -> std::pair -{ - LockGuard my_guard = lock(std::defer_lock); - LockGuard other_guard = other.lock(std::defer_lock); - - std::lock(my_guard, other_guard); - - return std::make_pair(std::move(my_guard), std::move(other_guard)); -} -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -void Cache::import(Coll& collection) -{ - LockGuard guard(lock()); - - for (auto& [key, value] : collection) { - const auto key_size = static_cast(m_measure_key(key)); - const auto value_size = static_cast(m_measure_value(value)); - CacheItem item{key_size, std::move(value), value_size}; - - if (!m_constraint_policy->prepare_insert(key, item).is_satisfied()) { - return; + if constexpr (detail::traits::event::HasOnCacheHit) { + m_eviction_policy->on_cache_hit(key, item); } - insert_or_update(std::move(key), std::move(item)); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -bool Cache::collect_evictions(const K& candidate_key, ConstraintTicket& ticket) -{ - std::vector> keys_to_evict; - - while (!ticket.is_satisfied()) { - auto eviction_ticket = m_eviction_policy->pop_victim(); - auto key_and_item = m_data.find(eviction_ticket.key()); - - // If this trips, the eviction policy recommended the eviction of a key not in cache: the eviction policy and the - // cache are out of sync. - assert(key_and_item != m_data.end()); - - if (!m_insertion_policy->should_replace(key_and_item->first, candidate_key)) { - m_eviction_policy->rollback(std::move(eviction_ticket)); - for (auto it = keys_to_evict.rbegin(); it != keys_to_evict.rend(); ++it) { - m_eviction_policy->rollback(std::move(it->first)); - } - return false; + if constexpr (detail::traits::event::HasOnCacheHit) { + m_constraint_policy->on_cache_hit(key, item); } - - ticket.register_eviction(key_and_item->first, key_and_item->second); - keys_to_evict.emplace_back(std::move(eviction_ticket), key_and_item); - } - - for (auto& [_, key_and_item] : keys_to_evict) { - remove_popped_victim(key_and_item); - } - - return true; -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -bool Cache::check_insert(const K& key, const CacheItem& item) -{ - auto insertion_ticket = m_constraint_policy->prepare_insert(key, item); - - if (insertion_ticket.is_satisfied()) { - // The constraint is already satisfied, so we just check the insertion policy. - return m_insertion_policy->should_add(key); - } - - if (!insertion_ticket.is_satisfiable()) { - return false; // item can't be inserted even if we evict everything. - } - - return collect_evictions(key, insertion_ticket); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -bool Cache::check_replace(const K& key, const CacheItem& old_item, const CacheItem& new_item) -{ - auto replacement_ticket = m_constraint_policy->prepare_replace(key, old_item, new_item); - - if (replacement_ticket.is_satisfied()) { - return true; - } - - if (!replacement_ticket.is_satisfiable()) { - return false; - } - - return collect_evictions(key, replacement_ticket); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::insert_or_update(K&& key, CacheItem&& item) -{ - auto key_and_item = m_data.find(key); - if (key_and_item != m_data.end()) { - using std::swap; - swap(key_and_item->second, item); - on_update(key_and_item->first, item, key_and_item->second); - } else { - // Insert. - const auto it_and_ok = m_data.insert_or_assign(std::move(key), std::move(item)); - assert(it_and_ok.second); - on_insert(it_and_ok.first->first, it_and_ok.first->second); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::remove(DataMapIt it) -{ - on_evict(it->first, it->second); - m_data.erase(it); -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::remove_popped_victim(DataMapIt it) -{ - if constexpr (detail::traits::event::HasOnEvict) { - m_insertion_policy->on_evict(it->first, it->second); - } - - if constexpr (detail::traits::event::HasOnEvict) { - m_constraint_policy->on_evict(it->first, it->second); } - m_data.erase(it); -} + template void on_cache_miss(const KeyView& key) const + { + // Update the cache hit rate accumulators. + m_hit_rate_acc(0); + m_byte_hit_rate_acc(0); -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::on_insert(const K& key, const CacheItem& item) const -{ - // Call event handler iif the method is defined in the policy. - if constexpr (detail::traits::event::HasOnInsert) { - m_insertion_policy->on_insert(key, item); - } - - if constexpr (detail::traits::event::HasOnInsert) { - m_eviction_policy->on_insert(key, item); - } - - if constexpr (detail::traits::event::HasOnInsert) { - m_constraint_policy->on_insert(key, item); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::on_update(const K& key, const CacheItem& old_item, const CacheItem& new_item) const -{ - // Call event handler iif the method is defined in the policy. - if constexpr (detail::traits::event::HasOnUpdate) { - m_insertion_policy->on_update(key, old_item, new_item); - } - - if constexpr (detail::traits::event::HasOnUpdate) { - m_eviction_policy->on_update(key, old_item, new_item); - } - - if constexpr (detail::traits::event::HasOnUpdate) { - m_constraint_policy->on_update(key, old_item, new_item); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::on_cache_hit(const K& key, const CacheItem& item) const -{ - // Update the cache hit rate accumulators. - m_hit_rate_acc(1); - m_byte_hit_rate_acc(static_cast(item.m_value_size)); - - // Call event handler iif the method is defined in the policy. - if constexpr (detail::traits::event::HasOnCacheHit) { - m_insertion_policy->on_cache_hit(key, item); - } - - if constexpr (detail::traits::event::HasOnCacheHit) { - m_eviction_policy->on_cache_hit(key, item); - } - - if constexpr (detail::traits::event::HasOnCacheHit) { - m_constraint_policy->on_cache_hit(key, item); - } -} - -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -template -void Cache::on_cache_miss(const KeyView& key) const -{ - // Update the cache hit rate accumulators. - m_hit_rate_acc(0); - m_byte_hit_rate_acc(0); - - // Call event handler iif the method is defined in the policy. - if constexpr (detail::traits::event::HasOnCacheMiss) { - m_insertion_policy->on_cache_miss(key); - } + // Call event handler iif the method is defined in the policy. + if constexpr (detail::traits::event::HasOnCacheMiss) { + m_insertion_policy->on_cache_miss(key); + } - if constexpr (detail::traits::event::HasOnCacheMiss) { - m_eviction_policy->on_cache_miss(key); - } + if constexpr (detail::traits::event::HasOnCacheMiss) { + m_eviction_policy->on_cache_miss(key); + } - if constexpr (detail::traits::event::HasOnCacheMiss) { - m_constraint_policy->on_cache_miss(key); + if constexpr (detail::traits::event::HasOnCacheMiss) { + m_constraint_policy->on_cache_miss(key); + } } -} -template class I, - template class E, - template class C, - class SV, - class SK, - class KH, - LockingStrategy L> -void Cache::on_evict(const K& key, const CacheItem& item) const -{ - // Call event handler iif the method is defined in the policy. - if constexpr (detail::traits::event::HasOnEvict) { - m_insertion_policy->on_evict(key, item); - } + void on_evict(const Key& key, const CacheItem& item) const + { + if constexpr (detail::traits::event::HasOnEvict) { + m_insertion_policy->on_evict(key, item); + } - if constexpr (detail::traits::event::HasOnEvict) { - m_eviction_policy->on_evict(key, item); - } + if constexpr (detail::traits::event::HasOnEvict) { + m_eviction_policy->on_evict(key, item); + } - if constexpr (detail::traits::event::HasOnEvict) { - m_constraint_policy->on_evict(key, item); + if constexpr (detail::traits::event::HasOnEvict) { + m_constraint_policy->on_evict(key, item); + } } -} +}; template class CompositeKey template static void HashTuple(H& h, const T& key) { if constexpr (detail::traits::BasicString) { - h = H::combine_contiguous(std::move(h), key.data(), key.size()); + h = H::combine_contiguous(std::move(h), key.c_str(), key.size()); } else if constexpr (detail::traits::BasicStringView) { h = H::combine_contiguous(std::move(h), key.data(), key.size()); } else { diff --git a/include/cachemere/policy/constraint_count.h b/include/cachemere/policy/constraint_count.h index 3b94c78..61833b3 100644 --- a/include/cachemere/policy/constraint_count.h +++ b/include/cachemere/policy/constraint_count.h @@ -3,6 +3,7 @@ #include #include +#include namespace cachemere::policy { @@ -64,100 +65,80 @@ template class ConstraintCount } }; - explicit ConstraintCount(size_t maximum_count); + explicit ConstraintCount(size_t maximum_count) + { + update(maximum_count); + } /// @brief Clears the policy. - void clear(); + void clear() + { + m_count = 0; + } + + [[nodiscard]] InsertionTicket prepare_insert(const Key& /* key */, const CacheItem& /* item */) + { + const size_t count_to_free = (m_count + 1 > m_maximum_count) ? ((m_count + 1) - m_maximum_count) : 0; + return InsertionTicket{count_to_free, m_maximum_count > 0}; + } - [[nodiscard]] InsertionTicket prepare_insert(const Key& key, const CacheItem& item); - [[nodiscard]] ReplacementTicket prepare_replace(const Key& key, const CacheItem& old_item, const CacheItem& new_item); + [[nodiscard]] ReplacementTicket prepare_replace(const Key& /* key */, const CacheItem& /* old_item */, const CacheItem& /* new_item */) + { + return ReplacementTicket{}; + } /// @brief Returns whether the constraint is satisfied. /// @details Used by the cache after a constraint update to compute how many items should be evicted, if any. /// @return Whether the cache constraint is satisfied. - [[nodiscard]] bool is_satisfied(); + [[nodiscard]] bool is_satisfied() + { + return m_count <= m_maximum_count; + } /// @brief Update the cache constraint. /// @details Sets a new maximum count. /// @param maximum_count The new number of items in cache. - void update(size_t maximum_count); + void update(size_t maximum_count) + { + m_maximum_count = maximum_count; + } /// @brief Insertion event handler. /// @details Adds one to the number of items in cache. /// @param key The key of the inserted item. /// @param item The item that has been inserted in cache. - void on_insert(const Key& key, const CacheItem& item); + void on_insert(const Key& /* key */, const CacheItem& /* item */) + { + ++m_count; + } /// @brief Eviction event handler. /// @details Removes one from the number of items in cache. /// @param key The key that was evicted. /// @param item The item that was evicted. - void on_evict(const Key& key, const CacheItem& item); + void on_evict(const Key& /* key */, const CacheItem& /* item */) + { + assert(m_count > 0); + --m_count; + } /// @brief Get the number of items currently in the cache. /// @return The number of items in cache. - [[nodiscard]] size_t count() const; + [[nodiscard]] size_t count() const + { + return m_count; + } /// @brief Get the maximum number of items allowed in cache. /// @return The maximum number of items allowed in cache. - [[nodiscard]] size_t maximum_count() const; + [[nodiscard]] size_t maximum_count() const + { + return m_maximum_count; + } private: size_t m_maximum_count; size_t m_count = 0; }; -template ConstraintCount::ConstraintCount(size_t maximum_count) -{ - update(maximum_count); -} - -template void ConstraintCount::clear() -{ - m_count = 0; -} - -template auto ConstraintCount::prepare_insert(const K& /* key */, const CacheItem& /* item */) -> InsertionTicket -{ - const size_t count_to_free = (m_count + 1 > m_maximum_count) ? ((m_count + 1) - m_maximum_count) : 0; - return InsertionTicket{count_to_free, m_maximum_count > 0}; -} - -template -auto ConstraintCount::prepare_replace(const K& /* key */, const CacheItem& /* old_item */, const CacheItem& /* new_item */) -> ReplacementTicket -{ - return ReplacementTicket{}; -} - -template bool ConstraintCount::is_satisfied() -{ - return m_count <= m_maximum_count; -} - -template void ConstraintCount::update(size_t maximum_count) -{ - m_maximum_count = maximum_count; -} - -template void ConstraintCount::on_insert(const K& /* key */, const CacheItem& /* item */) -{ - ++m_count; -} - -template void ConstraintCount::on_evict(const K& /* key */, const CacheItem& /* item */) -{ - assert(m_count > 0); - --m_count; -} - -template size_t ConstraintCount::count() const -{ - return m_count; -} - -template size_t ConstraintCount::maximum_count() const -{ - return m_maximum_count; -} - } // namespace cachemere::policy diff --git a/include/cachemere/policy/constraint_memory.h b/include/cachemere/policy/constraint_memory.h index cb1dd15..53b0bfc 100644 --- a/include/cachemere/policy/constraint_memory.h +++ b/include/cachemere/policy/constraint_memory.h @@ -91,17 +91,27 @@ template class ConstraintMemory /// @brief Constructor. /// @param max_memory The maximum amount of memory to be used by the cache, in bytes. - explicit ConstraintMemory(size_t max_memory); + explicit ConstraintMemory(size_t max_memory) + { + update(max_memory); + } /// @brief Clears the policy. - void clear(); + void clear() + { + m_memory = 0; + } /// @brief Determines whether an insertion candidate can be added into the cache. /// @details That is, whether the constraint would still be satisfied after inserting the candidate. /// @param key The key of the insertion candidate. /// @param item The candidate item. /// @return Whether the item can be added in cache. - [[nodiscard]] InsertionTicket prepare_insert(const Key& key, const CacheItem& item); + [[nodiscard]] InsertionTicket prepare_insert(const Key& /* key */, const CacheItem& item) + { + const size_t memory_to_free = (m_memory + item.m_total_size > m_maximum_memory) ? ((m_memory + item.m_total_size) - m_maximum_memory) : 0; + return InsertionTicket{memory_to_free, item.m_total_size <= m_maximum_memory}; + } /// @brief Determines how much memory would need to be freed to insert a candidate. /// @details The returned ticket can be updated with evictions until it becomes satisfied. @@ -109,118 +119,85 @@ template class ConstraintMemory /// @param old_item The current value of the key in cache. /// @param new_item The value that would replace the current value. /// @return A ticket describing whether the replacement can be satisfied. - [[nodiscard]] ReplacementTicket prepare_replace(const Key& key, const CacheItem& old_item, const CacheItem& new_item); + [[nodiscard]] ReplacementTicket prepare_replace(const Key& key, const CacheItem& old_item, const CacheItem& new_item) + { + assert(old_item.m_key_size == new_item.m_key_size); // Key size *really* shouldn't have changed since the key is supposed to be const. + + const size_t memory_to_free_with_replace = ((m_memory - old_item.m_value_size) + new_item.m_value_size > m_maximum_memory) + ? (((m_memory - old_item.m_value_size) + new_item.m_value_size) - m_maximum_memory) + : 0; + + const size_t memory_to_free_with_insert = + (m_memory + new_item.m_total_size > m_maximum_memory) ? ((m_memory + new_item.m_total_size) - m_maximum_memory) : 0; + + return ReplacementTicket{key, memory_to_free_with_replace, memory_to_free_with_insert, new_item.m_total_size <= m_maximum_memory}; + } /// @brief Returns whether the constraint is satisfied. /// @details Used by the cache after a constraint update to compute how many items should be evicted, if any. /// @return Whether the cache constraint is satisfied. - [[nodiscard]] bool is_satisfied(); + [[nodiscard]] bool is_satisfied() + { + return m_memory <= m_maximum_memory; + } /// @brief Update the cache constraint. /// @details Sets a new maximum amount of memory. /// @param max_memory The new maximum amount of memory to be used by the cache. - void update(size_t max_memory); + void update(size_t max_memory) + { + m_maximum_memory = max_memory; + } /// @brief Get the amount of memory currently used by the cache. /// @return The current amount of memory used, in bytes. - [[nodiscard]] size_t memory() const; + [[nodiscard]] size_t memory() const + { + return m_memory; + } /// @brief Get the maximum amount of memory that can be used by the cache. /// @return The maximum amount of memory, in bytes. - [[nodiscard]] size_t maximum_memory() const; + [[nodiscard]] size_t maximum_memory() const + { + return m_maximum_memory; + } /// @brief Insertion event handler. /// @details Adds the size of this item to the amount of memory used. /// @param key The key of the inserted item. /// @param item The item that has been inserted in cache. - void on_insert(const Key& key, const CacheItem& item); + void on_insert(const Key& /* key */, const CacheItem& item) + { + m_memory += item.m_total_size; + assert(m_memory <= m_maximum_memory); + } /// @brief Update event handler. /// @details Updates the amount of memory used to reflect the size difference between the old and the new value. /// @param key The key that has been updated in the cache. /// @param old_item The old value for this key. /// @param new_item The new value for this key - void on_update(const Key& key, const CacheItem& old_item, const CacheItem& new_item); + void on_update(const Key& /* key */, const CacheItem& old_item, const CacheItem& new_item) + { + m_memory -= old_item.m_value_size; + m_memory += new_item.m_value_size; + assert(m_memory <= m_maximum_memory); + } /// @brief Eviction event handler. /// @details Removes the item at the back of the list - ensuring it has the provided key. /// @param key The key that was evicted. /// @param item The item that was evicted. - void on_evict(const Key& key, const CacheItem& item); + void on_evict(const Key& /* key */, const CacheItem& item) + { + assert(item.m_key_size + item.m_value_size <= m_memory); + m_memory -= item.m_total_size; + } private: size_t m_maximum_memory; size_t m_memory = 0; }; -template ConstraintMemory::ConstraintMemory(size_t max_memory) -{ - update(max_memory); -} - -template void ConstraintMemory::clear() -{ - m_memory = 0; -} - -template auto ConstraintMemory::prepare_insert(const K& /* key */, const CacheItem& item) -> InsertionTicket -{ - const size_t memory_to_free = (m_memory + item.m_total_size > m_maximum_memory) ? ((m_memory + item.m_total_size) - m_maximum_memory) : 0; - return InsertionTicket{memory_to_free, item.m_total_size <= m_maximum_memory}; -} - -template -auto ConstraintMemory::prepare_replace(const K& key, const CacheItem& old_item, const CacheItem& new_item) -> ReplacementTicket -{ - assert(old_item.m_key_size == new_item.m_key_size); // Key size *really* shouldn't have changed since the key is supposed to be const. - - const size_t memory_to_free_with_replace = ((m_memory - old_item.m_value_size) + new_item.m_value_size > m_maximum_memory) - ? (((m_memory - old_item.m_value_size) + new_item.m_value_size) - m_maximum_memory) - : 0; - - const size_t memory_to_free_with_insert = - (m_memory + new_item.m_total_size > m_maximum_memory) ? ((m_memory + new_item.m_total_size) - m_maximum_memory) : 0; - - return ReplacementTicket{key, memory_to_free_with_replace, memory_to_free_with_insert, new_item.m_total_size <= m_maximum_memory}; -} - -template bool ConstraintMemory::is_satisfied() -{ - return m_memory <= m_maximum_memory; -} - -template void ConstraintMemory::update(size_t max_memory) -{ - m_maximum_memory = max_memory; -} - -template size_t ConstraintMemory::memory() const -{ - return m_memory; -} - -template size_t ConstraintMemory::maximum_memory() const -{ - return m_maximum_memory; -} - -template void ConstraintMemory::on_insert(const K& /* key */, const CacheItem& item) -{ - m_memory += item.m_total_size; - assert(m_memory <= m_maximum_memory); -} - -template void ConstraintMemory::on_update(const K& /* key */, const CacheItem& old_item, const CacheItem& new_item) -{ - m_memory -= old_item.m_value_size; - m_memory += new_item.m_value_size; - assert(m_memory <= m_maximum_memory); -} - -template void ConstraintMemory::on_evict(const K& /* key */, const CacheItem& item) -{ - assert(item.m_total_size <= m_memory); - m_memory -= item.m_total_size; -} - } // namespace cachemere::policy diff --git a/include/cachemere/policy/detail/bloom_filter.h b/include/cachemere/policy/detail/bloom_filter.h index 1bb9459..569eb7b 100644 --- a/include/cachemere/policy/detail/bloom_filter.h +++ b/include/cachemere/policy/detail/bloom_filter.h @@ -36,32 +36,70 @@ template class BloomFilter /// while having an estimate that is too low will drastically reduce the accuracy of the filter. /// /// @param cardinality The expected cardinality of the set to be inserted in the filter. - BloomFilter(uint32_t cardinality); + BloomFilter(uint32_t cardinality) + : m_cardinality{cardinality}, + m_filter{optimal_filter_size(cardinality), false}, + m_nb_hashes{optimal_nb_of_hash_functions(cardinality, m_filter.size())} + { + } /// @brief Add an item to the filter. /// @param item The item to insert. - template void add(const ItemKey& item); + template void add(const ItemKey& item) + { + HashMixer mixer{item, m_filter.size()}; + + for (size_t i = 0; i < m_nb_hashes; ++i) { + const size_t filter_idx = mixer(); + assert(filter_idx < m_filter.size()); + + m_filter.set(filter_idx); + } + } /// @brief Clear the filter while keeping the allocated memory. - void clear(); + void clear() + { + m_filter.reset(); + } /// @brief Test membership of the specified item. /// @details A bloom filter can return false positives, but not false negatives. /// This method returning `true` only means that the set _might_ contain the specified item, /// while a return value of `false` means that the set _certainly_ does not contain the specified item. /// @param item The item to test. - template [[nodiscard]] bool maybe_contains(const ItemKey& item) const; + template [[nodiscard]] bool maybe_contains(const ItemKey& item) const + { + HashMixer mixer{item, m_filter.size()}; + + for (size_t i = 0; i < m_nb_hashes; ++i) { + const size_t filter_idx = mixer(); + assert(filter_idx < m_filter.size()); + + if (!m_filter.test(filter_idx)) { + return false; + } + } + return true; + } /// @brief Get an estimate of the memory consumption of the filter. /// @return The memory used by the filter, in bytes. - [[nodiscard]] size_t memory_used() const noexcept; + [[nodiscard]] size_t memory_used() const noexcept + { + return m_filter.num_blocks() * sizeof(BitsetBlock) + sizeof(m_nb_hashes); + } /// @brief Get the saturation of the filter. /// @details As filter saturations increases, so will the probability of false positives. /// A filter saturation of 1.0 means that all underlying bits are set to `1`, so every call to `maybe_contains` will return `true`. /// Increasing the filter cardinality will slow down the saturation of the filter, at the cost of using more memory. /// @return The saturation of the filter, as a fraction - [[nodiscard]] double saturation() const noexcept; + [[nodiscard]] double saturation() const noexcept + { + assert(m_filter.size() > 0); + return static_cast(m_filter.count()) / static_cast(m_filter.size()); + } private: using BitsetBlock = uint8_t; @@ -72,56 +110,4 @@ template class BloomFilter uint32_t m_nb_hashes; }; -template -BloomFilter::BloomFilter(uint32_t cardinality) - : m_cardinality{cardinality}, - m_filter{optimal_filter_size(cardinality), false}, - m_nb_hashes{optimal_nb_of_hash_functions(cardinality, m_filter.size())} -{ -} - -template template void BloomFilter::add(const ItemKey& item) -{ - HashMixer mixer{item, m_filter.size()}; - - for (size_t i = 0; i < m_nb_hashes; ++i) { - const size_t filter_idx = mixer(); - assert(filter_idx < m_filter.size()); - - m_filter.set(filter_idx); - } -} - -template void BloomFilter::clear() -{ - m_filter.reset(); -} - -template template bool BloomFilter::maybe_contains(const ItemKey& item) const -{ - HashMixer mixer{item, m_filter.size()}; - - for (size_t i = 0; i < m_nb_hashes; ++i) { - const size_t filter_idx = mixer(); - assert(filter_idx < m_filter.size()); - - if (!m_filter.test(filter_idx)) { - return false; - } - } - - return true; -} - -template size_t BloomFilter::memory_used() const noexcept -{ - return m_filter.num_blocks() * sizeof(BitsetBlock) + sizeof(m_nb_hashes); -} - -template double BloomFilter::saturation() const noexcept -{ - assert(m_filter.size() > 0); - return static_cast(m_filter.count()) / static_cast(m_filter.size()); -} - } // namespace cachemere::policy::detail diff --git a/include/cachemere/policy/detail/counting_bloom_filter.h b/include/cachemere/policy/detail/counting_bloom_filter.h index e5dfb10..b651989 100644 --- a/include/cachemere/policy/detail/counting_bloom_filter.h +++ b/include/cachemere/policy/detail/counting_bloom_filter.h @@ -28,20 +28,52 @@ template class CountingBloomFilter /// while having an estimate that is too low will drastically reduce the accuracy of the filter. /// /// @param cardinality The expected cardinality of the set to be inserted in the filter. - CountingBloomFilter(uint32_t cardinality); + CountingBloomFilter(uint32_t cardinality) + : m_cardinality{cardinality}, + m_filter(optimal_filter_size(cardinality), 0), + m_nb_hashes{optimal_nb_of_hash_functions(cardinality, m_filter.size())} + { + } /// @brief Increment the count for a given item by one. /// @param item The item of the counter to increment. - template void add(const ItemKey& item); + template void add(const ItemKey& item) + { + const auto [indices, minimum_val] = indices_and_minimum(item); + + // Increment all filter slots corresponding to the minimum value. + const uint8_t is_zero_increment = minimum_val == 0 ? 1 : 0; + for (const size_t idx : indices) { + if (m_filter[idx] == minimum_val) { + ++m_filter[idx]; + + // We track the number of non-zero values in the filter. + // This is used to compute the filter saturation. + m_nb_nonzero += is_zero_increment; + } + } + } /// @brief Clear the filter while keeping the allocated memory. - void clear(); + void clear() + { + std::ranges::fill(m_filter.begin(), m_filter.end(), 0); + m_nb_nonzero = 0; + } /// @brief Divide counter values by two. /// @details If the user of this filter doesn't rely on the absolute value of the counters, calling this /// regularly will decrease the saturation of the filter by removing items that are very /// rarely seen. - void decay(); + void decay() + { + for (auto& counter : m_filter) { + if (counter == 1) { + --m_nb_nonzero; + } + counter /= 2; + } + } /// @brief Get the counter estimate for a given item. /// @param item The item for which to estimate the count. @@ -50,15 +82,41 @@ template class CountingBloomFilter /// the real counter value - the real count is guaranteed to be less or equal to the estimate /// returned. /// @return The counter estimate for the item. - template [[nodiscard]] uint32_t estimate(const ItemKey& item) const; + template [[nodiscard]] uint32_t estimate(const ItemKey& item) const + { + assert(m_nb_hashes > 0); + + HashMixer mixer{item, m_filter.size()}; + + uint32_t minimum_val = std::numeric_limits::max(); + for (size_t i = 0; i < m_nb_hashes; ++i) { + const size_t idx = mixer(); + assert(idx < m_filter.size()); + + minimum_val = std::min(m_filter[idx], minimum_val); + } + + return minimum_val; + } /// @brief Get the cardinality of the filter. /// @return The cardinality of the filter, as configured. - [[nodiscard]] uint32_t cardinality() const noexcept; + [[nodiscard]] uint32_t cardinality() const noexcept + { + return m_cardinality; + } /// @brief Get an estimate of the memory consumption of the filter. /// @return The memory used by the filter, in bytes. - [[nodiscard]] size_t memory_used() const noexcept; + [[nodiscard]] size_t memory_used() const noexcept + { + // Note: + // The amount of memory could be reduced by using variable-length counters. + // Since we know that reset is triggered once we reach a count of + // m_Cardinality, we could use the smallest numeric type that holds + // m_Cardinality for the internal counters. + return m_filter.size() * sizeof(uint32_t) + sizeof(m_cardinality) + sizeof(m_nb_hashes) + sizeof(m_nb_nonzero); + } /// @brief Get the saturation of the filter. /// @details As filter saturations increases, so will the probability of false positives. @@ -66,106 +124,35 @@ template class CountingBloomFilter /// a counter value greater than 1. /// Increasing the filter cardinality will slow down the saturation of the filter, at the cost of using more memory. /// @return The saturation of the filter, as a fraction. - [[nodiscard]] double saturation() const noexcept; + [[nodiscard]] double saturation() const noexcept + { + assert(m_filter.size() > 0); + return static_cast(m_nb_nonzero) / static_cast(m_filter.size()); + } private: uint32_t m_cardinality; std::vector m_filter; uint32_t m_nb_hashes; uint32_t m_nb_nonzero = 0; -}; - -template -CountingBloomFilter::CountingBloomFilter(uint32_t cardinality) - : m_cardinality{cardinality}, - m_filter(optimal_filter_size(cardinality), 0), - m_nb_hashes{optimal_nb_of_hash_functions(cardinality, m_filter.size())} -{ -} - -template template void CountingBloomFilter::add(const ItemKey& item) -{ - HashMixer mixer{item, m_filter.size()}; - - std::vector indices; - indices.reserve(m_nb_hashes); - uint32_t minimum_val = std::numeric_limits::max(); + template [[nodiscard]] std::pair, uint32_t> indices_and_minimum(const ItemKey& item) const + { + HashMixer mixer{item, m_filter.size()}; + std::vector indices; + indices.reserve(m_nb_hashes); + uint32_t minimum_val = std::numeric_limits::max(); - // Generate the indices and find the minimum. - for (size_t i = 0; i < m_nb_hashes; ++i) { - const size_t idx = mixer(); - assert(idx < m_filter.size()); + for (size_t i = 0; i < m_nb_hashes; ++i) { + const size_t idx = mixer(); + assert(idx < m_filter.size()); - indices.push_back(idx); - minimum_val = std::min(m_filter[idx], minimum_val); - } - - // Increment all filter slots corresponding to the minimum value. - const uint8_t is_zero_increment = minimum_val == 0 ? 1 : 0; - for (const size_t idx : indices) { - if (m_filter[idx] == minimum_val) { - ++m_filter[idx]; - - // We track the number of non-zero values in the filter. - // This is used to compute the filter saturation. - m_nb_nonzero += is_zero_increment; + indices.push_back(idx); + minimum_val = std::min(m_filter[idx], minimum_val); } - } -} - -template void CountingBloomFilter::clear() -{ - std::ranges::fill(m_filter.begin(), m_filter.end(), 0); - m_nb_nonzero = 0; -} -template void CountingBloomFilter::decay() -{ - for (auto& counter : m_filter) { - if (counter == 1) { - --m_nb_nonzero; - } - counter /= 2; + return {indices, minimum_val}; } -} - -template template uint32_t CountingBloomFilter::estimate(const ItemKey& item) const -{ - assert(m_nb_hashes > 0); - - HashMixer mixer{item, m_filter.size()}; - - uint32_t minimum_val = std::numeric_limits::max(); - for (size_t i = 0; i < m_nb_hashes; ++i) { - const size_t idx = mixer(); - assert(idx < m_filter.size()); - - minimum_val = std::min(m_filter[idx], minimum_val); - } - - return minimum_val; -} - -template uint32_t CountingBloomFilter::cardinality() const noexcept -{ - return m_cardinality; -} - -template size_t CountingBloomFilter::memory_used() const noexcept -{ - // Note: - // The amount of memory could be reduced by using variable-length counters. - // Since we know that reset is triggered once we reach a count of - // m_Cardinality, we could use the smallest numeric type that holds - // m_Cardinality for the internal counters. - return m_filter.size() * sizeof(uint32_t) + sizeof(m_cardinality) + sizeof(m_nb_hashes) + sizeof(m_nb_nonzero); -} - -template double CountingBloomFilter::saturation() const noexcept -{ - assert(m_filter.size() > 0); - return static_cast(m_nb_nonzero) / static_cast(m_filter.size()); -} +}; } // namespace cachemere::policy::detail diff --git a/include/cachemere/policy/detail/hash_mixer.h b/include/cachemere/policy/detail/hash_mixer.h index 88dd25e..21f0918 100644 --- a/include/cachemere/policy/detail/hash_mixer.h +++ b/include/cachemere/policy/detail/hash_mixer.h @@ -15,28 +15,23 @@ template class HashMixer : private KeyHash /// @param key The key to use to seed this instance. /// @param value_range The upper bound of the value range. /// This mixer will return values in the range `[0, value_range)`. - HashMixer(const Key& key, size_t value_range); + HashMixer(const Key& key, size_t value_range) + : KeyHash{}, + m_rng{static_cast(KeyHash::operator()(key))}, + m_value_range{value_range} + { + } /// @brief Generate the next value in the random sequence. /// @return The next value in the sequence. - [[nodiscard]] size_t operator()(); + [[nodiscard]] size_t operator()() + { + return m_rng() % m_value_range; + } private: std::minstd_rand m_rng; size_t m_value_range; }; -template -HashMixer::HashMixer(const Key& key, size_t value_range) - : KeyHash{}, - m_rng{static_cast(KeyHash::operator()(key))}, - m_value_range{value_range} -{ -} - -template size_t HashMixer::operator()() -{ - return m_rng() % m_value_range; -} - } // namespace cachemere::policy::detail diff --git a/include/cachemere/policy/eviction_gdsf.h b/include/cachemere/policy/eviction_gdsf.h index b740b8e..810607d 100644 --- a/include/cachemere/policy/eviction_gdsf.h +++ b/include/cachemere/policy/eviction_gdsf.h @@ -30,9 +30,14 @@ template class Ev using KeyRef = std::reference_wrapper; struct PriorityEntry { - PriorityEntry(KeyRef key, double coefficient); + PriorityEntry(KeyRef key, double coefficient) : m_key{key}, m_h_coefficient{coefficient} + { + } - bool operator<(const PriorityEntry& other) const; + bool operator<(const PriorityEntry& other) const + { + return m_h_coefficient < other.m_h_coefficient; + } KeyRef m_key; double m_h_coefficient; @@ -61,7 +66,12 @@ template class Ev using CacheItem = Item; /// @brief Clears the policy. - void clear(); + void clear() + { + m_priority_set.clear(); + m_iterator_map.clear(); + m_frequency_sketch.clear(); + } /// @brief Set the cardinality of the policy. /// @details The set cardinality should be a decent approximation of the cardinality @@ -69,32 +79,64 @@ template class Ev /// accurate estimate is very important, because an underestimation will severely /// decrease the accuracy of the policy, while an overestimation will use too much memory. /// @param cardinality The expected cardinality of the set of items. - void set_cardinality(uint32_t cardinality); + void set_cardinality(uint32_t cardinality) + { + m_frequency_sketch = detail::CountingBloomFilter{cardinality}; + } /// @brief Insertion event handler. /// @details Computes a coefficient for the item and inserts it in the priority queue. /// @param key The key of the inserted item. /// @param item The item that has been inserted in cache. - void on_insert(const Key& key, const CacheItem& item); + void on_insert(const Key& key, const CacheItem& item) + { + m_frequency_sketch.add(key); + + assert(m_iterator_map.find(std::ref(key)) == m_iterator_map.end()); + + const auto it = m_priority_set.emplace(std::ref(key), get_h_coefficient(key, item)); + m_iterator_map.emplace(std::ref(key), it); + } /// @brief Update event handler. /// @details Updates the coefficient for this item and changes its position in the priority queue. /// @param key The key that has been updated in the cache. /// @param old_item The old value for this key. /// @param new_item The new value for this key. - void on_update(const Key& key, const CacheItem& old_item, const CacheItem& new_item); + void on_update(const Key& key, const CacheItem& /* old_item */, const CacheItem& new_item) + { + on_cache_hit(key, new_item); + } /// @brief Cache hit event handler. /// @details Updates the coefficient for this item and changes its position in the priority queue. /// @param key The key that has been hit. /// @param item The item that has been hit. - void on_cache_hit(const Key& key, const CacheItem& item); + void on_cache_hit(const Key& key, const CacheItem& item) + { + auto keyref_and_it = m_iterator_map.find(std::ref(key)); + assert(keyref_and_it != m_iterator_map.end()); + + m_frequency_sketch.add(key); + m_priority_set.erase(keyref_and_it->second); + keyref_and_it->second = m_priority_set.emplace(std::ref(key), get_h_coefficient(key, item)); + } /// @brief Eviction event handler. /// @details Removes the item from the priority queue. /// @param key The key that was evicted. /// @param item The item that was evicted. - void on_evict(const Key& key, const CacheItem& item); + void on_evict(const Key& key, const CacheItem& /* item */) + { + auto keyref_and_it = m_iterator_map.find(std::ref(key)); + assert(keyref_and_it != m_iterator_map.end()); + + const auto priority_it = keyref_and_it->second; + update_clock(static_cast(priority_it->m_h_coefficient)); + + m_priority_set.erase(priority_it); + m_iterator_map.erase(keyref_and_it); + } Ticket pop_victim() { @@ -135,80 +177,16 @@ template class Ev uint64_t m_clock{0}; - [[nodiscard]] double get_h_coefficient(const Key& key, const CacheItem& item) const noexcept; - void update_clock(uint64_t new_clock) + [[nodiscard]] double get_h_coefficient(const Key& key, const CacheItem& item) const noexcept + { + return static_cast(m_clock) + static_cast(m_frequency_sketch.estimate(key)) * + (static_cast(m_measure_cost(key, item)) / static_cast(item.m_total_size)); + } + + void update_clock(uint64_t new_clock) { m_clock = std::max(m_clock, new_clock); } }; -template -EvictionGDSF::PriorityEntry::PriorityEntry(KeyRef key, double coefficient) : m_key(key), - m_h_coefficient(coefficient) -{ -} - -template -bool EvictionGDSF::PriorityEntry::operator<(const PriorityEntry& other) const -{ - return m_h_coefficient < other.m_h_coefficient; -} - -template void EvictionGDSF::clear() -{ - m_priority_set.clear(); - m_iterator_map.clear(); - m_frequency_sketch.clear(); -} - -template void EvictionGDSF::set_cardinality(uint32_t cardinality) -{ - m_frequency_sketch = detail::CountingBloomFilter{cardinality}; -} - -template void EvictionGDSF::on_insert(const Key& key, const CacheItem& item) -{ - m_frequency_sketch.add(key); - - assert(m_iterator_map.find(std::ref(key)) == m_iterator_map.end()); - - const auto it = m_priority_set.emplace(std::ref(key), get_h_coefficient(key, item)); - m_iterator_map.emplace(std::ref(key), it); -} - -template -void EvictionGDSF::on_update(const Key& key, const CacheItem& /* old_item */, const CacheItem& new_item) -{ - on_cache_hit(key, new_item); -} - -template void EvictionGDSF::on_cache_hit(const Key& key, const CacheItem& item) -{ - auto keyref_and_it = m_iterator_map.find(std::ref(key)); - assert(keyref_and_it != m_iterator_map.end()); - - m_frequency_sketch.add(key); - m_priority_set.erase(keyref_and_it->second); - keyref_and_it->second = m_priority_set.emplace(std::ref(key), get_h_coefficient(key, item)); -} - -template void EvictionGDSF::on_evict(const Key& key, const CacheItem& /* item */) -{ - auto keyref_and_it = m_iterator_map.find(std::ref(key)); - assert(keyref_and_it != m_iterator_map.end()); - - const auto priority_it = keyref_and_it->second; - update_clock(static_cast(priority_it->m_h_coefficient)); - - m_priority_set.erase(priority_it); - m_iterator_map.erase(keyref_and_it); -} - -template -double EvictionGDSF::get_h_coefficient(const Key& key, const CacheItem& item) const noexcept -{ - return static_cast(m_clock) + - static_cast(m_frequency_sketch.estimate(key)) * (static_cast(m_measure_cost(key, item)) / static_cast(item.m_total_size)); -} - } // namespace cachemere::policy diff --git a/include/cachemere/policy/eviction_lru.h b/include/cachemere/policy/eviction_lru.h index 9d16b52..070c2d1 100644 --- a/include/cachemere/policy/eviction_lru.h +++ b/include/cachemere/policy/eviction_lru.h @@ -41,107 +41,92 @@ template class EvictionLRU }; /// @brief Clears the policy. - void clear(); + void clear() + { + m_keys.clear(); + m_nodes.clear(); + } /// @brief Insertion event handler. /// @details Inserts the provided item at the front of the list. /// @param key The key of the inserted item. /// @param item The item that has been inserted in cache. - void on_insert(const Key& key, const CacheItem& item); + void on_insert(const Key& key, const CacheItem& /* item */) + { + assert(m_nodes.find(std::ref(key)) == m_nodes.end()); // Validate the item is not already in policy. + m_keys.emplace_front(std::ref(key)); + m_nodes.emplace(std::ref(key), m_keys.begin()); + } /// @brief Update event handler. /// @details Moves the provided item to the front of the list. /// @param key The key that has been updated in the cache. /// @param old_item The old value for this key. /// @param new_item The new value for this key - void on_update(const Key& key, const CacheItem& old_item, const CacheItem& new_item); + void on_update(const Key& key, const CacheItem& /* old_item */, const CacheItem& new_item) + { + on_cache_hit(key, new_item); + } /// @brief Cache hit event handler. /// @details Moves the provided item at the front of the list. /// @param key The key that has been hit. /// @param item The item that has been hit. - void on_cache_hit(const Key& key, const CacheItem& item); + void on_cache_hit(const Key& key, const CacheItem& /* item */) + { + auto node_it = m_nodes.find(key); + if (node_it != m_nodes.end()) { + // No need to shuffle stuff around if item is already the hottest item in cache. + if (node_it->second != m_keys.begin()) { + m_keys.splice(m_keys.begin(), m_keys, node_it->second); + } + } else { + // If this is tripped, there is a disconnect between the contents of the policy and the contents of the cache. + assert(false); + } + } /// @brief Eviction event handler. /// @details Removes the item at the back of the list - ensuring it has the provided key. /// @param key The key that was evicted. /// @param item The item that was evicted. - void on_evict(const Key& key, const CacheItem& item); - - [[nodiscard]] Ticket pop_victim(); - void rollback(Ticket ticket); - -private: - std::list m_keys; - KeyRefMap m_nodes; -}; - -template void EvictionLRU::clear() -{ - m_keys.clear(); - m_nodes.clear(); -} - -template void EvictionLRU::on_insert(const Key& key, const CacheItem& /* item */) -{ - assert(m_nodes.find(std::ref(key)) == m_nodes.end()); // Validate the item is not already in policy. - - m_keys.emplace_front(std::ref(key)); - m_nodes.emplace(std::ref(key), m_keys.begin()); -} - -template -void EvictionLRU::on_update(const Key& key, const CacheItem& /* old_item */, const CacheItem& new_item) -{ - on_cache_hit(key, new_item); -} - -template void EvictionLRU::on_cache_hit(const Key& key, const CacheItem& /* item */) -{ - auto node_it = m_nodes.find(key); - if (node_it != m_nodes.end()) { - // No need to shuffle stuff around if item is already the hottest item in cache. - if (node_it->second != m_keys.begin()) { - m_keys.splice(m_keys.begin(), m_keys, node_it->second); + void on_evict(const Key& key, const CacheItem& /* item */) + { + assert(!m_nodes.empty()); + assert(!m_keys.empty()); + + if (m_keys.back().get() == key) { + m_nodes.erase(key); + m_keys.pop_back(); + } else { + auto it = m_nodes.find(key); + assert(it != m_nodes.end()); + m_keys.erase(it->second); + m_nodes.erase(it); } - } else { - // If this is tripped, there is a disconnect between the contents of the policy and the contents of the cache. - assert(false); } -} -template void EvictionLRU::on_evict(const Key& key, const CacheItem& /* item */) -{ - assert(!m_nodes.empty()); - assert(!m_keys.empty()); + [[nodiscard]] Ticket pop_victim() + { + assert(!m_keys.empty()); - if (m_keys.back().get() == key) { - m_nodes.erase(key); + const KeyRef victim_key = m_keys.back(); + m_nodes.erase(victim_key); m_keys.pop_back(); - } else { - auto it = m_nodes.find(key); - assert(it != m_nodes.end()); - m_keys.erase(it->second); - m_nodes.erase(it); + return Ticket{victim_key}; } -} - -template auto EvictionLRU::pop_victim() -> Ticket -{ - assert(!m_keys.empty()); - const KeyRef victim_key = m_keys.back(); - m_nodes.erase(victim_key); - m_keys.pop_back(); - return Ticket{victim_key}; -} + void rollback(Ticket ticket) + { + assert(m_nodes.find(ticket.m_key) == m_nodes.end()); -template void EvictionLRU::rollback(Ticket ticket) -{ - assert(m_nodes.find(ticket.m_key) == m_nodes.end()); + m_keys.emplace_back(ticket.m_key); + m_nodes.emplace(ticket.m_key, std::prev(m_keys.end())); + } - m_keys.emplace_back(ticket.m_key); - m_nodes.emplace(ticket.m_key, std::prev(m_keys.end())); -} +private: + std::list m_keys; + KeyRefMap m_nodes; +}; } // namespace cachemere::policy diff --git a/include/cachemere/policy/eviction_segmented_lru.h b/include/cachemere/policy/eviction_segmented_lru.h index 2f74de5..7adb834 100644 --- a/include/cachemere/policy/eviction_segmented_lru.h +++ b/include/cachemere/policy/eviction_segmented_lru.h @@ -31,9 +31,8 @@ template class EvictionSegmented public: using CacheItem = cachemere::Item; - struct EvictionTicket - { - EvictionTicket(KeyRef key, bool from_probation) : m_key{key}, m_from_probation{from_probation} + struct Ticket { + Ticket(KeyRef key, bool from_probation) : m_key{key}, m_from_probation{from_probation} { } @@ -46,20 +45,34 @@ template class EvictionSegmented bool m_from_probation; }; - using Ticket = EvictionTicket; - /// @brief Clears the policy. - void clear(); + void clear() + { + m_probation_list.clear(); + m_probation_nodes.clear(); + + m_protected_list.clear(); + m_protected_nodes.clear(); + } /// @brief Set the maximum number of items in the protected LRU segment. /// @param size The maximum number of items in the protected segment. - void set_protected_segment_size(size_t size); + void set_protected_segment_size(const size_t size) + { + m_protected_segment_size = size; + } /// @brief Insertion event handler. /// @details Inserts the provided item at the front of the probation segment. /// @param key The key of the inserted item. /// @param item The item that has been inserted in the cache. - void on_insert(const Key& key, const CacheItem& item); + void on_insert(const Key& key, const CacheItem& /* item */) + { + assert(m_probation_nodes.find(key) == m_probation_nodes.end()); + + m_probation_list.emplace_front(std::ref(key)); + m_probation_nodes.emplace(std::ref(key), m_probation_list.begin()); + } /// @brief Update event handler. /// @details If the item is in the probation segment, it is moved to the protected @@ -68,7 +81,10 @@ template class EvictionSegmented /// @param key The key that has been updated in the cache. /// @param old_item The old value for this key. /// @param new_item The new value for this key - void on_update(const Key& key, const CacheItem& old_item, const CacheItem& new_item); + void on_update(const Key& key, const CacheItem& /* old_item */, const CacheItem& new_item) + { + on_cache_hit(key, new_item); + } /// @brief Cache hit event handler. /// @details If the item is in the probation segment, it is moved to the protected @@ -76,16 +92,69 @@ template class EvictionSegmented /// the protected segment. /// @param key The key that has been hit. /// @param item The item that has been hit. - void on_cache_hit(const Key& key, const CacheItem& item); + void on_cache_hit(const Key& key, const CacheItem& /* item */) + { + assert(m_probation_nodes.size() == m_probation_list.size()); + assert(m_protected_nodes.size() == m_protected_list.size()); + + auto protected_node_it = m_protected_nodes.find(key); + if (protected_node_it != m_protected_nodes.end()) { + if (protected_node_it->second != m_protected_list.begin()) { + // If the node is in the protected segment, move it to the front of the protected segment. + m_protected_list.splice(m_protected_list.begin(), m_protected_list, protected_node_it->second); + } + } else { + // If the node is in probation, move it to the protected segment. + [[maybe_unused]] const bool promotion_ok = move_to_protected(key); + assert(promotion_ok); + } + + trim_protected_segment(); + + assert(m_probation_nodes.size() == m_probation_list.size()); + assert(m_protected_nodes.size() == m_protected_list.size()); + } /// @brief Eviction event handler. /// @details Removes the item from the segment it belongs to. /// @param key The key that was evicted. /// @param item The item that was evicted. - void on_evict(const Key& key, const CacheItem& item); + void on_evict(const Key& key, const CacheItem& /* item */) + { + assert((!m_protected_list.empty()) || !m_probation_list.empty()); + + auto key_and_it = m_probation_nodes.find(key); + if (key_and_it != m_probation_nodes.end()) { + m_probation_list.erase(key_and_it->second); + m_probation_nodes.erase(key_and_it); + } else { + key_and_it = m_protected_nodes.find(key); + assert(key_and_it != m_protected_nodes.end()); + m_protected_list.erase(key_and_it->second); + m_protected_nodes.erase(key_and_it); + } + } - [[nodiscard]] Ticket pop_victim(); - void rollback(Ticket ticket); + [[nodiscard]] Ticket pop_victim() + { + if (!m_probation_list.empty()) { + return pop_victim_from_probation(); + } + return pop_victim_from_protected(); + } + + void rollback(Ticket ticket) + { + if (ticket.m_from_probation) { + assert(m_probation_nodes.find(ticket.m_key) == m_probation_nodes.end()); + m_probation_list.emplace_back(ticket.m_key); + m_probation_nodes.emplace(ticket.m_key, std::prev(m_probation_list.end())); + } else { + assert(m_protected_nodes.find(ticket.m_key) == m_protected_nodes.end()); + m_protected_list.emplace_back(ticket.m_key); + m_protected_nodes.emplace(ticket.m_key, std::prev(m_protected_list.end())); + } + } private: size_t m_protected_segment_size; @@ -96,133 +165,57 @@ template class EvictionSegmented std::list m_protected_list; KeyRefMap m_protected_nodes; - bool move_to_protected(const Key& key); - bool pop_to_probation(); -}; - -template void EvictionSegmentedLRU::clear() -{ - m_probation_list.clear(); - m_probation_nodes.clear(); - - m_protected_list.clear(); - m_protected_nodes.clear(); -} - -template void EvictionSegmentedLRU::set_protected_segment_size(size_t size) -{ - m_protected_segment_size = size; -} - -template void EvictionSegmentedLRU::on_insert(const Key& key, const CacheItem& /* item */) -{ - assert(m_probation_nodes.find(key) == m_probation_nodes.end()); - - m_probation_list.emplace_front(std::ref(key)); - m_probation_nodes.emplace(std::ref(key), m_probation_list.begin()); -} + bool move_to_protected(const Key& key) + { + auto probation_node_it = m_probation_nodes.find(key); + if (probation_node_it == m_probation_nodes.end()) { + return false; + } -template -void EvictionSegmentedLRU::on_update(const Key& key, const CacheItem& /* old_item */, const CacheItem& new_item) -{ - on_cache_hit(key, new_item); -} + m_protected_list.splice(m_protected_list.begin(), m_probation_list, probation_node_it->second); + m_probation_nodes.erase(key); + m_protected_nodes.emplace(key, m_protected_list.begin()); + return true; + } -template void EvictionSegmentedLRU::on_cache_hit(const Key& key, const CacheItem& /* item */) -{ - assert(m_probation_nodes.size() == m_probation_list.size()); - assert(m_protected_nodes.size() == m_protected_list.size()); - - auto protected_node_it = m_protected_nodes.find(key); - if (protected_node_it != m_protected_nodes.end()) { - if (protected_node_it->second != m_protected_list.begin()) { - // If the node is in the protected segment, move it to the front of the protected segment. - m_protected_list.splice(m_protected_list.begin(), m_protected_list, protected_node_it->second); + bool pop_to_probation() + { + if (m_protected_list.empty()) { + return false; } - } else { - // If the node is in probation, move it to the protected segment. - [[maybe_unused]] const bool promotion_ok = move_to_protected(key); - assert(promotion_ok); - } - while (m_protected_list.size() > m_protected_segment_size) { - [[maybe_unused]] const bool demotion_ok = pop_to_probation(); - assert(demotion_ok); - assert(m_protected_list.size() == m_protected_segment_size); + m_probation_list.splice(m_probation_list.begin(), m_protected_list, --m_protected_list.end()); + m_protected_nodes.erase(*m_probation_list.begin()); + m_probation_nodes.emplace(*m_probation_list.begin(), m_probation_list.begin()); + return true; } - assert(m_probation_nodes.size() == m_probation_list.size()); - assert(m_protected_nodes.size() == m_protected_list.size()); -} - -template void EvictionSegmentedLRU::on_evict(const Key& key, const CacheItem& /* item */) -{ - assert((!m_protected_list.empty()) || !m_probation_list.empty()); - - auto key_and_it = m_probation_nodes.find(key); - if (key_and_it != m_probation_nodes.end()) { - m_probation_list.erase(key_and_it->second); - m_probation_nodes.erase(key_and_it); - } else { - key_and_it = m_protected_nodes.find(key); - assert(key_and_it != m_protected_nodes.end()); - m_protected_list.erase(key_and_it->second); - m_protected_nodes.erase(key_and_it); + void trim_protected_segment() + { + while (m_protected_list.size() > m_protected_segment_size) { + [[maybe_unused]] const bool demotion_ok = pop_to_probation(); + assert(demotion_ok); + assert(m_protected_list.size() == m_protected_segment_size); + } } -} -template auto EvictionSegmentedLRU::pop_victim() -> Ticket -{ - if (!m_probation_list.empty()) { + Ticket pop_victim_from_probation() + { + assert(!m_probation_list.empty()); const KeyRef victim_key = m_probation_list.back(); m_probation_nodes.erase(victim_key); m_probation_list.pop_back(); return Ticket{victim_key, true}; } - assert(!m_protected_list.empty()); - const KeyRef victim_key = m_protected_list.back(); - m_protected_nodes.erase(victim_key); - m_protected_list.pop_back(); - return Ticket{victim_key, false}; -} - -template void EvictionSegmentedLRU::rollback(Ticket ticket) -{ - if (ticket.m_from_probation) { - assert(m_probation_nodes.find(ticket.m_key) == m_probation_nodes.end()); - m_probation_list.emplace_back(ticket.m_key); - m_probation_nodes.emplace(ticket.m_key, std::prev(m_probation_list.end())); - } else { - assert(m_protected_nodes.find(ticket.m_key) == m_protected_nodes.end()); - m_protected_list.emplace_back(ticket.m_key); - m_protected_nodes.emplace(ticket.m_key, std::prev(m_protected_list.end())); - } -} - -template bool EvictionSegmentedLRU::move_to_protected(const Key& key) -{ - auto probation_node_it = m_probation_nodes.find(key); - if (probation_node_it == m_probation_nodes.end()) { - return false; - } - - m_protected_list.splice(m_protected_list.begin(), m_probation_list, probation_node_it->second); - m_probation_nodes.erase(key); - m_protected_nodes.emplace(key, m_protected_list.begin()); - return true; -} - -template bool EvictionSegmentedLRU::pop_to_probation() -{ - if (m_protected_list.empty()) { - return false; + Ticket pop_victim_from_protected() + { + assert(!m_protected_list.empty()); + const KeyRef victim_key = m_protected_list.back(); + m_protected_nodes.erase(victim_key); + m_protected_list.pop_back(); + return Ticket{victim_key, false}; } - - m_probation_list.splice(m_probation_list.begin(), m_protected_list, --m_protected_list.end()); - m_protected_nodes.erase(*m_probation_list.begin()); - m_probation_nodes.emplace(*m_probation_list.begin(), m_probation_list.begin()); - return true; -} +}; } // namespace cachemere::policy diff --git a/include/cachemere/policy/insertion_always.h b/include/cachemere/policy/insertion_always.h index 7edf179..231bf69 100644 --- a/include/cachemere/policy/insertion_always.h +++ b/include/cachemere/policy/insertion_always.h @@ -10,35 +10,28 @@ template class InsertionAlways { public: /// @brief Clears the policy. - void clear(); + void clear() + { + } /// @brief Determines whether a given key should be inserted into the cache. /// @details For this policy, `should_add` always returns true. /// @param key The key of the insertion candidate. /// @return Whether the candidate should be inserted into the cache. - bool should_add(const Key& key); + bool should_add(const Key& /* key */) + { + return true; + } /// @brief Determines whether a given victim should be replaced by a given candidate. /// @details For this policy, `should_replace` always returns true. /// @param victim The key of the victim the candidate will be compared to. /// @param candidate The key of the insertion candidate. /// @return Whether the victim should be replaced by the candidate. - bool should_replace(const Key& victim, const Key& candidate); + bool should_replace(const Key& /* victim */, const Key& /* candidate */) + { + return true; + } }; -template void InsertionAlways::clear() -{ -} - -template bool InsertionAlways::should_add(const Key& /* key */) -{ - return true; -} - -template -bool InsertionAlways::should_replace(const Key& /* key */, const Key& /* candidate */) -{ - return true; -} - } // namespace cachemere::policy diff --git a/include/cachemere/policy/insertion_tinylfu.h b/include/cachemere/policy/insertion_tinylfu.h index afef54e..3361cbe 100644 --- a/include/cachemere/policy/insertion_tinylfu.h +++ b/include/cachemere/policy/insertion_tinylfu.h @@ -23,18 +23,28 @@ template class InsertionTinyLFU using CacheItem = cachemere::Item; /// @brief Clears the policy. - void clear(); + void clear() + { + m_gatekeeper.clear(); + m_frequency_sketch.clear(); + } /// @brief Cache hit event handler. /// @details Updates the internal frequency sketches for the given item. /// @param key The key that has been hit. /// @param item The item that has been hit. - void on_cache_hit(const Key& key, const CacheItem& item); + void on_cache_hit(const Key& key, const CacheItem& /* item */) + { + touch_item(key); + } /// @brief Cache miss event handler. /// @details Updates the internal frequency sketches for the given key. /// @param key The key that was missed. - template void on_cache_miss(const KeyType& key); + template void on_cache_miss(const KeyType& key) + { + touch_item(key); + } /// @brief Set the cardinality of the policy. /// @details The set cardinality should be a decent approximation of the cardinality @@ -42,12 +52,19 @@ template class InsertionTinyLFU /// accurate estimate is very important, because an underestimation will severely /// decrease the accuracy of the policy, while an overestimation will use too much memory. /// @param cardinality The expected cardinality of the set of items. - void set_cardinality(uint32_t cardinality); + void set_cardinality(uint32_t cardinality) + { + m_gatekeeper = detail::BloomFilter(cardinality); + m_frequency_sketch = detail::CountingBloomFilter(cardinality); + } /// @brief Determines whether a given key should be inserted into the cache. /// @details TinyLFU always accepts insertions of items if there is room for them in the cache. /// @param key The key of the insertion candidate. - bool should_add(const Key& key); + bool should_add(const Key& key) + { + return m_gatekeeper.maybe_contains(key); + } /// @brief Determines whether a given victim should be replaced by a given candidate. /// @details TinyLFU will use its internal sketches to build a frequency @@ -55,77 +72,43 @@ template class InsertionTinyLFU /// that the candidate key is accessed more frequently than the victim key. /// @param victim The key of the victim the candidate will be compared to. /// @param candidate The replacement candidate. - bool should_replace(const Key& victim, const Key& candidate); + bool should_replace(const Key& victim, const Key& candidate) + { + return estimate_count_for_key(candidate) > estimate_count_for_key(victim); + } private: const static uint32_t DEFAULT_CACHE_CARDINALITY = 2000; detail::BloomFilter m_gatekeeper{DEFAULT_CACHE_CARDINALITY}; // TODO: Investigate using cuckoo filter here instead. detail::CountingBloomFilter m_frequency_sketch{DEFAULT_CACHE_CARDINALITY}; // TODO: Replace with count-min sketch to get rid of cardinality param. - uint32_t estimate_count_for_key(const Key& key) const; - void reset(); - - template void touch_item(const KeyType& key); -}; - -template void InsertionTinyLFU::clear() -{ - m_gatekeeper.clear(); - m_frequency_sketch.clear(); -} - -template void InsertionTinyLFU::on_cache_hit(const Key& key, const CacheItem& /* item */) -{ - touch_item(key); -} - -template template void InsertionTinyLFU::on_cache_miss(const KeyType& key) -{ - touch_item(key); -} - -template void InsertionTinyLFU::set_cardinality(uint32_t cardinality) -{ - m_gatekeeper = detail::BloomFilter(cardinality); - m_frequency_sketch = detail::CountingBloomFilter(cardinality); -} - -template bool InsertionTinyLFU::should_add(const Key& key) -{ - return m_gatekeeper.maybe_contains(key); -} - -template bool InsertionTinyLFU::should_replace(const Key& victim, const Key& candidate) -{ - return estimate_count_for_key(candidate) > estimate_count_for_key(victim); -} + uint32_t estimate_count_for_key(const Key& key) const + { + uint32_t sketch_estimation = m_frequency_sketch.estimate(key); + if (m_gatekeeper.maybe_contains(key)) { + ++sketch_estimation; + } -template uint32_t InsertionTinyLFU::estimate_count_for_key(const Key& key) const -{ - uint32_t sketch_estimation = m_frequency_sketch.estimate(key); - if (m_gatekeeper.maybe_contains(key)) { - ++sketch_estimation; + return sketch_estimation; } - return sketch_estimation; -} - -template void InsertionTinyLFU::reset() -{ - m_gatekeeper.clear(); - m_frequency_sketch.decay(); -} + void reset() + { + m_gatekeeper.clear(); + m_frequency_sketch.decay(); + } -template template void InsertionTinyLFU::touch_item(const KeyType& key) -{ - if (m_gatekeeper.maybe_contains(key)) { - m_frequency_sketch.add(key); - if (m_frequency_sketch.estimate(key) > m_frequency_sketch.cardinality()) { - reset(); + template void touch_item(const KeyType& key) + { + if (m_gatekeeper.maybe_contains(key)) { + m_frequency_sketch.add(key); + if (m_frequency_sketch.estimate(key) > m_frequency_sketch.cardinality()) { + reset(); + } + } else { + m_gatekeeper.add(key); } - } else { - m_gatekeeper.add(key); } -} +}; } // namespace cachemere::policy From cbb7f85e7b812d3795079971214edd75fbdc2858 Mon Sep 17 00:00:00 2001 From: William Dussault Date: Tue, 26 May 2026 13:22:37 -0400 Subject: [PATCH 2/3] Use more concepts to constrain generics --- include/cachemere/cache.h | 18 +++++------ include/cachemere/detail/traits.h | 31 +++++++++++++++++++ include/cachemere/policy/constraint_count.h | 4 +-- include/cachemere/policy/constraint_memory.h | 3 +- .../cachemere/policy/detail/bloom_filter.h | 5 +-- include/cachemere/policy/detail/hash_mixer.h | 4 ++- include/cachemere/policy/eviction_gdsf.h | 8 ++++- include/cachemere/policy/eviction_lru.h | 3 +- .../cachemere/policy/eviction_segmented_lru.h | 3 +- include/cachemere/policy/insertion_always.h | 4 ++- include/cachemere/policy/insertion_tinylfu.h | 3 +- 11 files changed, 66 insertions(+), 20 deletions(-) diff --git a/include/cachemere/cache.h b/include/cachemere/cache.h index cd4d8c6..f550135 100644 --- a/include/cachemere/cache.h +++ b/include/cachemere/cache.h @@ -47,15 +47,15 @@ namespace cachemere { /// @tparam MeasureKey A functor returning the size of a cache key. /// @tparam KeyHash A default-constructible callable type returning a hash of a key. Defaults to `absl::Hash`. /// @tparam Locking The locking strategy used to protect cache operations. Defaults to `LockingStrategy::Mutex`. -template class InsertionPolicy, template class EvictionPolicy, template class ConstraintPolicy, - typename MeasureValue = measurement::Size, - typename MeasureKey = measurement::Size, - typename KeyHash = absl::Hash, - LockingStrategy Locking = LockingStrategy::Mutex> + detail::traits::MeasureFor MeasureValue = measurement::Size, + detail::traits::MeasureFor MeasureKey = measurement::Size, + detail::traits::HasherFor KeyHash = absl::Hash, + LockingStrategy Locking = LockingStrategy::Mutex> class Cache { public: @@ -112,7 +112,7 @@ class Cache /// @tparam KeyView The type of the key used for retrieving items. /// @param key The key whose presence to test. /// @return Whether the key is in cache. - template bool contains(const KeyView& key) const + template KeyView> bool contains(const KeyView& key) const { const LockGuard guard{lock()}; return m_data.find(key) != m_data.end(); @@ -281,7 +281,7 @@ class Cache /// @param predicate_fn The predicate function. /// @tparam P The type of the predicate function. /// The predicate should have the signature `bool fn(const Key& key, const Value& value)`. - template void retain(P predicate_fn) + template P> void retain(P predicate_fn) { const LockGuard guard{lock()}; for (auto it = m_data.begin(); it != m_data.end();) { @@ -298,7 +298,7 @@ class Cache /// @brief Apply a function to all objects in cache. /// @param unary_function The function to be applied to all items in cache. /// The function should have the signature `void fn(const Key& key, const Value& value)`. - template void for_each(F unary_function) + template F> void for_each(F unary_function) { const LockGuard guard{lock()}; for (const auto& [key, value] : m_data) { @@ -751,7 +751,7 @@ class Cache } }; -template class I, template class E, diff --git a/include/cachemere/detail/traits.h b/include/cachemere/detail/traits.h index 513c411..e131bd8 100644 --- a/include/cachemere/detail/traits.h +++ b/include/cachemere/detail/traits.h @@ -7,6 +7,7 @@ #include #include +#include namespace cachemere::detail::traits { @@ -82,4 +83,34 @@ concept HasOnEvict = requires(P policy, K key, Item item) { } // namespace event +template +concept Key = std::movable && std::move_constructible && std::assignable_from && requires(T t) { + { t == t } -> std::convertible_to; +}; + +template +concept HasherFor = requires(T t, V v) { + { t(v) } -> std::convertible_to; +}; + +template +concept LookupKeyFor = HasherFor && requires(KeyHash hash, KeyView key_view, Key key) { + { TransparentEq{}(key, key_view) } -> std::same_as; +}; + +template +concept MeasureFor = requires(T t, V v) { + { t(v) } -> std::convertible_to; +}; + +template +concept PredicateFn = requires(T t, Key key, Value value) { + { t(key, value) } -> std::same_as; +}; + +template +concept UnaryFn = requires(T t, Key key, Value value) { + { t(key, value) } -> std::same_as; +}; + } // namespace cachemere::detail::traits diff --git a/include/cachemere/policy/constraint_count.h b/include/cachemere/policy/constraint_count.h index 61833b3..e116d32 100644 --- a/include/cachemere/policy/constraint_count.h +++ b/include/cachemere/policy/constraint_count.h @@ -3,7 +3,7 @@ #include #include -#include +#include namespace cachemere::policy { @@ -12,7 +12,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template class ConstraintCount +template KeyHash, typename Value> class ConstraintCount { using CacheItem = Item; diff --git a/include/cachemere/policy/constraint_memory.h b/include/cachemere/policy/constraint_memory.h index 53b0bfc..d455f51 100644 --- a/include/cachemere/policy/constraint_memory.h +++ b/include/cachemere/policy/constraint_memory.h @@ -4,6 +4,7 @@ #include #include +#include namespace cachemere::policy { @@ -12,7 +13,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template class ConstraintMemory +template KeyHash, typename Value> class ConstraintMemory { using CacheItem = Item; diff --git a/include/cachemere/policy/detail/bloom_filter.h b/include/cachemere/policy/detail/bloom_filter.h index 569eb7b..87d3911 100644 --- a/include/cachemere/policy/detail/bloom_filter.h +++ b/include/cachemere/policy/detail/bloom_filter.h @@ -2,8 +2,6 @@ #include -#include "bloom_filter_math.h" - #ifdef _WIN32 # pragma warning(push) # pragma warning(disable : 4244) @@ -16,6 +14,9 @@ # pragma warning(pop) #endif +#include + +#include "bloom_filter_math.h" #include "hash_mixer.h" namespace cachemere::policy::detail { diff --git a/include/cachemere/policy/detail/hash_mixer.h b/include/cachemere/policy/detail/hash_mixer.h index 21f0918..c33023d 100644 --- a/include/cachemere/policy/detail/hash_mixer.h +++ b/include/cachemere/policy/detail/hash_mixer.h @@ -2,13 +2,15 @@ #include +#include + namespace cachemere::policy::detail { /// @brief Functor used for generating a uniform sequence of numbers in a given value range for a given key. /// @tparam Key The type of the key to be used as seed. /// @tparam KeyHash The functor to use for turning the provided key into a seed for the internal /// pseudo-random number generator. -template class HashMixer : private KeyHash +template KeyHash> class HashMixer : private KeyHash { public: /// @brief Constructor. diff --git a/include/cachemere/policy/eviction_gdsf.h b/include/cachemere/policy/eviction_gdsf.h index 810607d..c6177a4 100644 --- a/include/cachemere/policy/eviction_gdsf.h +++ b/include/cachemere/policy/eviction_gdsf.h @@ -8,11 +8,17 @@ #include #include +#include #include "detail/counting_bloom_filter.h" namespace cachemere::policy { +template +concept CostFn = requires(T t, Key key, Item item) { + { t(key, item) } -> std::convertible_to; +}; + /// @brief Greedy-Dual-Size-Frequency (GDSF) eviction policy. /// @details Generally, GDSF tries to first evict the items that will be the least costly /// to reload, while taking into account access frequency. @@ -24,7 +30,7 @@ namespace cachemere::policy { /// @tparam Cost A functor taking a a `Key&` and a `const Item&` returning the cost to load this item in cache. // The cost must not exceed 2^64 and should ideally be quite a bit below this limit since the cost of the item is added to the // policy's internal 64-bit clock on every insertion. -template class EvictionGDSF +template KeyHash, typename Value, CostFn Cost> class EvictionGDSF { private: using KeyRef = std::reference_wrapper; diff --git a/include/cachemere/policy/eviction_lru.h b/include/cachemere/policy/eviction_lru.h index 070c2d1..18920c0 100644 --- a/include/cachemere/policy/eviction_lru.h +++ b/include/cachemere/policy/eviction_lru.h @@ -7,6 +7,7 @@ #include #include +#include namespace cachemere::policy { @@ -17,7 +18,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template class EvictionLRU +template KeyHash, typename Value> class EvictionLRU { private: using KeyRef = std::reference_wrapper; diff --git a/include/cachemere/policy/eviction_segmented_lru.h b/include/cachemere/policy/eviction_segmented_lru.h index 7adb834..81db551 100644 --- a/include/cachemere/policy/eviction_segmented_lru.h +++ b/include/cachemere/policy/eviction_segmented_lru.h @@ -8,6 +8,7 @@ #include #include +#include namespace cachemere::policy { @@ -21,7 +22,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template class EvictionSegmentedLRU +template KeyHash, typename Value> class EvictionSegmentedLRU { private: using KeyRef = std::reference_wrapper; diff --git a/include/cachemere/policy/insertion_always.h b/include/cachemere/policy/insertion_always.h index 231bf69..ebc8229 100644 --- a/include/cachemere/policy/insertion_always.h +++ b/include/cachemere/policy/insertion_always.h @@ -1,12 +1,14 @@ #pragma once +#include + namespace cachemere::policy { /// @brief Simplest insertion policy. Always accepts insertions. /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template class InsertionAlways +template KeyHash, typename Value> class InsertionAlways { public: /// @brief Clears the policy. diff --git a/include/cachemere/policy/insertion_tinylfu.h b/include/cachemere/policy/insertion_tinylfu.h index 3361cbe..328a6b9 100644 --- a/include/cachemere/policy/insertion_tinylfu.h +++ b/include/cachemere/policy/insertion_tinylfu.h @@ -3,6 +3,7 @@ #include #include +#include #include "detail/bloom_filter.h" #include "detail/counting_bloom_filter.h" @@ -17,7 +18,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template class InsertionTinyLFU +template KeyHash, typename Value> class InsertionTinyLFU { public: using CacheItem = cachemere::Item; From ddcaec28f3b48c8c67c705c794e62a150072b733 Mon Sep 17 00:00:00 2001 From: William Dussault Date: Wed, 27 May 2026 08:57:18 -0400 Subject: [PATCH 3/3] Fix misc. comments --- include/cachemere/detail/traits.h | 4 ++-- include/cachemere/policy/constraint_count.h | 2 +- include/cachemere/policy/constraint_memory.h | 2 +- include/cachemere/policy/eviction_gdsf.h | 2 +- include/cachemere/policy/eviction_lru.h | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/cachemere/detail/traits.h b/include/cachemere/detail/traits.h index e131bd8..99fc95a 100644 --- a/include/cachemere/detail/traits.h +++ b/include/cachemere/detail/traits.h @@ -89,7 +89,7 @@ concept Key = std::movable && std::move_constructible && std::assignable_f }; template -concept HasherFor = requires(T t, V v) { +concept HasherFor = std::default_initializable && requires(T t, V v) { { t(v) } -> std::convertible_to; }; @@ -99,7 +99,7 @@ concept LookupKeyFor = HasherFor && requires(KeyHash hash, Key }; template -concept MeasureFor = requires(T t, V v) { +concept MeasureFor = std::default_initializable && requires(T t, V v) { { t(v) } -> std::convertible_to; }; diff --git a/include/cachemere/policy/constraint_count.h b/include/cachemere/policy/constraint_count.h index e116d32..d8bfd1e 100644 --- a/include/cachemere/policy/constraint_count.h +++ b/include/cachemere/policy/constraint_count.h @@ -12,7 +12,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template KeyHash, typename Value> class ConstraintCount +template KeyHash, typename Value> class ConstraintCount { using CacheItem = Item; diff --git a/include/cachemere/policy/constraint_memory.h b/include/cachemere/policy/constraint_memory.h index d455f51..1f6720d 100644 --- a/include/cachemere/policy/constraint_memory.h +++ b/include/cachemere/policy/constraint_memory.h @@ -13,7 +13,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template KeyHash, typename Value> class ConstraintMemory +template KeyHash, typename Value> class ConstraintMemory { using CacheItem = Item; diff --git a/include/cachemere/policy/eviction_gdsf.h b/include/cachemere/policy/eviction_gdsf.h index c6177a4..b146d07 100644 --- a/include/cachemere/policy/eviction_gdsf.h +++ b/include/cachemere/policy/eviction_gdsf.h @@ -87,7 +87,7 @@ template{cardinality}; + m_frequency_sketch = detail::CountingBloomFilter{cardinality}; } /// @brief Insertion event handler. diff --git a/include/cachemere/policy/eviction_lru.h b/include/cachemere/policy/eviction_lru.h index 18920c0..7a6bb3a 100644 --- a/include/cachemere/policy/eviction_lru.h +++ b/include/cachemere/policy/eviction_lru.h @@ -18,7 +18,7 @@ namespace cachemere::policy { /// @tparam Key The type of the keys used to identify items in the cache. /// @tparam KeyHash The type of the hasher used to hash item keys. /// @tparam Value The type of the values stored in the cache. -template KeyHash, typename Value> class EvictionLRU +template KeyHash, typename Value> class EvictionLRU { private: using KeyRef = std::reference_wrapper;