#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2023 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License #ifndef NUCLEX_SUPPORT_COLLECTIONS_SEQUENTIALSLOTCACHE_H #define NUCLEX_SUPPORT_COLLECTIONS_SEQUENTIALSLOTCACHE_H #include "Nuclex/Support/Config.h" #include "Nuclex/Support/Collections/Cache.h" // for Cache #include "Nuclex/Support/Errors/KeyNotFoundError.h" // for KeyNotFoundError #include // for std::uint8_t namespace Nuclex { namespace Support { namespace Collections { // ------------------------------------------------------------------------------------------- // /// Caches items that can be addressed through a linear, zero-based index /// Type of the key the cache uses, must be an integer /// Type of values that are stored in the cache /// /// /// This type of cache is ideal if you have a fixed number of items (for example, /// files in a directory or frames in a video) that can be addressed through a simple /// integer index. /// /// /// /// /// template class SequentialSlotCache : public Cache { /// Initializes a new slot cache with the specified number of slots /// Number of slots the cache will provide public: SequentialSlotCache(std::size_t slotCount); /// Frees all memory used by the sequential slot cache public: virtual ~SequentialSlotCache(); /// Stores a value in the map /// Key under which the value can be looked up later /// Value that will be stored under its key in the map /// /// True if the key did not exist before and was inserted, /// false if the key already existed and its value was replaced. /// public: bool Insert(const TKey &key, const TValue &value) override; /// Stores a value in the map if it doesn't exist yet /// Key under which the value can be looked up later /// Value that will be stored under its key in the map /// /// True if the key did not exist before and was inserted, /// false if the key already existed and left unchanged /// public: bool TryInsert(const TKey &key, const TValue &value) override; /// Returns the value of the specified element in the map /// Key of the element that will be looked up public: const TValue &Get(const TKey &key) const override; /// Tries to look up an element in the map /// Key of the element that will be looked up /// Will receive the value if the element was found /// /// True if an element was returned, false if the key didn't exist /// public: bool TryGet(const TKey &key, TValue &value) const override; /// Tries to take an element from the map (removing it) /// Key of the element that will be taken from the map /// Will receive the value taken from the map /// /// True if an element was found and removed from the map, false if the key didn't exist /// public: bool TryTake(const TKey &key, TValue &value) override; // CHECK: Could provide a TryTake() with output method to allow for move semantics. //public: virtual bool TryTake(const TKey &key, const std::function &valueCallback) = 0; /// Removes the specified element from the map if it exists /// Key of the element that will be removed if present /// True if the element was found and removed, false otherwise public: bool TryRemove(const TKey &key) override; /// Removes all items from the map public: void Clear() override; /// /// Evicts items from the cache until at most items remain /// /// Maximum number of items that will be left behind public: void EvictDownTo(std::size_t itemCount) override; /// Evicts items from the cache until reaching a user-defined criterion /// Callback that decides whether to evict an entry public: void EvictWhere( const Events::Delegate &policyCallback ) override; /// Counts the numebr of elements currently in the map /// /// The approximate number of elements that had been in the map during the call /// public: std::size_t Count() const override; /// Checks if the map is empty /// True if the map had been empty during the call public: bool IsEmpty() const override; //private: Cache(const Cache &) = delete; //private: Cache &operator =(const Cache &) = delete; #pragma region struct SlotState /// Status of a slot, including its place in the MRU list /// /// The slot index is not stored separately as it can easily be obtained via pointer /// arithmetic (since the states are all stored in a single linear array) /// private: struct SlotState { /// Whether this slot is occupied or empty public: bool IsOccupied; /// Link to the previous element in the MRU doubly linked list public: SlotState *LessRecentlyUsed; /// Link to the next element in the MRU doubly linked list public: SlotState *MoreRecentlyUsed; }; #pragma endregion // struct SlotState /// /// Moves the specified slot state to the top of the most recently used list /// /// Slot that will become the most recently used private: void makeMostRecentlyUsed(SlotState &slotState) const; // <- in mutable state /// Integrates the specified slot state into the most recently used list /// Slot state that will be integratedf into the MRU list private: void linkMostRecentlyUsed(SlotState &slotState) const; // <- in mutable state /// Removes the specified slot state from the most recently used list /// Slot state that will be removed from the MRU list private: void unlinkMostRecentlyUsed(SlotState &slotState) const; // <- in mutable state /// /// Calculates the amount of memory needed for buffer holding both the slot states /// and the values that can be stored in the cache /// /// Number of slots for which the memory is calculated /// The required memory to store slots and values with alignment private: constexpr static std::size_t getRequiredMemory(std::size_t slotCount) { return ( (sizeof(SlotState[2]) * slotCount / 2) + (sizeof(TValue[2]) * slotCount / 2) + (alignof(SlotState) - 1) + // for initial alignment padding if needed ( alignof(TValue) > alignof(SlotState) ? (alignof(TValue) - alignof(SlotState)) : 0 ) ); } /// Number of slots currently filled in the cache private: std::size_t count; /// Memory allocated to store the slot states and values private: std::uint8_t *memory; /// Values stored in each of the slots private: TValue *values; /// Keeps track of the state of each individual slot private: mutable SlotState *states; /// Pointer to the state of the most recently used slot private: mutable SlotState *mostRecentlyUsed; /// Pointer to the state of the least recently used slot private: mutable SlotState *leastRecentlyUsed; }; // ------------------------------------------------------------------------------------------- // template SequentialSlotCache::SequentialSlotCache(std::size_t slotCount) : count(0), memory(new std::uint8_t[getRequiredMemory(slotCount)]), values(), states(), mostRecentlyUsed(nullptr), leastRecentlyUsed(nullptr) { // Calculate the aligned memory address where slot states will be stored { std::size_t misalignment = ( reinterpret_cast(this->memory) % alignof(SlotState) ); if(misalignment > 0) { this->states = reinterpret_cast( this->memory + alignof(SlotState) - misalignment ); } else { this->states = reinterpret_cast(this->memory); } } // Place the values directly behind the slot state array, with alignment padding // if the type of values used has greater alignment requirements this->values = reinterpret_cast( reinterpret_cast(this->states) + (sizeof(SlotState[2]) * slotCount / 2) + ( alignof(TValue) > alignof(SlotState) ? (alignof(TValue) - alignof(SlotState)) : 0 ) ); // Initialize all IsOccupied values to false so we don't accidentally try to destroy // values that weren't present (but where the uninitialized memory in which we built // the slot state array happened to have the appropriate bits set to make IsOccupied true). for(std::size_t index = 0; index < slotCount; ++index) { this->states[index].IsOccupied = false; } } // ------------------------------------------------------------------------------------------- // template SequentialSlotCache::~SequentialSlotCache() { Clear(); delete[] this->memory; } // ------------------------------------------------------------------------------------------- // template bool SequentialSlotCache::Insert(const TKey &key, const TValue &value) { SlotState &state = this->states[key]; if(state.IsOccupied) { TValue *address = this->values + key; address->~TValue(); new(address) TValue(value); makeMostRecentlyUsed(state); return false; } else { new(this->values + key) TValue(value); state.IsOccupied = true; ++this->count; linkMostRecentlyUsed(state); return true; } } // ------------------------------------------------------------------------------------------- // template bool SequentialSlotCache::TryInsert(const TKey &key, const TValue &value) { SlotState &state = this->states[key]; if(state.IsOccupied) { return false; } else { new(this->values + key) TValue(value); state.IsOccupied = true; ++this->count; linkMostRecentlyUsed(state); return true; } } // ------------------------------------------------------------------------------------------- // template const TValue &SequentialSlotCache::Get(const TKey &key) const { SlotState &state = this->states[key]; if(state.IsOccupied) { makeMostRecentlyUsed(state); return this->values[key]; } else { throw Errors::KeyNotFoundError(std::string(u8"Requested cache slot is empty", 29)); } } // ------------------------------------------------------------------------------------------- // template bool SequentialSlotCache::TryGet(const TKey &key, TValue &value) const { SlotState &state = this->states[key]; if(state.IsOccupied) { makeMostRecentlyUsed(state); value = this->values[key]; return true; } else { return false; } } // ------------------------------------------------------------------------------------------- // template bool SequentialSlotCache::TryTake(const TKey &key, TValue &value) { SlotState &state = this->states[key]; if(state.IsOccupied) { TValue *address = this->values + key; value = *address; address->~TValue(); state.IsOccupied = false; unlinkMostRecentlyUsed(state); --this->count; return true; } else { return false; } } // ------------------------------------------------------------------------------------------- // template bool SequentialSlotCache::TryRemove(const TKey &key) { SlotState &state = this->states[key]; if(state.IsOccupied) { TValue *address = this->values + key; address->~TValue(); state.IsOccupied = false; unlinkMostRecentlyUsed(state); --this->count; return true; } else { return false; } } // ------------------------------------------------------------------------------------------- // template void SequentialSlotCache::Clear() { SlotState *current = this->mostRecentlyUsed; while(current != nullptr) { std::ptrdiff_t index = current - this->states; this->values[index].~TValue(); current->IsOccupied = false; current = current->LessRecentlyUsed; } this->count = 0; this->leastRecentlyUsed = this->mostRecentlyUsed = nullptr; } // ------------------------------------------------------------------------------------------- // template void SequentialSlotCache::EvictDownTo(std::size_t itemCount) { SlotState *current = this->leastRecentlyUsed; while(current != nullptr) { if(itemCount >= this->count) { break; } std::ptrdiff_t index = current - this->states; this->values[index].~TValue(); current->IsOccupied = false; --this->count; current = current->MoreRecentlyUsed; } if(current == nullptr) { this->leastRecentlyUsed = this->mostRecentlyUsed = nullptr; } else { current->LessRecentlyUsed = nullptr; this->leastRecentlyUsed = current; } } // ------------------------------------------------------------------------------------------- // template void SequentialSlotCache::EvictWhere( const Events::Delegate &policyCallback ) { SlotState *current = this->leastRecentlyUsed; while(current != nullptr) { std::ptrdiff_t index = current - this->states; bool evict = policyCallback(this->values[index]); if(evict) { unlinkMostRecentlyUsed(*current); this->values[index].~TValue(); current->IsOccupied = false; --this->count; } current = current->MoreRecentlyUsed; } } // ------------------------------------------------------------------------------------------- // template std::size_t SequentialSlotCache::Count() const { return this->count; } // ------------------------------------------------------------------------------------------- // template bool SequentialSlotCache::IsEmpty() const { return (this->count == 0); } // ------------------------------------------------------------------------------------------- // template void SequentialSlotCache::makeMostRecentlyUsed(SlotState &slotState) const { // Only do something if the slot in question isn't already the most recent used one if(slotState.MoreRecentlyUsed != nullptr) { slotState.MoreRecentlyUsed->LessRecentlyUsed = slotState.LessRecentlyUsed; if(slotState.LessRecentlyUsed == nullptr) { this->leastRecentlyUsed = slotState.MoreRecentlyUsed; } else { slotState.LessRecentlyUsed->MoreRecentlyUsed = slotState.MoreRecentlyUsed; } slotState.LessRecentlyUsed = this->mostRecentlyUsed; slotState.MoreRecentlyUsed = nullptr; this->mostRecentlyUsed->MoreRecentlyUsed = &slotState; this->mostRecentlyUsed = &slotState; } } // ------------------------------------------------------------------------------------------- // template void SequentialSlotCache::linkMostRecentlyUsed(SlotState &slotState) const { if(this->mostRecentlyUsed == nullptr) { slotState.LessRecentlyUsed = slotState.MoreRecentlyUsed = nullptr; this->leastRecentlyUsed = this->mostRecentlyUsed = &slotState; } else { slotState.LessRecentlyUsed = this->mostRecentlyUsed; slotState.MoreRecentlyUsed = nullptr; this->mostRecentlyUsed->MoreRecentlyUsed = &slotState; this->mostRecentlyUsed = &slotState; } } // ------------------------------------------------------------------------------------------- // template void SequentialSlotCache::unlinkMostRecentlyUsed(SlotState &slotState) const { if(slotState.LessRecentlyUsed == nullptr) { this->leastRecentlyUsed = slotState.MoreRecentlyUsed; } else { slotState.LessRecentlyUsed->MoreRecentlyUsed = slotState.MoreRecentlyUsed; } if(slotState.MoreRecentlyUsed == nullptr) { this->mostRecentlyUsed = slotState.LessRecentlyUsed; } else { slotState.MoreRecentlyUsed->LessRecentlyUsed = slotState.LessRecentlyUsed; } } // ------------------------------------------------------------------------------------------- // }}} // namespace Nuclex::Support::Collections #endif // NUCLEX_SUPPORT_COLLECTIONS_SEQUENTIALSLOTCACHE_H