Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
803 lines (722 sloc)
25.3 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) Meta Platforms, Inc. and 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 <mutex> | |
#include <folly/Optional.h> | |
#include <folly/concurrency/detail/ConcurrentHashMap-detail.h> | |
#include <folly/synchronization/Hazptr.h> | |
namespace folly { | |
/** | |
* Implementations of high-performance Concurrent Hashmaps that | |
* support erase and update. | |
* | |
* Readers are always wait-free. | |
* Writers are sharded, but take a lock. | |
* | |
* Multithreaded performance beats anything except the lock-free | |
* atomic maps (AtomicUnorderedMap, AtomicHashMap), BUT only | |
* if you can perfectly size the atomic maps, and you don't | |
* need erase(). If you don't know the size in advance or | |
* your workload needs erase(), this is the better choice. | |
* | |
* The interface is as close to std::unordered_map as possible, but there | |
* are a handful of changes: | |
* | |
* * Iterators hold hazard pointers to the returned elements. Elements can only | |
* be accessed while Iterators are still valid! | |
* | |
* * Therefore operator[] and at() return copies, since they do not | |
* return an iterator. The returned value is const, to remind you | |
* that changes do not affect the value in the map. | |
* | |
* * erase() calls the hash function, and may fail if the hash | |
* function throws an exception. | |
* | |
* * clear() initializes new segments, and is not noexcept. | |
* | |
* * The interface adds assign_if_equal, since find() doesn't take a lock. | |
* | |
* * Only const version of find() is supported, and const iterators. | |
* Mutation must use functions provided, like assign(). | |
* | |
* * iteration iterates over all the buckets in the table, unlike | |
* std::unordered_map which iterates over a linked list of elements. | |
* If the table is sparse, this may be more expensive. | |
* | |
* * Allocator must be stateless. | |
* | |
* 1: ConcurrentHashMap, based on Java's ConcurrentHashMap. | |
* Very similar to std::unodered_map in performance. | |
* | |
* 2: ConcurrentHashMapSIMD, based on F14ValueMap. If the map is | |
* larger than the cache size, it has superior performance due to | |
* vectorized key lookup. | |
* | |
* | |
* | |
* USAGE FAQs | |
* | |
* Q: Is simultaneous iteration and erase() threadsafe? | |
* Example: | |
* | |
* ConcurrentHashMap<int, int> map; | |
* | |
* Thread 1: auto it = map.begin(); | |
* while (it != map.end()) { | |
* // Do something with it | |
* it++; | |
* } | |
* | |
* Thread 2: map.insert(2, 2); map.erase(2); | |
* | |
* A: Yes, this is safe. However, the iterating thread is not | |
* garanteed to see (or not see) concurrent insertions and erasures. | |
* Inserts may cause a rehash, but the old table is still valid as | |
* long as any iterator pointing to it exists. | |
* | |
* Q: How do I update an existing object atomically? | |
* | |
* A: assign_if_equal is the recommended way - readers will see the | |
* old value until the new value is completely constructed and | |
* inserted. | |
* | |
* Q: Why do map.erase() and clear() not actually destroy elements? | |
* | |
* A: Hazard Pointers are used to improve the performance of | |
* concurrent access. They can be thought of as a simple Garbage | |
* Collector. To reduce the GC overhead, a GC pass is only run after | |
* reaching a cetain memory bound. erase() will remove the element | |
* from being accessed via the map, but actual destruction may happen | |
* later, after iterators that may point to it have been deleted. | |
* | |
* The only guarantee is that a GC pass will be run on map destruction | |
* - no elements will remain after map destruction. | |
* | |
* Q: Are pointers to values safe to access *without* holding an | |
* iterator? | |
* | |
* A: The SIMD version guarantees that references to elements are | |
* stable across rehashes, the non-SIMD version does *not*. Note that | |
* unless you hold an iterator, you need to ensure there are no | |
* concurrent deletes/updates to that key if you are accessing it via | |
* reference. | |
*/ | |
template < | |
typename KeyType, | |
typename ValueType, | |
typename HashFn = std::hash<KeyType>, | |
typename KeyEqual = std::equal_to<KeyType>, | |
typename Allocator = std::allocator<uint8_t>, | |
uint8_t ShardBits = 8, | |
template <typename> class Atom = std::atomic, | |
class Mutex = std::mutex, | |
template < | |
typename, | |
typename, | |
uint8_t, | |
typename, | |
typename, | |
typename, | |
template <typename> | |
class, | |
class> | |
class Impl = detail::concurrenthashmap::bucket::BucketTable> | |
class ConcurrentHashMap { | |
using SegmentT = detail::ConcurrentHashMapSegment< | |
KeyType, | |
ValueType, | |
ShardBits, | |
HashFn, | |
KeyEqual, | |
Allocator, | |
Atom, | |
Mutex, | |
Impl>; | |
template <typename K, typename T> | |
using EnableHeterogeneousFind = std::enable_if_t< | |
detail::EligibleForHeterogeneousFind<KeyType, HashFn, KeyEqual, K>::value, | |
T>; | |
float load_factor_ = SegmentT::kDefaultLoadFactor; | |
static constexpr uint64_t NumShards = (1 << ShardBits); | |
public: | |
class ConstIterator; | |
typedef KeyType key_type; | |
typedef ValueType mapped_type; | |
typedef std::pair<const KeyType, ValueType> value_type; | |
typedef std::size_t size_type; | |
typedef HashFn hasher; | |
typedef KeyEqual key_equal; | |
typedef ConstIterator const_iterator; | |
private: | |
template <typename K, typename T> | |
using EnableHeterogeneousInsert = std::enable_if_t< | |
::folly::detail:: | |
EligibleForHeterogeneousInsert<KeyType, HashFn, KeyEqual, K>::value, | |
T>; | |
template <typename K> | |
using IsIter = std::is_same<ConstIterator, remove_cvref_t<K>>; | |
template <typename K, typename T> | |
using EnableHeterogeneousErase = std::enable_if_t< | |
::folly::detail::EligibleForHeterogeneousFind< | |
KeyType, | |
HashFn, | |
KeyEqual, | |
std::conditional_t<IsIter<K>::value, KeyType, K>>::value && | |
!IsIter<K>::value, | |
T>; | |
public: | |
/* | |
* Construct a ConcurrentHashMap with 1 << ShardBits shards, size | |
* and max_size given. Both size and max_size will be rounded up to | |
* the next power of two, if they are not already a power of two, so | |
* that we can index in to Shards efficiently. | |
* | |
* Insertion functions will throw bad_alloc if max_size is exceeded. | |
*/ | |
explicit ConcurrentHashMap(size_t size = 8, size_t max_size = 0) { | |
size_ = folly::nextPowTwo(size); | |
if (max_size != 0) { | |
max_size_ = folly::nextPowTwo(max_size); | |
} | |
CHECK(max_size_ == 0 || max_size_ >= size_); | |
for (uint64_t i = 0; i < NumShards; i++) { | |
segments_[i].store(nullptr, std::memory_order_relaxed); | |
} | |
} | |
ConcurrentHashMap(ConcurrentHashMap&& o) noexcept | |
: size_(o.size_), max_size_(o.max_size_) { | |
for (uint64_t i = 0; i < NumShards; i++) { | |
segments_[i].store( | |
o.segments_[i].load(std::memory_order_relaxed), | |
std::memory_order_relaxed); | |
o.segments_[i].store(nullptr, std::memory_order_relaxed); | |
} | |
cohort_.store(o.cohort(), std::memory_order_relaxed); | |
o.cohort_.store(nullptr, std::memory_order_relaxed); | |
beginSeg_.store( | |
o.beginSeg_.load(std::memory_order_relaxed), std::memory_order_relaxed); | |
o.beginSeg_.store(NumShards, std::memory_order_relaxed); | |
endSeg_.store( | |
o.endSeg_.load(std::memory_order_relaxed), std::memory_order_relaxed); | |
o.endSeg_.store(0, std::memory_order_relaxed); | |
} | |
ConcurrentHashMap& operator=(ConcurrentHashMap&& o) { | |
for (uint64_t i = 0; i < NumShards; i++) { | |
auto seg = segments_[i].load(std::memory_order_relaxed); | |
if (seg) { | |
seg->~SegmentT(); | |
Allocator().deallocate((uint8_t*)seg, sizeof(SegmentT)); | |
} | |
segments_[i].store( | |
o.segments_[i].load(std::memory_order_relaxed), | |
std::memory_order_relaxed); | |
o.segments_[i].store(nullptr, std::memory_order_relaxed); | |
} | |
size_ = o.size_; | |
max_size_ = o.max_size_; | |
cohort_shutdown_cleanup(); | |
cohort_.store(o.cohort(), std::memory_order_relaxed); | |
o.cohort_.store(nullptr, std::memory_order_relaxed); | |
beginSeg_.store( | |
o.beginSeg_.load(std::memory_order_relaxed), std::memory_order_relaxed); | |
o.beginSeg_.store(NumShards, std::memory_order_relaxed); | |
endSeg_.store( | |
o.endSeg_.load(std::memory_order_relaxed), std::memory_order_relaxed); | |
o.endSeg_.store(0, std::memory_order_relaxed); | |
return *this; | |
} | |
~ConcurrentHashMap() { | |
uint64_t begin = beginSeg_.load(std::memory_order_acquire); | |
uint64_t end = endSeg_.load(std::memory_order_acquire); | |
for (uint64_t i = begin; i < end; ++i) { | |
auto seg = segments_[i].load(std::memory_order_relaxed); | |
if (seg) { | |
seg->~SegmentT(); | |
Allocator().deallocate((uint8_t*)seg, sizeof(SegmentT)); | |
} | |
} | |
cohort_shutdown_cleanup(); | |
} | |
bool empty() const noexcept { | |
uint64_t begin = beginSeg_.load(std::memory_order_acquire); | |
uint64_t end = endSeg_.load(std::memory_order_acquire); | |
// Note: beginSeg_ and endSeg_ are only conservative hints of the | |
// range of non-empty segments. This function cannot conclude that | |
// a map is nonempty merely because beginSeg_ < endSeg_. | |
for (uint64_t i = begin; i < end; ++i) { | |
auto seg = segments_[i].load(std::memory_order_acquire); | |
if (seg) { | |
if (!seg->empty()) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
ConstIterator find(const KeyType& k) const { return findImpl(k); } | |
template <typename K, EnableHeterogeneousFind<K, int> = 0> | |
ConstIterator find(const K& k) const { | |
return findImpl(k); | |
} | |
ConstIterator cend() const noexcept { return ConstIterator(NumShards); } | |
ConstIterator cbegin() const noexcept { return ConstIterator(this); } | |
ConstIterator end() const noexcept { return cend(); } | |
ConstIterator begin() const noexcept { return cbegin(); } | |
std::pair<ConstIterator, bool> insert( | |
std::pair<key_type, mapped_type>&& foo) { | |
return insertImpl(std::move(foo)); | |
} | |
template <typename Key, EnableHeterogeneousInsert<Key, int> = 0> | |
std::pair<ConstIterator, bool> insert(std::pair<Key, mapped_type>&& foo) { | |
return insertImpl(std::move(foo)); | |
} | |
template <typename Key, typename Value> | |
std::pair<ConstIterator, bool> insert(Key&& k, Value&& v) { | |
auto segment = pickSegment(k); | |
std::pair<ConstIterator, bool> res( | |
std::piecewise_construct, | |
std::forward_as_tuple(this, segment), | |
std::forward_as_tuple(false)); | |
res.second = ensureSegment(segment)->insert( | |
res.first.it_, std::forward<Key>(k), std::forward<Value>(v)); | |
return res; | |
} | |
template <typename Key, typename... Args> | |
std::pair<ConstIterator, bool> try_emplace(Key&& k, Args&&... args) { | |
auto segment = pickSegment(k); | |
std::pair<ConstIterator, bool> res( | |
std::piecewise_construct, | |
std::forward_as_tuple(this, segment), | |
std::forward_as_tuple(false)); | |
res.second = ensureSegment(segment)->try_emplace( | |
res.first.it_, std::forward<Key>(k), std::forward<Args>(args)...); | |
return res; | |
} | |
template <typename... Args> | |
std::pair<ConstIterator, bool> emplace(Args&&... args) { | |
using Node = typename SegmentT::Node; | |
auto node = (Node*)Allocator().allocate(sizeof(Node)); | |
new (node) Node(ensureCohort(), std::forward<Args>(args)...); | |
auto segment = pickSegment(node->getItem().first); | |
std::pair<ConstIterator, bool> res( | |
std::piecewise_construct, | |
std::forward_as_tuple(this, segment), | |
std::forward_as_tuple(false)); | |
res.second = ensureSegment(segment)->emplace( | |
res.first.it_, node->getItem().first, node); | |
if (!res.second) { | |
node->~Node(); | |
Allocator().deallocate((uint8_t*)node, sizeof(Node)); | |
} | |
return res; | |
} | |
/* | |
* The bool component will always be true if the map has been updated via | |
* either insertion or assignment. Note that this is different from the | |
* std::map::insert_or_assign interface. | |
*/ | |
template <typename Key, typename Value> | |
std::pair<ConstIterator, bool> insert_or_assign(Key&& k, Value&& v) { | |
auto segment = pickSegment(k); | |
std::pair<ConstIterator, bool> res( | |
std::piecewise_construct, | |
std::forward_as_tuple(this, segment), | |
std::forward_as_tuple(false)); | |
res.second = ensureSegment(segment)->insert_or_assign( | |
res.first.it_, std::forward<Key>(k), std::forward<Value>(v)); | |
return res; | |
} | |
template <typename Key, typename Value> | |
folly::Optional<ConstIterator> assign(Key&& k, Value&& v) { | |
auto segment = pickSegment(k); | |
ConstIterator res(this, segment); | |
auto seg = segments_[segment].load(std::memory_order_acquire); | |
if (!seg) { | |
return none; | |
} else { | |
auto r = | |
seg->assign(res.it_, std::forward<Key>(k), std::forward<Value>(v)); | |
if (!r) { | |
return none; | |
} | |
} | |
return std::move(res); | |
} | |
// Assign to desired if and only if the predicate returns true | |
// for the current value. | |
template <typename Key, typename Value, typename Predicate> | |
folly::Optional<ConstIterator> assign_if( | |
Key&& k, Value&& desired, Predicate&& predicate) { | |
auto segment = pickSegment(k); | |
ConstIterator res(this, segment); | |
auto seg = segments_[segment].load(std::memory_order_acquire); | |
if (!seg) { | |
return none; | |
} else { | |
auto r = seg->assign_if( | |
res.it_, | |
std::forward<Key>(k), | |
std::forward<Value>(desired), | |
std::forward<Predicate>(predicate)); | |
if (!r) { | |
return none; | |
} | |
} | |
return std::move(res); | |
} | |
// Assign to desired if and only if key k is equal to expected | |
template <typename Key, typename Value> | |
folly::Optional<ConstIterator> assign_if_equal( | |
Key&& k, const ValueType& expected, Value&& desired) { | |
auto segment = pickSegment(k); | |
ConstIterator res(this, segment); | |
auto seg = segments_[segment].load(std::memory_order_acquire); | |
if (!seg) { | |
return none; | |
} else { | |
auto r = seg->assign_if_equal( | |
res.it_, | |
std::forward<Key>(k), | |
expected, | |
std::forward<Value>(desired)); | |
if (!r) { | |
return none; | |
} | |
} | |
return std::move(res); | |
} | |
// Copying wrappers around insert and find. | |
// Only available for copyable types. | |
const ValueType operator[](const KeyType& key) { | |
auto item = insert(key, ValueType()); | |
return item.first->second; | |
} | |
template <typename Key, EnableHeterogeneousInsert<Key, int> = 0> | |
const ValueType operator[](const Key& key) { | |
auto item = insert(key, ValueType()); | |
return item.first->second; | |
} | |
const ValueType at(const KeyType& key) const { return atImpl(key); } | |
template <typename K, EnableHeterogeneousFind<K, int> = 0> | |
const ValueType at(const K& key) const { | |
return atImpl(key); | |
} | |
// TODO update assign interface, operator[], at | |
size_type erase(const key_type& k) { return eraseImpl(k); } | |
template <typename K, EnableHeterogeneousErase<K, int> = 0> | |
size_type erase(const K& k) { | |
return eraseImpl(k); | |
} | |
// Calls the hash function, and therefore may throw. | |
ConstIterator erase(ConstIterator& pos) { | |
auto segment = pickSegment(pos->first); | |
ConstIterator res(this, segment); | |
ensureSegment(segment)->erase(res.it_, pos.it_); | |
res.advanceIfAtSegmentEnd(); | |
return res; | |
} | |
// Erase if and only if key k is equal to expected | |
size_type erase_if_equal(const key_type& k, const ValueType& expected) { | |
return erase_key_if( | |
k, [&expected](const ValueType& v) { return v == expected; }); | |
} | |
template <typename K, EnableHeterogeneousErase<K, int> = 0> | |
size_type erase_if_equal(const K& k, const ValueType& expected) { | |
return erase_key_if( | |
k, [&expected](const ValueType& v) { return v == expected; }); | |
} | |
// Erase if predicate evaluates to true on the existing value | |
template <typename Predicate> | |
size_type erase_key_if(const key_type& k, Predicate&& predicate) { | |
return eraseKeyIfImpl(k, std::forward<Predicate>(predicate)); | |
} | |
template < | |
typename K, | |
typename Predicate, | |
EnableHeterogeneousErase<K, int> = 0> | |
size_type erase_key_if(const K& k, Predicate&& predicate) { | |
return eraseKeyIfImpl(k, std::forward<Predicate>(predicate)); | |
} | |
// NOT noexcept, initializes new shard segments vs. | |
void clear() { | |
uint64_t begin = beginSeg_.load(std::memory_order_acquire); | |
uint64_t end = endSeg_.load(std::memory_order_acquire); | |
for (uint64_t i = begin; i < end; ++i) { | |
auto seg = segments_[i].load(std::memory_order_acquire); | |
if (seg) { | |
seg->clear(); | |
} | |
} | |
} | |
void reserve(size_t count) { | |
count = count >> ShardBits; | |
if (!count) | |
return; | |
uint64_t begin = beginSeg_.load(std::memory_order_acquire); | |
uint64_t end = endSeg_.load(std::memory_order_acquire); | |
for (uint64_t i = begin; i < end; ++i) { | |
auto seg = segments_[i].load(std::memory_order_acquire); | |
if (seg) { | |
seg->rehash(count); | |
} | |
} | |
} | |
// This is a rolling size, and is not exact at any moment in time. | |
size_t size() const noexcept { | |
size_t res = 0; | |
uint64_t begin = beginSeg_.load(std::memory_order_acquire); | |
uint64_t end = endSeg_.load(std::memory_order_acquire); | |
for (uint64_t i = begin; i < end; ++i) { | |
auto seg = segments_[i].load(std::memory_order_acquire); | |
if (seg) { | |
res += seg->size(); | |
} | |
} | |
return res; | |
} | |
float max_load_factor() const { return load_factor_; } | |
void max_load_factor(float factor) { | |
uint64_t begin = beginSeg_.load(std::memory_order_acquire); | |
uint64_t end = endSeg_.load(std::memory_order_acquire); | |
for (uint64_t i = begin; i < end; ++i) { | |
auto seg = segments_[i].load(std::memory_order_acquire); | |
if (seg) { | |
seg->max_load_factor(factor); | |
} | |
} | |
} | |
class ConstIterator { | |
public: | |
friend class ConcurrentHashMap; | |
const value_type& operator*() const { return *it_; } | |
const value_type* operator->() const { return &*it_; } | |
ConstIterator& operator++() { | |
++it_; | |
advanceIfAtSegmentEnd(); | |
return *this; | |
} | |
bool operator==(const ConstIterator& o) const { | |
return it_ == o.it_ && segment_ == o.segment_; | |
} | |
bool operator!=(const ConstIterator& o) const { return !(*this == o); } | |
ConstIterator& operator=(const ConstIterator& o) = delete; | |
ConstIterator& operator=(ConstIterator&& o) noexcept { | |
if (this != &o) { | |
it_ = std::move(o.it_); | |
segment_ = std::exchange(o.segment_, uint64_t(NumShards)); | |
parent_ = std::exchange(o.parent_, nullptr); | |
} | |
return *this; | |
} | |
ConstIterator(const ConstIterator& o) = delete; | |
ConstIterator(ConstIterator&& o) noexcept | |
: it_(std::move(o.it_)), | |
segment_(std::exchange(o.segment_, uint64_t(NumShards))), | |
parent_(std::exchange(o.parent_, nullptr)) {} | |
ConstIterator(const ConcurrentHashMap* parent, uint64_t segment) | |
: segment_(segment), parent_(parent) {} | |
private: | |
// cbegin iterator | |
explicit ConstIterator(const ConcurrentHashMap* parent) | |
: it_(nullptr), | |
segment_(parent->beginSeg_.load(std::memory_order_acquire)), | |
parent_(parent) { | |
advanceToSegmentBegin(); | |
} | |
// cend iterator | |
explicit ConstIterator(uint64_t shards) : it_(nullptr), segment_(shards) {} | |
void advanceIfAtSegmentEnd() { | |
DCHECK_LT(segment_, parent_->NumShards); | |
SegmentT* seg = | |
parent_->segments_[segment_].load(std::memory_order_acquire); | |
DCHECK(seg); | |
if (it_ == seg->cend()) { | |
++segment_; | |
advanceToSegmentBegin(); | |
} | |
} | |
FOLLY_ALWAYS_INLINE void advanceToSegmentBegin() { | |
// Advance to the beginning of the next nonempty segment | |
// starting from segment_. | |
uint64_t end = parent_->endSeg_.load(std::memory_order_acquire); | |
while (segment_ < end) { | |
SegmentT* seg = | |
parent_->segments_[segment_].load(std::memory_order_acquire); | |
if (seg) { | |
it_ = seg->cbegin(); | |
if (it_ != seg->cend()) { | |
return; | |
} | |
} | |
++segment_; | |
} | |
// All segments are empty. Advance to end. | |
segment_ = parent_->NumShards; | |
} | |
typename SegmentT::Iterator it_; | |
uint64_t segment_; | |
const ConcurrentHashMap* parent_; | |
}; | |
private: | |
template <typename K> | |
ConstIterator findImpl(const K& k) const { | |
auto segment = pickSegment(k); | |
ConstIterator res(this, segment); | |
auto seg = segments_[segment].load(std::memory_order_acquire); | |
if (!seg || !seg->find(res.it_, k)) { | |
res.segment_ = NumShards; | |
} | |
return res; | |
} | |
template <typename K> | |
const ValueType atImpl(const K& k) const { | |
auto item = find(k); | |
if (item == cend()) { | |
throw_exception<std::out_of_range>("at(): key not in map"); | |
} | |
return item->second; | |
} | |
template <typename Key> | |
std::pair<ConstIterator, bool> insertImpl(std::pair<Key, mapped_type>&& foo) { | |
auto segment = pickSegment(foo.first); | |
std::pair<ConstIterator, bool> res( | |
std::piecewise_construct, | |
std::forward_as_tuple(this, segment), | |
std::forward_as_tuple(false)); | |
res.second = ensureSegment(segment)->insert(res.first.it_, std::move(foo)); | |
return res; | |
} | |
template <typename K> | |
size_type eraseImpl(const K& k) { | |
auto segment = pickSegment(k); | |
auto seg = segments_[segment].load(std::memory_order_acquire); | |
if (!seg) { | |
return 0; | |
} else { | |
return seg->erase(k); | |
} | |
} | |
template <typename K, typename Predicate> | |
size_type eraseKeyIfImpl(const K& k, Predicate&& predicate) { | |
auto segment = pickSegment(k); | |
auto seg = segments_[segment].load(std::memory_order_acquire); | |
if (!seg) { | |
return 0; | |
} | |
return seg->erase_key_if(k, std::forward<Predicate>(predicate)); | |
} | |
template <typename K> | |
uint64_t pickSegment(const K& k) const { | |
auto h = HashFn()(k); | |
// Use the lowest bits for our shard bits. | |
// | |
// This works well even if the hash function is biased towards the | |
// low bits: The sharding will happen in the segments_ instead of | |
// in the segment buckets, so we'll still get write sharding as | |
// well. | |
// | |
// Low-bit bias happens often for std::hash using small numbers, | |
// since the integer hash function is the identity function. | |
return h & (NumShards - 1); | |
} | |
SegmentT* ensureSegment(uint64_t i) const { | |
SegmentT* seg = segments_[i].load(std::memory_order_acquire); | |
if (!seg) { | |
auto b = ensureCohort(); | |
SegmentT* newseg = (SegmentT*)Allocator().allocate(sizeof(SegmentT)); | |
newseg = new (newseg) | |
SegmentT(size_ >> ShardBits, load_factor_, max_size_ >> ShardBits, b); | |
if (!segments_[i].compare_exchange_strong(seg, newseg)) { | |
// seg is updated with new value, delete ours. | |
newseg->~SegmentT(); | |
Allocator().deallocate((uint8_t*)newseg, sizeof(SegmentT)); | |
} else { | |
seg = newseg; | |
updateBeginAndEndSegments(i); | |
} | |
} | |
return seg; | |
} | |
void updateBeginAndEndSegments(uint64_t i) const { | |
uint64_t val = beginSeg_.load(std::memory_order_acquire); | |
while (i < val && !casSeg(beginSeg_, val, i)) { | |
} | |
val = endSeg_.load(std::memory_order_acquire); | |
while (i + 1 > val && !casSeg(endSeg_, val, i + 1)) { | |
} | |
} | |
bool casSeg(Atom<uint64_t>& seg, uint64_t& expval, uint64_t newval) const { | |
return seg.compare_exchange_weak( | |
expval, newval, std::memory_order_acq_rel, std::memory_order_acquire); | |
} | |
hazptr_obj_cohort<Atom>* cohort() const noexcept { | |
return cohort_.load(std::memory_order_acquire); | |
} | |
hazptr_obj_cohort<Atom>* ensureCohort() const { | |
auto b = cohort(); | |
if (!b) { | |
auto storage = Allocator().allocate(sizeof(hazptr_obj_cohort<Atom>)); | |
auto newcohort = new (storage) hazptr_obj_cohort<Atom>(); | |
if (cohort_.compare_exchange_strong(b, newcohort)) { | |
b = newcohort; | |
} else { | |
newcohort->~hazptr_obj_cohort<Atom>(); | |
Allocator().deallocate(storage, sizeof(hazptr_obj_cohort<Atom>)); | |
} | |
} | |
return b; | |
} | |
void cohort_shutdown_cleanup() { | |
auto b = cohort(); | |
if (b) { | |
b->~hazptr_obj_cohort<Atom>(); | |
Allocator().deallocate((uint8_t*)b, sizeof(hazptr_obj_cohort<Atom>)); | |
} | |
} | |
mutable Atom<SegmentT*> segments_[NumShards]; | |
size_t size_{0}; | |
size_t max_size_{0}; | |
mutable Atom<hazptr_obj_cohort<Atom>*> cohort_{nullptr}; | |
mutable Atom<uint64_t> beginSeg_{NumShards}; | |
mutable Atom<uint64_t> endSeg_{0}; | |
}; | |
template < | |
typename KeyType, | |
typename ValueType, | |
typename HashFn = std::hash<KeyType>, | |
typename KeyEqual = std::equal_to<KeyType>, | |
typename Allocator = std::allocator<uint8_t>, | |
uint8_t ShardBits = 8, | |
template <typename> class Atom = std::atomic, | |
class Mutex = std::mutex> | |
using ConcurrentHashMapSIMD = ConcurrentHashMap< | |
KeyType, | |
ValueType, | |
HashFn, | |
KeyEqual, | |
Allocator, | |
ShardBits, | |
Atom, | |
Mutex, | |
#if FOLLY_SSE_PREREQ(4, 2) && !FOLLY_MOBILE | |
detail::concurrenthashmap::simd::SIMDTable | |
#else | |
// fallback to regular impl | |
detail::concurrenthashmap::bucket::BucketTable | |
#endif | |
>; | |
} // namespace folly |