diff --git a/.cache_benchmark_build/cache_benchmark b/.cache_benchmark_build/cache_benchmark deleted file mode 100755 index eb757be..0000000 Binary files a/.cache_benchmark_build/cache_benchmark and /dev/null differ diff --git a/.cache_benchmark_build/cache_benchmark.cpp b/.cache_benchmark_build/cache_benchmark.cpp deleted file mode 100644 index 385276c..0000000 --- a/.cache_benchmark_build/cache_benchmark.cpp +++ /dev/null @@ -1,343 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "CachePolicy.h" -#include "LRU.h" -#include "LFU.h" -#include "ARC.h" - -using Key = int; -using Value = int; - -struct Operation { - // 0 = get, 1 = put - std::uint8_t type{}; - Key key{}; - Value value{}; -}; - -struct BenchResult { - std::string name; - double milliseconds{}; - double mops{}; - std::uint64_t gets{}; - std::uint64_t puts{}; - std::uint64_t hits{}; - std::uint64_t misses{}; - std::uint64_t evictions{}; - bool evictionsKnown{}; - std::size_t finalSize{}; -}; - -struct LocalCounters { - std::uint64_t gets{}; - std::uint64_t puts{}; - std::uint64_t hits{}; - std::uint64_t misses{}; -}; - -class LRUAdapter { -public: - LRUAdapter(std::size_t capacity, std::size_t shards) - : cache_(capacity, static_cast(std::max(1, shards))) {} - - bool get(const Key& key, Value& value) { return cache_.get(key, value); } - void put(const Key& key, const Value& value) { cache_.put(key, value); } - std::size_t size() const { return cache_.size(); } - std::uint64_t evictions() const { return 0; } - bool evictionsKnown() const { return false; } - -private: - KamaCache::KHighConcurrencyCache cache_; -}; - -class LRUKAdapter { -public: - LRUKAdapter(std::size_t capacity, std::size_t shards) - : cache_(capacity, capacity, 2, static_cast(std::max(1, shards))) {} - - bool get(const Key& key, Value& value) { return cache_.get(key, value); } - void put(const Key& key, const Value& value) { cache_.put(key, value); } - std::size_t size() const { return cache_.mainSize() + cache_.historySize(); } - std::uint64_t evictions() const { return 0; } - bool evictionsKnown() const { return false; } - -private: - KamaCache::KLruKCache cache_; -}; - -class LFUAdapter { -public: - LFUAdapter(std::size_t capacity, std::size_t shards) - : cache_(capacity, std::max(1, shards)) {} - - bool get(const Key& key, Value& value) { return cache_.get(key, value); } - void put(const Key& key, const Value& value) { cache_.put(key, value); } - std::size_t size() const { return cache_.size(); } - std::uint64_t evictions() const { return cache_.stats().evictions; } - bool evictionsKnown() const { return true; } - -private: - KamaCache::ShardedLFUCache cache_; -}; - -class ARCAdapter { -public: - ARCAdapter(std::size_t capacity, std::size_t shards) - : cache_(capacity, std::max(1, shards)) {} - - bool get(const Key& key, Value& value) { return cache_.get(key, value); } - void put(const Key& key, const Value& value) { cache_.put(key, value); } - std::size_t size() const { return cache_.size(); } - std::uint64_t evictions() const { return cache_.stats().evictions; } - bool evictionsKnown() const { return true; } - -private: - KamaCache::ShardedARCCache cache_; -}; - -std::vector makeMixedWorkload( - std::size_t operations, - std::size_t keyspace, - std::size_t hotKeys, - double getRatio, - double hotRatio, - std::uint64_t seed) -{ - std::vector workload; - workload.reserve(operations); - - std::mt19937_64 rng(seed); - std::uniform_real_distribution prob(0.0, 1.0); - std::uniform_int_distribution hotDist(0, static_cast(std::max(1, hotKeys) - 1)); - std::uniform_int_distribution coldDist( - static_cast(std::max(1, hotKeys)), - static_cast(std::max(hotKeys + 1, keyspace) - 1)); - - for (std::size_t i = 0; i < operations; ++i) { - const bool isGet = prob(rng) < getRatio; - const bool isHot = prob(rng) < hotRatio; - const int key = isHot ? hotDist(rng) : coldDist(rng); - workload.push_back(Operation{ - static_cast(isGet ? 0 : 1), - key, - key - }); - } - - return workload; -} - -std::vector makeScanWorkload( - std::size_t operations, - std::size_t keyspace, - double getRatio) -{ - std::vector workload; - workload.reserve(operations); - - for (std::size_t i = 0; i < operations; ++i) { - const bool isGet = static_cast(i % 1000) / 1000.0 < getRatio; - const int key = static_cast(i % keyspace); - workload.push_back(Operation{ - static_cast(isGet ? 0 : 1), - key, - key - }); - } - - return workload; -} - -template -BenchResult runBenchmark( - const std::string& name, - const std::vector& workload, - std::size_t capacity, - std::size_t shards, - std::size_t threadCount) -{ - Adapter cache(capacity, shards); - - // Prefill: keep every policy at a comparable warm starting point. - for (std::size_t i = 0; i < capacity; ++i) { - cache.put(static_cast(i), static_cast(i)); - } - - threadCount = std::max(1, threadCount); - std::vector counters(threadCount); - std::vector workers; - workers.reserve(threadCount); - - const auto start = std::chrono::steady_clock::now(); - - for (std::size_t t = 0; t < threadCount; ++t) { - const std::size_t begin = workload.size() * t / threadCount; - const std::size_t end = workload.size() * (t + 1) / threadCount; - - workers.emplace_back([&, begin, end, t]() { - Value value{}; - LocalCounters local; - - for (std::size_t i = begin; i < end; ++i) { - const Operation& op = workload[i]; - - if (op.type == 0) { - ++local.gets; - if (cache.get(op.key, value)) { - ++local.hits; - } else { - ++local.misses; - // Read-through cache behavior: miss then insert. - cache.put(op.key, op.value); - ++local.puts; - } - } else { - cache.put(op.key, op.value); - ++local.puts; - } - } - - counters[t] = local; - }); - } - - for (auto& worker : workers) { - worker.join(); - } - - const auto finish = std::chrono::steady_clock::now(); - const double ms = std::chrono::duration(finish - start).count(); - - BenchResult result; - result.name = name; - result.milliseconds = ms; - result.mops = workload.empty() || ms == 0.0 - ? 0.0 - : static_cast(workload.size()) / ms / 1000.0; - - for (const auto& c : counters) { - result.gets += c.gets; - result.puts += c.puts; - result.hits += c.hits; - result.misses += c.misses; - } - - result.evictions = cache.evictions(); - result.evictionsKnown = cache.evictionsKnown(); - result.finalSize = cache.size(); - return result; -} - -void printResult(const BenchResult& r) -{ - const double hitRate = r.gets == 0 ? 0.0 : static_cast(r.hits) * 100.0 / r.gets; - const double missRate = r.gets == 0 ? 0.0 : static_cast(r.misses) * 100.0 / r.gets; - - std::cout - << std::left << std::setw(14) << r.name - << std::right << std::setw(12) << std::fixed << std::setprecision(2) << r.milliseconds - << std::setw(12) << std::fixed << std::setprecision(3) << r.mops - << std::setw(12) << std::fixed << std::setprecision(2) << hitRate - << std::setw(12) << std::fixed << std::setprecision(2) << missRate - << std::setw(14) << r.puts - << std::setw(14) << r.finalSize; - - if (r.evictionsKnown) { - std::cout << std::setw(14) << r.evictions; - } else { - std::cout << std::setw(14) << "N/A"; - } - - std::cout << '\n'; -} - -void printHeader() -{ - std::cout - << std::left << std::setw(14) << "cache" - << std::right << std::setw(12) << "ms" - << std::setw(12) << "Mops/s" - << std::setw(12) << "hit%" - << std::setw(12) << "miss%" - << std::setw(14) << "puts" - << std::setw(14) << "final_size" - << std::setw(14) << "evictions" - << '\n'; - - std::cout << std::string(104, '-') << '\n'; -} - -void runScenario( - const std::string& scenarioName, - const std::vector& workload, - std::size_t capacity, - std::size_t shards, - std::size_t threads) -{ - std::cout << "\nScenario: " << scenarioName << '\n'; - printHeader(); - printResult(runBenchmark("LRU", workload, capacity, shards, threads)); - printResult(runBenchmark("LRU-K(k=2)", workload, capacity, shards, threads)); - printResult(runBenchmark("LFU", workload, capacity, shards, threads)); - printResult(runBenchmark("ARC", workload, capacity, shards, threads)); -} - -int main(int argc, char** argv) -{ - const std::size_t operations = argc > 1 ? static_cast(std::strtoull(argv[1], nullptr, 10)) : 1000000; - const std::size_t capacity = argc > 2 ? static_cast(std::strtoull(argv[2], nullptr, 10)) : 16384; - const std::size_t threads = argc > 3 ? static_cast(std::strtoull(argv[3], nullptr, 10)) : 1; - const std::size_t shards = argc > 4 ? static_cast(std::strtoull(argv[4], nullptr, 10)) : threads; - - const std::size_t keyspace = std::max(capacity * 16, capacity + 1); - const std::size_t hotKeys = std::max(64, capacity / 8); - - std::cout << "cache benchmark\n" - << "operations_per_scenario=" << operations - << ", capacity=" << capacity - << ", keyspace=" << keyspace - << ", hot_keys=" << hotKeys - << ", threads=" << threads - << ", shards=" << shards - << "\n"; - - runScenario( - "hot_read_95_get_90_hot", - makeMixedWorkload(operations, keyspace, hotKeys, 0.95, 0.90, 42), - capacity, - shards, - threads); - - runScenario( - "mixed_80_get_80_hot", - makeMixedWorkload(operations, keyspace, hotKeys, 0.80, 0.80, 43), - capacity, - shards, - threads); - - runScenario( - "scan_95_get", - makeScanWorkload(operations, keyspace, 0.95), - capacity, - shards, - threads); - - runScenario( - "write_heavy_50_get_70_hot", - makeMixedWorkload(operations, keyspace, hotKeys, 0.50, 0.70, 44), - capacity, - shards, - threads); - - return 0; -} diff --git a/.cache_benchmark_build/include/ARC.h b/.cache_benchmark_build/include/ARC.h deleted file mode 100644 index 2588c27..0000000 --- a/.cache_benchmark_build/include/ARC.h +++ /dev/null @@ -1,738 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "CachePolicy.h" - -namespace KamaCache { - -struct ARCState { - std::size_t recent{0}; - std::size_t frequent{0}; - std::size_t recentGhost{0}; - std::size_t frequentGhost{0}; - std::size_t recentTarget{0}; - - [[nodiscard]] std::size_t resident() const noexcept - { - return recent + frequent; - } - - [[nodiscard]] std::size_t ghost() const noexcept - { - return recentGhost + frequentGhost; - } - - [[nodiscard]] std::size_t tracked() const noexcept - { - return resident() + ghost(); - } -}; - -// ============================================================================ -// 1. 单分片 ARC 缓存 -// -// ARC = Adaptive Replacement Cache -// -// 四个核心队列: -// 1. recent_ : T1,最近只访问过一次的真实缓存数据 -// 2. frequent_ : T2,访问过至少两次的真实缓存数据 -// 3. recentGhost_ : B1,recent_ 被淘汰后的 key 历史,不保存 value -// 4. frequentGhost_ : B2,frequent_ 被淘汰后的 key 历史,不保存 value -// -// recentTarget_ 对应 ARC 论文中的 p: -// - B1 命中:说明 recent 区太小,增大 recentTarget_ -// - B2 命中:说明 frequent 区太小,减小 recentTarget_ -// -// 工程优化: -// 1. resident 节点使用 list 保存 key/value/listId -// 2. residentIndex_ 只保存 Key -> ResidentIterator -// 3. ghost 节点使用 list 保存 key/listId -// 4. ghostIndex_ 只保存 Key -> GhostIterator -// 5. 节点迁移使用 list::splice,避免重复构造 key/value -// 6. 分片内一把 mutex,保证单 shard 状态一致 -// ============================================================================ -template < - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to> -class alignas(64) ARCCache final : public CachePolicy { -public: - explicit ARCCache(std::size_t capacity) - : ARCCache(capacity, Hash{}, KeyEqual{}) - { - } - - ARCCache(std::size_t capacity, const Hash& hash, const KeyEqual& equal) - : capacity_(capacity) - , residentIndex_(0, hash, equal) - , ghostIndex_(0, hash, equal) - { - residentIndex_.reserve(capacity_); - ghostIndex_.reserve(capacity_); - } - - ~ARCCache() override = default; - - ARCCache(const ARCCache&) = delete; - ARCCache& operator=(const ARCCache&) = delete; - - ARCCache(ARCCache&&) = delete; - ARCCache& operator=(ARCCache&&) = delete; - - void put(const Key& key, const Value& value) override - { - static_cast(putAndReport(key, value)); - } - - template - CacheWriteResult putAndReport(K&& key, V&& value) - { - if (capacity_ == 0) { - return CacheWriteResult::ignored; - } - - std::lock_guard lock(mutex_); - - auto resident = residentIndex_.find(key); - if (resident != residentIndex_.end()) { - resident->second->value = std::forward(value); - promoteToFrequent(resident->second); - return CacheWriteResult::updated; - } - - auto ghost = ghostIndex_.find(key); - if (ghost != ghostIndex_.end()) { - const Key stableKey = ghost->second->key; - const bool frequentGhostHit = - ghost->second->list == ListId::frequentGhost; - - adaptTarget(frequentGhostHit); - const bool evicted = replace(frequentGhostHit); - - // replace() 可能向 ghostIndex_ 插入新元素并触发 rehash, - // 因此这里必须重新查找,不能继续使用 replace() 之前的 ghost 迭代器。 - insertResident(stableKey, std::forward(value), ListId::frequent); - - auto ghostAfterReplace = ghostIndex_.find(stableKey); - if (ghostAfterReplace != ghostIndex_.end()) { - eraseGhost(ghostAfterReplace); - } - - trimGhostHistory(); - - return evicted - ? CacheWriteResult::insertedWithEviction - : CacheWriteResult::inserted; - } - - const bool evicted = insertCold( - std::forward(key), - std::forward(value)); - - trimGhostHistory(); - - return evicted - ? CacheWriteResult::insertedWithEviction - : CacheWriteResult::inserted; - } - - bool get(const Key& key, Value& value) override - { - std::lock_guard lock(mutex_); - - auto found = residentIndex_.find(key); - if (found == residentIndex_.end()) { - misses_.fetch_add(1, std::memory_order_relaxed); - return false; - } - - value = found->second->value; - promoteToFrequent(found->second); - - hits_.fetch_add(1, std::memory_order_relaxed); - return true; - } - - using CachePolicy::get; - - [[nodiscard]] bool peek(const Key& key, Value& value) const override - { - std::lock_guard lock(mutex_); - - const auto found = residentIndex_.find(key); - if (found == residentIndex_.end()) { - return false; - } - - value = found->second->value; - return true; - } - - bool erase(const Key& key) override - { - std::lock_guard lock(mutex_); - - auto resident = residentIndex_.find(key); - if (resident != residentIndex_.end()) { - eraseResident(resident); - return true; - } - - auto ghost = ghostIndex_.find(key); - if (ghost == ghostIndex_.end()) { - return false; - } - - eraseGhost(ghost); - return true; - } - - void clear() override - { - std::lock_guard lock(mutex_); - - residentIndex_.clear(); - ghostIndex_.clear(); - - recent_.clear(); - frequent_.clear(); - recentGhost_.clear(); - frequentGhost_.clear(); - - recentTarget_ = 0; - } - - void purge() - { - clear(); - } - - [[nodiscard]] bool contains(const Key& key) const override - { - std::lock_guard lock(mutex_); - return residentIndex_.find(key) != residentIndex_.end(); - } - - [[nodiscard]] bool isGhost(const Key& key) const - { - std::lock_guard lock(mutex_); - return ghostIndex_.find(key) != ghostIndex_.end(); - } - - [[nodiscard]] std::size_t size() const override - { - std::lock_guard lock(mutex_); - return residentIndex_.size(); - } - - [[nodiscard]] std::size_t capacity() const noexcept override - { - return capacity_; - } - - [[nodiscard]] ARCState state() const - { - std::lock_guard lock(mutex_); - - return ARCState{ - recent_.size(), - frequent_.size(), - recentGhost_.size(), - frequentGhost_.size(), - recentTarget_, - }; - } - - [[nodiscard]] CacheStats stats() const noexcept override - { - return CacheStats{ - hits_.load(std::memory_order_relaxed), - misses_.load(std::memory_order_relaxed), - evictions_.load(std::memory_order_relaxed), - }; - } - - void resetStats() noexcept override - { - hits_.store(0, std::memory_order_relaxed); - misses_.store(0, std::memory_order_relaxed); - evictions_.store(0, std::memory_order_relaxed); - } - -private: - enum class ListId { - recent, - frequent, - recentGhost, - frequentGhost, - }; - - struct ResidentNode { - Key key; - Value value; - ListId list; - - template - ResidentNode(K&& k, V&& v, ListId listId) - : key(std::forward(k)) - , value(std::forward(v)) - , list(listId) - { - } - }; - - struct GhostNode { - Key key; - ListId list; - - template - GhostNode(K&& k, ListId listId) - : key(std::forward(k)) - , list(listId) - { - } - }; - - using ResidentList = std::list; - using ResidentIterator = typename ResidentList::iterator; - using ResidentIndex = std::unordered_map; - using ResidentIndexIterator = typename ResidentIndex::iterator; - - using GhostList = std::list; - using GhostIterator = typename GhostList::iterator; - using GhostIndex = std::unordered_map; - using GhostIndexIterator = typename GhostIndex::iterator; - -private: - ResidentList& residentListFor(ListId list) - { - return list == ListId::frequent ? frequent_ : recent_; - } - - GhostList& ghostListFor(ListId list) - { - return list == ListId::frequentGhost ? frequentGhost_ : recentGhost_; - } - - void promoteToFrequent(ResidentIterator node) - { - if (node->list == ListId::recent) { - frequent_.splice(frequent_.begin(), recent_, node); - node->list = ListId::frequent; - return; - } - - frequent_.splice(frequent_.begin(), frequent_, node); - } - - void adaptTarget(bool frequentGhostHit) - { - if (frequentGhostHit) { - const std::size_t divisor = - std::max(1, frequentGhost_.size()); - - const std::size_t delta = - std::max(1, recentGhost_.size() / divisor); - - recentTarget_ = delta >= recentTarget_ ? 0 : recentTarget_ - delta; - return; - } - - const std::size_t divisor = - std::max(1, recentGhost_.size()); - - const std::size_t delta = - std::max(1, frequentGhost_.size() / divisor); - - recentTarget_ = std::min(capacity_, recentTarget_ + delta); - } - - bool replace(bool frequentGhostHit) - { - if (residentIndex_.empty()) { - return false; - } - - if (!recent_.empty() - && (recent_.size() > recentTarget_ - || (frequentGhostHit && recent_.size() == recentTarget_))) { - return moveResidentLruToGhost(ListId::recent, ListId::recentGhost); - } - - if (!frequent_.empty()) { - return moveResidentLruToGhost(ListId::frequent, ListId::frequentGhost); - } - - if (!recent_.empty()) { - return moveResidentLruToGhost(ListId::recent, ListId::recentGhost); - } - - return false; - } - - bool moveResidentLruToGhost(ListId residentList, ListId ghostList) - { - auto& source = residentListFor(residentList); - if (source.empty()) { - return false; - } - - auto victim = std::prev(source.end()); - auto& destination = ghostListFor(ghostList); - - destination.emplace_front(victim->key, ghostList); - auto ghostNode = destination.begin(); - - try { - ghostIndex_.emplace(ghostNode->key, ghostNode); - } catch (...) { - destination.pop_front(); - throw; - } - - residentIndex_.erase(victim->key); - source.erase(victim); - - evictions_.fetch_add(1, std::memory_order_relaxed); - return true; - } - - template - bool insertCold(K&& key, V&& value) - { - bool evicted = false; - - const std::size_t recentSide = recent_.size() + recentGhost_.size(); - - if (recentSide == capacity_) { - if (recent_.size() < capacity_) { - removeGhostLru(ListId::recentGhost); - evicted = replace(false); - } else { - evicted = removeResidentLru(ListId::recent); - } - } else { - const std::size_t tracked = - residentIndex_.size() + ghostIndex_.size(); - - if (tracked >= capacity_) { - if (tracked >= 2 * capacity_) { - if (!removeGhostLru(ListId::frequentGhost)) { - removeGhostLru(ListId::recentGhost); - } - } - - evicted = replace(false); - } - } - - insertResident(std::forward(key), std::forward(value), ListId::recent); - - return evicted; - } - - template - void insertResident(K&& key, V&& value, ListId list) - { - auto& destination = residentListFor(list); - - destination.emplace_front(std::forward(key), std::forward(value), list); - auto node = destination.begin(); - - try { - auto insertion = residentIndex_.emplace(node->key, node); - - if (!insertion.second) { - insertion.first->second->value = std::move(node->value); - destination.pop_front(); - promoteToFrequent(insertion.first->second); - } - } catch (...) { - destination.pop_front(); - throw; - } - } - - bool removeResidentLru(ListId list) - { - auto& source = residentListFor(list); - if (source.empty()) { - return false; - } - - auto victim = std::prev(source.end()); - - residentIndex_.erase(victim->key); - source.erase(victim); - - evictions_.fetch_add(1, std::memory_order_relaxed); - return true; - } - - bool removeGhostLru(ListId list) - { - auto& source = ghostListFor(list); - if (source.empty()) { - return false; - } - - auto victim = std::prev(source.end()); - - ghostIndex_.erase(victim->key); - source.erase(victim); - - return true; - } - - void eraseResident(ResidentIndexIterator resident) - { - auto node = resident->second; - - residentListFor(node->list).erase(node); - residentIndex_.erase(resident); - } - - void eraseGhost(GhostIndexIterator ghost) - { - auto node = ghost->second; - - ghostListFor(node->list).erase(node); - ghostIndex_.erase(ghost); - } - - void trimGhostHistory() - { - while (ghostIndex_.size() > capacity_) { - if (!removeGhostLru(ListId::frequentGhost)) { - removeGhostLru(ListId::recentGhost); - } - } - } - -private: - const std::size_t capacity_; - std::size_t recentTarget_{0}; - - ResidentList recent_; - ResidentList frequent_; - GhostList recentGhost_; - GhostList frequentGhost_; - - ResidentIndex residentIndex_; - GhostIndex ghostIndex_; - - mutable std::mutex mutex_; - - std::atomic hits_{0}; - std::atomic misses_{0}; - std::atomic evictions_{0}; -}; - -// ============================================================================ -// 2. 分片 ARC 缓存 -// -// 设计目标: -// 1. 将全局大锁拆成多把 shard 小锁 -// 2. 每个 shard 内部是完整 ARC -// 3. 整体是分片近似 ARC,不是全局严格 ARC -// 4. 保留统一统计接口 -// ============================================================================ -template < - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to> -class ShardedARCCache final : public CachePolicy { -public: - explicit ShardedARCCache( - std::size_t capacity, - std::size_t shardCount = std::thread::hardware_concurrency(), - const Hash& hash = Hash{}, - const KeyEqual& equal = KeyEqual{}) - : capacity_(capacity) - , hash_(hash) - , shardCount_(normalizeShardCount(capacity, shardCount)) - { - shards_.reserve(shardCount_); - - const std::size_t baseCapacity = capacity_ / shardCount_; - const std::size_t extraCapacity = capacity_ % shardCount_; - - for (std::size_t index = 0; index < shardCount_; ++index) { - const std::size_t shardCapacity = - baseCapacity + (index < extraCapacity ? 1U : 0U); - - shards_.push_back( - std::make_unique(shardCapacity, hash, equal)); - } - } - - ~ShardedARCCache() override = default; - - ShardedARCCache(const ShardedARCCache&) = delete; - ShardedARCCache& operator=(const ShardedARCCache&) = delete; - - ShardedARCCache(ShardedARCCache&&) = delete; - ShardedARCCache& operator=(ShardedARCCache&&) = delete; - - void put(const Key& key, const Value& value) override - { - shardFor(key).put(key, value); - } - - bool get(const Key& key, Value& value) override - { - return shardFor(key).get(key, value); - } - - using CachePolicy::get; - - [[nodiscard]] bool peek(const Key& key, Value& value) const override - { - return shardFor(key).peek(key, value); - } - - bool erase(const Key& key) override - { - return shardFor(key).erase(key); - } - - void clear() override - { - for (auto& shard : shards_) { - shard->clear(); - } - } - - void purge() - { - clear(); - } - - [[nodiscard]] bool contains(const Key& key) const override - { - return shardFor(key).contains(key); - } - - [[nodiscard]] bool isGhost(const Key& key) const - { - return shardFor(key).isGhost(key); - } - - [[nodiscard]] std::size_t size() const override - { - std::size_t total = 0; - - for (const auto& shard : shards_) { - total += shard->size(); - } - - return total; - } - - [[nodiscard]] std::size_t capacity() const noexcept override - { - return capacity_; - } - - [[nodiscard]] std::size_t shardCount() const noexcept - { - return shardCount_; - } - - [[nodiscard]] CacheStats stats() const noexcept override - { - CacheStats total; - - for (const auto& shard : shards_) { - const CacheStats shardStats = shard->stats(); - - total.hits += shardStats.hits; - total.misses += shardStats.misses; - total.evictions += shardStats.evictions; - } - - return total; - } - - void resetStats() noexcept override - { - for (auto& shard : shards_) { - shard->resetStats(); - } - } - - [[nodiscard]] std::vector shardStates() const - { - std::vector states; - states.reserve(shards_.size()); - - for (const auto& shard : shards_) { - states.push_back(shard->state()); - } - - return states; - } - -private: - using Shard = ARCCache; - -private: - static std::size_t normalizeShardCount( - std::size_t capacity, - std::size_t requestedShardCount) noexcept - { - if (capacity == 0) { - return 1; - } - - if (requestedShardCount == 0) { - requestedShardCount = 1; - } - - return std::min(capacity, requestedShardCount); - } - - [[nodiscard]] std::size_t shardIndex(const Key& key) const - { - return hash_(key) % shardCount_; - } - - Shard& shardFor(const Key& key) - { - return *shards_[shardIndex(key)]; - } - - const Shard& shardFor(const Key& key) const - { - return *shards_[shardIndex(key)]; - } - -private: - const std::size_t capacity_; - const Hash hash_; - const std::size_t shardCount_; - - std::vector> shards_; -}; - -template -using KArcCache = ARCCache; - -template -using KHashArcCache = ShardedARCCache; - -} // namespace KamaCache diff --git a/.cache_benchmark_build/include/CachePolicy.h b/.cache_benchmark_build/include/CachePolicy.h deleted file mode 100644 index 35ba715..0000000 --- a/.cache_benchmark_build/include/CachePolicy.h +++ /dev/null @@ -1,221 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace KamaCache { - -// ============================================================================ -// 1. 写入结果 -// ============================================================================ -enum class CacheWriteResult { - ignored, // 容量为 0,或策略决定忽略本次写入 - inserted, // 新插入,未发生淘汰 - updated, // key 已存在,更新 value - insertedWithEviction, // 新插入,并触发淘汰 -}; - -// ============================================================================ -// 2. 缓存统计信息 -// ============================================================================ -struct CacheStats { - std::uint64_t hits{0}; - std::uint64_t misses{0}; - std::uint64_t evictions{0}; - - [[nodiscard]] std::uint64_t requests() const noexcept - { - return hits + misses; - } - - [[nodiscard]] bool empty() const noexcept - { - return requests() == 0 && evictions == 0; - } - - [[nodiscard]] double hitRate() const noexcept - { - const std::uint64_t total = requests(); - - return total == 0 - ? 0.0 - : static_cast(hits) / static_cast(total); - } - - [[nodiscard]] double missRate() const noexcept - { - const std::uint64_t total = requests(); - - return total == 0 - ? 0.0 - : static_cast(misses) / static_cast(total); - } - - void reset() noexcept - { - hits = 0; - misses = 0; - evictions = 0; - } -}; - -// ============================================================================ -// 3. 缓存策略基类 -// -// 设计原则: -// 1. get(key, value) 是核心接口,用 bool 表达是否命中。 -// 2. tryGet(key) 是安全便捷接口,用 optional 避免 miss 和默认值混淆。 -// 3. getOrDefault(key, defaultValue) 用于业务允许默认值兜底的场景。 -// 4. peek(key, value) 默认不提供真实实现,具体缓存策略可覆盖。 -// 5. stats/resetStats 提供统一监控接口,具体缓存策略可覆盖。 -// ============================================================================ -template -class CachePolicy { -public: - virtual ~CachePolicy() = default; - - CachePolicy() = default; - - CachePolicy(const CachePolicy&) = delete; - CachePolicy& operator=(const CachePolicy&) = delete; - - CachePolicy(CachePolicy&&) = delete; - CachePolicy& operator=(CachePolicy&&) = delete; - - // ------------------------------------------------------------------------ - // 写入接口 - // - // 注意: - // 由于这是虚函数接口,不能做完美转发版本: - // template put(K&&, V&&) - // - // 如果具体缓存实现需要移动语义,可以在派生类中额外提供模板 put。 - // ------------------------------------------------------------------------ - virtual void put(const Key& key, const Value& value) = 0; - - // ------------------------------------------------------------------------ - // 查询接口 - // - // 命中返回 true,并通过 value 输出结果。 - // 未命中返回 false。 - // - // 对 LRU / LFU 来说,get 通常会改变缓存状态: - // LRU:刷新访问顺序 - // LFU:增加访问频率 - // ------------------------------------------------------------------------ - virtual bool get(const Key& key, Value& value) = 0; - - // ------------------------------------------------------------------------ - // 安全便捷查询接口 - // - // 推荐业务代码优先使用 tryGet,而不是 get(key) 返回默认值。 - // ------------------------------------------------------------------------ - [[nodiscard]] std::optional tryGet(const Key& key) - { - Value value{}; - - if (!get(key, value)) { - return std::nullopt; - } - - return value; - } - - // ------------------------------------------------------------------------ - // 默认值兜底查询接口 - // - // 适合业务明确允许默认值的场景。 - // ------------------------------------------------------------------------ - [[nodiscard]] Value getOrDefault(const Key& key, Value defaultValue = Value{}) - { - Value value{}; - - if (!get(key, value)) { - return defaultValue; - } - - return value; - } - - // ------------------------------------------------------------------------ - // 只读查看接口 - // - // peek 和 get 的区别: - // get 可能改变缓存状态; - // peek 不应该改变缓存状态。 - // - // 默认实现返回 false。 - // 支持 peek 的缓存策略应覆盖该方法。 - // ------------------------------------------------------------------------ - [[nodiscard]] virtual bool peek(const Key& key, Value& value) const - { - static_cast(key); - static_cast(value); - return false; - } - - [[nodiscard]] std::optional tryPeek(const Key& key) const - { - Value value{}; - - if (!peek(key, value)) { - return std::nullopt; - } - - return value; - } - - // ------------------------------------------------------------------------ - // 删除与清理 - // ------------------------------------------------------------------------ - virtual bool erase(const Key& key) = 0; - - virtual void clear() = 0; - - void purge() - { - clear(); - } - - // ------------------------------------------------------------------------ - // 状态查询 - // ------------------------------------------------------------------------ - [[nodiscard]] virtual bool contains(const Key& key) const = 0; - - [[nodiscard]] virtual std::size_t size() const = 0; - - [[nodiscard]] virtual std::size_t capacity() const noexcept = 0; - - [[nodiscard]] bool empty() const - { - return size() == 0; - } - - // ------------------------------------------------------------------------ - // 统计接口 - // - // 普通缓存可以不覆盖,默认返回空统计。 - // 分片缓存、生产级缓存建议覆盖。 - // ------------------------------------------------------------------------ - [[nodiscard]] virtual CacheStats stats() const noexcept - { - return CacheStats{}; - } - - virtual void resetStats() noexcept - { - } -}; - -// ============================================================================ -// 4. 兼容旧命名 -// ============================================================================ -template -using KICachePolicy = CachePolicy; - -template -using ICachePolicy = CachePolicy; - -} // namespace KamaCache \ No newline at end of file diff --git a/.cache_benchmark_build/include/LFU.h b/.cache_benchmark_build/include/LFU.h deleted file mode 100644 index 97b7539..0000000 --- a/.cache_benchmark_build/include/LFU.h +++ /dev/null @@ -1,590 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "CachePolicy.h" - -namespace KamaCache { - -// ============================================================================ -// 1. 单分片 LFU 缓存 -// 设计目标: -// 1. O(1) 查找 -// 2. O(1) 频次提升 -// 3. O(1) 淘汰最低频率节点 -// 4. 同频率下按 LRU 淘汰 -// -// 数据结构: -// 1. frequencyBuckets_: -// frequency -> list -// -// 2. nodeIndex_: -// key -> list::iterator -// -// 说明: -// CacheNode 内部同时保存 key / value / frequency。 -// 这样可以避免 value、frequency、bucket key 分散在多个容器中造成状态不一致。 -// ============================================================================ -template < - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to> -class alignas(64) LFUCache final : public CachePolicy { -public: - explicit LFUCache( - std::size_t capacity, - std::size_t maxFrequency = defaultMaxFrequency()) - : LFUCache(capacity, maxFrequency, Hash{}, KeyEqual{}) - { - } - - LFUCache( - std::size_t capacity, - std::size_t maxFrequency, - const Hash& hash, - const KeyEqual& equal) - : capacity_(capacity) - , maxFrequency_(std::max(2, maxFrequency)) - , nodeIndex_(0, hash, equal) - { - nodeIndex_.reserve(capacity_); - frequencyBuckets_.reserve(capacity_); - } - - ~LFUCache() override = default; - - LFUCache(const LFUCache&) = delete; - LFUCache& operator=(const LFUCache&) = delete; - - LFUCache(LFUCache&&) = delete; - LFUCache& operator=(LFUCache&&) = delete; - - void put(const Key& key, const Value& value) override - { - static_cast(putAndReport(key, value)); - } - - template - CacheWriteResult putAndReport(K&& key, V&& value) - { - if (capacity_ == 0) { - return CacheWriteResult::ignored; - } - - std::lock_guard lock(mutex_); - - auto found = nodeIndex_.find(key); - if (found != nodeIndex_.end()) { - found->second->value = std::forward(value); - promote(found->second); - return CacheWriteResult::updated; - } - - const bool needsEviction = nodeIndex_.size() >= capacity_; - - auto& bucket = frequencyBuckets_[1]; - bucket.emplace_front(std::forward(key), std::forward(value), 1); - - auto insertedNode = bucket.begin(); - - try { - auto insertion = nodeIndex_.emplace(insertedNode->key, insertedNode); - if (!insertion.second) { - insertion.first->second->value = insertedNode->value; - - bucket.erase(insertedNode); - if (bucket.empty()) { - frequencyBuckets_.erase(1); - } - - promote(insertion.first->second); - return CacheWriteResult::updated; - } - } catch (...) { - bucket.erase(insertedNode); - if (bucket.empty()) { - frequencyBuckets_.erase(1); - } - throw; - } - - if (needsEviction) { - evictOneLocked(); - } - - minFrequency_ = 1; - - return needsEviction - ? CacheWriteResult::insertedWithEviction - : CacheWriteResult::inserted; - } - - bool get(const Key& key, Value& value) override - { - std::lock_guard lock(mutex_); - - auto found = nodeIndex_.find(key); - if (found == nodeIndex_.end()) { - return false; - } - - value = found->second->value; - promote(found->second); - - return true; - } - - using CachePolicy::get; - - [[nodiscard]] bool peek(const Key& key, Value& value) const override - { - std::lock_guard lock(mutex_); - - const auto found = nodeIndex_.find(key); - if (found == nodeIndex_.end()) { - return false; - } - - value = found->second->value; - return true; - } - - bool erase(const Key& key) override - { - std::lock_guard lock(mutex_); - - auto found = nodeIndex_.find(key); - if (found == nodeIndex_.end()) { - return false; - } - - const std::size_t removedFrequency = found->second->frequency; - - eraseNodeFromBucket(found->second); - nodeIndex_.erase(found); - - if (nodeIndex_.empty()) { - minFrequency_ = 0; - } else if ( - removedFrequency == minFrequency_ - && frequencyBuckets_.find(removedFrequency) == frequencyBuckets_.end()) { - recomputeMinFrequency(); - } - - return true; - } - - void clear() override - { - std::lock_guard lock(mutex_); - - nodeIndex_.clear(); - frequencyBuckets_.clear(); - minFrequency_ = 0; - } - - void purge() - { - clear(); - } - - [[nodiscard]] bool contains(const Key& key) const override - { - std::lock_guard lock(mutex_); - return nodeIndex_.find(key) != nodeIndex_.end(); - } - - [[nodiscard]] std::size_t size() const override - { - std::lock_guard lock(mutex_); - return nodeIndex_.size(); - } - - [[nodiscard]] std::size_t capacity() const noexcept override - { - return capacity_; - } - - [[nodiscard]] std::size_t maxFrequency() const noexcept - { - return maxFrequency_; - } - - static constexpr std::size_t defaultMaxFrequency() noexcept - { - return 1U << 20U; - } - -private: - struct CacheNode { - Key key; - Value value; - std::size_t frequency; - - template - CacheNode(K&& k, V&& v, std::size_t f) - : key(std::forward(k)) - , value(std::forward(v)) - , frequency(f) - { - } - }; - - using NodeList = std::list; - using NodeIterator = typename NodeList::iterator; - - using NodeIndex = std::unordered_map; - using FrequencyBuckets = std::unordered_map; - -private: - void promote(NodeIterator node) - { - if (node->frequency >= maxFrequency_) { - ageFrequenciesLocked(); - } - - const std::size_t oldFrequency = node->frequency; - const std::size_t newFrequency = oldFrequency + 1; - - auto oldBucketIt = frequencyBuckets_.find(oldFrequency); - auto& oldBucket = oldBucketIt->second; - - auto newBucketIt = frequencyBuckets_.try_emplace(newFrequency).first; - auto& newBucket = newBucketIt->second; - - newBucket.splice(newBucket.begin(), oldBucket, node); - node->frequency = newFrequency; - - if (oldBucket.empty()) { - frequencyBuckets_.erase(oldFrequency); - - if (minFrequency_ == oldFrequency) { - minFrequency_ = newFrequency; - } - } - } - - void eraseNodeFromBucket(NodeIterator node) - { - const std::size_t frequency = node->frequency; - - auto bucketIt = frequencyBuckets_.find(frequency); - bucketIt->second.erase(node); - - if (bucketIt->second.empty()) { - frequencyBuckets_.erase(bucketIt); - } - } - - void evictOneLocked() - { - if (nodeIndex_.empty()) { - minFrequency_ = 0; - return; - } - - auto bucketIt = frequencyBuckets_.find(minFrequency_); - - if (bucketIt == frequencyBuckets_.end() || bucketIt->second.empty()) { - recomputeMinFrequency(); - bucketIt = frequencyBuckets_.find(minFrequency_); - } - - auto& bucket = bucketIt->second; - - // 同频率下,链表头部是最近访问,尾部是最久未访问。 - auto victim = std::prev(bucket.end()); - - nodeIndex_.erase(victim->key); - bucket.erase(victim); - - if (bucket.empty()) { - frequencyBuckets_.erase(bucketIt); - } - - if (nodeIndex_.empty()) { - minFrequency_ = 0; - } - } - - void ageFrequenciesLocked() - { - if (frequencyBuckets_.empty()) { - minFrequency_ = 0; - return; - } - - std::vector frequencies; - frequencies.reserve(frequencyBuckets_.size()); - - for (const auto& bucketPair : frequencyBuckets_) { - frequencies.push_back(bucketPair.first); - } - - std::sort(frequencies.begin(), frequencies.end()); - - FrequencyBuckets agedBuckets; - agedBuckets.reserve(frequencyBuckets_.size()); - - // 先创建目标桶,降低迁移中途异常导致状态不一致的风险。 - for (const std::size_t frequency : frequencies) { - const std::size_t agedFrequency = - std::max(1, frequency / 2); - - agedBuckets.try_emplace(agedFrequency); - } - - for (const std::size_t frequency : frequencies) { - auto sourceIt = frequencyBuckets_.find(frequency); - if (sourceIt == frequencyBuckets_.end()) { - continue; - } - - const std::size_t agedFrequency = - std::max(1, frequency / 2); - - auto& source = sourceIt->second; - auto& destination = agedBuckets.find(agedFrequency)->second; - - // 从旧桶尾部搬到新桶头部,可以保持原有 MRU -> LRU 的相对顺序。 - while (!source.empty()) { - auto node = std::prev(source.end()); - - node->frequency = agedFrequency; - destination.splice(destination.begin(), source, node); - } - } - - frequencyBuckets_.swap(agedBuckets); - recomputeMinFrequency(); - } - - void recomputeMinFrequency() - { - minFrequency_ = std::numeric_limits::max(); - - for (const auto& bucket : frequencyBuckets_) { - minFrequency_ = std::min(minFrequency_, bucket.first); - } - - if (frequencyBuckets_.empty()) { - minFrequency_ = 0; - } - } - -private: - const std::size_t capacity_; - const std::size_t maxFrequency_; - - std::size_t minFrequency_{0}; - - NodeIndex nodeIndex_; - FrequencyBuckets frequencyBuckets_; - - mutable std::mutex mutex_; -}; - -// ============================================================================ -// 2. 分片 LFU 缓存 -// 设计目标: -// 1. 将一把全局大锁拆成多把 shard 小锁 -// 2. 每个 shard 内部是精确 LFU -// 3. 整体是分片近似 LFU,不是全局严格 LFU -// 4. 保留命中、未命中、淘汰统计 -// ============================================================================ -template < - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to> -class ShardedLFUCache final : public CachePolicy { -public: - explicit ShardedLFUCache( - std::size_t capacity, - std::size_t shardCount = std::thread::hardware_concurrency(), - std::size_t maxFrequency = - LFUCache::defaultMaxFrequency(), - const Hash& hash = Hash{}, - const KeyEqual& equal = KeyEqual{}) - : capacity_(capacity) - , hash_(hash) - , shardCount_(normalizeShardCount(capacity, shardCount)) - { - shards_.reserve(shardCount_); - - const std::size_t baseCapacity = capacity_ / shardCount_; - const std::size_t extraCapacity = capacity_ % shardCount_; - - for (std::size_t index = 0; index < shardCount_; ++index) { - const std::size_t shardCapacity = - baseCapacity + (index < extraCapacity ? 1U : 0U); - - shards_.push_back( - std::make_unique( - shardCapacity, - maxFrequency, - hash, - equal)); - } - } - - ~ShardedLFUCache() override = default; - - ShardedLFUCache(const ShardedLFUCache&) = delete; - ShardedLFUCache& operator=(const ShardedLFUCache&) = delete; - - ShardedLFUCache(ShardedLFUCache&&) = delete; - ShardedLFUCache& operator=(ShardedLFUCache&&) = delete; - - void put(const Key& key, const Value& value) override - { - const CacheWriteResult result = shardFor(key).putAndReport(key, value); - - if (result == CacheWriteResult::insertedWithEviction) { - evictions_.fetch_add(1, std::memory_order_relaxed); - } - } - - bool get(const Key& key, Value& value) override - { - if (shardFor(key).get(key, value)) { - hits_.fetch_add(1, std::memory_order_relaxed); - return true; - } - - misses_.fetch_add(1, std::memory_order_relaxed); - return false; - } - - using CachePolicy::get; - - [[nodiscard]] bool peek(const Key& key, Value& value) const override - { - return shardFor(key).peek(key, value); - } - - bool erase(const Key& key) override - { - return shardFor(key).erase(key); - } - - void clear() override - { - for (auto& shard : shards_) { - shard->clear(); - } - } - - void purge() - { - clear(); - } - - [[nodiscard]] bool contains(const Key& key) const override - { - return shardFor(key).contains(key); - } - - [[nodiscard]] std::size_t size() const override - { - std::size_t total = 0; - - for (const auto& shard : shards_) { - total += shard->size(); - } - - return total; - } - - [[nodiscard]] std::size_t capacity() const noexcept override - { - return capacity_; - } - - [[nodiscard]] std::size_t shardCount() const noexcept - { - return shardCount_; - } - - [[nodiscard]] CacheStats stats() const noexcept override - { - return CacheStats{ - hits_.load(std::memory_order_relaxed), - misses_.load(std::memory_order_relaxed), - evictions_.load(std::memory_order_relaxed), - }; - } - - void resetStats() noexcept override - { - hits_.store(0, std::memory_order_relaxed); - misses_.store(0, std::memory_order_relaxed); - evictions_.store(0, std::memory_order_relaxed); - } - -private: - using Shard = LFUCache; - -private: - static std::size_t normalizeShardCount( - std::size_t capacity, - std::size_t requestedShardCount) noexcept - { - if (capacity == 0) { - return 1; - } - - if (requestedShardCount == 0) { - requestedShardCount = 1; - } - - return std::min(capacity, requestedShardCount); - } - - [[nodiscard]] std::size_t shardIndex(const Key& key) const - { - return hash_(key) % shardCount_; - } - - Shard& shardFor(const Key& key) - { - return *shards_[shardIndex(key)]; - } - - const Shard& shardFor(const Key& key) const - { - return *shards_[shardIndex(key)]; - } - -private: - const std::size_t capacity_; - const Hash hash_; - const std::size_t shardCount_; - - std::vector> shards_; - - std::atomic hits_{0}; - std::atomic misses_{0}; - std::atomic evictions_{0}; -}; - -template -using KLfuCache = LFUCache; - -template -using KHashLfuCache = ShardedLFUCache; - -} // namespace KamaCache diff --git a/.cache_benchmark_build/include/LRU.h b/.cache_benchmark_build/include/LRU.h deleted file mode 100644 index 9978c81..0000000 --- a/.cache_benchmark_build/include/LRU.h +++ /dev/null @@ -1,691 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace KamaCache -{ - -// ============================================================================ -// 1. 单分片 LRU 缓存 -// 设计目标:O(1) 查找、O(1) 提权、O(1) 淘汰 -// 数据结构:std::list + std::unordered_map -// ============================================================================ -template< - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to -> -class alignas(64) KLruCacheShard -{ -public: - struct CacheNode { - Key key; - Value value; - - template - CacheNode(K&& k, V&& v) - : key(std::forward(k)) - , value(std::forward(v)) - {} - }; - - using ListType = std::list; - using ListIter = typename ListType::iterator; - using MapType = std::unordered_map; - -public: - explicit KLruCacheShard(std::size_t capacity) - : capacity_(capacity) - { - cacheMap_.reserve(capacity_); - } - - ~KLruCacheShard() = default; - - KLruCacheShard(const KLruCacheShard&) = delete; - KLruCacheShard& operator=(const KLruCacheShard&) = delete; - - KLruCacheShard(KLruCacheShard&&) = delete; - KLruCacheShard& operator=(KLruCacheShard&&) = delete; - - bool get(const Key& key, Value& value) - { - std::lock_guard lock(mutex_); - - auto it = cacheMap_.find(key); - if (it == cacheMap_.end()) { - return false; - } - - // LRU 语义:命中后移动到头部 - cacheList_.splice(cacheList_.begin(), cacheList_, it->second); - value = it->second->value; - - return true; - } - - bool peek(const Key& key, Value& value) const - { - std::lock_guard lock(mutex_); - - auto it = cacheMap_.find(key); - if (it == cacheMap_.end()) { - return false; - } - - // peek 只读 value,不改变 LRU 顺序 - value = it->second->value; - return true; - } - - template - bool updateIfExists(const Key& key, V&& value) - { - std::lock_guard lock(mutex_); - - auto it = cacheMap_.find(key); - if (it == cacheMap_.end()) { - return false; - } - - it->second->value = std::forward(value); - cacheList_.splice(cacheList_.begin(), cacheList_, it->second); - - return true; - } - - template - void put(K&& key, V&& value) - { - if (capacity_ == 0) { - return; - } - - std::lock_guard lock(mutex_); - - auto it = cacheMap_.find(key); - if (it != cacheMap_.end()) { - it->second->value = std::forward(value); - cacheList_.splice(cacheList_.begin(), cacheList_, it->second); - return; - } - - if (cacheList_.size() >= capacity_) { - auto& backNode = cacheList_.back(); - cacheMap_.erase(backNode.key); - cacheList_.pop_back(); - } - - cacheList_.emplace_front(std::forward(key), std::forward(value)); - - try { - cacheMap_.emplace(cacheList_.front().key, cacheList_.begin()); - } catch (...) { - cacheList_.pop_front(); - throw; - } - } - - bool remove(const Key& key) - { - std::lock_guard lock(mutex_); - - auto it = cacheMap_.find(key); - if (it == cacheMap_.end()) { - return false; - } - - cacheList_.erase(it->second); - cacheMap_.erase(it); - - return true; - } - - void clear() - { - std::lock_guard lock(mutex_); - cacheList_.clear(); - cacheMap_.clear(); - } - - std::size_t size() const - { - std::lock_guard lock(mutex_); - return cacheList_.size(); - } - - std::size_t capacity() const noexcept - { - return capacity_; - } - -private: - const std::size_t capacity_; - - ListType cacheList_; - MapType cacheMap_; - - mutable std::mutex mutex_; -}; - - -// ============================================================================ -// 2. 分片 LRU 缓存 -// 设计目标:将一把大锁拆成多把小锁,降低高并发访问下的锁竞争 -// 注意:这是分片加锁,不是 lock-free -// ============================================================================ -template< - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to -> -class KHighConcurrencyCache -{ -public: - explicit KHighConcurrencyCache(std::size_t totalCapacity, int shardNum = 0) - : shardNum_(normalizeShardNum(shardNum)) - { - const std::size_t shardCapacity = ceilDiv(totalCapacity, shardNum_); - - shards_.reserve(shardNum_); - - for (std::size_t i = 0; i < shardNum_; ++i) { - shards_.emplace_back( - std::make_unique(shardCapacity) - ); - } - } - - KHighConcurrencyCache(const KHighConcurrencyCache&) = delete; - KHighConcurrencyCache& operator=(const KHighConcurrencyCache&) = delete; - - template - void put(K&& key, V&& value) - { - const std::size_t index = getShardIndex(key); - shards_[index]->put(std::forward(key), std::forward(value)); - } - - bool get(const Key& key, Value& value) - { - return shards_[getShardIndex(key)]->get(key, value); - } - - bool peek(const Key& key, Value& value) const - { - return shards_[getShardIndex(key)]->peek(key, value); - } - - template - bool updateIfExists(const Key& key, V&& value) - { - return shards_[getShardIndex(key)]->updateIfExists( - key, - std::forward(value) - ); - } - - bool remove(const Key& key) - { - return shards_[getShardIndex(key)]->remove(key); - } - - void clear() - { - for (auto& shard : shards_) { - shard->clear(); - } - } - - std::size_t size() const - { - std::size_t total = 0; - - for (const auto& shard : shards_) { - total += shard->size(); - } - - return total; - } - - std::size_t shardNum() const noexcept - { - return shardNum_; - } - -private: - using ShardType = KLruCacheShard; - - static std::size_t normalizeShardNum(int shardNum) - { - if (shardNum > 0) { - return static_cast(shardNum); - } - - const unsigned int hardwareNum = std::thread::hardware_concurrency(); - - if (hardwareNum == 0) { - return 1; - } - - return static_cast(hardwareNum); - } - - static std::size_t ceilDiv(std::size_t a, std::size_t b) - { - return (a + b - 1) / b; - } - - std::size_t getShardIndex(const Key& key) const - { - return hash_(key) % shardNum_; - } - -private: - const std::size_t shardNum_; - - Hash hash_; - - std::vector> shards_; -}; - - -// ============================================================================ -// 3. LRU-K 历史区分片 -// 设计目标:把 count 和 value 放在同一个节点里,避免两个容器之间的数据不一致 -// ============================================================================ -enum class KHistoryAccessResult -{ - Miss, - HitHistory, - Promoted -}; - -template< - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to -> -class KHistoryShard -{ -public: - struct HistoryNode { - Key key; - Value value; - std::size_t count; - - template - HistoryNode(K&& k, V&& v, std::size_t c) - : key(std::forward(k)) - , value(std::forward(v)) - , count(c) - {} - }; - - using ListType = std::list; - using ListIter = typename ListType::iterator; - using MapType = std::unordered_map; - -public: - explicit KHistoryShard(std::size_t capacity) - : capacity_(capacity) - { - historyMap_.reserve(capacity_); - } - - KHistoryShard(const KHistoryShard&) = delete; - KHistoryShard& operator=(const KHistoryShard&) = delete; - - KHistoryShard(KHistoryShard&&) = delete; - KHistoryShard& operator=(KHistoryShard&&) = delete; - - KHistoryAccessResult get(const Key& key, Value& value, std::size_t k) - { - std::lock_guard lock(mutex_); - - auto it = historyMap_.find(key); - if (it == historyMap_.end()) { - return KHistoryAccessResult::Miss; - } - - HistoryNode& node = *(it->second); - - ++node.count; - value = node.value; - - if (node.count >= k) { - historyList_.erase(it->second); - historyMap_.erase(it); - return KHistoryAccessResult::Promoted; - } - - historyList_.splice(historyList_.begin(), historyList_, it->second); - return KHistoryAccessResult::HitHistory; - } - - template - std::optional put(const Key& key, V&& value, std::size_t k) - { - if (capacity_ == 0) { - return std::nullopt; - } - - std::lock_guard lock(mutex_); - - auto it = historyMap_.find(key); - if (it != historyMap_.end()) { - HistoryNode& node = *(it->second); - - node.value = std::forward(value); - ++node.count; - - if (node.count >= k) { - std::optional promotedValue = node.value; - - historyList_.erase(it->second); - historyMap_.erase(it); - - return promotedValue; - } - - historyList_.splice(historyList_.begin(), historyList_, it->second); - return std::nullopt; - } - - if (historyList_.size() >= capacity_) { - auto& backNode = historyList_.back(); - historyMap_.erase(backNode.key); - historyList_.pop_back(); - } - - historyList_.emplace_front(key, std::forward(value), 1); - - try { - historyMap_.emplace(historyList_.front().key, historyList_.begin()); - } catch (...) { - historyList_.pop_front(); - throw; - } - - return std::nullopt; - } - - bool remove(const Key& key) - { - std::lock_guard lock(mutex_); - - auto it = historyMap_.find(key); - if (it == historyMap_.end()) { - return false; - } - - historyList_.erase(it->second); - historyMap_.erase(it); - - return true; - } - - void clear() - { - std::lock_guard lock(mutex_); - historyList_.clear(); - historyMap_.clear(); - } - - std::size_t size() const - { - std::lock_guard lock(mutex_); - return historyList_.size(); - } - -private: - const std::size_t capacity_; - - ListType historyList_; - MapType historyMap_; - - mutable std::mutex mutex_; -}; - - -// ============================================================================ -// 4. 分片 LRU-K 历史缓存 -// ============================================================================ -template< - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to -> -class KShardedHistoryCache -{ -public: - explicit KShardedHistoryCache(std::size_t totalCapacity, int shardNum = 0) - : shardNum_(normalizeShardNum(shardNum)) - { - const std::size_t shardCapacity = ceilDiv(totalCapacity, shardNum_); - - shards_.reserve(shardNum_); - - for (std::size_t i = 0; i < shardNum_; ++i) { - shards_.emplace_back( - std::make_unique(shardCapacity) - ); - } - } - - KHistoryAccessResult get(const Key& key, Value& value, std::size_t k) - { - return shards_[getShardIndex(key)]->get(key, value, k); - } - - template - std::optional put(const Key& key, V&& value, std::size_t k) - { - return shards_[getShardIndex(key)]->put( - key, - std::forward(value), - k - ); - } - - bool remove(const Key& key) - { - return shards_[getShardIndex(key)]->remove(key); - } - - void clear() - { - for (auto& shard : shards_) { - shard->clear(); - } - } - - std::size_t size() const - { - std::size_t total = 0; - - for (const auto& shard : shards_) { - total += shard->size(); - } - - return total; - } - -private: - using ShardType = KHistoryShard; - - static std::size_t normalizeShardNum(int shardNum) - { - if (shardNum > 0) { - return static_cast(shardNum); - } - - const unsigned int hardwareNum = std::thread::hardware_concurrency(); - - if (hardwareNum == 0) { - return 1; - } - - return static_cast(hardwareNum); - } - - static std::size_t ceilDiv(std::size_t a, std::size_t b) - { - return (a + b - 1) / b; - } - - std::size_t getShardIndex(const Key& key) const - { - return hash_(key) % shardNum_; - } - -private: - const std::size_t shardNum_; - - Hash hash_; - - std::vector> shards_; -}; - - -// ============================================================================ -// 5. 升级版 LRU-K 缓存 -// 设计目标: -// 1. 主缓存只放热点数据 -// 2. 历史区记录未达到 K 次访问的数据 -// 3. get / put 都参与访问计数 -// 4. count 和 value 在同一个历史节点中维护,避免跨容器不一致 -// ============================================================================ -template< - typename Key, - typename Value, - typename Hash = std::hash, - typename KeyEqual = std::equal_to -> -class KLruKCache -{ -public: - KLruKCache( - std::size_t capacity, - std::size_t historyCapacity, - std::size_t k, - int shardNum = 0 - ) - : k_(checkK(k)) - , mainCache_(capacity, shardNum) - , historyCache_(historyCapacity, shardNum) - { - if (k_ > 1 && historyCapacity == 0) { - throw std::invalid_argument( - "historyCapacity must be greater than 0 when k > 1" - ); - } - } - - KLruKCache(const KLruKCache&) = delete; - KLruKCache& operator=(const KLruKCache&) = delete; - - bool get(const Key& key, Value& value) - { - if (mainCache_.get(key, value)) { - return true; - } - - if (k_ == 1) { - return false; - } - - const KHistoryAccessResult result = historyCache_.get(key, value, k_); - - if (result == KHistoryAccessResult::Miss) { - return false; - } - - if (result == KHistoryAccessResult::Promoted) { - mainCache_.put(key, value); - } - - return true; - } - - template - void put(const Key& key, V&& value) - { - if (mainCache_.updateIfExists(key, value)) { - return; - } - - if (k_ == 1) { - mainCache_.put(key, std::forward(value)); - return; - } - - std::optional promotedValue = - historyCache_.put(key, std::forward(value), k_); - - if (promotedValue.has_value()) { - mainCache_.put(key, std::move(*promotedValue)); - } - } - - bool remove(const Key& key) - { - const bool removedFromMain = mainCache_.remove(key); - const bool removedFromHistory = historyCache_.remove(key); - - return removedFromMain || removedFromHistory; - } - - void clear() - { - mainCache_.clear(); - historyCache_.clear(); - } - - std::size_t mainSize() const - { - return mainCache_.size(); - } - - std::size_t historySize() const - { - return historyCache_.size(); - } - - std::size_t k() const noexcept - { - return k_; - } - -private: - static std::size_t checkK(std::size_t k) - { - if (k == 0) { - throw std::invalid_argument("k must be greater than 0"); - } - - return k; - } - -private: - const std::size_t k_; - - KHighConcurrencyCache mainCache_; - KShardedHistoryCache historyCache_; -}; - -} // namespace KamaCache \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..36abd0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + + steps: + - uses: actions/checkout@v4 + + - name: Configure + env: + CXX: ${{ matrix.compiler == 'gcc' && 'g++' || 'clang++' }} + run: > + cmake -S . -B build + -DCMAKE_BUILD_TYPE=Debug + -DCONCURRENT_CACHE_ENABLE_SANITIZERS=ON + + - name: Build + run: cmake --build build --parallel 2 + + - name: Test + run: ctest --test-dir build --output-on-failure + + - name: Smoke benchmark + run: ./run_cache_benchmark.sh . 10000 256 2 2 diff --git a/.gitignore b/.gitignore index 9b1646e..38503cb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ # C/C++ build files build/ +build-*/ cmake-build-*/ +.cache_benchmark_build/ *.o *.out *.a diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..415aba0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.16) + +project(ConcurrentCacheSystem VERSION 0.1.0 LANGUAGES CXX) + +option(CONCURRENT_CACHE_BUILD_TESTS "Build the test executable" ON) +option(CONCURRENT_CACHE_ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) + +add_library(concurrent_cache INTERFACE) +add_library(ConcurrentCacheSystem::concurrent_cache ALIAS concurrent_cache) + +target_include_directories( + concurrent_cache + INTERFACE + $ + $ +) +target_compile_features(concurrent_cache INTERFACE cxx_std_17) + +if(CONCURRENT_CACHE_BUILD_TESTS) + enable_testing() + find_package(Threads REQUIRED) + + add_executable(cache_tests tests/cache_tests.cpp) + target_link_libraries(cache_tests PRIVATE concurrent_cache Threads::Threads) + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(cache_tests PRIVATE -Wall -Wextra -Wpedantic -Werror) + endif() + + if(CONCURRENT_CACHE_ENABLE_SANITIZERS) + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + message(FATAL_ERROR "Sanitizers require Clang or GCC") + endif() + target_compile_options( + cache_tests PRIVATE + -fsanitize=address,undefined + -fno-omit-frame-pointer + ) + target_link_options( + cache_tests PRIVATE + -fsanitize=address,undefined + -fno-omit-frame-pointer + ) + endif() + + add_test(NAME cache_tests COMMAND cache_tests) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..37ccf4f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 FILWYZ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LRU.h b/LRU.h index 5ecea93..752f5e9 100644 --- a/LRU.h +++ b/LRU.h @@ -1,6 +1,9 @@ #pragma once +#include +#include #include +#include #include #include #include @@ -12,7 +15,9 @@ #include #include -namespace Cachae +#include "CachePolicy.h" + +namespace Cache { // ============================================================================ @@ -98,7 +103,7 @@ class alignas(64) LruCacheShard } template - bool updateifExists(K&& key, V&& value) + bool updateIfExists(K&& key, V&& value) { std::lock_guard lock(mutex_); @@ -112,10 +117,10 @@ class alignas(64) LruCacheShard } template - void put(K&& key, V&& value) + CacheWriteResult put(K&& key, V&& value) { if (capacity_ == 0) { - return; + return CacheWriteResult::ignored; } std::lock_guard lock(mutex_); @@ -124,10 +129,11 @@ class alignas(64) LruCacheShard if (it != cacheMap_.end()) { it->second->value = std::forward(value); cacheList_.splice(cacheList_.begin(), cacheList_, it->second); - return; + return CacheWriteResult::updated; } - if (cacheMap_.size() >= capacity_) { + const bool needsEviction = cacheMap_.size() >= capacity_; + if (needsEviction) { auto& backNode = cacheList_.back(); cacheMap_.erase(backNode.key); cacheList_.pop_back(); @@ -141,6 +147,10 @@ class alignas(64) LruCacheShard cacheList_.pop_front(); throw; } + + return needsEviction + ? CacheWriteResult::insertedWithEviction + : CacheWriteResult::inserted; } bool remove(const Key& key) @@ -176,6 +186,12 @@ class alignas(64) LruCacheShard { return capacity_; } + + bool contains(const Key& key) const + { + std::lock_guard lock(mutex_); + return cacheMap_.find(key) != cacheMap_.end(); + } }; @@ -190,33 +206,31 @@ template< typename Hash = std::hash, typename KeyEqual = std::equal_to > -class HighConcurrencyCache +class HighConcurrencyCache final : public CachePolicy { private: using ShardType = LruCacheShard; + const std::size_t capacity_; const std::size_t shardNum_; Hash hash_; std::vector> shards_; + std::atomic hits_{0}; + std::atomic misses_{0}; + std::atomic evictions_{0}; private: - static std::size_t normalizeShardNum(int shardNum) + static std::size_t normalizeShardNum(std::size_t capacity, int shardNum) { + std::size_t normalized = 0; if (shardNum > 0) { - return static_cast(shardNum); - } - - const unsigned int hardwareNum = std::thread::hardware_concurrency(); - if (hardwareNum == 0) { - return 1; + normalized = static_cast(shardNum); + } else { + normalized = static_cast(std::thread::hardware_concurrency()); } - return static_cast(hardwareNum); - } - - static std::size_t ceilDiv(std::size_t a, std::size_t b) - { - return (a + b - 1) / b; + normalized = std::max(1, normalized); + return capacity == 0 ? 1 : std::min(capacity, normalized); } std::size_t getShardIndex(const Key& key) const @@ -226,14 +240,18 @@ class HighConcurrencyCache public: explicit HighConcurrencyCache(std::size_t totalCapacity, int shardNum = 0) - : shardNum_(normalizeShardNum(shardNum)) + : capacity_(totalCapacity) + , shardNum_(normalizeShardNum(totalCapacity, shardNum)) { - const std::size_t shardCapacity = ceilDiv(totalCapacity, shardNum_); shards_.reserve(shardNum_); + const std::size_t baseCapacity = capacity_ / shardNum_; + const std::size_t extraCapacity = capacity_ % shardNum_; for (std::size_t i = 0; i < shardNum_; ++i) { shards_.emplace_back( - std::make_unique(shardCapacity) + std::make_unique( + baseCapacity + (i < extraCapacity ? 1U : 0U) + ) ); } } @@ -243,44 +261,64 @@ class HighConcurrencyCache HighConcurrencyCache(HighConcurrencyCache&&) = default; HighConcurrencyCache& operator=(HighConcurrencyCache&&) = default; + void put(const Key& key, const Value& value) override + { + putImpl(key, value); + } + template void put(K&& key, V&& value) { - // 先通过 key 引用计算出 index,再完美转发,避免生命周期提前结束 - const std::size_t index = getShardIndex(key); - shards_[index]->put(std::forward(key), std::forward(value)); + putImpl(std::forward(key), std::forward(value)); } - bool get(const Key& key, Value& value) + bool get(const Key& key, Value& value) override { - return shards_[getShardIndex(key)]->get(key, value); + if (shards_[getShardIndex(key)]->get(key, value)) { + hits_.fetch_add(1, std::memory_order_relaxed); + return true; + } + misses_.fetch_add(1, std::memory_order_relaxed); + return false; } - bool peek(const Key& key, Value& value) const + using CachePolicy::get; + + bool peek(const Key& key, Value& value) const override { return shards_[getShardIndex(key)]->peek(key, value); } template - bool updateifExists(K&& key, V&& value) + bool updateIfExists(K&& key, V&& value) { const std::size_t index = getShardIndex(key); - return shards_[index]->updateifExists(std::forward(key), std::forward(value)); + return shards_[index]->updateIfExists(std::forward(key), std::forward(value)); } - bool remove(const Key& key) + bool erase(const Key& key) override { return shards_[getShardIndex(key)]->remove(key); } - void clear() + bool remove(const Key& key) + { + return erase(key); + } + + void clear() override { for (auto& shard : shards_) { shard->clear(); } } - std::size_t size() const + bool contains(const Key& key) const override + { + return shards_[getShardIndex(key)]->contains(key); + } + + std::size_t size() const override { std::size_t total = 0; for (const auto& shard : shards_) { @@ -293,6 +331,39 @@ class HighConcurrencyCache { return shardNum_; } + + std::size_t capacity() const noexcept override + { + return capacity_; + } + + CacheStats stats() const noexcept override + { + return CacheStats{ + hits_.load(std::memory_order_relaxed), + misses_.load(std::memory_order_relaxed), + evictions_.load(std::memory_order_relaxed), + }; + } + + void resetStats() noexcept override + { + hits_.store(0, std::memory_order_relaxed); + misses_.store(0, std::memory_order_relaxed); + evictions_.store(0, std::memory_order_relaxed); + } + +private: + template + void putImpl(K&& key, V&& value) + { + const std::size_t index = getShardIndex(key); + if (shards_[index]->put( + std::forward(key), + std::forward(value)) == CacheWriteResult::insertedWithEviction) { + evictions_.fetch_add(1, std::memory_order_relaxed); + } + } }; @@ -622,8 +693,7 @@ class LruKCache template void put(K&& key, V&& value) { - // 关键修复:方法名大小写统一调整为 updateifExists,并修正生命周期引用 - if (mainCache_.updateifExists(key, value)) { + if (mainCache_.updateIfExists(key, value)) { return; } @@ -670,4 +740,4 @@ class LruKCache } }; -} // namespace Cachae \ No newline at end of file +} // namespace Cache diff --git a/README.md b/README.md index 497ce0a..b4a876b 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,141 @@ -# High-Performance Multi-Policy Concurrent Cache System -# 工业级高性能多策略并发缓存系统 (C++17) +# ConcurrentCacheSystem -本项目是一个基于 C++17 实现的高性能、线程安全、低锁内聚的生产级内存缓存库。系统不仅实现了传统缓存淘汰算法,更针对现代多核高并发场景进行了深度的**分片锁优化(Lock Sharding)**,并对经典算法在生产环境中的缺陷(如缓存污染、历史热点退化等)引入了**LRU-K 动态追溯**、**LFU 延迟老化(Lazy Ageing)**以及**ARC 自适应平衡**等大厂级硬核改进。 +[![CI](https://github.com/FILWYZ/ConcurrentCacheSystem/actions/workflows/ci.yml/badge.svg)](https://github.com/FILWYZ/ConcurrentCacheSystem/actions/workflows/ci.yml) +[![C++17](https://img.shields.io/badge/C%2B%2B-17-blue.svg)](https://en.cppreference.com/w/cpp/17) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -全库采用 Header-only 架构设计,无第三方依赖,开箱即用,具备极致的编译期优化性能。 +一个无第三方运行时依赖的 C++17 header-only 并发缓存实验项目。它实现了分片 +LRU、LRU-K、带延迟老化的 LFU 与 ARC,并提供统一接口、线程安全访问、原子统计 +和可复现的基准测试。 ---- +> 项目定位:用于学习并验证缓存淘汰策略、锁分片和并发工程实践。当前版本不是 +> 经过生产环境长期验证的通用缓存组件。 -## 🚀 核心架构与技术亮点 +## 特性 -### 1. 高并发分片无锁/低锁设计 (Lock Sharding) -* **痛点**:传统缓存使用全局单锁(如 `std::mutex`),在多线程高并发读写时,锁竞争(Lock Contention)会导致严重的 CPU 上下文切换泥潭,吞吐量断层式下跌。 -* **解耦方案**:系统引入 `ShardedCache` 封装层,通过高性能哈希函数将 Key 路由至不同的独立分片(Shard)。每个分片拥有独占的线程锁,将全局锁竞争稀释至 $1/N$($N$ 为分片数),大幅提升多核并行的物理吞吐极限(Mops/s)。 -* **伪共享防御**:核心单分片类采用 `alignas(64)` 进行**缓存行对齐**,彻底消灭多核多线程在频繁修改锁状态时的**伪共享 (False Sharing)** 硬件级性能陷阱。 +| 策略 | 实现 | 主要用途 | +| --- | --- | --- | +| LRU | `Cache::HighConcurrencyCache` | 通用、访问局部性较强的负载 | +| LRU-K | `Cache::LruKCache` | 降低一次性扫描造成的缓存污染 | +| LFU | `Cache::ShardedLFUCache` | 热点访问频率相对稳定的负载 | +| ARC | `Cache::ShardedARCCache` | 在近期性与访问频率之间自适应 | -### 2. 生产级算法矩阵与硬核改进 -本项目拒绝学院派的简单实现,完全对齐大厂核心基础库的鲁棒性标准: -* **LRU (Least Recently Used)**:标准双向链表 + 哈希表组合,实现严格 $O(1)$ 的查找、提权与淘汰。 -* **LRU-K (抗缓存污染)**:引入历史访问队列与多级晋升机制。通过追溯 Key 的第 $K$ 次访问时间(默认 $K=2$),精准过滤突发性的“全表扫描/冷数据涌入”,彻底解决传统 LRU 的**缓存污染**痛点。 -* **LFU + Lazy Ageing (时效自适应)**:针对 LFU 长期运行下“早期极热数据在后期转冷,但因频次极高无法被淘汰”的**历史残留毒瘤缺陷**,引入了高效的**延迟老化机制**。在单分片内以 $O(1)$ 的时间复杂度平摊衰减历史频次,使其快速适应业务流量波峰的动态切换。 -* **ARC (Adaptive Replacement Cache)**:动态自适应缓存。内部维持 `T1` (近期访问)、`T2` (频繁访问) 两个真实数据队列,以及 `B1`、`B2` 两个**幽灵历史队列 (Ghost List)**。通过反馈控制原理,在运行时无需人工调优即可自适应业务冷热突发波形。 +- 锁分片降低不同 key 之间的锁竞争。 +- `CachePolicy` 提供统一的 `put/get/peek/erase/contains` 接口。 +- 命中、未命中和淘汰计数使用 relaxed atomic,不占用分片锁。 +- CMake、CTest、AddressSanitizer、UndefinedBehaviorSanitizer 和 GitHub Actions。 +- 基准脚本包含热点读、混合读写、顺序扫描和写密集四类负载。 -### 3. 指标向上提权与 Lock-Free 监控 -* 为防止监控打点(吞吐量、命中率、淘汰数)串行访问各分片引发**级联死锁**,系统采用“指标向上提权、全局共享”设计。 -* 外部监控接口 `stats()` 采用 `std::atomic` 和 `std::memory_order_relaxed` 读写,保证监控统计具备 **Lock-Free(无锁)** 的极致非阻塞性能。 +## 快速开始 ---- - -## 🛠️ API 核心使用范例 - -### 1. 基础 LRU 缓存使用 ```cpp #include "LRU.h" -#include + #include +#include + +int main() +{ + Cache::HighConcurrencyCache cache(1024, 8); + cache.put(42, "answer"); -int main() { - // 创建一个容量为 1000 的标准单线程安全 LRU 缓存 - Cache::HighConcurrencyCache cache(1000, 1); - - // 写入 - cache.put(42, "Deep Answer"); - - // 读取 std::string value; if (cache.get(42, value)) { - std::cout << "Hit! Value: " << value << std::endl; + std::cout << value << '\n'; } - return 0; -} \ No newline at end of file + + const Cache::CacheStats stats = cache.stats(); + std::cout << "hit rate: " << stats.hitRate() << '\n'; +} +``` + +直接使用时,将所需头文件和 `CachePolicy.h` 加入 include path 即可。 + +## 构建与测试 + +要求:CMake 3.16+,支持 C++17 的 Clang 或 GCC。 + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +启用 Sanitizer: + +```bash +cmake -S . -B build-sanitized \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCONCURRENT_CACHE_ENABLE_SANITIZERS=ON +cmake --build build-sanitized --parallel +ctest --test-dir build-sanitized --output-on-failure +``` + +## 运行基准 + +```bash +./run_cache_benchmark.sh . 1000000 16384 8 8 +``` + +参数依次为:头文件目录、每个场景的操作数、缓存容量、线程数、分片数。结果会输出 +耗时、吞吐量、命中率、写入数、最终大小和已知淘汰数。 + +也可以使用 Docker: + +```bash +docker compose up --build +``` + +基准结果与 CPU、编译器、线程数、数据分布密切相关。请在固定环境中多次运行,并 +使用中位数比较;不要把单次微基准结果直接等同于生产性能。 + +## 设计概览 + +```text +key + └─ hash(key) % shard_count + └─ shard mutex + ├─ policy metadata + └─ key/value storage + +global stats + └─ relaxed atomics (hits / misses / evictions) +``` + +每个 key 始终路由到固定分片。不同分片可以并行执行,同一分片内由互斥锁保护策略 +元数据。`size()` 等跨分片操作是逐分片快照,不保证与并发写入构成全局线性一致的 +瞬时视图。 + +## 复杂度 + +| 操作 | LRU | LRU-K | LFU | ARC | +| --- | --- | --- | --- | --- | +| `get` | 平均 O(1) | 平均 O(1) | O(log F) | 平均 O(1) | +| `put` | 平均 O(1) | 平均 O(1) | O(log F) | 平均 O(1) | +| 空间 | O(C) | O(C + H) | O(C) | O(C) | + +`C` 是缓存容量,`H` 是 LRU-K 历史区容量,`F` 是当前频率桶数量。 + +## 项目结构 + +```text +. +├── CachePolicy.h +├── LRU.h +├── LFU.h +├── ARC.h +├── tests/cache_tests.cpp +├── run_cache_benchmark.sh +└── .github/workflows/ci.yml +``` + +## 后续计划 + +- 增加 ThreadSanitizer 独立任务和长时间并发压力测试。 +- 增加固定硬件环境下的基准历史与性能回归阈值。 +- 补充自定义 allocator、TTL 和容量动态调整实验。 +- 对比 oneTBB、Folly 等成熟实现,明确适用边界。 + +## License + +[MIT](LICENSE) diff --git a/tests/cache_tests.cpp b/tests/cache_tests.cpp new file mode 100644 index 0000000..c91af33 --- /dev/null +++ b/tests/cache_tests.cpp @@ -0,0 +1,136 @@ +#include "ARC.h" +#include "LFU.h" +#include "LRU.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + throw std::runtime_error( \ + std::string("check failed: ") + #condition); \ + } \ + } while (false) + +void testLruEvictionAndStats() +{ + Cache::HighConcurrencyCache cache(2, 1); + cache.put(1, 10); + cache.put(2, 20); + + int value = 0; + CHECK(cache.get(1, value)); + CHECK(value == 10); + cache.put(3, 30); + + CHECK(!cache.contains(2)); + CHECK(cache.contains(1)); + CHECK(cache.contains(3)); + CHECK(!cache.get(99, value)); + + const auto stats = cache.stats(); + CHECK(stats.hits == 1); + CHECK(stats.misses == 1); + CHECK(stats.evictions == 1); + CHECK(cache.capacity() == 2); + CHECK(cache.size() == 2); +} + +void testExactCapacityAcrossShards() +{ + Cache::HighConcurrencyCache cache(3, 16); + CHECK(cache.shardNum() == 3); + + for (int key = 0; key < 30; ++key) { + cache.put(key, key); + } + CHECK(cache.size() <= cache.capacity()); +} + +void testLruKPromotion() +{ + Cache::LruKCache cache(2, 4, 2, 1); + cache.put(7, 70); + CHECK(cache.historySize() == 1); + cache.put(7, 71); + CHECK(cache.historySize() == 0); + CHECK(cache.mainSize() == 1); + + int value = 0; + CHECK(cache.get(7, value)); + CHECK(value == 71); +} + +template +void testBasicPolicy(CacheType& cache) +{ + cache.put(1, 10); + cache.put(2, 20); + + int value = 0; + CHECK(cache.get(1, value)); + CHECK(value == 10); + CHECK(cache.peek(2, value)); + CHECK(value == 20); + CHECK(cache.erase(2)); + CHECK(!cache.contains(2)); +} + +void testOtherPolicies() +{ + Cache::ShardedLFUCache lfu(4, 2); + Cache::ShardedARCCache arc(4, 2); + testBasicPolicy(lfu); + testBasicPolicy(arc); +} + +void testConcurrentAccess() +{ + constexpr std::size_t threadCount = 8; + constexpr std::size_t keysPerThread = 500; + Cache::HighConcurrencyCache cache( + threadCount * keysPerThread, + static_cast(threadCount)); + + std::vector workers; + workers.reserve(threadCount); + for (std::size_t thread = 0; thread < threadCount; ++thread) { + workers.emplace_back([&, thread]() { + const std::size_t begin = thread * keysPerThread; + const std::size_t end = begin + keysPerThread; + for (std::size_t key = begin; key < end; ++key) { + cache.put(key, key * 2); + } + }); + } + for (auto& worker : workers) { + worker.join(); + } + + CHECK(cache.size() == threadCount * keysPerThread); + for (std::size_t key = 0; key < threadCount * keysPerThread; ++key) { + std::size_t value = 0; + CHECK(cache.get(key, value)); + CHECK(value == key * 2); + } +} + +} // namespace + +int main() +{ + testLruEvictionAndStats(); + testExactCapacityAcrossShards(); + testLruKPromotion(); + testOtherPolicies(); + testConcurrentAccess(); + std::cout << "All cache tests passed\n"; +}