2 * Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration.
5 * @file CxxUtils/ConcurrentHashmapImpl.h
6 * @author scott snyder <snyder@bnl.gov>
8 * @brief Hash table allowing concurrent, lockless reads.
22 * @param hash The hash of the entry we're looking for.
23 * @param mask Table mask; i.e., the table capacity - 1.
24 * @param maskBits Number of 1 bits in mask.
25 * @param probeLimit Maximum number of probes to try before failing.
27 template <unsigned ENTRIES_PER_CACHELINE>
29 CHMTableIterator<ENTRIES_PER_CACHELINE>::CHMTableIterator
30 (size_t hash, size_t mask, size_t maskBits, size_t probeLimit)
31 // Mask limited to the table. Also mask off the part within a cache line.
32 : m_mask ((mask & ~ENTRIES_PER_CACHELINE_MASK)),
33 // Offset of hash within a cache line.
34 m_boffs (hash & ENTRIES_PER_CACHELINE_MASK),
35 // Starting increment between hash probes.
36 // Must be large enough to go to a new cache line, which is why
37 // we or in ENTRIES_PER_CACHELINE.
38 m_stride (((hash >> maskBits) | hash | ENTRIES_PER_CACHELINE) & ENTRIES_PER_CACHELINE_MASK),
39 m_probeLimit (probeLimit),
40 // Index at the start of the cache line containing hash.
41 m_bucket (hash & m_mask),
48 * @brief Offset of the element currently being probed.
50 template <unsigned ENTRIES_PER_CACHELINE>
52 size_t CHMTableIterator<ENTRIES_PER_CACHELINE>::offset() const
54 // m_bucket the starting index of the current cache line.
55 // Count within the cache line in a circular manner starting
57 return m_bucket + ((m_nprobes + m_boffs)&ENTRIES_PER_CACHELINE_MASK);
62 * @brief Return the number of probes performed so far.
64 template <unsigned ENTRIES_PER_CACHELINE>
66 size_t CHMTableIterator<ENTRIES_PER_CACHELINE>::nprobes() const
73 * @brief Move to the next probe.
74 * Returns true if we should continue, or false if we've hit the maximum
77 template <unsigned ENTRIES_PER_CACHELINE>
79 bool CHMTableIterator<ENTRIES_PER_CACHELINE>::next()
81 // Increment number of probes and stop if we've hit the maximum.
82 if (++m_nprobes >= m_probeLimit) {
85 // We've finished a cache line if the low bits are back to 0.
86 if ((m_nprobes & ENTRIES_PER_CACHELINE_MASK) == 0) {
87 // Move to a different cacheline.
88 // cf. knuth AOCP exercise 6.4.20.
89 // By construction, the low bits (within the cacheline) of
90 // m_bucket, m_nprobes, and m_stride should all be 0.
91 m_bucket = (m_bucket + m_nprobes + m_stride) & m_mask;
97 //*****************************************************************************
101 template <template <class> class UPDATER_, \
104 uintptr_t NULLVAL_, \
105 uintptr_t TOMBSTONE_>
107 #define CHMIMPL ConcurrentHashmapImpl<UPDATER_, HASHER_, MATCHER_, NULLVAL_, TOMBSTONE_>
111 * @brief Constructor.
112 * @param capacity Number of entries in the table. Must be a power of 2.
113 * @param hasher Hash object to use.
114 * @param matcher Key match object to use.
117 CHMIMPL::Table::Table (size_t capacity,
118 const Hasher_t& hasher /*= Hasher_t()*/,
119 const Matcher_t& matcher /*= Matcher_t()*/)
120 : m_capacity (capacity),
121 m_maxProbe (capacity / 4),
123 m_maskBits (std::countr_zero (capacity)),
128 // Clear all the keys.
129 for (size_t i = 0; i < capacity; i++) {
130 m_entries[i].m_key = nullval;
136 * @brief Allocator for table objects.
137 * @param capacity Size of the table (must be a power of 2).
139 * Allocate with enough space for the table of entries.
140 * Also align on a cache line.
143 void* CHMIMPL::Table::operator new (size_t, size_t capacity)
145 void* memptr = nullptr;
146 // Allocate aligned memory block.
147 // The Table structure includes one entry at the end,
148 // so subtract 1 from capacity.
149 posix_memalign (&memptr, CACHELINE, sizeof(Table) + (capacity-1)*sizeof(entry_t));
150 if (!memptr) std::abort();
156 * @brief Deallocator for table objects.
159 void CHMIMPL::Table::operator delete (void* p)
166 * @brief Find a table entry for reading.
167 * @param key The key for which to search.
168 * @param hash The hash of the key.
170 * Returns the matching entry, or nullptr.
173 size_t CHMIMPL::Table::probeRead (val_t key, size_t hash) const
175 // Iterate over table probes.
176 // We don't need to check more than the longest probe sequence
178 TableIterator it (hash, m_mask, m_maskBits, m_longestProbe);
180 size_t offset = it.offset();
181 const entry_t* ent = m_entries + offset;
182 if (ent->m_key == nullval) {
183 // If we hit an empty key, report failure.
186 if (m_matcher (ent->m_key, key)) {
187 // Found a matching key.
191 // Too many probes --- return failure.
197 * @brief Find a table entry for writing.
198 * @param key The key for which to search.
199 * @param hash The hash of the key.
200 * @param entry[out] The entry found.
202 * If we find the entry, return true with @c entry pointing at it.
203 * If we don't find it, and there's still room in the table, return false
204 * with @c entry pointing at the next empty entry.
205 * Otherwise, return false with @c entry set to nullptr.
208 size_t CHMIMPL::Table::probeWrite (val_t key, size_t hash, bool& insert)
210 // Iterate over table probes.
211 TableIterator it (hash, m_mask, m_maskBits, m_maxProbe);
213 size_t offset = it.offset();
214 entry_t* ent = m_entries + offset;
215 if (ent->m_key == nullval) {
216 // We hit an empty key; a new entry could be added here.
217 // Update the longest probe count.
218 CxxUtils::atomic_fetch_max (&m_longestProbe, it.nprobes()+1);
222 if (m_matcher (ent->m_key, key)) {
223 // Found a matching key.
228 // Too many probes --- return failure.
234 * @brief The number of entries in the table.
238 size_t CHMIMPL::Table::capacity() const
245 * @brief Return the entry for an offset.
246 * @param offset The index of the desired entry.
250 const typename CHMIMPL::entry_t& CHMIMPL::Table::entry (size_t offset) const
252 return m_entries[offset];
257 * @brief Return the entry for an offset (non-const).
258 * @param offset The index of the desired entry.
262 typename CHMIMPL::entry_t& CHMIMPL::Table::entry (size_t offset)
264 return m_entries[offset];
268 //*****************************************************************************
272 * @brief Constructor.
273 * @param updater Object used to manage memory
274 * (see comments at the start of the class).
275 * @param capacity Minimum initial table size.
276 * @param hasher Hash object to use.
277 * @param matcher Key match object to use.
278 * @param ctx Execution context.
281 CHMIMPL::ConcurrentHashmapImpl (Updater_t&& updater,
283 const Hasher_t& hasher,
284 const Matcher_t& matcher,
285 const typename Updater_t::Context_t& ctx)
286 : m_updater (std::move (updater)),
292 // Round up capacity to a power of 2.
293 size_t capacity = round_up (capacity_in);
295 // cppcheck-suppress noDestructor -- ownership goes to the updater
296 m_table = new (capacity) Table (capacity, hasher, matcher);
297 m_updater.update (std::unique_ptr<Table> (m_table), ctx);
302 * @brief Take a lock on the container.
304 * Take a lock on the container.
305 * The lock can then be passed to put(), allowing to factor out the locking
306 * when put() gets used in a loop. The lock will be released when the
307 * lock object is destroyed.
311 typename CHMIMPL::Lock_t CHMIMPL::lock()
313 return Lock_t (m_mutex);
318 * @brief Add an entry to the table, with external locking.
319 * @param lock The lock object returned from lock().
320 * @param key The key to insert.
321 * @param hash The hash of the key.
322 * @param val The value to insert.
323 * @param overwrite If true, then overwrite an existing entry.
324 * If false, an existing entry will not be changed.
325 * @param ctx Execution context.
327 * If the key already exists, then its value will be updated.
328 * Returns an iterator pointing at the entry and a flag which is
329 * true if a new element was added.
332 std::pair<typename CHMIMPL::const_iterator, bool>
333 CHMIMPL::put (const Lock_t& lock,
334 val_t key, size_t hash, val_t val, bool overwrite,
335 const typename Updater_t::Context_t& ctx)
337 assert (key != nullval && key != tombstone);
341 size_t offset = m_table->probeWrite (key, hash, insert);
342 if (offset != INVALID) {
343 entry_t& ent = m_table->entry (offset);
345 // Found a place to put it.
346 // Be sure not to set the key until the value is in place.
352 // Found --- update the entry if wanted.
354 if (val != ent.m_val) {
359 return std::make_pair (const_iterator (*m_table, offset), insert);
362 // Need to grow the table.
363 } while (grow (lock, ctx));
366 return std::make_pair (end(), false);
371 * @brief Add an entry to the table.
372 * @param key The key to insert.
373 * @param hash The hash of the key.
374 * @param val The value to insert.
375 * @param overwrite If true, then overwrite an existing entry.
376 * If false, an existing entry will not be changed.
377 * @param ctx Execution context.
379 * If the key already exists, then its value will be updated.
380 * Returns an iterator pointing at the entry and a flag which is
381 * true if a new element was added.
385 std::pair<typename CHMIMPL::const_iterator, bool>
386 CHMIMPL::put (val_t key, size_t hash, val_t val, bool overwrite,
387 const typename Updater_t::Context_t& ctx)
390 return put (l, key, hash, val, overwrite, ctx);
395 * @brief Look up an entry in the table.
396 * @param key The key to find.
397 * @param hash The hash of the key.
399 * Returns an iterator pointing at the found entry, or end().
402 typename CHMIMPL::const_iterator CHMIMPL::get (val_t key, size_t hash) const
404 const Table& table = m_updater.get();
405 size_t offset = table.probeRead (key, hash);
406 // Offset will be -1 if not found --- invalid iterator.
407 return const_iterator (table, offset);
412 * @brief Erase an entry from the table, with external locking.
413 * @param lock The lock object returned from lock().
414 * @param key The key to find.
415 * @param hash The hash of the key.
417 * Mark the corresponding entry as deleted.
418 * Return true on success, false on failure (key not found).
420 * The tombstone value must be different from the null value.
422 * Take care if the key or value types require memory allocation.
424 * This may cause the key type returned by an iterator to change
425 * asynchronously to the tombstone value.
429 CHMIMPL::erase (const Lock_t& /*lock*/, val_t key, size_t hash)
431 static_assert (nullval != tombstone);
432 const Table& table = m_updater.get();
433 size_t offset = table.probeRead (key, hash);
434 if (offset != INVALID) {
435 entry_t& ent = m_table->entry (offset);
438 ent.m_key = tombstone;
446 * @brief Erase an entry from the table.
447 * @param key The key to find.
448 * @param hash The hash of the key.
450 * Mark the corresponding entry as deleted.
451 * Return true on success, false on failure (key not found).
453 * The tombstone value must be different from the null value.
455 * Take care if the key or value types require memory allocation.
457 * This may cause the key type returned by an iterator to change
458 * asynchronously to the tombstone value.
463 CHMIMPL::erase (val_t key, size_t hash)
465 return erase (lock(), key, hash);
470 * @brief Return the number of items currently stored.
473 size_t CHMIMPL::size() const
480 * @brief Return the current table size.
483 size_t CHMIMPL::capacity() const
485 const Table& table = m_updater.get();
486 return table.capacity();
491 * @brief The number of erased elements in the current table.
494 size_t CHMIMPL::erased() const
501 * @brief Return the hasher object.
505 const typename CHMIMPL::Hasher_t& CHMIMPL::hasher() const
512 * @brief Return the matcher object.
516 const typename CHMIMPL::Matcher_t& CHMIMPL::matcher() const
523 * @brief Constructor.
524 * @param table The table instance we're referencing.
525 * @param end If true, initialize this to an end iterator.
526 * Otherwise, initialize it to a a begin iterator.
530 CHMIMPL::const_iterator::const_iterator (const Table& table, bool end)
534 // For an end iterator, we want offset to be -1.
535 // For a begin iterator, we need to advance to the first non-null entry.
543 * @brief Constructor.
544 * @param table The table instance we're referencing.
545 * @param offset Offset of the iterator within the table.
546 * (Must point at an occupied entry.)
549 CHMIMPL::const_iterator::const_iterator (const Table& table, size_t offset)
553 assert (offset == INVALID || m_table.entry (offset).m_key != nullval);
558 * @brief Advance the iterator to the next occupied entry.
562 void CHMIMPL::const_iterator::next()
567 if (m_offset >= m_table.capacity()) {
571 key = m_table.entry (m_offset).m_key;
572 } while (key == nullval || key == tombstone);
577 * @brief Move the iterator back to the previous occupied entry.
581 void CHMIMPL::const_iterator::prev()
583 if (m_offset == INVALID) {
584 m_offset = m_table.capacity();
589 if (m_offset >= m_table.capacity()) {
593 key = m_table.entry (m_offset).m_key;
594 } while (key == nullval || key == tombstone);
599 * @brief Return the key for this iterator.
600 * If deletions are allowed, then the key may change asynchronously
601 * to the tombstone value.
605 typename CHMIMPL::val_t CHMIMPL::const_iterator::key() const
607 return m_table.entry (m_offset).m_key;
612 * @brief Return the value for this iterator.
616 typename CHMIMPL::val_t CHMIMPL::const_iterator::value() const
618 return m_table.entry (m_offset).m_val;
623 * @brief Compare two iterators.
627 bool CHMIMPL::const_iterator::operator!= (const const_iterator& other) const
629 if (m_offset != other.m_offset) return true;
630 if (m_offset == INVALID) return false;
631 return &m_table != &other.m_table;
636 * @brief Check that the iterator is valid (not pointing at the end).
640 bool CHMIMPL::const_iterator::valid() const
642 return m_offset != INVALID;
647 * @brief Return a range that can be used to iterate over the container.
651 typename CHMIMPL::const_iterator_range CHMIMPL::range() const
653 const Table& table = m_updater.get();
654 return const_iterator_range (const_iterator (table, false),
655 const_iterator (table, true));
660 * @brief A begin iterator for the container.
664 typename CHMIMPL::const_iterator CHMIMPL::begin() const
666 const Table& table = m_updater.get();
667 return const_iterator (table, false);
672 * @brief An end iterator for the container.
676 typename CHMIMPL::const_iterator CHMIMPL::end() const
678 const Table& table = m_updater.get();
679 return const_iterator (table, true);
684 * @brief Erase the table and change the capacity.
685 * @param capacity The new table capacity.
686 * @param ctx Execution context.
688 * Returns an iterator pointing at the start of the old table.
691 typename CHMIMPL::const_iterator
692 CHMIMPL::clear (size_t capacity,
693 const typename Updater_t::Context_t& ctx)
695 capacity = round_up (capacity);
696 Lock_t lock (m_mutex);
697 std::unique_ptr<Table> new_table (new (capacity) Table (capacity,
701 // Save an iterator to the old table.
702 const_iterator it = begin();
704 // Publish the new table.
707 // cppcheck-suppress danglingLifetime -- ok
708 m_table = new_table.get();
709 m_updater.update (std::move (new_table), ctx);
715 * @brief Erase the table (don't change the capacity).
716 * @param ctx Execution context.
718 * Returns an iterator pointing at the start of the old table.
721 typename CHMIMPL::const_iterator
722 CHMIMPL::clear (const typename Updater_t::Context_t& ctx)
724 const Table& table = m_updater.get();
725 // Possible race here in that the container capacity could increase
726 // before we take out the lock in clear(). In practice, this should
727 // not actually be a problem.
728 return clear (table.capacity(), ctx);
733 * @brief Erase the table by filling it with nulls.
735 * This method is not safe to use concurrently --- no other threads
736 * may be accessing the container at the same time, either for read
740 void CHMIMPL::forceClear()
742 Lock_t lock (m_mutex);
743 size_t cap = m_table->capacity();
744 for (size_t i = 0; i < cap; i++) {
745 m_table->entry(i).m_key.store (nullval, std::memory_order_relaxed);
749 std::atomic_thread_fence (std::memory_order_seq_cst);
754 * @brief Increase the table capacity.
755 * @param capacity The new table capacity.
756 * @param ctx Execution context.
758 * No action will be taken if @c capacity is smaller
759 * than the current capacity.
762 void CHMIMPL::reserve (size_t capacity,
763 const typename Updater_t::Context_t& ctx)
765 Lock_t lock (m_mutex);
766 if (capacity < m_table->capacity()) return;
767 grow (lock, round_up (capacity), ctx);
772 * @brief Called when this thread is no longer referencing anything
773 * from this container.
774 * @param ctx Execution context.
777 void CHMIMPL::quiescent (const typename Updater_t::Context_t& ctx)
779 m_updater.quiescent (ctx);
784 * @brief Swap this container with another.
785 * @param other The container with which to swap.
787 * This will also call swap on the Updater object; hence, the Updater
788 * object must also support swap. The Hasher and Matcher instances
791 * This operation is NOT thread-safe. No other threads may be accessing
792 * either container during this operation.
795 void CHMIMPL::swap (ConcurrentHashmapImpl& other)
797 // Shouldn't be needed since we specified that no other threads can be
799 Lock_t lock (m_mutex);
800 Lock_t lock_other (other.m_mutex);
802 m_updater.swap (other.m_updater);
803 std::swap (m_table, other.m_table);
805 auto swap_atomic = [] (std::atomic<size_t>& a, std::atomic<size_t>& b)
807 size_t tmp = a.load (std::memory_order_relaxed);
808 a.store (b.load (std::memory_order_relaxed),
809 std::memory_order_relaxed);
810 b.store (tmp, std::memory_order_relaxed);
813 swap_atomic (m_size, other.m_size);
814 swap_atomic (m_erased, other.m_erased);
819 * @brief Access the Updater instance.
823 typename CHMIMPL::Updater_t& CHMIMPL::updater()
830 * @brief Make the table larger.
831 * @param ctx Execution context.
833 * Must be holding a lock on the mutex to call this.
836 bool CHMIMPL::grow (const Lock_t& lock, const typename Updater_t::Context_t& ctx)
838 // Allocate a new table with twice the capacity, unless there
839 // have been many erasures.
840 size_t new_capacity = m_erased >= m_table->capacity()/2 ?
841 m_table->capacity() : 2*m_table->capacity();
842 return grow (lock, new_capacity, ctx);
847 * @brief Make the table larger.
848 * @param new_capacity The new table capacity (must be a power of 2).
849 * @param ctx Execution context.
851 * Must be holding a lock on the mutex to call this.
854 bool CHMIMPL::grow (const Lock_t& /*lock*/,
856 const typename Updater_t::Context_t& ctx)
858 // The current table.
859 const Table& table = *m_table;
861 std::unique_ptr<Table> new_table (new (new_capacity) Table (new_capacity,
865 // Copy data from the existing table to the new one.
866 size_t capacity = table.capacity();
867 for (size_t i = 0; i < capacity; i++) {
868 const entry_t& ent = table.entry(i);
869 if (ent.m_key != nullval) {
870 size_t hash = m_hasher (ent.m_key);
872 size_t offset = new_table->probeWrite (ent.m_key, hash, insert);
873 if (offset == INVALID) {
876 entry_t& new_entry = new_table->entry (offset);
877 new_entry.m_key = static_cast<val_t> (ent.m_key);
878 new_entry.m_val = static_cast<val_t> (ent.m_val);
884 // Publish the new table.
885 // cppcheck-suppress danglingLifetime -- ok
886 m_table = new_table.get();
887 m_updater.update (std::move (new_table), ctx);
894 * @brief Round up to a power of 2.
897 uint64_t CHMIMPL::round_up (uint64_t x)
899 if (x <= 64) return 64;
900 return std::bit_ceil (x);
909 } // namespace detail
910 } // namespace CxxUtils