| // 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()); |
| } |