blob: 20f0adee911b16d7f955054038ac5faccdff53ec [file]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
#include "main.h"
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include <Eigen/src/Core/util/LruCache.h>
using Eigen::internal::LruCache;
// Move-only value with a side-effecting destructor, used to assert that
// eviction and clear() actually destroy values.
struct DestructorTracer {
int id;
std::vector<int>* sink;
DestructorTracer(int id_, std::vector<int>* sink_) : id(id_), sink(sink_) {}
DestructorTracer(const DestructorTracer&) = delete;
DestructorTracer& operator=(const DestructorTracer&) = delete;
DestructorTracer(DestructorTracer&& o) noexcept : id(o.id), sink(o.sink) { o.sink = nullptr; }
DestructorTracer& operator=(DestructorTracer&&) = delete;
~DestructorTracer() {
if (sink) sink->push_back(id);
}
};
static void test_basic_get_and_miss() {
LruCache<int, std::string> cache(3);
VERIFY(cache.empty());
VERIFY_IS_EQUAL(cache.size(), std::size_t(0));
VERIFY_IS_EQUAL(cache.capacity(), std::size_t(3));
VERIFY(cache.find(42) == nullptr);
cache.insert(1, "one");
cache.insert(2, "two");
VERIFY_IS_EQUAL(cache.size(), std::size_t(2));
VERIFY(!cache.empty());
std::string* p = cache.find(1);
VERIFY(p != nullptr);
VERIFY_IS_EQUAL(*p, std::string("one"));
VERIFY(cache.find(2) != nullptr);
VERIFY(cache.find(3) == nullptr);
}
static void test_lru_eviction() {
LruCache<int, int> cache(3);
cache.insert(1, 100);
cache.insert(2, 200);
cache.insert(3, 300);
// Fourth insert evicts 1 (least-recently-used so far).
cache.insert(4, 400);
VERIFY_IS_EQUAL(cache.size(), std::size_t(3));
VERIFY(cache.find(1) == nullptr);
VERIFY(cache.find(2) != nullptr);
VERIFY(cache.find(3) != nullptr);
VERIFY(cache.find(4) != nullptr);
}
static void test_hit_promotes_to_mru() {
LruCache<int, int> cache(3);
cache.insert(1, 100);
cache.insert(2, 200);
cache.insert(3, 300);
// Touch 1 — it is now MRU, so 2 becomes LRU.
VERIFY(cache.find(1) != nullptr);
cache.insert(4, 400);
VERIFY(cache.find(1) != nullptr);
VERIFY(cache.find(2) == nullptr);
VERIFY(cache.find(3) != nullptr);
VERIFY(cache.find(4) != nullptr);
}
static void test_replace_existing_key() {
LruCache<int, std::string> cache(3);
cache.insert(1, "one");
cache.insert(2, "two");
// Re-inserting the same key replaces the value and promotes to MRU.
std::string* p = cache.insert(1, "ONE");
VERIFY(p != nullptr);
VERIFY_IS_EQUAL(*p, std::string("ONE"));
VERIFY_IS_EQUAL(cache.size(), std::size_t(2));
// Re-inserting 1 made 2 the LRU; filling to capacity then exceeding evicts 2.
cache.insert(3, "three");
cache.insert(4, "four");
VERIFY(cache.find(1) != nullptr);
VERIFY(cache.find(2) == nullptr);
VERIFY(cache.find(3) != nullptr);
VERIFY(cache.find(4) != nullptr);
}
static void test_replace_destroys_old_value() {
std::vector<int> destructions;
LruCache<int, DestructorTracer> cache(2);
cache.insert(1, DestructorTracer{1, &destructions});
cache.insert(2, DestructorTracer{2, &destructions});
destructions.clear();
// Replacement destroys the old entry and does not require Value to be
// move-assignable.
DestructorTracer* p = cache.insert(1, DestructorTracer{10, &destructions});
VERIFY(p != nullptr);
VERIFY_IS_EQUAL(p->id, 10);
VERIFY_IS_EQUAL(destructions.size(), std::size_t(1));
VERIFY_IS_EQUAL(destructions.front(), 1);
// Replacing 1 promoted it to MRU, so the next insert evicts 2.
destructions.clear();
cache.insert(3, DestructorTracer{3, &destructions});
VERIFY_IS_EQUAL(destructions.size(), std::size_t(1));
VERIFY_IS_EQUAL(destructions.front(), 2);
VERIFY(cache.find(1) != nullptr);
VERIFY(cache.find(3) != nullptr);
}
static void test_evicted_value_is_destroyed() {
std::vector<int> destructions;
{
LruCache<int, DestructorTracer> cache(2);
cache.insert(1, DestructorTracer{1, &destructions});
cache.insert(2, DestructorTracer{2, &destructions});
destructions.clear(); // discard any destructor calls from the temporaries above
// Insert 3 — entry 1 (LRU) must be destroyed before 3 is added.
cache.insert(3, DestructorTracer{3, &destructions});
VERIFY_IS_EQUAL(destructions.size(), std::size_t(1));
VERIFY_IS_EQUAL(destructions.front(), 1);
destructions.clear();
}
// Destruction of the cache destroys the remaining entries (2 and 3, in some
// order). We do not pin down the order, only the multiset.
VERIFY_IS_EQUAL(destructions.size(), std::size_t(2));
std::sort(destructions.begin(), destructions.end());
VERIFY_IS_EQUAL(destructions[0], 2);
VERIFY_IS_EQUAL(destructions[1], 3);
}
static void test_clear() {
std::vector<int> destructions;
LruCache<int, DestructorTracer> cache(4);
cache.insert(1, DestructorTracer{1, &destructions});
cache.insert(2, DestructorTracer{2, &destructions});
destructions.clear();
cache.clear();
VERIFY(cache.empty());
VERIFY_IS_EQUAL(destructions.size(), std::size_t(2));
VERIFY(cache.find(1) == nullptr);
// Reusable after clear.
cache.insert(7, DestructorTracer{7, &destructions});
VERIFY(cache.find(7) != nullptr);
}
static void test_move_semantics() {
LruCache<int, int> a(2);
a.insert(1, 100);
a.insert(2, 200);
LruCache<int, int> b(std::move(a));
VERIFY_IS_EQUAL(b.size(), std::size_t(2));
VERIFY(b.find(1) != nullptr);
VERIFY(b.find(2) != nullptr);
LruCache<int, int> c(1);
c = std::move(b);
VERIFY_IS_EQUAL(c.size(), std::size_t(2)); // capacity moved too
VERIFY_IS_EQUAL(c.capacity(), std::size_t(2));
VERIFY(c.find(1) != nullptr);
}
static void test_capacity_one() {
LruCache<int, int> cache(1);
cache.insert(1, 100);
VERIFY(cache.find(1) != nullptr);
cache.insert(2, 200);
VERIFY(cache.find(1) == nullptr);
VERIFY(cache.find(2) != nullptr);
}
static void test_complex_key() {
// Verify the cache works with a user-supplied Hash / KeyEqual.
struct Key {
int a;
int b;
bool operator==(const Key& o) const { return a == o.a && b == o.b; }
};
struct KeyHash {
std::size_t operator()(const Key& k) const noexcept { return std::hash<int>{}(k.a) * 31u + std::hash<int>{}(k.b); }
};
LruCache<Key, int, KeyHash> cache(2);
cache.insert(Key{1, 2}, 12);
cache.insert(Key{3, 4}, 34);
int* p = cache.find(Key{1, 2});
VERIFY(p != nullptr);
VERIFY_IS_EQUAL(*p, 12);
VERIFY(cache.find(Key{9, 9}) == nullptr);
}
EIGEN_DECLARE_TEST(lru_cache) {
CALL_SUBTEST(test_basic_get_and_miss());
CALL_SUBTEST(test_lru_eviction());
CALL_SUBTEST(test_hit_promotes_to_mru());
CALL_SUBTEST(test_replace_existing_key());
CALL_SUBTEST(test_replace_destroys_old_value());
CALL_SUBTEST(test_evicted_value_is_destroyed());
CALL_SUBTEST(test_clear());
CALL_SUBTEST(test_move_semantics());
CALL_SUBTEST(test_capacity_one());
CALL_SUBTEST(test_complex_key());
}