diff --git a/src/common-tests/CMakeLists.txt b/src/common-tests/CMakeLists.txt index ebd07e84d..fbd05bda7 100644 --- a/src/common-tests/CMakeLists.txt +++ b/src/common-tests/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable(common-tests gsvector_yuvtorgb_test.cpp hash_tests.cpp heap_array_tests.cpp + lru_cache_tests.cpp misc_tests.cpp path_tests.cpp rectangle_tests.cpp diff --git a/src/common-tests/common-tests.vcxproj b/src/common-tests/common-tests.vcxproj index b1d22082e..4fd9de3a0 100644 --- a/src/common-tests/common-tests.vcxproj +++ b/src/common-tests/common-tests.vcxproj @@ -8,6 +8,7 @@ + @@ -39,4 +40,4 @@ - \ No newline at end of file + diff --git a/src/common-tests/common-tests.vcxproj.filters b/src/common-tests/common-tests.vcxproj.filters index 9db1e8e85..074a6d511 100644 --- a/src/common-tests/common-tests.vcxproj.filters +++ b/src/common-tests/common-tests.vcxproj.filters @@ -13,7 +13,8 @@ + - \ No newline at end of file + diff --git a/src/common-tests/lru_cache_tests.cpp b/src/common-tests/lru_cache_tests.cpp new file mode 100644 index 000000000..720c308ca --- /dev/null +++ b/src/common-tests/lru_cache_tests.cpp @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "common/lru_cache.h" + +#include + +#include +#include +#include +#include + +namespace { +struct TrackedValue +{ + TrackedValue(int value_, int* destruction_count_) : value(value_), destruction_count(destruction_count_) {} + ~TrackedValue() { (*destruction_count)++; } + + int value; + int* destruction_count; +}; + +struct PoolDeleter +{ + void operator()(std::unique_ptr&& value) const { pool->push_back(std::move(value)); } + + std::vector>* pool; +}; +} // namespace + +TEST(LRUCache, InsertLookupClearAndCapacityAccessors) +{ + LRUCache cache(2); + EXPECT_EQ(cache.GetSize(), 0u); + EXPECT_EQ(cache.GetMaxCapacity(), 2u); + EXPECT_EQ(cache.Lookup(1), nullptr); + + int* value = cache.Insert(1, 10); + ASSERT_NE(value, nullptr); + EXPECT_EQ(*value, 10); + EXPECT_EQ(cache.GetSize(), 1u); + + value = cache.Lookup(1); + ASSERT_NE(value, nullptr); + EXPECT_EQ(*value, 10); + + cache.Clear(); + EXPECT_EQ(cache.GetSize(), 0u); + EXPECT_EQ(cache.Lookup(1), nullptr); +} + +TEST(LRUCache, LookupUpdatesLeastRecentlyUsedOrder) +{ + LRUCache cache(2); + cache.Insert(1, 10); + cache.Insert(2, 20); + ASSERT_NE(cache.Lookup(1), nullptr); + + cache.Insert(3, 30); + EXPECT_NE(cache.Lookup(1), nullptr); + EXPECT_EQ(cache.Lookup(2), nullptr); + EXPECT_NE(cache.Lookup(3), nullptr); +} + +TEST(LRUCache, LookupDistinguishesMissingRawPointerFromCachedNullPointer) +{ + LRUCache cache(1); + EXPECT_EQ(cache.Lookup(1), nullptr); + + int** inserted_value = cache.Insert(1, nullptr); + ASSERT_NE(inserted_value, nullptr); + EXPECT_EQ(*inserted_value, nullptr); + + int** cached_value = cache.Lookup(1); + ASSERT_NE(cached_value, nullptr); + EXPECT_EQ(*cached_value, nullptr); +} + +TEST(LRUCache, SetMaxCapacityEvictsLeastRecentlyUsedItems) +{ + LRUCache cache(3); + cache.Insert(1, 10); + cache.Insert(2, 20); + cache.Insert(3, 30); + ASSERT_NE(cache.Lookup(1), nullptr); + + cache.SetMaxCapacity(2); + EXPECT_EQ(cache.GetMaxCapacity(), 2u); + EXPECT_EQ(cache.GetSize(), 2u); + EXPECT_NE(cache.Lookup(1), nullptr); + EXPECT_EQ(cache.Lookup(2), nullptr); + EXPECT_NE(cache.Lookup(3), nullptr); + + cache.SetMaxCapacity(0); + EXPECT_EQ(cache.GetMaxCapacity(), 0u); + EXPECT_EQ(cache.GetSize(), 0u); +} + +TEST(LRUCache, EvictRemovesRequestedNumberOfItems) +{ + LRUCache cache(4); + cache.Insert(1, 10); + cache.Insert(2, 20); + cache.Insert(3, 30); + cache.Insert(4, 40); + ASSERT_NE(cache.Lookup(1), nullptr); + + cache.Evict(0); + EXPECT_EQ(cache.GetSize(), 4u); + + cache.Evict(2); + EXPECT_EQ(cache.GetSize(), 2u); + EXPECT_NE(cache.Lookup(1), nullptr); + EXPECT_EQ(cache.Lookup(2), nullptr); + EXPECT_EQ(cache.Lookup(3), nullptr); + EXPECT_NE(cache.Lookup(4), nullptr); + + cache.Evict(10); + EXPECT_EQ(cache.GetSize(), 0u); + cache.Evict(); + EXPECT_EQ(cache.GetSize(), 0u); +} + +TEST(LRUCache, RemoveAndRemoveMatchingItems) +{ + LRUCache cache(5); + for (int i = 1; i <= 5; i++) + cache.Insert(i, i * 10); + + EXPECT_FALSE(cache.Remove(6)); + EXPECT_TRUE(cache.Remove(1)); + EXPECT_FALSE(cache.Remove(1)); + EXPECT_EQ(cache.RemoveMatchingItems([](int key) { return (key % 2) == 0; }), 2u); + EXPECT_EQ(cache.RemoveMatchingItems([](int) { return false; }), 0u); + + EXPECT_EQ(cache.GetSize(), 2u); + EXPECT_NE(cache.Lookup(3), nullptr); + EXPECT_NE(cache.Lookup(5), nullptr); +} + +TEST(LRUCache, ManualEvictionCanTemporarilyExceedCapacity) +{ + LRUCache cache(2, true); + cache.Insert(1, 10); + cache.Insert(2, 20); + cache.Insert(3, 30); + EXPECT_EQ(cache.GetSize(), 3u); + + ASSERT_NE(cache.Lookup(1), nullptr); + cache.ManualEvict(); + EXPECT_EQ(cache.GetSize(), 2u); + EXPECT_NE(cache.Lookup(1), nullptr); + EXPECT_EQ(cache.Lookup(2), nullptr); + EXPECT_NE(cache.Lookup(3), nullptr); + + cache.SetManualEvict(true); + cache.Insert(4, 40); + EXPECT_EQ(cache.GetSize(), 3u); + ASSERT_NE(cache.Lookup(1), nullptr); + cache.SetManualEvict(false); + EXPECT_EQ(cache.GetSize(), 2u); + EXPECT_NE(cache.Lookup(1), nullptr); + EXPECT_EQ(cache.Lookup(3), nullptr); + EXPECT_NE(cache.Lookup(4), nullptr); +} + +TEST(LRUCache, ApplyVisitsAndCanModifyEveryItem) +{ + LRUCache cache(3); + cache.Insert(3, 30); + cache.Insert(1, 10); + cache.Insert(2, 20); + + std::vector visited_keys; + cache.Apply([&visited_keys](const int& key, int& value) { + visited_keys.push_back(key); + value += key; + }); + + EXPECT_EQ(visited_keys, (std::vector{1, 2, 3})); + EXPECT_EQ(*cache.Lookup(1), 11); + EXPECT_EQ(*cache.Lookup(2), 22); + EXPECT_EQ(*cache.Lookup(3), 33); +} + +TEST(LRUCache, StringKeysSupportHeterogeneousLookupAndRemoval) +{ + LRUCache cache(2); + cache.Insert("first", 1); + cache.Insert("second", 2); + + const std::string_view first_key = "first"; + ASSERT_NE(cache.Lookup(first_key), nullptr); + EXPECT_EQ(*cache.Lookup(first_key), 1); + EXPECT_TRUE(cache.Remove(first_key)); + EXPECT_EQ(cache.Lookup(first_key), nullptr); + EXPECT_EQ(cache.GetSize(), 1u); +} + +TEST(LRUCache, DefaultDeleterDeletesRawPointers) +{ + int destruction_count = 0; + { + LRUCache cache(1); + cache.Insert(1, new TrackedValue(1, &destruction_count)); + cache.Insert(2, new TrackedValue(2, &destruction_count)); + EXPECT_EQ(destruction_count, 1); + + EXPECT_TRUE(cache.Remove(2)); + EXPECT_EQ(destruction_count, 2); + } + EXPECT_EQ(destruction_count, 2); +} + +TEST(LRUCache, DefaultDeleterAllowsUniquePointersToDestroyNormally) +{ + int destruction_count = 0; + { + LRUCache> cache(1); + cache.Insert(1, std::make_unique(1, &destruction_count)); + cache.Insert(1, std::make_unique(2, &destruction_count)); + + EXPECT_EQ(cache.GetSize(), 1u); + EXPECT_EQ(destruction_count, 1); + } + EXPECT_EQ(destruction_count, 2); +} + +TEST(LRUCache, CustomDeleterCanPoolUniquePointers) +{ + int destruction_count = 0; + std::vector> pool; + { + LRUCache, PoolDeleter> cache(2, false, PoolDeleter{&pool}); + cache.Insert(1, std::make_unique(1, &destruction_count)); + cache.Insert(2, std::make_unique(2, &destruction_count)); + cache.Insert(1, std::make_unique(3, &destruction_count)); + + ASSERT_EQ(pool.size(), 1u); + EXPECT_EQ(pool.front()->value, 1); + EXPECT_NE(cache.Lookup(2), nullptr); + EXPECT_EQ(destruction_count, 0); + + cache.Evict(); + cache.Clear(); + EXPECT_EQ(pool.size(), 3u); + EXPECT_EQ(destruction_count, 0); + } + + pool.clear(); + EXPECT_EQ(destruction_count, 3); +} diff --git a/src/common/lru_cache.h b/src/common/lru_cache.h index addc05e75..6e8332a6c 100644 --- a/src/common/lru_cache.h +++ b/src/common/lru_cache.h @@ -3,10 +3,24 @@ #pragma once #include "heterogeneous_containers.h" +#include "types.h" #include #include +#include +#include +#include -template +template +struct LRUCacheDefaultDeleter +{ + void operator()(V&& value) const noexcept + { + if constexpr (std::is_pointer_v) + std::default_delete>{}(value); + } +}; + +template> class LRUCache { using CounterType = std::uint64_t; @@ -20,16 +34,24 @@ class LRUCache using MapType = std::conditional_t, StringMap, std::map>; public: - LRUCache(std::size_t max_capacity = 16, bool manual_evict = false) - : m_max_capacity(max_capacity), m_manual_evict(manual_evict) + LRUCache(std::size_t max_capacity = 16, bool manual_evict = false, Deleter deleter = Deleter()) + : m_max_capacity(max_capacity), m_deleter(std::move(deleter)), m_manual_evict(manual_evict) + { + } + ~LRUCache() { + Clear(); } - ~LRUCache() = default; std::size_t GetSize() const { return m_items.size(); } std::size_t GetMaxCapacity() const { return m_max_capacity; } - void Clear() { m_items.clear(); } + void Clear() + { + for (auto it = m_items.rbegin(); it != m_items.rend(); ++it) + m_deleter(std::move(it->second.value)); + m_items.clear(); + } void SetMaxCapacity(std::size_t capacity) { @@ -51,17 +73,18 @@ public: V* Insert(K key, V value) { - ShrinkForNewItem(); - auto iter = m_items.find(key); if (iter != m_items.end()) { + m_deleter(std::move(iter->second.value)); iter->second.value = std::move(value); iter->second.last_access = ++m_last_counter; return &iter->second.value; } else { + ShrinkForNewItem(); + Item it; it.last_access = ++m_last_counter; it.value = std::move(value); @@ -80,6 +103,7 @@ public: if (lowest == m_items.end() || iter->second.last_access < lowest->second.last_access) lowest = iter; } + m_deleter(std::move(lowest->second.value)); m_items.erase(lowest); count--; } @@ -93,6 +117,7 @@ public: { if (pred(iter->first)) { + m_deleter(std::move(iter->second.value)); iter = m_items.erase(iter); removed_count++; } @@ -110,6 +135,7 @@ public: auto iter = m_items.find(key); if (iter == m_items.end()) return false; + m_deleter(std::move(iter->second.value)); m_items.erase(iter); return true; } @@ -145,5 +171,6 @@ private: MapType m_items; CounterType m_last_counter = 0; std::size_t m_max_capacity = 0; + NO_UNIQUE_ADDRESS Deleter m_deleter; bool m_manual_evict = false; -}; \ No newline at end of file +};