libeigen/eigen!2543 Co-authored-by: Rasmus Munk Larsen <rlarsen@nvidia.com>
228 lines
7.1 KiB
C++
228 lines
7.1 KiB
C++
// 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());
|
|
}
|