cachelib/allocator/MMLru.h (198 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>
#include <cstring>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"
#include <folly/Format.h>
#pragma GCC diagnostic pop
#include <folly/container/Array.h>
#include <folly/lang/Aligned.h>
#include <folly/synchronization/DistributedMutex.h>
#include "cachelib/allocator/Cache.h"
#include "cachelib/allocator/CacheStats.h"
#include "cachelib/allocator/Util.h"
#include "cachelib/allocator/datastruct/DList.h"
#include "cachelib/allocator/memory/serialize/gen-cpp2/objects_types.h"
#include "cachelib/common/CompilerUtils.h"
#include "cachelib/common/Mutex.h"
namespace facebook {
namespace cachelib {
// CacheLib's modified LRU policy.
// In classic LRU, the items form a queue according to the last access time.
// Items are inserted to the head of the queue and removed from the tail of the
// queue. Items accessed (used) are moved (promoted) to the head of the queue.
// CacheLib made two variations on top of the classic LRU:
// 1. Insertion point. The items are inserted into a configured insertion point
// instead of always to the head.
// 2. Delayed promotion. Items get promoted at most once in any lru refresh time
// window. lru refresh time and lru refresh ratio controls this internval
// length.
class MMLru {
public:
// unique identifier per MMType
static const int kId;
// forward declaration;
template <typename T>
using Hook = DListHook<T>;
using SerializationType = serialization::MMLruObject;
using SerializationConfigType = serialization::MMLruConfig;
using SerializationTypeContainer = serialization::MMLruCollection;
// This is not applicable for MMLru, just for compile of cache allocator
enum LruType { NumTypes };
// Config class for MMLru
struct Config {
// create from serialized config
explicit Config(SerializationConfigType configState)
: Config(*configState.lruRefreshTime(),
*configState.lruRefreshRatio(),
*configState.updateOnWrite(),
*configState.updateOnRead(),
*configState.tryLockUpdate(),
static_cast<uint8_t>(*configState.lruInsertionPointSpec())) {}
// @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, false, 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 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 ipSpec insertion point spec, which is the inverse power of
// length from the end of the queue. For example, value 1
// means the insertion point is 1/2 from the end of LRU;
// value 2 means 1/4 from the end of LRU.
Config(uint32_t time, bool updateOnW, bool updateOnR, uint8_t ipSpec)
: Config(time, updateOnW, updateOnR, false, ipSpec) {}
Config(uint32_t time,
bool updateOnW,
bool updateOnR,
bool tryLockU,
uint8_t ipSpec)
: Config(time, 0., updateOnW, updateOnR, tryLockU, ipSpec) {}
// @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 ipSpec insertion point spec, which is the inverse power of
// length from the end of the queue. For example, value 1
// means the insertion point is 1/2 from the end of LRU;
// value 2 means 1/4 from the end of LRU.
Config(uint32_t time,
double ratio,
bool updateOnW,
bool updateOnR,
bool tryLockU,
uint8_t ipSpec)
: Config(time, ratio, updateOnW, updateOnR, tryLockU, ipSpec, 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 ipSpec insertion point spec, which is the inverse power of
// length from the end of the queue. For example, value 1
// means the insertion point is 1/2 from the end of LRU;
// value 2 means 1/4 from the end of LRU.
// @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,
uint8_t ipSpec,
uint32_t mmReconfigureInterval)
: defaultLruRefreshTime(time),
lruRefreshRatio(ratio),
updateOnWrite(updateOnW),
updateOnRead(updateOnR),
tryLockUpdate(tryLockU),
lruInsertionPointSpec(ipSpec),
mmReconfigureIntervalSecs(
std::chrono::seconds(mmReconfigureInterval)) {}
Config() = default;
Config(const Config& rhs) = default;
Config(Config&& rhs) = default;
Config& operator=(const Config& rhs) = default;
Config& operator=(Config&& rhs) = default;
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};
// By default insertions happen at the head of the LRU. If we need
// insertions at the middle of lru we can adjust this to be a non-zero.
// Ex: lruInsertionPointSpec = 1, we insert at the middle (1/2 from end)
// lruInsertionPointSpec = 2, we insert at a point 1/4th from tail
uint8_t lruInsertionPointSpec{0};
// 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 = DList<T, HookPtr>;
using Mutex = folly::DistributedMutex;
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)
: compressor_(std::move(compressor)),
lru_(compressor_),
config_(std::move(c)) {
lruRefreshTime_ = config_.lruRefreshTime;
nextReconfigureTime_ =
config_.mmReconfigureIntervalSecs.count() == 0
? std::numeric_limits<Time>::max()
: static_cast<Time>(util::getCurrentTimeSec()) +
config_.mmReconfigureIntervalSecs.count();
}
Container(serialization::MMLruObject object, PtrCompressor compressor);
Container(const Container&) = delete;
Container& operator=(const Container&) = delete;
// 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 LruList::Iterator {
public:
// noncopyable but movable.
Iterator(const Iterator&) = delete;
Iterator& operator=(const Iterator&) = delete;
Iterator(Iterator&&) noexcept = default;
// 1. Invalidate this iterator
// 2. Unlock
void destroy() {
LruList::Iterator::reset();
if (l_.owns_lock()) {
l_.unlock();
}
}
// Reset this iterator to the beginning
void resetToBegin() {
if (!l_.owns_lock()) {
l_.lock();
}
LruList::Iterator::resetToBegin();
}
private:
// private because it's easy to misuse and cause deadlock for MMLru
Iterator& operator=(Iterator&&) noexcept = default;
// create an lru iterator with the lock being held.
Iterator(LockHolder l, const typename LruList::Iterator& iter) noexcept;
// only the container can create iterators
friend Container<T, HookPtr>;
// lock protecting the validity of the iterator
LockHolder l_;
};
// 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;
using Iterator = 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;
// 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;
// get copy of current config
Config getConfig() const;
// override the existing config with the new one.
void setConfig(const Config& newConfig);
bool isEmpty() const noexcept { return size() == 0; }
// reconfigure the MMContainer: update refresh time according to current
// tail age
void reconfigureLocked(const Time& currTime);
// returns the number of elements in the container
size_t size() const noexcept {
return lruMutex_->lock_combine([this]() { return lru_.size(); });
}
// Returns the eviction age stats. See CacheStats.h for details
EvictionAgeStat getEvictionAgeStat(uint64_t projectedLength) 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::MMLruObject saveState() const noexcept;
// return the stats for this container.
MMContainerStat getStats() const noexcept;
static LruType getLruType(const T& /* node */) noexcept {
return LruType{};
}
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);
}
// This function is invoked by remove or record access prior to
// removing the node (or moving the node to head) to ensure that
// the node being moved is not the insertion point and if it is
// adjust it accordingly.
void ensureNotInsertionPoint(T& node) noexcept;
// update the lru insertion point after doing an insert or removal.
// We need to ensure the insertionPoint_ is set to the correct node
// to maintain the tailSize_, for the next insertion.
void updateLruInsertionPoint() noexcept;
// remove node from lru and adjust insertion points
// @param node node to remove
void removeLocked(T& node);
// Bit MM_BIT_0 is used to record if the item is in tail. This
// is used to implement LRU insertion points
void markTail(T& node) noexcept {
node.template setFlag<RefFlags::kMMFlag0>();
}
void unmarkTail(T& node) noexcept {
node.template unSetFlag<RefFlags::kMMFlag0>();
}
bool isTail(T& node) const noexcept {
return node.template isFlagSet<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.
void markAccessed(T& node) noexcept {
node.template setFlag<RefFlags::kMMFlag1>();
}
void unmarkAccessed(T& node) noexcept {
node.template unSetFlag<RefFlags::kMMFlag1>();
}
bool isAccessed(const T& node) const noexcept {
return node.template isFlagSet<RefFlags::kMMFlag1>();
}
// 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 folly::cacheline_aligned<Mutex> lruMutex_;
const PtrCompressor compressor_{};
// the lru
LruList lru_{};
// insertion point
T* insertionPoint_{nullptr};
// size of tail after insertion point
size_t tailSize_{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_{};
// Config for this lru.
// Write access to the MMLru Config is serialized.
// Reads may be racy.
Config config_{};
// Max lruFreshTime.
static constexpr uint32_t kLruRefreshTimeCap{900};
FRIEND_TEST(MMLruTest, Reconfigure);
};
};
} // namespace cachelib
} // namespace facebook
#include "cachelib/allocator/MMLru-inl.h"