LRUCache: Add custom deleter override

pull/3784/head^2
Stenzek 1 month ago
parent 96e0c4d389
commit 824a1072e2
No known key found for this signature in database

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

@ -8,6 +8,7 @@
<ClCompile Include="file_system_tests.cpp" />
<ClCompile Include="gsvector_tests.cpp" />
<ClCompile Include="heap_array_tests.cpp" />
<ClCompile Include="lru_cache_tests.cpp" />
<ClCompile Include="misc_tests.cpp" />
<ClCompile Include="path_tests.cpp" />
<ClCompile Include="rectangle_tests.cpp" />
@ -39,4 +40,4 @@
</Link>
</ItemDefinitionGroup>
<Import Project="..\..\dep\vsprops\Targets.props" />
</Project>
</Project>

@ -13,7 +13,8 @@
<ClCompile Include="small_string_tests.cpp" />
<ClCompile Include="binary_reader_writer_tests.cpp" />
<ClCompile Include="heap_array_tests.cpp" />
<ClCompile Include="lru_cache_tests.cpp" />
<ClCompile Include="string_pool_tests.cpp" />
<ClCompile Include="misc_tests.cpp" />
</ItemGroup>
</Project>
</Project>

@ -0,0 +1,252 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "common/lru_cache.h"
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
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<TrackedValue>&& value) const { pool->push_back(std::move(value)); }
std::vector<std::unique_ptr<TrackedValue>>* pool;
};
} // namespace
TEST(LRUCache, InsertLookupClearAndCapacityAccessors)
{
LRUCache<int, int> 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<int, int> 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<int, int*> 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<int, int> 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<int, int> 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<int, int> 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<int, int> 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<int, int> cache(3);
cache.Insert(3, 30);
cache.Insert(1, 10);
cache.Insert(2, 20);
std::vector<int> visited_keys;
cache.Apply([&visited_keys](const int& key, int& value) {
visited_keys.push_back(key);
value += key;
});
EXPECT_EQ(visited_keys, (std::vector<int>{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<std::string, int> 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<int, TrackedValue*> 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<int, std::unique_ptr<TrackedValue>> cache(1);
cache.Insert(1, std::make_unique<TrackedValue>(1, &destruction_count));
cache.Insert(1, std::make_unique<TrackedValue>(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<std::unique_ptr<TrackedValue>> pool;
{
LRUCache<int, std::unique_ptr<TrackedValue>, PoolDeleter> cache(2, false, PoolDeleter{&pool});
cache.Insert(1, std::make_unique<TrackedValue>(1, &destruction_count));
cache.Insert(2, std::make_unique<TrackedValue>(2, &destruction_count));
cache.Insert(1, std::make_unique<TrackedValue>(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);
}

@ -3,10 +3,24 @@
#pragma once
#include "heterogeneous_containers.h"
#include "types.h"
#include <cstdint>
#include <map>
#include <memory>
#include <type_traits>
#include <utility>
template<class K, class V>
template<class V>
struct LRUCacheDefaultDeleter
{
void operator()(V&& value) const noexcept
{
if constexpr (std::is_pointer_v<V>)
std::default_delete<std::remove_pointer_t<V>>{}(value);
}
};
template<class K, class V, class Deleter = LRUCacheDefaultDeleter<V>>
class LRUCache
{
using CounterType = std::uint64_t;
@ -20,16 +34,24 @@ class LRUCache
using MapType = std::conditional_t<std::is_same_v<K, std::string>, StringMap<Item>, std::map<K, Item>>;
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;
};
};

Loading…
Cancel
Save