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 m_table = new (capacity) Table (capacity, hasher, matcher);
296 m_updater.update (std::unique_ptr<Table> (m_table), ctx);
301 * @brief Take a lock on the container.
303 * Take a lock on the container.
304 * The lock can then be passed to put(), allowing to factor out the locking
305 * when put() gets used in a loop. The lock will be released when the
306 * lock object is destroyed.
310 typename CHMIMPL::Lock_t CHMIMPL::lock()
312 return Lock_t (m_mutex);
317 * @brief Add an entry to the table, with external locking.
318 * @param lock The lock object returned from lock().
319 * @param key The key to insert.
320 * @param hash The hash of the key.
321 * @param val The value to insert.
322 * @param overwrite If true, then overwrite an existing entry.
323 * If false, an existing entry will not be changed.
324 * @param ctx Execution context.
326 * If the key already exists, then its value will be updated.
327 * Returns an iterator pointing at the entry and a flag which is
328 * true if a new element was added.
331 std::pair<typename CHMIMPL::const_iterator, bool>
332 CHMIMPL::put (const Lock_t& lock,
333 val_t key, size_t hash, val_t val, bool overwrite,
334 const typename Updater_t::Context_t& ctx)
336 assert (key != nullval && key != tombstone);
340 size_t offset = m_table->probeWrite (key, hash, insert);
341 if (offset != INVALID) {
342 entry_t& ent = m_table->entry (offset);
344 // Found a place to put it.
345 // Be sure not to set the key until the value is in place.
351 // Found --- update the entry if wanted.
353 if (val != ent.m_val) {
358 return std::make_pair (const_iterator (*m_table, offset), insert);
361 // Need to grow the table.
362 } while (grow (lock, ctx));
365 return std::make_pair (end(), false);
370 * @brief Add an entry to the table.
371 * @param key The key to insert.
372 * @param hash The hash of the key.
373 * @param val The value to insert.
374 * @param overwrite If true, then overwrite an existing entry.
375 * If false, an existing entry will not be changed.
376 * @param ctx Execution context.
378 * If the key already exists, then its value will be updated.
379 * Returns an iterator pointing at the entry and a flag which is
380 * true if a new element was added.
384 std::pair<typename CHMIMPL::const_iterator, bool>
385 CHMIMPL::put (val_t key, size_t hash, val_t val, bool overwrite,
386 const typename Updater_t::Context_t& ctx)
389 return put (l, key, hash, val, overwrite, ctx);
394 * @brief Look up an entry in the table.
395 * @param key The key to find.
396 * @param hash The hash of the key.
398 * Returns an iterator pointing at the found entry, or end().
401 typename CHMIMPL::const_iterator CHMIMPL::get (val_t key, size_t hash) const
403 const Table& table = m_updater.get();
404 size_t offset = table.probeRead (key, hash);
405 // Offset will be -1 if not found --- invalid iterator.
406 return const_iterator (table, offset);
411 * @brief Erase an entry from the table, with external locking.
412 * @param lock The lock object returned from lock().
413 * @param key The key to find.
414 * @param hash The hash of the key.
416 * Mark the corresponding entry as deleted.
417 * Return true on success, false on failure (key not found).
419 * The tombstone value must be different from the null value.
421 * Take care if the key or value types require memory allocation.
423 * This may cause the key type returned by an iterator to change
424 * asynchronously to the tombstone value.
428 CHMIMPL::erase (const Lock_t& /*lock*/, val_t key, size_t hash)
430 static_assert (nullval != tombstone);
431 const Table& table = m_updater.get();
432 size_t offset = table.probeRead (key, hash);
433 if (offset != INVALID) {
434 entry_t& ent = m_table->entry (offset);
437 ent.m_key = tombstone;
445 * @brief Erase an entry from the table.
446 * @param key The key to find.
447 * @param hash The hash of the key.
449 * Mark the corresponding entry as deleted.
450 * Return true on success, false on failure (key not found).
452 * The tombstone value must be different from the null value.
454 * Take care if the key or value types require memory allocation.
456 * This may cause the key type returned by an iterator to change
457 * asynchronously to the tombstone value.
462 CHMIMPL::erase (val_t key, size_t hash)
464 return erase (lock(), key, hash);
469 * @brief Return the number of items currently stored.
472 size_t CHMIMPL::size() const
479 * @brief Return the current table size.
482 size_t CHMIMPL::capacity() const
484 const Table& table = m_updater.get();
485 return table.capacity();
490 * @brief The number of erased elements in the current table.
493 size_t CHMIMPL::erased() const
500 * @brief Return the hasher object.
504 const typename CHMIMPL::Hasher_t& CHMIMPL::hasher() const
511 * @brief Return the matcher object.
515 const typename CHMIMPL::Matcher_t& CHMIMPL::matcher() const
522 * @brief Constructor.
523 * @param table The table instance we're referencing.
524 * @param end If true, initialize this to an end iterator.
525 * Otherwise, initialize it to a a begin iterator.
529 CHMIMPL::const_iterator::const_iterator (const Table& table, bool end)
533 // For an end iterator, we want offset to be -1.
534 // For a begin iterator, we need to advance to the first non-null entry.
542 * @brief Constructor.
543 * @param table The table instance we're referencing.
544 * @param offset Offset of the iterator within the table.
545 * (Must point at an occupied entry.)
548 CHMIMPL::const_iterator::const_iterator (const Table& table, size_t offset)
552 assert (offset == INVALID || m_table.entry (offset).m_key != nullval);
557 * @brief Advance the iterator to the next occupied entry.
561 void CHMIMPL::const_iterator::next()
566 if (m_offset >= m_table.capacity()) {
570 key = m_table.entry (m_offset).m_key;
571 } while (key == nullval || key == tombstone);
576 * @brief Move the iterator back to the previous occupied entry.
580 void CHMIMPL::const_iterator::prev()
582 if (m_offset == INVALID) {
583 m_offset = m_table.capacity();
588 if (m_offset >= m_table.capacity()) {
592 key = m_table.entry (m_offset).m_key;
593 } while (key == nullval || key == tombstone);
598 * @brief Return the key for this iterator.
599 * If deletions are allowed, then the key may change asynchronously
600 * to the tombstone value.
604 typename CHMIMPL::val_t CHMIMPL::const_iterator::key() const
606 return m_table.entry (m_offset).m_key;
611 * @brief Return the value for this iterator.
615 typename CHMIMPL::val_t CHMIMPL::const_iterator::value() const
617 return m_table.entry (m_offset).m_val;
622 * @brief Compare two iterators.
626 bool CHMIMPL::const_iterator::operator!= (const const_iterator& other) const
628 if (m_offset != other.m_offset) return true;
629 if (m_offset == INVALID) return false;
630 return &m_table != &other.m_table;
635 * @brief Check that the iterator is valid (not pointing at the end).
639 bool CHMIMPL::const_iterator::valid() const
641 return m_offset != INVALID;
646 * @brief Return a range that can be used to iterate over the container.
650 typename CHMIMPL::const_iterator_range CHMIMPL::range() const
652 const Table& table = m_updater.get();
653 return const_iterator_range (const_iterator (table, false),
654 const_iterator (table, true));
659 * @brief A begin iterator for the container.
663 typename CHMIMPL::const_iterator CHMIMPL::begin() const
665 const Table& table = m_updater.get();
666 return const_iterator (table, false);
671 * @brief An end iterator for the container.
675 typename CHMIMPL::const_iterator CHMIMPL::end() const
677 const Table& table = m_updater.get();
678 return const_iterator (table, true);
683 * @brief Erase the table and change the capacity.
684 * @param capacity The new table capacity.
685 * @param ctx Execution context.
687 * Returns an iterator pointing at the start of the old table.
690 typename CHMIMPL::const_iterator
691 CHMIMPL::clear (size_t capacity,
692 const typename Updater_t::Context_t& ctx)
694 capacity = round_up (capacity);
695 Lock_t lock (m_mutex);
696 std::unique_ptr<Table> new_table (new (capacity) Table (capacity,
700 // Save an iterator to the old table.
701 const_iterator it = begin();
703 // Publish the new table.
706 m_table = new_table.get();
707 m_updater.update (std::move (new_table), ctx);
713 * @brief Erase the table (don't change the capacity).
714 * @param ctx Execution context.
716 * Returns an iterator pointing at the start of the old table.
719 typename CHMIMPL::const_iterator
720 CHMIMPL::clear (const typename Updater_t::Context_t& ctx)
722 const Table& table = m_updater.get();
723 // Possible race here in that the container capacity could increase
724 // before we take out the lock in clear(). In practice, this should
725 // not actually be a problem.
726 return clear (table.capacity(), ctx);
731 * @brief Erase the table by filling it with nulls.
733 * This method is not safe to use concurrently --- no other threads
734 * may be accessing the container at the same time, either for read
738 void CHMIMPL::forceClear()
740 Lock_t lock (m_mutex);
741 size_t cap = m_table->capacity();
742 for (size_t i = 0; i < cap; i++) {
743 m_table->entry(i).m_key.store (nullval, std::memory_order_relaxed);
747 std::atomic_thread_fence (std::memory_order_seq_cst);
752 * @brief Increase the table capacity.
753 * @param capacity The new table capacity.
754 * @param ctx Execution context.
756 * No action will be taken if @c capacity is smaller
757 * than the current capacity.
760 void CHMIMPL::reserve (size_t capacity,
761 const typename Updater_t::Context_t& ctx)
763 Lock_t lock (m_mutex);
764 if (capacity < m_table->capacity()) return;
765 grow (lock, round_up (capacity), ctx);
770 * @brief Called when this thread is no longer referencing anything
771 * from this container.
772 * @param ctx Execution context.
775 void CHMIMPL::quiescent (const typename Updater_t::Context_t& ctx)
777 m_updater.quiescent (ctx);
782 * @brief Swap this container with another.
783 * @param other The container with which to swap.
785 * This will also call swap on the Updater object; hence, the Updater
786 * object must also support swap. The Hasher and Matcher instances
789 * This operation is NOT thread-safe. No other threads may be accessing
790 * either container during this operation.
793 void CHMIMPL::swap (ConcurrentHashmapImpl& other)
795 // Shouldn't be needed since we specified that no other threads can be
797 Lock_t lock (m_mutex);
798 Lock_t lock_other (other.m_mutex);
800 m_updater.swap (other.m_updater);
801 std::swap (m_table, other.m_table);
803 auto swap_atomic = [] (std::atomic<size_t>& a, std::atomic<size_t>& b)
805 size_t tmp = a.load (std::memory_order_relaxed);
806 a.store (b.load (std::memory_order_relaxed),
807 std::memory_order_relaxed);
808 b.store (tmp, std::memory_order_relaxed);
811 swap_atomic (m_size, other.m_size);
812 swap_atomic (m_erased, other.m_erased);
817 * @brief Access the Updater instance.
821 typename CHMIMPL::Updater_t& CHMIMPL::updater()
828 * @brief Make the table larger.
829 * @param ctx Execution context.
831 * Must be holding a lock on the mutex to call this.
834 bool CHMIMPL::grow (const Lock_t& lock, const typename Updater_t::Context_t& ctx)
836 // Allocate a new table with twice the capacity, unless there
837 // have been many erasures.
838 size_t new_capacity = m_erased >= m_table->capacity()/2 ?
839 m_table->capacity() : 2*m_table->capacity();
840 return grow (lock, new_capacity, ctx);
845 * @brief Make the table larger.
846 * @param new_capacity The new table capacity (must be a power of 2).
847 * @param ctx Execution context.
849 * Must be holding a lock on the mutex to call this.
852 bool CHMIMPL::grow (const Lock_t& /*lock*/,
854 const typename Updater_t::Context_t& ctx)
856 // The current table.
857 const Table& table = *m_table;
859 std::unique_ptr<Table> new_table (new (new_capacity) Table (new_capacity,
863 // Copy data from the existing table to the new one.
864 size_t capacity = table.capacity();
865 for (size_t i = 0; i < capacity; i++) {
866 const entry_t& ent = table.entry(i);
867 if (ent.m_key != nullval) {
868 size_t hash = m_hasher (ent.m_key);
870 size_t offset = new_table->probeWrite (ent.m_key, hash, insert);
871 if (offset == INVALID) {
874 entry_t& new_entry = new_table->entry (offset);
875 new_entry.m_key = static_cast<val_t> (ent.m_key);
876 new_entry.m_val = static_cast<val_t> (ent.m_val);
882 // Publish the new table.
883 m_table = new_table.get();
884 m_updater.update (std::move (new_table), ctx);
891 * @brief Round up to a power of 2.
894 uint64_t CHMIMPL::round_up (uint64_t x)
896 if (x <= 64) return 64;
897 return std::bit_ceil (x);
906 } // namespace detail
907 } // namespace CxxUtils