cachelib/allocator/MMTinyLFU.h (315 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <atomic> #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wconversion" #include <folly/Format.h> #include <folly/Math.h> #pragma GCC diagnostic pop #include "cachelib/allocator/Cache.h" #include "cachelib/allocator/CacheStats.h" #include "cachelib/allocator/Util.h" #include "cachelib/allocator/datastruct/MultiDList.h" #include "cachelib/allocator/memory/serialize/gen-cpp2/objects_types.h" #include "cachelib/common/CompilerUtils.h" #include "cachelib/common/CountMinSketch.h" #include "cachelib/common/Mutex.h" namespace facebook { namespace cachelib { // Implements the W-TinyLFU cache eviction policy as described in - // https://arxiv.org/pdf/1512.00727.pdf // // The cache is split into 2 parts, the main cache and the tiny cache. // The tiny cache is typically sized to be 1% of the total cache with // the main cache being the rest 99%. Both caches are implemented using // LRUs. New items land in tiny cache. During eviction, the tail item // from the tiny cache is promoted to main cache if its frequency is // higher than the tail item of of main cache, and the tail of main // cache is evicted. This gives the frequency based admission into main // cache. Hits in each cache simply move the item to the head of each // LRU cache. // The frequency counts are maintained in CountMinSketch approximate // counters - // Counter Overhead: // The windowToCacheSizeRatio determines the size of counters. The default // value is 32 which means the counting window size is 32 times the // cache size. After every 32 X cache_capacity number of items, the // counts are halved to weigh frequency by recency. The function // counterSize() returns the size of the counters // in bytes. See MMTinyLFU-inl.h::maybeGrowAccessCountersLocked() // implementation for how the size is computed. // // Tiny cache size: // This default to 1%. There's no need to tune this parameter. class MMTinyLFU { public: // unique identifier per MMType static const int kId; // forward declaration; template <typename T> using Hook = DListHook<T>; using SerializationType = serialization::MMTinyLFUObject; using SerializationConfigType = serialization::MMTinyLFUConfig; using SerializationTypeContainer = serialization::MMTinyLFUCollection; enum LruType { Main, Tiny, NumTypes }; // Config class for MMTinfyLFU struct Config { // create from serialized config explicit Config(SerializationConfigType configState) : Config(*configState.lruRefreshTime(), *configState.lruRefreshRatio(), *configState.updateOnWrite(), *configState.updateOnRead(), *configState.tryLockUpdate(), *configState.windowToCacheSizeRatio(), *configState.tinySizePercent()) {} // @param time the LRU refresh time in seconds. // An item will be promoted only once in each lru refresh // time depite the number of accesses it gets. // @param udpateOnW whether to promote the item on write // @param updateOnR whether to promote the item on read Config(uint32_t time, bool updateOnW, bool updateOnR) : Config(time, updateOnW, updateOnR, /* try lock update */ false, 16, 1) {} // @param time the LRU refresh time in seconds. // An item will be promoted only once in each lru // refresh time depite the number of accesses it // gets. // @param udpateOnW whether to promote the item on write // @param updateOnR whether to promote the item on read // @param windowToCacheSize multiplier of window size to cache size // @param tinySizePct percentage number of tiny size to overall size Config(uint32_t time, bool updateOnW, bool updateOnR, size_t windowToCacheSize, size_t tinySizePct) : Config(time, updateOnW, updateOnR, /* try lock update */ false, windowToCacheSize, tinySizePct) {} // @param time the LRU refresh time in seconds. // An item will be promoted only once in each lru // refresh time depite the number of accesses it // gets. // @param udpateOnW whether to promote the item on write // @param updateOnR whether to promote the item on read // @param tryLockU whether to use a try lock when doing update. // @param windowToCacheSize multiplier of window size to cache size // @param tinySizePct percentage number of tiny size to overall size Config(uint32_t time, bool updateOnW, bool updateOnR, bool tryLockU, size_t windowToCacheSize, size_t tinySizePct) : Config(time, 0., updateOnW, updateOnR, tryLockU, windowToCacheSize, tinySizePct) {} // @param time the LRU refresh time in seconds. // An item will be promoted only once in each // lru refresh time depite the number of // accesses it gets. // @param ratio the lru refresh ratio. The ratio times the // oldest element's lifetime in warm queue // would be the minimum value of LRU refresh // time. // @param udpateOnW whether to promote the item on write // @param updateOnR whether to promote the item on read // @param tryLockU whether to use a try lock when doing // update. // @param windowToCacheSize multiplier of window size to cache size // @param tinySizePct percentage number of tiny size to overall // size Config(uint32_t time, double ratio, bool updateOnW, bool updateOnR, bool tryLockU, size_t windowToCacheSize, size_t tinySizePct) : Config(time, ratio, updateOnW, updateOnR, tryLockU, windowToCacheSize, tinySizePct, 0) {} // @param time the LRU refresh time in seconds. // An item will be promoted only once in each // lru refresh time depite the number of // accesses it gets. // @param ratio the lru refresh ratio. The ratio times the // oldest element's lifetime in warm queue // would be the minimum value of LRU refresh // time. // @param udpateOnW whether to promote the item on write // @param updateOnR whether to promote the item on read // @param tryLockU whether to use a try lock when doing // update. // @param windowToCacheSize multiplier of window size to cache size // @param tinySizePct percentage number of tiny size to overall // size // @param mmReconfigureInterval Time interval for recalculating lru // refresh time according to the ratio. Config(uint32_t time, double ratio, bool updateOnW, bool updateOnR, bool tryLockU, size_t windowToCacheSize, size_t tinySizePct, uint32_t mmReconfigureInterval) : defaultLruRefreshTime(time), lruRefreshRatio(ratio), updateOnWrite(updateOnW), updateOnRead(updateOnR), tryLockUpdate(tryLockU), windowToCacheSizeRatio(windowToCacheSize), tinySizePercent(tinySizePct), mmReconfigureIntervalSecs( std::chrono::seconds(mmReconfigureInterval)) { checkConfig(); } Config() = default; Config(const Config& rhs) = default; Config(Config&& rhs) = default; Config& operator=(const Config& rhs) = default; Config& operator=(Config&& rhs) = default; void checkConfig() { if (tinySizePercent < 1 || tinySizePercent > 50) { throw std::invalid_argument( folly::sformat("Invalid tiny cache size {}. Tiny cache size " "must be between 1% and 50% of total cache size ", tinySizePercent)); } if (windowToCacheSizeRatio < 2 || windowToCacheSizeRatio > 128) { throw std::invalid_argument( folly::sformat("Invalid window to cache size ratio {}. The ratio " "must be between 2 and 128", windowToCacheSizeRatio)); } } template <typename... Args> void addExtraConfig(Args...) {} // threshold value in seconds to compare with a node's update time to // determine if we need to update the position of the node in the linked // list. By default this is 60s to reduce the contention on the lru lock. uint32_t defaultLruRefreshTime{60}; uint32_t lruRefreshTime{defaultLruRefreshTime}; // ratio of LRU refresh time to the tail age. If a refresh time computed // according to this ratio is larger than lruRefreshtime, we will adopt // this one instead of the lruRefreshTime set. double lruRefreshRatio{0.}; // whether the lru needs to be updated on writes for recordAccess. If // false, accessing the cache for writes does not promote the cached item // to the head of the lru. bool updateOnWrite{false}; // whether the lru needs to be updated on reads for recordAccess. If // false, accessing the cache for reads does not promote the cached item // to the head of the lru. bool updateOnRead{true}; // whether to tryLock or lock the lru lock when attempting promotion on // access. If set, and tryLock fails, access will not result in promotion. bool tryLockUpdate{false}; // The multiplier for window size given the cache size. size_t windowToCacheSizeRatio{32}; // The size of tiny cache, as a percentage of the total size. size_t tinySizePercent{1}; // Minimum interval between reconfigurations. If 0, reconfigure is never // called. std::chrono::seconds mmReconfigureIntervalSecs{}; }; // The container object which can be used to keep track of objects of type // T. T must have a public member of type Hook. This object is wrapper // around DList, is thread safe and can be accessed from multiple threads. // The current implementation models an LRU using the above DList // implementation. template <typename T, Hook<T> T::*HookPtr> struct Container { private: using LruList = MultiDList<T, HookPtr>; using Mutex = folly::SpinLock; using LockHolder = std::unique_lock<Mutex>; using PtrCompressor = typename T::PtrCompressor; using Time = typename Hook<T>::Time; using CompressedPtr = typename T::CompressedPtr; using RefFlags = typename T::Flags; public: Container() = default; Container(Config c, PtrCompressor compressor) : lru_(LruType::NumTypes, std::move(compressor)), config_(std::move(c)) { maybeGrowAccessCountersLocked(); lruRefreshTime_ = config_.lruRefreshTime; nextReconfigureTime_ = config_.mmReconfigureIntervalSecs.count() == 0 ? std::numeric_limits<Time>::max() : static_cast<Time>(util::getCurrentTimeSec()) + config_.mmReconfigureIntervalSecs.count(); } Container(serialization::MMTinyLFUObject object, PtrCompressor compressor); Container(const Container&) = delete; Container& operator=(const Container&) = delete; // records the information that the node was accessed. This could bump up // the node to the head of the lru depending on the time when the node was // last updated in lru and the kLruRefreshTime. If the node was moved to // the head in the lru, the node's updateTime will be updated // accordingly. // // @param node node that we want to mark as relevant/accessed // @param mode the mode for the access operation. // // @return True if the information is recorded and bumped the node // to the head of the lru, returns false otherwise bool recordAccess(T& node, AccessMode mode) noexcept; // adds the given node into the container and marks it as being present in // the container. The node is added to the head of the lru. // // @param node The node to be added to the container. // @return True if the node was successfully added to the container. False // if the node was already in the contianer. On error state of node // is unchanged. bool add(T& node) noexcept; // removes the node from the lru and sets it previous and next to nullptr. // // @param node The node to be removed from the container. // @return True if the node was successfully removed from the container. // False if the node was not part of the container. On error, the // state of node is unchanged. bool remove(T& node) noexcept; class Iterator; // same as the above but uses an iterator context. The iterator is updated // on removal of the corresponding node to point to the next node. The // iterator context holds the lock on the lru. // // iterator will be advanced to the next node after removing the node // // @param it Iterator that will be removed void remove(Iterator& it) noexcept; // replaces one node with another, at the same position // // @param oldNode node being replaced // @param newNode node to replace oldNode with // // @return true If the replace was successful. Returns false if the // destination node did not exist in the container, or if the // source node already existed. bool replace(T& oldNode, T& newNode) noexcept; // context for iterating the MM container. At any given point of time, // there can be only one iterator active since we need to lock the LRU for // iteration. we can support multiple iterators at same time, by using a // shared ptr in the context for the lock holder in the future. class Iterator { public: using ListIterator = typename LruList::DListIterator; // noncopyable but movable. Iterator(const Iterator&) = delete; Iterator& operator=(const Iterator&) = delete; Iterator(Iterator&&) noexcept = default; Iterator& operator++() noexcept { ++getIter(); return *this; } Iterator& operator--() { throw std::invalid_argument( "Decrementing eviction iterator is not supported"); } T* operator->() const noexcept { return getIter().operator->(); } T& operator*() const noexcept { return getIter().operator*(); } bool operator==(const Iterator& other) const noexcept { return &c_ == &other.c_ && tIter_ == other.tIter_ && mIter_ == other.mIter_; } bool operator!=(const Iterator& other) const noexcept { return !(*this == other); } explicit operator bool() const noexcept { return tIter_ || mIter_; } T* get() const noexcept { return getIter().get(); } // Invalidates this iterator void reset() noexcept { // Point iterator to first list's rend tIter_.reset(); mIter_.reset(); } // 1. Invalidate this iterator // 2. Unlock void destroy() { reset(); if (l_.owns_lock()) { l_.unlock(); } } // Reset this iterator to the beginning void resetToBegin() { if (!l_.owns_lock()) { l_.lock(); } tIter_.resetToBegin(); mIter_.resetToBegin(); } private: // private because it's easy to misuse and cause deadlock for MMTinyLFU Iterator& operator=(Iterator&&) noexcept = default; // create an lru iterator with the lock being held. explicit Iterator(LockHolder l, const Container<T, HookPtr>& c) noexcept; const ListIterator& getIter() const noexcept { return evictTiny() ? tIter_ : mIter_; } ListIterator& getIter() noexcept { return const_cast<ListIterator&>( static_cast<const Iterator*>(this)->getIter()); } bool evictTiny() const noexcept { if (!mIter_) { return true; } if (!tIter_) { return false; } // Since iterators don't change the state of the container, we evict // from tiny or main depending on whether the tiny node would be // admitted to main cache. If it would be, we evict from main cache, // otherwise tiny cache. return !c_.admitToMain(*tIter_, *mIter_); } // only the container can create iterators friend Container<T, HookPtr>; const Container<T, HookPtr>& c_; // Tiny and main cache iterators ListIterator tIter_; ListIterator mIter_; // lock protecting the validity of the iterator LockHolder l_; }; Config getConfig() const; void setConfig(const Config& newConfig); bool isEmpty() const noexcept { LockHolder l(lruMutex_); return lru_.size() == 0; } size_t size() const noexcept { LockHolder l(lruMutex_); return lru_.size(); } // reconfigure the MMContainer: update refresh time according to current // tail age void reconfigureLocked(const Time& currTime); size_t counterSize() const noexcept { LockHolder l(lruMutex_); return accessFreq_.getByteSize(); } // Returns the eviction age stats. See CacheStats.h for details EvictionAgeStat getEvictionAgeStat(uint64_t projectedLength) const noexcept; // Obtain an iterator that start from the tail and can be used // to search for evictions. This iterator holds a lock to this // container and only one such iterator can exist at a time Iterator getEvictionIterator() const noexcept; // for saving the state of the lru // // precondition: serialization must happen without any reader or writer // present. Any modification of this object afterwards will result in an // invalid, inconsistent state for the serialized data. // serialization::MMTinyLFUObject saveState() const noexcept; // return the stats for this container. MMContainerStat getStats() const noexcept; static LruType getLruType(const T& node) noexcept { return isTiny(node) ? LruType::Tiny : LruType::Main; } private: EvictionAgeStat getEvictionAgeStatLocked( uint64_t projectedLength) const noexcept; static Time getUpdateTime(const T& node) noexcept { return (node.*HookPtr).getUpdateTime(); } static void setUpdateTime(T& node, Time time) noexcept { (node.*HookPtr).setUpdateTime(time); } // As the cache grows, the frequency counters may need to grow. void maybeGrowAccessCountersLocked() noexcept; // Update frequency count for the node. Halve all counts if // we've reached the end of the window. void updateFrequenciesLocked(const T& node) noexcept; // Promote the tail of tiny cache to main cache if it has higher // frequency count than the tail of the main cache. void maybePromoteTailLocked() noexcept; // Returns the hash of node's key static size_t hashNode(const T& node) noexcept { return folly::hasher<folly::StringPiece>()(node.getKey()); } // Returns true if tiny node must be admitted to main cache since its // frequency is higher than that of the main node. bool admitToMain(const T& tinyNode, const T& mainNode) const noexcept { XDCHECK(isTiny(tinyNode)); XDCHECK(!isTiny(mainNode)); auto tinyFreq = accessFreq_.getCount(hashNode(tinyNode)); auto mainFreq = accessFreq_.getCount(hashNode(mainNode)); return tinyFreq >= mainFreq; } // remove node from lru and adjust insertion points // // @param node node to remove void removeLocked(T& node) noexcept; static bool isTiny(const T& node) noexcept { return node.template isFlagSet<RefFlags::kMMFlag0>(); } static bool isAccessed(const T& node) noexcept { return node.template isFlagSet<RefFlags::kMMFlag1>(); } // Bit MM_BIT_0 is used to record if the item is in tiny cache. static void markTiny(T& node) noexcept { node.template setFlag<RefFlags::kMMFlag0>(); } static void unmarkTiny(T& node) noexcept { node.template unSetFlag<RefFlags::kMMFlag0>(); } // Bit MM_BIT_1 is used to record if the item has been accessed since being // written in cache. Unaccessed items are ignored when determining projected // update time. static void markAccessed(T& node) noexcept { node.template setFlag<RefFlags::kMMFlag1>(); } static void unmarkAccessed(T& node) noexcept { node.template unSetFlag<RefFlags::kMMFlag1>(); } // Initial cache capacity estimate for count-min-sketch static constexpr size_t kDefaultCapacity = 100; // Number of hashes static constexpr size_t kHashCount = 4; // The error threshold for frequency calculation static constexpr size_t kErrorThreshold = 5; // decay rate for frequency static constexpr double kDecayFactor = 0.5; // protects all operations on the lru. We never really just read the state // of the LRU. Hence we dont really require a RW mutex at this point of // time. mutable Mutex lruMutex_; // the lru LruList lru_; // the window size counter size_t windowSize_{0}; // maximum value of window size which when hit the counters are halved size_t maxWindowSize_{0}; // The capacity for which the counters are sized size_t capacity_{0}; // The next time to reconfigure the container. std::atomic<Time> nextReconfigureTime_{}; // How often to promote an item in the eviction queue. std::atomic<uint32_t> lruRefreshTime_{}; // Max lruFreshTime. static constexpr uint32_t kLruRefreshTimeCap{900}; // Config for this lru. // Write access to the MMTinyLFU Config is serialized. // Reads may be racy. Config config_{}; // Approximate streaming frequency counters. The counts are halved every // time the maxWindowSize is hit. facebook::cachelib::util::CountMinSketch accessFreq_{}; FRIEND_TEST(MMTinyLFUTest, SegmentStress); FRIEND_TEST(MMTinyLFUTest, TinyLFUBasic); FRIEND_TEST(MMTinyLFUTest, Reconfigure); }; }; } // namespace cachelib } // namespace facebook #include "cachelib/allocator/MMTinyLFU-inl.h"