cachelib/allocator/ChainedHashTable.h (296 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 <folly/Optional.h> #include <cstdint> #include <map> #include <type_traits> #include "cachelib/allocator/Cache.h" #include "cachelib/allocator/memory/serialize/gen-cpp2/objects_types.h" #include "cachelib/common/CompilerUtils.h" #include "cachelib/common/Mutex.h" #include "cachelib/common/Throttler.h" #include "cachelib/shm/Shm.h" namespace facebook { namespace cachelib { /** * Implementation of a hash table with chaining. The elements of the hash * table need to have a public member of type Hook . Expects T to provide a * getKey(), getHash<Hasher>() and appropriate key comparison operators for * doing the key comparisons. The hashtable container guarantees thread * safety. The container acts as an intrusive member-hook hashtable. */ class ChainedHashTable { public: // unique identifier per AccessType static const int kId; template <typename T> struct Hook; private: // Implements a hash table with chaining. template <typename T, Hook<T> T::*HookPtr> class Impl { public: using Key = typename T::Key; using BucketId = size_t; using CompressedPtr = typename T::CompressedPtr; using PtrCompressor = typename T::PtrCompressor; // allocate memory for hash table; the memory is managed by Impl. // // @param numBuckets the number of buckets to be allocated, power of two // @param compressor object used to compress/decompress node pointers // @param hasher object used to hash the key for its bucket id Impl(size_t numBuckets, const PtrCompressor& compressor, const Hasher& hasher); // allocate memory for hash table; the memory is managed by the user. // // @param numBuckets the number of buckets to be allocated, power of two // @param memStart user managed memory. The size must be enough to // accommodate the number of the buckets // @param compressor object used to compress/decompress node pointers // @param hasher object used to hash the key for its bucket id // @param resetMem fill memory with CompressedPtr{} Impl(size_t numBuckets, void* memStart, const PtrCompressor& compressor, const Hasher& hasher, bool resetMem = false); // hash table memory is not released if managed by user. // i.e. Impl::isRestorable() == true ~Impl(); // prohibit copying Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; T* getHashNext(const T& node) const noexcept { return (node.*HookPtr).getHashNext(compressor_); } CompressedPtr getHashNextCompressed(const T& node) const noexcept { return (node.*HookPtr).getHashNext(); } void setHashNext(T& node, T* next) const noexcept { (node.*HookPtr).setHashNext(next, compressor_); } void setHashNext(T& node, CompressedPtr next) { (node.*HookPtr).setHashNext(next); } // inserts the element into the bucket. // // @param node node to be inserted into the hashtable // @param bucket the hashtable bucket that the node belongs to // @return True if the insertion was success. False if not. Insertion // fails if there is already a node with similar key in the // hashtable. bool insertInBucket(T& node, BucketId bucket) noexcept; // inserts or replaces the element into the bucket. // // @param node node to be inserted into the hashtable // @param bucket the hashtable bucket that the node belongs to // @return old node if it exists, nullptr otherwise T* insertOrReplaceInBucket(T& node, BucketId bucket) noexcept; // removes the node from the bucket. // // precondition: node must be in the bucket. // @param node the node to be removed. // @param bucket the hashtable bucket that the node belongs to void removeFromBucket(T& node, BucketId bucket) noexcept; // finds the node corresponding to the key from the bucket and returns it // if found. // // @param key the key for the node we are looking for. // @param bucket the hashtable bucket that the key belongs to // @return a T* corresponding to the node or nullptr if there is no such // node with the key in the bucket. T* findInBucket(Key key, BucketId bucket) const noexcept; // gets the bucket for the key by using the corresponding hash function. BucketId getBucket(Key k) const noexcept; // Call 'func' on each element in the given bucket. // // @param bucket the bucket id to fetch. template <typename F> void forEachBucketElem(BucketId bucket, F&& func) const; // fetch the number of elements of a given bucket // // @param bucket the bucket id to fetch. unsigned int getBucketNumElems(BucketId bucket) const; // true if the hash table can be restored bool isRestorable() const noexcept { return restorable_; } // return the hashtable size in bytes size_t size() const noexcept { return numBuckets_ * sizeof(CompressedPtr); } // return the number of buckets in hash table size_t getNumBuckets() const noexcept { return numBuckets_; } private: // finds the previous node in the hash chain for this node if one exists // such that prev->next is node. // // @param node the node for which we are looking for the previous // @param bucket the hashtable bucket that the node belongs to // @return previous node for this node in the hash chain or nullptr if // this node is in the head of the hash chain. T* findPrevInBucket(const T& node, BucketId bucket) const noexcept; // number of buckets we have in the hashtable, must be power of two const size_t numBuckets_{0}; // materialized value of numBuckets_ - 1 const size_t numBucketsMask_{0}; // actual buckets. std::unique_ptr<CompressedPtr[]> hashTable_; // indicate whether or not the hash table uses user-managed memory and // is thus restorable from serialized state const bool restorable_{false}; // object used to compress/decompress node pointers to reduce memory // footprint of Hook const PtrCompressor compressor_; // Hash the key const Hasher hasher_; }; public: using SerializationType = serialization::ChainedHashTableObject; // node used for chaining the hash table for collision. template <typename T> struct CACHELIB_PACKED_ATTR Hook { using CompressedPtr = typename T::CompressedPtr; using PtrCompressor = typename T::PtrCompressor; // sets the next in the hash chain to the passed in value. void setHashNext(T* n, const PtrCompressor& compressor) noexcept { next_ = compressor.compress(n); } void setHashNext(CompressedPtr n) noexcept { next_ = n; } // gets the next in hash chain for this node. T* getHashNext(const PtrCompressor& compressor) const noexcept { return compressor.unCompress(next_); } CompressedPtr getHashNext() const noexcept { return next_; } private: CompressedPtr next_{}; }; // Config class for the chained hash table. class Config { public: // Do not add 'noexcept' here - causes GCC to delete this method: // "config() is implicitly deleted because its exception-specification // does not match the implicit exception-specification // <noexcept (false)>" // followed by: // "CacheAllocatorConfig.h:522:29: error: use of deleted function // constexpr facebook::cachelib::ChainedHashTable::Config::Config() Config() = default; // @param bucketsPower number of buckets in base 2 logarithm // @param locksPower number of locks in base 2 logarithm // @param pageSize page size Config(unsigned int bucketsPower, unsigned int locksPower, PageSizeT pageSize = PageSizeT::NORMAL) : Config(bucketsPower, locksPower, std::make_shared<MurmurHash2>(), pageSize) {} // @param bucketsPower number of buckets in base 2 logarithm // @param locksPower number of locks in base 2 logarithm // @param hasher the key hash function // @param pageSize page size Config(unsigned int bucketsPower, unsigned int locksPower, Hasher hasher, PageSizeT pageSize = PageSizeT::NORMAL) : bucketsPower_(bucketsPower), locksPower_(locksPower), pageSize_(pageSize), hasher_(std::move(hasher)) { if (bucketsPower_ > kMaxBucketPower || locksPower_ > kMaxLockPower || locksPower_ > bucketsPower_) { throw std::invalid_argument(folly::sformat( "Invalid arguments to the config constructor bucketPower = {}, " "lockPower = {}", bucketsPower_, locksPower_)); } } Config(const Config&) = default; Config& operator=(const Config&) = default; size_t getNumBuckets() const noexcept { return static_cast<size_t>(1) << bucketsPower_; } size_t getNumLocks() const noexcept { return static_cast<size_t>(1) << locksPower_; } // Estimate bucketsPower and LocksPower based on cache entries. void sizeBucketsPowerAndLocksPower(size_t cacheEntries) noexcept { // The percentage of used buckets vs unused buckets is measured by a load // factor. For optimal performance, the load factor should not be more // than 60%. bucketsPower_ = static_cast<size_t>(ceil(log2(cacheEntries * 1.6 /* load factor */))); // 1 lock per 1000 buckets. locksPower_ = std::max<unsigned int>(1, bucketsPower_ - 10); } unsigned int getBucketsPower() const noexcept { return bucketsPower_; } unsigned int getLocksPower() const noexcept { return locksPower_; } const Hasher& getHasher() const noexcept { return hasher_; } std::map<std::string, std::string> serialize() const { std::map<std::string, std::string> configMap; configMap["BucketsPower"] = std::to_string(bucketsPower_); configMap["LocksPower"] = std::to_string(locksPower_); configMap["Hasher"] = hasher_->getMagicId() == 1 ? "FNVHash" : "MurmurHash2"; return configMap; } PageSizeT getPageSize() const { return pageSize_; } private: // 4 billion buckets should be good enough for everyone. static constexpr unsigned int kMaxBucketPower = 32; static constexpr unsigned int kMaxLockPower = 32; // The following are expressed as powers of two to make the modulo // arithmetic simpler. // total number of buckets in the hashtable expressed as power of two. unsigned int bucketsPower_{10}; // total number of locks for the hashtable expressed as a power of two. unsigned int locksPower_{5}; PageSizeT pageSize_{PageSizeT::NORMAL}; Hasher hasher_ = std::make_shared<MurmurHash2>(); }; // Interface for the Container that implements a hash table. Maintains // the node's isInAccessContainer state. T must implement an interface to // markAccessible(), unmarkAccessible() and isAccessible(). template <typename T, Hook<T> T::*HookPtr, typename LockT = facebook::cachelib::SharedMutexBuckets> struct Container { private: using BucketId = typename Impl<T, HookPtr>::BucketId; public: using Key = typename T::Key; using Handle = typename T::Handle; using HandleMaker = typename T::HandleMaker; using CompressedPtr = typename T::CompressedPtr; using PtrCompressor = typename T::PtrCompressor; // default handle maker that calls incRef static const HandleMaker kDefaultHandleMaker; // container with default config. Container() noexcept : Container(Config{}, PtrCompressor(), kDefaultHandleMaker) {} // create hash table container with local-managed memory // @param config the config for the hashtable // @param compressor object used to compress/decompress node pointers // @param hm the functor that creates a Handle from T* Container(Config c, const PtrCompressor& compressor, HandleMaker hm = kDefaultHandleMaker) : config_(std::move(c)), handleMaker_(std::move(hm)), ht_{config_.getNumBuckets(), compressor, config_.getHasher()}, locks_{config_.getLocksPower(), config_.getHasher()} {} // create hash table container with user-managed memory // // @param c config for hash table // @param memStart hash table memory managed by the user // @param compressor object used to compress/decompress node pointers // @param hm the functor that creates a Handle from T* Container(Config c, void* memStart, const PtrCompressor& compressor, HandleMaker hm = kDefaultHandleMaker) : config_(std::move(c)), handleMaker_(std::move(hm)), ht_{config_.getNumBuckets(), memStart, compressor, config_.getHasher(), true /* resetMem */}, locks_{config_.getLocksPower(), config_.getHasher()} {} // restore hash table from serialized data. // // @param object serialized object // @param newConfig the new set of configurations // @param memSegment shared memory segment for the hash table // @param compressor object used to compress/decompress node pointers // @param hm the functor that creates a Handle from T* // // @throw std::invalid argument if the bucket power in new config does not // match the previous state or the size of the memSegment does not // match the old state. Container(const serialization::ChainedHashTableObject& object, const Config& newConfig, ShmAddr memSegment, const PtrCompressor& compressor, HandleMaker hm = kDefaultHandleMaker); // restore hash table from previous state. This only works when the // hash table memory is managed by the user. // // @param object serialized object // @param newConfig the new set of configurations // @param memStart hash table memory managed by the user // @param nBytes size of memory allocation pointed to by memStart // @param compressor object used to compress/decompress node pointers // @param hm the functor that creates a Handle from T* // // @throw std::invalid argument if the bucket power in new config does not // match the previous state or the size of the memSegment does not // match the old state. Container(const serialization::ChainedHashTableObject& object, const Config& newConfig, void* memStart, size_t nBytes, const PtrCompressor& compressor, HandleMaker hm = kDefaultHandleMaker); Container(const Container&) = delete; Container& operator=(const Container&) = delete; // inserts the node into the hash table and marks it as being in the // hashtable upon success. If another node exists with the same key, the // insert fails. On failure the state of the node is unchanged. // // @param node the node to be inserted into the hashtable // @return True if the node was successfully inserted into the hashtable. // False if not. bool insert(T& node) noexcept; // inserts or replaces the node into the hash table and marks it being in // the hashtable upon success. If another node exists with the same key, the // that node is removed. On failure the state of the node is unchanged. // // @param node the node to be inserted into the hashtable // @return if the node was successfully inserted into the hashtable, // returns a null handle. If the node replaced an existing node, // a handle to the old node is returned. // // @throw std::overflow_error is the maximum item refcount is execeeded by // creating this item handle. Handle insertOrReplace(T& node); // replaces a node into the hash table, only if another node exists with // the same key and is marked accessible. // // @param oldNode expected current node in the hash table // @param newNode the new node for the key // // @return true if oldNode exists, is accessible, and was replaced // successfully. bool replaceIfAccessible(T& oldNode, T& newNode) noexcept; // replaces a node if predicate returns true on the existing node // // @param oldNode expected current node in the hash table // @param newNode the new node for the key // @param predicate asseses if condition is met for the oldNode to merit // a replace // // @return true if oldNode exists, is accessible, predicate is true, and // was replaced successfully. template <typename F> bool replaceIf(T& oldNode, T& newNode, F&& predicate); // removes the node from the hashtable and unmarks it as accessible. If // the node does not exists, returns False. // // @param node node to be removed from the hashtable. // @return True if the node was in the hashtable and if it was // successfully removed. False if the node was not in the // hashtable. bool remove(T& node) noexcept; // remove a node from the container if it exists for the key and the // predicate returns true for the node. This is intended to simplify the // eviction purposes to guarantee a good selection of candidate. // // @param node the node to be removed // @param predicate the predicate check for the node // // @return handle to the node if we successfully removed it. returns a // null handle if the node was either not in the container or the // predicate failed. Handle removeIf(T& node, const std::function<bool(const T& node)>& predicate); // finds the node corresponding to the key in the hashtable and returns a // handle to that node. // // @param key the lookup key // @param args arguments to construct a handle for T. // // @return Handle with valid T* if there is a node corresponding to the // key or a Handle with nullptr if not. // // @throw std::overflow_error is the maximum item refcount is execeeded by // creating this item handle. Handle find(Key key) const; // for saving the state of the hash table // // 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. // // @throw std::logic_error if the container has any pending iterators that // need to be destroyed or if the container can not be restored. serialization::ChainedHashTableObject saveState() const; // get the required size for the buckets. static size_t getRequiredSize(size_t numBuckets) noexcept { return sizeof(CompressedPtr) * numBuckets; } const Config& getConfig() const noexcept { return config_; } unsigned int getHashpower() const noexcept { return config_.getBucketsPower(); } // Iterator interface for the hashtable. Iterates over the hashtable // bucket by bucket and takes a snapshot of the bucket to iterate over. It // guarantees that all keys that were present when the iteration started // will be accessible unless they are removed. Keys that are // removed/inserted during the lifetime of an iterator are not guaranteed // to be either visited or not-visited. Adding/Removing from the hash // table while the iterator is alive will not invalidate any iterator or // the element that the iterator points at currently. The iterator // internally holds a Handle to the item. class Iterator { public: ~Iterator() { XDCHECK_GT(container_->numIterators_.load(), 0u); --container_->numIterators_; } Iterator(const Iterator&) = delete; Iterator& operator=(const Iterator&) = delete; Iterator(Iterator&&) noexcept; Iterator& operator=(Iterator&&) noexcept; enum EndIterT { EndIter }; // increment the iterator to the next element. // with/without throttler Iterator& operator++(); // dereference the current element that the iterator is pointing to. T& operator*(); T* operator->() { return &(*(*this)); } bool operator==(const Iterator& other) const noexcept { return container_ == other.container_ && currBucket_ == other.currBucket_ && curSor_ == other.curSor_; } bool operator!=(const Iterator& other) const noexcept { return !(*this == other); } // TODO(jiayueb): change to return ReadHandle after fixing all the breaks const Handle& asHandle() { return curr(); } // reset the Iterator to begin of container void reset(); private: // container for the iterator using C = Container<T, HookPtr, LockT>; // construct an iterator with the given friend C; explicit Iterator(C& ht, folly::Optional<util::Throttler::Config> throttlerConfig = folly::none); Iterator(C& ht, EndIterT); // the container over which we are iterating C* container_; // current bucket that the iterator is pointing to. BucketId currBucket_{0}; // cursor into the current bucket. unsigned int curSor_{0}; // current bucket. std::vector<Handle> bucketElems_; // optional throttler folly::Optional<util::Throttler> throttler_ = folly::none; // returns the handle for current item in the iterator. Handle& curr() { if (curSor_ < bucketElems_.size()) { return bucketElems_[curSor_]; } throw std::logic_error( "Iterator in invalid state with curSor_: " + folly::to<std::string>(curSor_) + ", currBucket_: " + folly::to<std::string>(currBucket_) + ", total buckets: " + folly::to<std::string>(container_->config_.getNumBuckets())); } }; // Iterator interface to the container. // whether it constructs iterator of begin with a throttler config Iterator begin(folly::Optional<util::Throttler::Config> throttlerConfig); Iterator begin() { return Iterator(*this); } Iterator end() { return Iterator(*this, Iterator::EndIter); } // Stats describing the distribution of items (keys) in the hash table struct DistributionStats { uint64_t numKeys{0}; uint64_t numBuckets{0}; // map from bucket id to number of items in the bucket. std::map<unsigned int, uint64_t> itemDistribution{}; }; struct Stats { uint64_t numKeys; uint64_t numBuckets; }; // Get the distribution stats. This function will use cached results // if the difference since last updated is not significant. This is // expensive. Call at your discretion. // // Critiera for refreshing the stats: // - 10 minutes since last update, OR // - 5% more or less number of keys in the hash table DistributionStats getDistributionStats() const; // lightweight stats that give the number of keys and buckets inside the // container. This is guaranteed to be fast. Stats getStats() const noexcept { return {numKeys_, ht_.getNumBuckets()}; } // Get the total number of keys inserted into the hash table uint64_t getNumKeys() const noexcept { return numKeys_; } private: using Hashtable = Impl<T, HookPtr>; // Fetch a vector of handle to the items belonging to a given bucket. This // is for use by the iterator. 'handles' will be cleared and then populated // with handles for the items in the given bucket. Items will be skipped if // the handle cannot be acquired for any reason. void getBucketElems(BucketId bucket, std::vector<Handle>& handles) const; // config for the hash table. const Config config_{}; // handle maker to convert the T* to T::Handle HandleMaker handleMaker_; // the hashtable buckets Hashtable ht_; // locks protecting the hashtable buckets mutable LockT locks_; std::atomic<unsigned int> numIterators_{0}; // Cached stats for distribution // This is updated if the number of keys changes by more than 5%, or // it has been 10 minutes since the stats has last been updated. mutable std::mutex cachedStatsLock_; mutable DistributionStats cachedStats_{}; // if we can recompute the cachedStats if it is too old. Set to false when // another thread is computing it. mutable bool canRecomputeDistributionStats_{true}; // when the distribution was last computed. mutable time_t cachedStatsUpdateTime_{0}; // number of the keys stored in this hash table std::atomic<uint64_t> numKeys_{0}; }; }; template <typename T, typename ChainedHashTable::Hook<T> T::*HookPtr, typename LockT> const typename T::HandleMaker ChainedHashTable::Container<T, HookPtr, LockT>::kDefaultHandleMaker = [](T* t) -> typename T::Handle { if (t) { t->incRef(); } return typename T::Handle{t}; }; } // namespace cachelib } // namespace facebook #include "cachelib/allocator/ChainedHashTable-inl.h"