diff options
author | Benjamin Barenblat <bbaren@google.com> | 2023-09-07 13:16:09 -0400 |
---|---|---|
committer | Benjamin Barenblat <bbaren@google.com> | 2023-09-07 13:16:09 -0400 |
commit | 6fdbff8bbce2a1debdc060df381f39e3dcfb65af (patch) | |
tree | 71f1ef38477a65d5cce472fc042c90087c2bb351 /absl/container/internal/raw_hash_set.h | |
parent | 8d4a80fe37176b1170d7dce0772dea9584ec3e32 (diff) | |
parent | 29bf8085f3bf17b84d30e34b3d7ff8248fda404e (diff) | |
download | abseil-6fdbff8bbce2a1debdc060df381f39e3dcfb65af.tar.gz abseil-6fdbff8bbce2a1debdc060df381f39e3dcfb65af.tar.bz2 abseil-6fdbff8bbce2a1debdc060df381f39e3dcfb65af.zip |
Merge new upstream LTS 20230802.0
Diffstat (limited to 'absl/container/internal/raw_hash_set.h')
-rw-r--r-- | absl/container/internal/raw_hash_set.h | 673 |
1 files changed, 437 insertions, 236 deletions
diff --git a/absl/container/internal/raw_hash_set.h b/absl/container/internal/raw_hash_set.h index 61ef196d..5f89d8ef 100644 --- a/absl/container/internal/raw_hash_set.h +++ b/absl/container/internal/raw_hash_set.h @@ -62,6 +62,8 @@ // pseudo-struct: // // struct BackingArray { +// // The number of elements we can insert before growing the capacity. +// size_t growth_left; // // Control bytes for the "real" slots. // ctrl_t ctrl[capacity]; // // Always `ctrl_t::kSentinel`. This is used by iterators to find when to @@ -115,7 +117,7 @@ // starting with that index and extract potential candidates: occupied slots // with a control byte equal to `H2(hash(x))`. If we find an empty slot in the // group, we stop and return an error. Each candidate slot `y` is compared with -// `x`; if `x == y`, we are done and return `&y`; otherwise we contine to the +// `x`; if `x == y`, we are done and return `&y`; otherwise we continue to the // next probe index. Tombstones effectively behave like full slots that never // match the value we're looking for. // @@ -174,21 +176,23 @@ #include <algorithm> #include <cmath> +#include <cstddef> #include <cstdint> #include <cstring> #include <iterator> #include <limits> #include <memory> +#include <string> #include <tuple> #include <type_traits> #include <utility> #include "absl/base/config.h" #include "absl/base/internal/endian.h" -#include "absl/base/internal/prefetch.h" #include "absl/base/internal/raw_logging.h" #include "absl/base/optimization.h" #include "absl/base/port.h" +#include "absl/base/prefetch.h" #include "absl/container/internal/common.h" #include "absl/container/internal/compressed_tuple.h" #include "absl/container/internal/container_memory.h" @@ -235,6 +239,14 @@ namespace container_internal { // We use uint8_t so we don't need to worry about padding. using GenerationType = uint8_t; +// A sentinel value for empty generations. Using 0 makes it easy to constexpr +// initialize an array of this value. +constexpr GenerationType SentinelEmptyGeneration() { return 0; } + +constexpr GenerationType NextGeneration(GenerationType generation) { + return ++generation == SentinelEmptyGeneration() ? ++generation : generation; +} + #ifdef ABSL_SWISSTABLE_ENABLE_GENERATIONS constexpr bool SwisstableGenerationsEnabled() { return true; } constexpr size_t NumGenerationBytes() { return sizeof(GenerationType); } @@ -367,12 +379,12 @@ class NonIterableBitMask { return static_cast<uint32_t>((bit_width(mask_) - 1) >> Shift); } - // Return the number of trailing zero *abstract* bits. + // Returns the number of trailing zero *abstract* bits. uint32_t TrailingZeros() const { return container_internal::TrailingZeros(mask_) >> Shift; } - // Return the number of leading zero *abstract* bits. + // Returns the number of leading zero *abstract* bits. uint32_t LeadingZeros() const { constexpr int total_significant_bits = SignificantBits << Shift; constexpr int extra_bits = sizeof(T) * 8 - total_significant_bits; @@ -475,19 +487,23 @@ static_assert(ctrl_t::kDeleted == static_cast<ctrl_t>(-2), "ctrl_t::kDeleted must be -2 to make the implementation of " "ConvertSpecialToEmptyAndFullToDeleted efficient"); -ABSL_DLL extern const ctrl_t kEmptyGroup[17]; +// See definition comment for why this is size 32. +ABSL_DLL extern const ctrl_t kEmptyGroup[32]; // Returns a pointer to a control byte group that can be used by empty tables. inline ctrl_t* EmptyGroup() { // Const must be cast away here; no uses of this function will actually write // to it, because it is only used for empty tables. - return const_cast<ctrl_t*>(kEmptyGroup); + return const_cast<ctrl_t*>(kEmptyGroup + 16); } -// Returns a pointer to the generation byte at the end of the empty group, if it -// exists. -inline GenerationType* EmptyGeneration() { - return reinterpret_cast<GenerationType*>(EmptyGroup() + 16); +// Returns a pointer to a generation to use for an empty hashtable. +GenerationType* EmptyGeneration(); + +// Returns whether `generation` is a generation for an empty hashtable that +// could be returned by EmptyGeneration(). +inline bool IsEmptyGeneration(const GenerationType* generation) { + return *generation == SentinelEmptyGeneration(); } // Mixes a randomly generated per-process seed with `hash` and `ctrl` to @@ -674,9 +690,10 @@ struct GroupAArch64Impl { void ConvertSpecialToEmptyAndFullToDeleted(ctrl_t* dst) const { uint64_t mask = vget_lane_u64(vreinterpret_u64_u8(ctrl), 0); constexpr uint64_t msbs = 0x8080808080808080ULL; - constexpr uint64_t lsbs = 0x0101010101010101ULL; - auto x = mask & msbs; - auto res = (~x + (x >> 7)) & ~lsbs; + constexpr uint64_t slsbs = 0x0202020202020202ULL; + constexpr uint64_t midbs = 0x7e7e7e7e7e7e7e7eULL; + auto x = slsbs & (mask >> 6); + auto res = (x + midbs) | msbs; little_endian::Store64(dst, res); } @@ -749,6 +766,15 @@ using Group = GroupAArch64Impl; using Group = GroupPortableImpl; #endif +// When there is an insertion with no reserved growth, we rehash with +// probability `min(1, RehashProbabilityConstant() / capacity())`. Using a +// constant divided by capacity ensures that inserting N elements is still O(N) +// in the average case. Using the constant 16 means that we expect to rehash ~8 +// times more often than when generations are disabled. We are adding expected +// rehash_probability * #insertions/capacity_growth = 16/capacity * ((7/8 - +// 7/16) * capacity)/capacity_growth = ~7 extra rehashes per capacity growth. +inline size_t RehashProbabilityConstant() { return 16; } + class CommonFieldsGenerationInfoEnabled { // A sentinel value for reserved_growth_ indicating that we just ran out of // reserved growth on the last insertion. When reserve is called and then @@ -760,8 +786,11 @@ class CommonFieldsGenerationInfoEnabled { public: CommonFieldsGenerationInfoEnabled() = default; CommonFieldsGenerationInfoEnabled(CommonFieldsGenerationInfoEnabled&& that) - : reserved_growth_(that.reserved_growth_), generation_(that.generation_) { + : reserved_growth_(that.reserved_growth_), + reservation_size_(that.reservation_size_), + generation_(that.generation_) { that.reserved_growth_ = 0; + that.reservation_size_ = 0; that.generation_ = EmptyGeneration(); } CommonFieldsGenerationInfoEnabled& operator=( @@ -769,19 +798,17 @@ class CommonFieldsGenerationInfoEnabled { // Whether we should rehash on insert in order to detect bugs of using invalid // references. We rehash on the first insertion after reserved_growth_ reaches - // 0 after a call to reserve. - // TODO(b/254649633): we could potentially do a rehash with low probability + // 0 after a call to reserve. We also do a rehash with low probability // whenever reserved_growth_ is zero. - bool should_rehash_for_bug_detection_on_insert() const { - return reserved_growth_ == kReservedGrowthJustRanOut; - } + bool should_rehash_for_bug_detection_on_insert(const ctrl_t* ctrl, + size_t capacity) const; void maybe_increment_generation_on_insert() { if (reserved_growth_ == kReservedGrowthJustRanOut) reserved_growth_ = 0; if (reserved_growth_ > 0) { if (--reserved_growth_ == 0) reserved_growth_ = kReservedGrowthJustRanOut; } else { - ++*generation_; + *generation_ = NextGeneration(*generation_); } } void reset_reserved_growth(size_t reservation, size_t size) { @@ -789,6 +816,8 @@ class CommonFieldsGenerationInfoEnabled { } size_t reserved_growth() const { return reserved_growth_; } void set_reserved_growth(size_t r) { reserved_growth_ = r; } + size_t reservation_size() const { return reservation_size_; } + void set_reservation_size(size_t r) { reservation_size_ = r; } GenerationType generation() const { return *generation_; } void set_generation(GenerationType g) { *generation_ = g; } GenerationType* generation_ptr() const { return generation_; } @@ -796,10 +825,14 @@ class CommonFieldsGenerationInfoEnabled { private: // The number of insertions remaining that are guaranteed to not rehash due to - // a prior call to reserve. Note: we store reserved growth rather than + // a prior call to reserve. Note: we store reserved growth in addition to // reservation size because calls to erase() decrease size_ but don't decrease // reserved growth. size_t reserved_growth_ = 0; + // The maximum argument to reserve() since the container was cleared. We need + // to keep track of this, in addition to reserved growth, because we reset + // reserved growth to this when erase(begin(), end()) is called. + size_t reservation_size_ = 0; // Pointer to the generation counter, which is used to validate iterators and // is stored in the backing array between the control bytes and the slots. // Note that we can't store the generation inside the container itself and @@ -820,11 +853,15 @@ class CommonFieldsGenerationInfoDisabled { CommonFieldsGenerationInfoDisabled& operator=( CommonFieldsGenerationInfoDisabled&&) = default; - bool should_rehash_for_bug_detection_on_insert() const { return false; } + bool should_rehash_for_bug_detection_on_insert(const ctrl_t*, size_t) const { + return false; + } void maybe_increment_generation_on_insert() {} void reset_reserved_growth(size_t, size_t) {} size_t reserved_growth() const { return 0; } void set_reserved_growth(size_t) {} + size_t reservation_size() const { return 0; } + void set_reservation_size(size_t) {} GenerationType generation() const { return 0; } void set_generation(GenerationType) {} GenerationType* generation_ptr() const { return nullptr; } @@ -867,6 +904,44 @@ using CommonFieldsGenerationInfo = CommonFieldsGenerationInfoDisabled; using HashSetIteratorGenerationInfo = HashSetIteratorGenerationInfoDisabled; #endif +// Returns whether `n` is a valid capacity (i.e., number of slots). +// +// A valid capacity is a non-zero integer `2^m - 1`. +inline bool IsValidCapacity(size_t n) { return ((n + 1) & n) == 0 && n > 0; } + +// Computes the offset from the start of the backing allocation of the control +// bytes. growth_left is stored at the beginning of the backing array. +inline size_t ControlOffset() { return sizeof(size_t); } + +// Returns the number of "cloned control bytes". +// +// This is the number of control bytes that are present both at the beginning +// of the control byte array and at the end, such that we can create a +// `Group::kWidth`-width probe window starting from any control byte. +constexpr size_t NumClonedBytes() { return Group::kWidth - 1; } + +// Given the capacity of a table, computes the offset (from the start of the +// backing allocation) of the generation counter (if it exists). +inline size_t GenerationOffset(size_t capacity) { + assert(IsValidCapacity(capacity)); + const size_t num_control_bytes = capacity + 1 + NumClonedBytes(); + return ControlOffset() + num_control_bytes; +} + +// Given the capacity of a table, computes the offset (from the start of the +// backing allocation) at which the slots begin. +inline size_t SlotOffset(size_t capacity, size_t slot_align) { + assert(IsValidCapacity(capacity)); + return (GenerationOffset(capacity) + NumGenerationBytes() + slot_align - 1) & + (~slot_align + 1); +} + +// Given the capacity of a table, computes the total size of the backing +// array. +inline size_t AllocSize(size_t capacity, size_t slot_size, size_t slot_align) { + return SlotOffset(capacity, slot_align) + capacity * slot_size; +} + // CommonFields hold the fields in raw_hash_set that do not depend // on template parameters. This allows us to conveniently pass all // of this state to helper functions as a single argument. @@ -884,72 +959,102 @@ class CommonFields : public CommonFieldsGenerationInfo { std::move(static_cast<CommonFieldsGenerationInfo&&>(that))), // Explicitly copying fields into "this" and then resetting "that" // fields generates less code then calling absl::exchange per field. - control_(that.control_), - slots_(that.slots_), - size_(that.size_), - capacity_(that.capacity_), - compressed_tuple_(that.growth_left(), std::move(that.infoz())) { - that.control_ = EmptyGroup(); - that.slots_ = nullptr; - that.size_ = 0; - that.capacity_ = 0; - that.growth_left() = 0; + control_(that.control()), + slots_(that.slot_array()), + capacity_(that.capacity()), + compressed_tuple_(that.size(), std::move(that.infoz())) { + that.set_control(EmptyGroup()); + that.set_slots(nullptr); + that.set_capacity(0); + that.set_size(0); } CommonFields& operator=(CommonFields&&) = default; + ctrl_t* control() const { return control_; } + void set_control(ctrl_t* c) { control_ = c; } + void* backing_array_start() const { + // growth_left is stored before control bytes. + assert(reinterpret_cast<uintptr_t>(control()) % alignof(size_t) == 0); + return control() - sizeof(size_t); + } + + // Note: we can't use slots() because Qt defines "slots" as a macro. + void* slot_array() const { return slots_; } + void set_slots(void* s) { slots_ = s; } + + // The number of filled slots. + size_t size() const { return compressed_tuple_.template get<0>(); } + void set_size(size_t s) { compressed_tuple_.template get<0>() = s; } + + // The total number of available slots. + size_t capacity() const { return capacity_; } + void set_capacity(size_t c) { + assert(c == 0 || IsValidCapacity(c)); + capacity_ = c; + } + // The number of slots we can still fill without needing to rehash. - size_t& growth_left() { return compressed_tuple_.template get<0>(); } + // This is stored in the heap allocation before the control bytes. + size_t growth_left() const { + return *reinterpret_cast<size_t*>(backing_array_start()); + } + void set_growth_left(size_t gl) { + *reinterpret_cast<size_t*>(backing_array_start()) = gl; + } HashtablezInfoHandle& infoz() { return compressed_tuple_.template get<1>(); } const HashtablezInfoHandle& infoz() const { return compressed_tuple_.template get<1>(); } + bool should_rehash_for_bug_detection_on_insert() const { + return CommonFieldsGenerationInfo:: + should_rehash_for_bug_detection_on_insert(control(), capacity()); + } void reset_reserved_growth(size_t reservation) { - CommonFieldsGenerationInfo::reset_reserved_growth(reservation, size_); + CommonFieldsGenerationInfo::reset_reserved_growth(reservation, size()); + } + + // The size of the backing array allocation. + size_t alloc_size(size_t slot_size, size_t slot_align) const { + return AllocSize(capacity(), slot_size, slot_align); } + // Returns the number of control bytes set to kDeleted. For testing only. + size_t TombstonesCount() const { + return static_cast<size_t>( + std::count(control(), control() + capacity(), ctrl_t::kDeleted)); + } + + private: // TODO(b/259599413): Investigate removing some of these fields: // - control/slots can be derived from each other - // - size can be moved into the slot array + // - we can use 6 bits for capacity since it's always a power of two minus 1 - // The control bytes (and, also, a pointer to the base of the backing array). + // The control bytes (and, also, a pointer near to the base of the backing + // array). // // This contains `capacity + 1 + NumClonedBytes()` entries, even // when the table is empty (hence EmptyGroup). + // + // Note that growth_left is stored immediately before this pointer. ctrl_t* control_ = EmptyGroup(); // The beginning of the slots, located at `SlotOffset()` bytes after // `control`. May be null for empty tables. void* slots_ = nullptr; - // The number of filled slots. - size_t size_ = 0; - - // The total number of available slots. size_t capacity_ = 0; - // Bundle together growth_left and HashtablezInfoHandle to ensure EBO for + // Bundle together size and HashtablezInfoHandle to ensure EBO for // HashtablezInfoHandle when sampling is turned off. absl::container_internal::CompressedTuple<size_t, HashtablezInfoHandle> compressed_tuple_{0u, HashtablezInfoHandle{}}; }; -// Returns he number of "cloned control bytes". -// -// This is the number of control bytes that are present both at the beginning -// of the control byte array and at the end, such that we can create a -// `Group::kWidth`-width probe window starting from any control byte. -constexpr size_t NumClonedBytes() { return Group::kWidth - 1; } - template <class Policy, class Hash, class Eq, class Alloc> class raw_hash_set; -// Returns whether `n` is a valid capacity (i.e., number of slots). -// -// A valid capacity is a non-zero integer `2^m - 1`. -inline bool IsValidCapacity(size_t n) { return ((n + 1) & n) == 0 && n > 0; } - // Returns the next valid capacity after `n`. inline size_t NextCapacity(size_t n) { assert(IsValidCapacity(n) || n == 0); @@ -1021,34 +1126,75 @@ size_t SelectBucketCountForIterRange(InputIter first, InputIter last, return 0; } -#define ABSL_INTERNAL_ASSERT_IS_FULL(ctrl, generation, generation_ptr, \ - operation) \ - do { \ - ABSL_HARDENING_ASSERT( \ - (ctrl != nullptr) && operation \ - " called on invalid iterator. The iterator might be an end() " \ - "iterator or may have been default constructed."); \ - if (SwisstableGenerationsEnabled() && generation != *generation_ptr) \ - ABSL_INTERNAL_LOG(FATAL, operation \ - " called on invalidated iterator. The table could " \ - "have rehashed since this iterator was initialized."); \ - ABSL_HARDENING_ASSERT( \ - (IsFull(*ctrl)) && operation \ - " called on invalid iterator. The element might have been erased or " \ - "the table might have rehashed."); \ - } while (0) +constexpr bool SwisstableDebugEnabled() { +#if defined(ABSL_SWISSTABLE_ENABLE_GENERATIONS) || \ + ABSL_OPTION_HARDENED == 1 || !defined(NDEBUG) + return true; +#else + return false; +#endif +} + +inline void AssertIsFull(const ctrl_t* ctrl, GenerationType generation, + const GenerationType* generation_ptr, + const char* operation) { + if (!SwisstableDebugEnabled()) return; + if (ctrl == nullptr) { + ABSL_INTERNAL_LOG(FATAL, + std::string(operation) + " called on end() iterator."); + } + if (ctrl == EmptyGroup()) { + ABSL_INTERNAL_LOG(FATAL, std::string(operation) + + " called on default-constructed iterator."); + } + if (SwisstableGenerationsEnabled()) { + if (generation != *generation_ptr) { + ABSL_INTERNAL_LOG(FATAL, + std::string(operation) + + " called on invalid iterator. The table could have " + "rehashed since this iterator was initialized."); + } + if (!IsFull(*ctrl)) { + ABSL_INTERNAL_LOG( + FATAL, + std::string(operation) + + " called on invalid iterator. The element was likely erased."); + } + } else { + if (!IsFull(*ctrl)) { + ABSL_INTERNAL_LOG( + FATAL, + std::string(operation) + + " called on invalid iterator. The element might have been erased " + "or the table might have rehashed. Consider running with " + "--config=asan to diagnose rehashing issues."); + } + } +} // Note that for comparisons, null/end iterators are valid. inline void AssertIsValidForComparison(const ctrl_t* ctrl, GenerationType generation, const GenerationType* generation_ptr) { - ABSL_HARDENING_ASSERT((ctrl == nullptr || IsFull(*ctrl)) && - "Invalid iterator comparison. The element might have " - "been erased or the table might have rehashed."); - if (SwisstableGenerationsEnabled() && generation != *generation_ptr) { - ABSL_INTERNAL_LOG(FATAL, - "Invalid iterator comparison. The table could have " - "rehashed since this iterator was initialized."); + if (!SwisstableDebugEnabled()) return; + const bool ctrl_is_valid_for_comparison = + ctrl == nullptr || ctrl == EmptyGroup() || IsFull(*ctrl); + if (SwisstableGenerationsEnabled()) { + if (generation != *generation_ptr) { + ABSL_INTERNAL_LOG(FATAL, + "Invalid iterator comparison. The table could have " + "rehashed since this iterator was initialized."); + } + if (!ctrl_is_valid_for_comparison) { + ABSL_INTERNAL_LOG( + FATAL, "Invalid iterator comparison. The element was likely erased."); + } + } else { + ABSL_HARDENING_ASSERT( + ctrl_is_valid_for_comparison && + "Invalid iterator comparison. The element might have been erased or " + "the table might have rehashed. Consider running with --config=asan to " + "diagnose rehashing issues."); } } @@ -1074,16 +1220,54 @@ inline bool AreItersFromSameContainer(const ctrl_t* ctrl_a, // Asserts that two iterators come from the same container. // Note: we take slots by reference so that it's not UB if they're uninitialized // as long as we don't read them (when ctrl is null). -// TODO(b/254649633): when generations are enabled, we can detect more cases of -// different containers by comparing the pointers to the generations - this -// can cover cases of end iterators that we would otherwise miss. inline void AssertSameContainer(const ctrl_t* ctrl_a, const ctrl_t* ctrl_b, const void* const& slot_a, - const void* const& slot_b) { - ABSL_HARDENING_ASSERT( - AreItersFromSameContainer(ctrl_a, ctrl_b, slot_a, slot_b) && - "Invalid iterator comparison. The iterators may be from different " - "containers or the container might have rehashed."); + const void* const& slot_b, + const GenerationType* generation_ptr_a, + const GenerationType* generation_ptr_b) { + if (!SwisstableDebugEnabled()) return; + const bool a_is_default = ctrl_a == EmptyGroup(); + const bool b_is_default = ctrl_b == EmptyGroup(); + if (a_is_default != b_is_default) { + ABSL_INTERNAL_LOG( + FATAL, + "Invalid iterator comparison. Comparing default-constructed iterator " + "with non-default-constructed iterator."); + } + if (a_is_default && b_is_default) return; + + if (SwisstableGenerationsEnabled()) { + if (generation_ptr_a == generation_ptr_b) return; + const bool a_is_empty = IsEmptyGeneration(generation_ptr_a); + const bool b_is_empty = IsEmptyGeneration(generation_ptr_b); + if (a_is_empty != b_is_empty) { + ABSL_INTERNAL_LOG(FATAL, + "Invalid iterator comparison. Comparing iterator from " + "a non-empty hashtable with an iterator from an empty " + "hashtable."); + } + if (a_is_empty && b_is_empty) { + ABSL_INTERNAL_LOG(FATAL, + "Invalid iterator comparison. Comparing iterators from " + "different empty hashtables."); + } + const bool a_is_end = ctrl_a == nullptr; + const bool b_is_end = ctrl_b == nullptr; + if (a_is_end || b_is_end) { + ABSL_INTERNAL_LOG(FATAL, + "Invalid iterator comparison. Comparing iterator with " + "an end() iterator from a different hashtable."); + } + ABSL_INTERNAL_LOG(FATAL, + "Invalid iterator comparison. Comparing non-end() " + "iterators from different hashtables."); + } else { + ABSL_HARDENING_ASSERT( + AreItersFromSameContainer(ctrl_a, ctrl_b, slot_a, slot_b) && + "Invalid iterator comparison. The iterators may be from different " + "containers or the container might have rehashed. Consider running " + "with --config=asan to diagnose rehashing issues."); + } } struct FindInfo { @@ -1106,11 +1290,13 @@ struct FindInfo { inline bool is_small(size_t capacity) { return capacity < Group::kWidth - 1; } // Begins a probing operation on `common.control`, using `hash`. -inline probe_seq<Group::kWidth> probe(const CommonFields& common, size_t hash) { - const ctrl_t* ctrl = common.control_; - const size_t capacity = common.capacity_; +inline probe_seq<Group::kWidth> probe(const ctrl_t* ctrl, const size_t capacity, + size_t hash) { return probe_seq<Group::kWidth>(H1(hash, ctrl), capacity); } +inline probe_seq<Group::kWidth> probe(const CommonFields& common, size_t hash) { + return probe(common.control(), common.capacity(), hash); +} // Probes an array of control bits using a probe sequence derived from `hash`, // and returns the offset corresponding to the first deleted or empty slot. @@ -1122,7 +1308,7 @@ inline probe_seq<Group::kWidth> probe(const CommonFields& common, size_t hash) { template <typename = void> inline FindInfo find_first_non_full(const CommonFields& common, size_t hash) { auto seq = probe(common, hash); - const ctrl_t* ctrl = common.control_; + const ctrl_t* ctrl = common.control(); while (true) { Group g{ctrl + seq.offset()}; auto mask = g.MaskEmptyOrDeleted(); @@ -1132,14 +1318,14 @@ inline FindInfo find_first_non_full(const CommonFields& common, size_t hash) { // In debug build we will randomly insert in either the front or back of // the group. // TODO(kfm,sbenza): revisit after we do unconditional mixing - if (!is_small(common.capacity_) && ShouldInsertBackwards(hash, ctrl)) { + if (!is_small(common.capacity()) && ShouldInsertBackwards(hash, ctrl)) { return {seq.offset(mask.HighestBitSet()), seq.index()}; } #endif return {seq.offset(mask.LowestBitSet()), seq.index()}; } seq.next(); - assert(seq.index() <= common.capacity_ && "full table!"); + assert(seq.index() <= common.capacity() && "full table!"); } } @@ -1153,18 +1339,18 @@ extern template FindInfo find_first_non_full(const CommonFields&, size_t); FindInfo find_first_non_full_outofline(const CommonFields&, size_t); inline void ResetGrowthLeft(CommonFields& common) { - common.growth_left() = CapacityToGrowth(common.capacity_) - common.size_; + common.set_growth_left(CapacityToGrowth(common.capacity()) - common.size()); } // Sets `ctrl` to `{kEmpty, kSentinel, ..., kEmpty}`, marking the entire // array as marked as empty. inline void ResetCtrl(CommonFields& common, size_t slot_size) { - const size_t capacity = common.capacity_; - ctrl_t* ctrl = common.control_; + const size_t capacity = common.capacity(); + ctrl_t* ctrl = common.control(); std::memset(ctrl, static_cast<int8_t>(ctrl_t::kEmpty), capacity + 1 + NumClonedBytes()); ctrl[capacity] = ctrl_t::kSentinel; - SanitizerPoisonMemoryRegion(common.slots_, slot_size * capacity); + SanitizerPoisonMemoryRegion(common.slot_array(), slot_size * capacity); ResetGrowthLeft(common); } @@ -1174,17 +1360,17 @@ inline void ResetCtrl(CommonFields& common, size_t slot_size) { // mirror the value to the cloned tail if necessary. inline void SetCtrl(const CommonFields& common, size_t i, ctrl_t h, size_t slot_size) { - const size_t capacity = common.capacity_; + const size_t capacity = common.capacity(); assert(i < capacity); - auto* slot_i = static_cast<const char*>(common.slots_) + i * slot_size; + auto* slot_i = static_cast<const char*>(common.slot_array()) + i * slot_size; if (IsFull(h)) { SanitizerUnpoisonMemoryRegion(slot_i, slot_size); } else { SanitizerPoisonMemoryRegion(slot_i, slot_size); } - ctrl_t* ctrl = common.control_; + ctrl_t* ctrl = common.control(); ctrl[i] = h; ctrl[((i - NumClonedBytes()) & capacity) + (NumClonedBytes() & capacity)] = h; } @@ -1195,56 +1381,41 @@ inline void SetCtrl(const CommonFields& common, size_t i, h2_t h, SetCtrl(common, i, static_cast<ctrl_t>(h), slot_size); } -// Given the capacity of a table, computes the offset (from the start of the -// backing allocation) of the generation counter (if it exists). -inline size_t GenerationOffset(size_t capacity) { - assert(IsValidCapacity(capacity)); - const size_t num_control_bytes = capacity + 1 + NumClonedBytes(); - return num_control_bytes; -} - -// Given the capacity of a table, computes the offset (from the start of the -// backing allocation) at which the slots begin. -inline size_t SlotOffset(size_t capacity, size_t slot_align) { - assert(IsValidCapacity(capacity)); - const size_t num_control_bytes = capacity + 1 + NumClonedBytes(); - return (num_control_bytes + NumGenerationBytes() + slot_align - 1) & - (~slot_align + 1); -} - -// Given the capacity of a table, computes the total size of the backing -// array. -inline size_t AllocSize(size_t capacity, size_t slot_size, size_t slot_align) { - return SlotOffset(capacity, slot_align) + capacity * slot_size; +// growth_left (which is a size_t) is stored with the backing array. +constexpr size_t BackingArrayAlignment(size_t align_of_slot) { + return (std::max)(align_of_slot, alignof(size_t)); } template <typename Alloc, size_t SizeOfSlot, size_t AlignOfSlot> ABSL_ATTRIBUTE_NOINLINE void InitializeSlots(CommonFields& c, Alloc alloc) { - assert(c.capacity_); + assert(c.capacity()); // Folks with custom allocators often make unwarranted assumptions about the // behavior of their classes vis-a-vis trivial destructability and what // calls they will or won't make. Avoid sampling for people with custom // allocators to get us out of this mess. This is not a hard guarantee but // a workaround while we plan the exact guarantee we want to provide. const size_t sample_size = - (std::is_same<Alloc, std::allocator<char>>::value && c.slots_ == nullptr) + (std::is_same<Alloc, std::allocator<char>>::value && + c.slot_array() == nullptr) ? SizeOfSlot : 0; - const size_t cap = c.capacity_; + const size_t cap = c.capacity(); + const size_t alloc_size = AllocSize(cap, SizeOfSlot, AlignOfSlot); + // growth_left (which is a size_t) is stored with the backing array. char* mem = static_cast<char*>( - Allocate<AlignOfSlot>(&alloc, AllocSize(cap, SizeOfSlot, AlignOfSlot))); + Allocate<BackingArrayAlignment(AlignOfSlot)>(&alloc, alloc_size)); const GenerationType old_generation = c.generation(); c.set_generation_ptr( reinterpret_cast<GenerationType*>(mem + GenerationOffset(cap))); - c.set_generation(old_generation + 1); - c.control_ = reinterpret_cast<ctrl_t*>(mem); - c.slots_ = mem + SlotOffset(cap, AlignOfSlot); + c.set_generation(NextGeneration(old_generation)); + c.set_control(reinterpret_cast<ctrl_t*>(mem + ControlOffset())); + c.set_slots(mem + SlotOffset(cap, AlignOfSlot)); ResetCtrl(c, SizeOfSlot); if (sample_size) { c.infoz() = Sample(sample_size); } - c.infoz().RecordStorageChanged(c.size_, cap); + c.infoz().RecordStorageChanged(c.size(), cap); } // PolicyFunctions bundles together some information for a particular @@ -1254,15 +1425,14 @@ ABSL_ATTRIBUTE_NOINLINE void InitializeSlots(CommonFields& c, Alloc alloc) { struct PolicyFunctions { size_t slot_size; - // Return the hash of the pointed-to slot. + // Returns the hash of the pointed-to slot. size_t (*hash_slot)(void* set, void* slot); // Transfer the contents of src_slot to dst_slot. void (*transfer)(void* set, void* dst_slot, void* src_slot); - // Deallocate the specified backing store which is sized for n slots. - void (*dealloc)(void* set, const PolicyFunctions& policy, ctrl_t* ctrl, - void* slot_array, size_t n); + // Deallocate the backing store from common. + void (*dealloc)(CommonFields& common, const PolicyFunctions& policy); }; // ClearBackingArray clears the backing array, either modifying it in place, @@ -1279,16 +1449,16 @@ void EraseMetaOnly(CommonFields& c, ctrl_t* it, size_t slot_size); // function body for raw_hash_set instantiations that have the // same slot alignment. template <size_t AlignOfSlot> -ABSL_ATTRIBUTE_NOINLINE void DeallocateStandard(void*, - const PolicyFunctions& policy, - ctrl_t* ctrl, void* slot_array, - size_t n) { +ABSL_ATTRIBUTE_NOINLINE void DeallocateStandard(CommonFields& common, + const PolicyFunctions& policy) { // Unpoison before returning the memory to the allocator. - SanitizerUnpoisonMemoryRegion(slot_array, policy.slot_size * n); + SanitizerUnpoisonMemoryRegion(common.slot_array(), + policy.slot_size * common.capacity()); std::allocator<char> alloc; - Deallocate<AlignOfSlot>(&alloc, ctrl, - AllocSize(n, policy.slot_size, AlignOfSlot)); + Deallocate<BackingArrayAlignment(AlignOfSlot)>( + &alloc, common.backing_array_start(), + common.alloc_size(policy.slot_size, AlignOfSlot)); } // For trivially relocatable types we use memcpy directly. This allows us to @@ -1419,22 +1589,19 @@ class raw_hash_set { // PRECONDITION: not an end() iterator. reference operator*() const { - ABSL_INTERNAL_ASSERT_IS_FULL(ctrl_, generation(), generation_ptr(), - "operator*()"); + AssertIsFull(ctrl_, generation(), generation_ptr(), "operator*()"); return PolicyTraits::element(slot_); } // PRECONDITION: not an end() iterator. pointer operator->() const { - ABSL_INTERNAL_ASSERT_IS_FULL(ctrl_, generation(), generation_ptr(), - "operator->"); + AssertIsFull(ctrl_, generation(), generation_ptr(), "operator->"); return &operator*(); } // PRECONDITION: not an end() iterator. iterator& operator++() { - ABSL_INTERNAL_ASSERT_IS_FULL(ctrl_, generation(), generation_ptr(), - "operator++"); + AssertIsFull(ctrl_, generation(), generation_ptr(), "operator++"); ++ctrl_; ++slot_; skip_empty_or_deleted(); @@ -1448,9 +1615,10 @@ class raw_hash_set { } friend bool operator==(const iterator& a, const iterator& b) { - AssertSameContainer(a.ctrl_, b.ctrl_, a.slot_, b.slot_); AssertIsValidForComparison(a.ctrl_, a.generation(), a.generation_ptr()); AssertIsValidForComparison(b.ctrl_, b.generation(), b.generation_ptr()); + AssertSameContainer(a.ctrl_, b.ctrl_, a.slot_, b.slot_, + a.generation_ptr(), b.generation_ptr()); return a.ctrl_ == b.ctrl_; } friend bool operator!=(const iterator& a, const iterator& b) { @@ -1469,7 +1637,7 @@ class raw_hash_set { } // For end() iterators. explicit iterator(const GenerationType* generation_ptr) - : HashSetIteratorGenerationInfo(generation_ptr) {} + : HashSetIteratorGenerationInfo(generation_ptr), ctrl_(nullptr) {} // Fixes up `ctrl_` to point to a full by advancing it and `slot_` until // they reach one. @@ -1484,7 +1652,9 @@ class raw_hash_set { if (ABSL_PREDICT_FALSE(*ctrl_ == ctrl_t::kSentinel)) ctrl_ = nullptr; } - ctrl_t* ctrl_ = nullptr; + // We use EmptyGroup() for default-constructed iterators so that they can + // be distinguished from end iterators, which have nullptr ctrl_. + ctrl_t* ctrl_ = EmptyGroup(); // To avoid uninitialized member warnings, put slot_ in an anonymous union. // The member is not initialized on singleton and end iterators. union { @@ -1537,9 +1707,9 @@ class raw_hash_set { // Note: can't use `= default` due to non-default noexcept (causes // problems for some compilers). NOLINTNEXTLINE raw_hash_set() noexcept( - std::is_nothrow_default_constructible<hasher>::value&& - std::is_nothrow_default_constructible<key_equal>::value&& - std::is_nothrow_default_constructible<allocator_type>::value) {} + std::is_nothrow_default_constructible<hasher>::value && + std::is_nothrow_default_constructible<key_equal>::value && + std::is_nothrow_default_constructible<allocator_type>::value) {} ABSL_ATTRIBUTE_NOINLINE explicit raw_hash_set( size_t bucket_count, const hasher& hash = hasher(), @@ -1547,7 +1717,7 @@ class raw_hash_set { const allocator_type& alloc = allocator_type()) : settings_(CommonFields{}, hash, eq, alloc) { if (bucket_count) { - common().capacity_ = NormalizeCapacity(bucket_count); + common().set_capacity(NormalizeCapacity(bucket_count)); initialize_slots(); } } @@ -1649,7 +1819,9 @@ class raw_hash_set { raw_hash_set(const raw_hash_set& that, const allocator_type& a) : raw_hash_set(0, that.hash_ref(), that.eq_ref(), a) { - reserve(that.size()); + const size_t size = that.size(); + if (size == 0) return; + reserve(size); // Because the table is guaranteed to be empty, we can do something faster // than a full `insert`. for (const auto& v : that) { @@ -1660,14 +1832,14 @@ class raw_hash_set { common().maybe_increment_generation_on_insert(); infoz().RecordInsert(hash, target.probe_length); } - common().size_ = that.size(); - growth_left() -= that.size(); + common().set_size(size); + set_growth_left(growth_left() - size); } ABSL_ATTRIBUTE_NOINLINE raw_hash_set(raw_hash_set&& that) noexcept( - std::is_nothrow_copy_constructible<hasher>::value&& - std::is_nothrow_copy_constructible<key_equal>::value&& - std::is_nothrow_copy_constructible<allocator_type>::value) + std::is_nothrow_copy_constructible<hasher>::value && + std::is_nothrow_copy_constructible<key_equal>::value && + std::is_nothrow_copy_constructible<allocator_type>::value) : // Hash, equality and allocator are copied instead of moved because // `that` must be left valid. If Hash is std::function<Key>, moving it // would create a nullptr functor that cannot be called. @@ -1696,9 +1868,9 @@ class raw_hash_set { } raw_hash_set& operator=(raw_hash_set&& that) noexcept( - absl::allocator_traits<allocator_type>::is_always_equal::value&& - std::is_nothrow_move_assignable<hasher>::value&& - std::is_nothrow_move_assignable<key_equal>::value) { + absl::allocator_traits<allocator_type>::is_always_equal::value && + std::is_nothrow_move_assignable<hasher>::value && + std::is_nothrow_move_assignable<key_equal>::value) { // TODO(sbenza): We should only use the operations from the noexcept clause // to make sure we actually adhere to that contract. // NOLINTNEXTLINE: not returning *this for performance. @@ -1714,30 +1886,36 @@ class raw_hash_set { // Unpoison before returning the memory to the allocator. SanitizerUnpoisonMemoryRegion(slot_array(), sizeof(slot_type) * cap); - Deallocate<alignof(slot_type)>( - &alloc_ref(), control(), + Deallocate<BackingArrayAlignment(alignof(slot_type))>( + &alloc_ref(), common().backing_array_start(), AllocSize(cap, sizeof(slot_type), alignof(slot_type))); infoz().Unregister(); } - iterator begin() { + iterator begin() ABSL_ATTRIBUTE_LIFETIME_BOUND { auto it = iterator_at(0); it.skip_empty_or_deleted(); return it; } - iterator end() { return iterator(common().generation_ptr()); } + iterator end() ABSL_ATTRIBUTE_LIFETIME_BOUND { + return iterator(common().generation_ptr()); + } - const_iterator begin() const { + const_iterator begin() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return const_cast<raw_hash_set*>(this)->begin(); } - const_iterator end() const { return iterator(common().generation_ptr()); } - const_iterator cbegin() const { return begin(); } - const_iterator cend() const { return end(); } + const_iterator end() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return iterator(common().generation_ptr()); + } + const_iterator cbegin() const ABSL_ATTRIBUTE_LIFETIME_BOUND { + return begin(); + } + const_iterator cend() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return end(); } bool empty() const { return !size(); } - size_t size() const { return common().size_; } - size_t capacity() const { return common().capacity_; } + size_t size() const { return common().size(); } + size_t capacity() const { return common().capacity(); } size_t max_size() const { return (std::numeric_limits<size_t>::max)(); } ABSL_ATTRIBUTE_REINITIALIZES void clear() { @@ -1753,10 +1931,10 @@ class raw_hash_set { // Already guaranteed to be empty; so nothing to do. } else { destroy_slots(); - ClearBackingArray(common(), GetPolicyFunctions(), - /*reuse=*/cap < 128); + ClearBackingArray(common(), GetPolicyFunctions(), /*reuse=*/cap < 128); } common().set_reserved_growth(0); + common().set_reservation_size(0); } inline void destroy_slots() { @@ -1780,7 +1958,7 @@ class raw_hash_set { template <class T, RequiresInsertable<T> = 0, class T2 = T, typename std::enable_if<IsDecomposable<T2>::value, int>::type = 0, T* = nullptr> - std::pair<iterator, bool> insert(T&& value) { + std::pair<iterator, bool> insert(T&& value) ABSL_ATTRIBUTE_LIFETIME_BOUND { return emplace(std::forward<T>(value)); } @@ -1795,13 +1973,11 @@ class raw_hash_set { // const char* p = "hello"; // s.insert(p); // - // TODO(romanp): Once we stop supporting gcc 5.1 and below, replace - // RequiresInsertable<T> with RequiresInsertable<const T&>. - // We are hitting this bug: https://godbolt.org/g/1Vht4f. template < - class T, RequiresInsertable<T> = 0, + class T, RequiresInsertable<const T&> = 0, typename std::enable_if<IsDecomposable<const T&>::value, int>::type = 0> - std::pair<iterator, bool> insert(const T& value) { + std::pair<iterator, bool> insert(const T& value) + ABSL_ATTRIBUTE_LIFETIME_BOUND { return emplace(value); } @@ -1810,7 +1986,8 @@ class raw_hash_set { // // flat_hash_map<std::string, int> s; // s.insert({"abc", 42}); - std::pair<iterator, bool> insert(init_type&& value) { + std::pair<iterator, bool> insert(init_type&& value) + ABSL_ATTRIBUTE_LIFETIME_BOUND { return emplace(std::move(value)); } @@ -1819,21 +1996,20 @@ class raw_hash_set { template <class T, RequiresInsertable<T> = 0, class T2 = T, typename std::enable_if<IsDecomposable<T2>::value, int>::type = 0, T* = nullptr> - iterator insert(const_iterator, T&& value) { + iterator insert(const_iterator, T&& value) ABSL_ATTRIBUTE_LIFETIME_BOUND { return insert(std::forward<T>(value)).first; } - // TODO(romanp): Once we stop supporting gcc 5.1 and below, replace - // RequiresInsertable<T> with RequiresInsertable<const T&>. - // We are hitting this bug: https://godbolt.org/g/1Vht4f. template < - class T, RequiresInsertable<T> = 0, + class T, RequiresInsertable<const T&> = 0, typename std::enable_if<IsDecomposable<const T&>::value, int>::type = 0> - iterator insert(const_iterator, const T& value) { + iterator insert(const_iterator, + const T& value) ABSL_ATTRIBUTE_LIFETIME_BOUND { return insert(value).first; } - iterator insert(const_iterator, init_type&& value) { + iterator insert(const_iterator, + init_type&& value) ABSL_ATTRIBUTE_LIFETIME_BOUND { return insert(std::move(value)).first; } @@ -1851,7 +2027,7 @@ class raw_hash_set { insert(ilist.begin(), ilist.end()); } - insert_return_type insert(node_type&& node) { + insert_return_type insert(node_type&& node) ABSL_ATTRIBUTE_LIFETIME_BOUND { if (!node) return {end(), false, node_type()}; const auto& elem = PolicyTraits::element(CommonAccess::GetSlot(node)); auto res = PolicyTraits::apply( @@ -1865,7 +2041,8 @@ class raw_hash_set { } } - iterator insert(const_iterator, node_type&& node) { + iterator insert(const_iterator, + node_type&& node) ABSL_ATTRIBUTE_LIFETIME_BOUND { auto res = insert(std::move(node)); node = std::move(res.node); return res.position; @@ -1882,7 +2059,8 @@ class raw_hash_set { // m.emplace("abc", "xyz"); template <class... Args, typename std::enable_if< IsDecomposable<Args...>::value, int>::type = 0> - std::pair<iterator, bool> emplace(Args&&... args) { + std::pair<iterator, bool> emplace(Args&&... args) + ABSL_ATTRIBUTE_LIFETIME_BOUND { return PolicyTraits::apply(EmplaceDecomposable{*this}, std::forward<Args>(args)...); } @@ -1892,7 +2070,8 @@ class raw_hash_set { // destroys. template <class... Args, typename std::enable_if< !IsDecomposable<Args...>::value, int>::type = 0> - std::pair<iterator, bool> emplace(Args&&... args) { + std::pair<iterator, bool> emplace(Args&&... args) + ABSL_ATTRIBUTE_LIFETIME_BOUND { alignas(slot_type) unsigned char raw[sizeof(slot_type)]; slot_type* slot = reinterpret_cast<slot_type*>(&raw); @@ -1902,14 +2081,16 @@ class raw_hash_set { } template <class... Args> - iterator emplace_hint(const_iterator, Args&&... args) { + iterator emplace_hint(const_iterator, + Args&&... args) ABSL_ATTRIBUTE_LIFETIME_BOUND { return emplace(std::forward<Args>(args)...).first; } // Extension API: support for lazy emplace. // // Looks up key in the table. If found, returns the iterator to the element. - // Otherwise calls `f` with one argument of type `raw_hash_set::constructor`. + // Otherwise calls `f` with one argument of type `raw_hash_set::constructor`, + // and returns an iterator to the new element. // // `f` must abide by several restrictions: // - it MUST call `raw_hash_set::constructor` with arguments as if a @@ -1952,7 +2133,8 @@ class raw_hash_set { }; template <class K = key_type, class F> - iterator lazy_emplace(const key_arg<K>& key, F&& f) { + iterator lazy_emplace(const key_arg<K>& key, + F&& f) ABSL_ATTRIBUTE_LIFETIME_BOUND { auto res = find_or_prepare_insert(key); if (res.second) { slot_type* slot = slot_array() + res.first; @@ -1997,13 +2179,25 @@ class raw_hash_set { // This overload is necessary because otherwise erase<K>(const K&) would be // a better match if non-const iterator is passed as an argument. void erase(iterator it) { - ABSL_INTERNAL_ASSERT_IS_FULL(it.ctrl_, it.generation(), it.generation_ptr(), - "erase()"); + AssertIsFull(it.ctrl_, it.generation(), it.generation_ptr(), "erase()"); PolicyTraits::destroy(&alloc_ref(), it.slot_); erase_meta_only(it); } - iterator erase(const_iterator first, const_iterator last) { + iterator erase(const_iterator first, + const_iterator last) ABSL_ATTRIBUTE_LIFETIME_BOUND { + // We check for empty first because ClearBackingArray requires that + // capacity() > 0 as a precondition. + if (empty()) return end(); + if (first == begin() && last == end()) { + // TODO(ezb): we access control bytes in destroy_slots so it could make + // sense to combine destroy_slots and ClearBackingArray to avoid cache + // misses when the table is large. Note that we also do this in clear(). + destroy_slots(); + ClearBackingArray(common(), GetPolicyFunctions(), /*reuse=*/true); + common().set_reserved_growth(common().reservation_size()); + return end(); + } while (first != last) { erase(first++); } @@ -2032,9 +2226,8 @@ class raw_hash_set { } node_type extract(const_iterator position) { - ABSL_INTERNAL_ASSERT_IS_FULL(position.inner_.ctrl_, - position.inner_.generation(), - position.inner_.generation_ptr(), "extract()"); + AssertIsFull(position.inner_.ctrl_, position.inner_.generation(), + position.inner_.generation_ptr(), "extract()"); auto node = CommonAccess::Transfer<node_type>(alloc_ref(), position.inner_.slot_); erase_meta_only(position); @@ -2064,8 +2257,7 @@ class raw_hash_set { void rehash(size_t n) { if (n == 0 && capacity() == 0) return; if (n == 0 && size() == 0) { - ClearBackingArray(common(), GetPolicyFunctions(), - /*reuse=*/false); + ClearBackingArray(common(), GetPolicyFunctions(), /*reuse=*/false); return; } @@ -2092,6 +2284,7 @@ class raw_hash_set { infoz().RecordReservation(n); } common().reset_reserved_growth(n); + common().set_reservation_size(n); } // Extension API: support for heterogeneous keys. @@ -2117,12 +2310,12 @@ class raw_hash_set { void prefetch(const key_arg<K>& key) const { (void)key; // Avoid probing if we won't be able to prefetch the addresses received. -#ifdef ABSL_INTERNAL_HAVE_PREFETCH +#ifdef ABSL_HAVE_PREFETCH prefetch_heap_block(); auto seq = probe(common(), hash_ref()(key)); - base_internal::PrefetchT0(control() + seq.offset()); - base_internal::PrefetchT0(slot_array() + seq.offset()); -#endif // ABSL_INTERNAL_HAVE_PREFETCH + PrefetchToLocalCache(control() + seq.offset()); + PrefetchToLocalCache(slot_array() + seq.offset()); +#endif // ABSL_HAVE_PREFETCH } // The API of find() has two extensions. @@ -2133,7 +2326,8 @@ class raw_hash_set { // 2. The type of the key argument doesn't have to be key_type. This is so // called heterogeneous key support. template <class K = key_type> - iterator find(const key_arg<K>& key, size_t hash) { + iterator find(const key_arg<K>& key, + size_t hash) ABSL_ATTRIBUTE_LIFETIME_BOUND { auto seq = probe(common(), hash); slot_type* slot_ptr = slot_array(); const ctrl_t* ctrl = control(); @@ -2151,17 +2345,19 @@ class raw_hash_set { } } template <class K = key_type> - iterator find(const key_arg<K>& key) { + iterator find(const key_arg<K>& key) ABSL_ATTRIBUTE_LIFETIME_BOUND { prefetch_heap_block(); return find(key, hash_ref()(key)); } template <class K = key_type> - const_iterator find(const key_arg<K>& key, size_t hash) const { + const_iterator find(const key_arg<K>& key, + size_t hash) const ABSL_ATTRIBUTE_LIFETIME_BOUND { return const_cast<raw_hash_set*>(this)->find(key, hash); } template <class K = key_type> - const_iterator find(const key_arg<K>& key) const { + const_iterator find(const key_arg<K>& key) const + ABSL_ATTRIBUTE_LIFETIME_BOUND { prefetch_heap_block(); return find(key, hash_ref()(key)); } @@ -2172,14 +2368,15 @@ class raw_hash_set { } template <class K = key_type> - std::pair<iterator, iterator> equal_range(const key_arg<K>& key) { + std::pair<iterator, iterator> equal_range(const key_arg<K>& key) + ABSL_ATTRIBUTE_LIFETIME_BOUND { auto it = find(key); if (it != end()) return {it, std::next(it)}; return {it, it}; } template <class K = key_type> std::pair<const_iterator, const_iterator> equal_range( - const key_arg<K>& key) const { + const key_arg<K>& key) const ABSL_ATTRIBUTE_LIFETIME_BOUND { auto it = find(key); if (it != end()) return {it, std::next(it)}; return {it, it}; @@ -2313,8 +2510,8 @@ class raw_hash_set { assert(IsValidCapacity(new_capacity)); auto* old_ctrl = control(); auto* old_slots = slot_array(); - const size_t old_capacity = common().capacity_; - common().capacity_ = new_capacity; + const size_t old_capacity = common().capacity(); + common().set_capacity(new_capacity); initialize_slots(); auto* new_slots = slot_array(); @@ -2333,8 +2530,8 @@ class raw_hash_set { if (old_capacity) { SanitizerUnpoisonMemoryRegion(old_slots, sizeof(slot_type) * old_capacity); - Deallocate<alignof(slot_type)>( - &alloc_ref(), old_ctrl, + Deallocate<BackingArrayAlignment(alignof(slot_type))>( + &alloc_ref(), old_ctrl - ControlOffset(), AllocSize(old_capacity, sizeof(slot_type), alignof(slot_type))); } infoz().RecordRehash(total_probe_length); @@ -2357,8 +2554,8 @@ class raw_hash_set { void rehash_and_grow_if_necessary() { const size_t cap = capacity(); if (cap > Group::kWidth && - // Do these calcuations in 64-bit to avoid overflow. - size() * uint64_t{32} <= cap* uint64_t{25}) { + // Do these calculations in 64-bit to avoid overflow. + size() * uint64_t{32} <= cap * uint64_t{25}) { // Squash DELETED without growing if there is enough capacity. // // Rehash in place if the current size is <= 25/32 of capacity. @@ -2481,8 +2678,8 @@ class raw_hash_set { rehash_and_grow_if_necessary(); target = find_first_non_full(common(), hash); } - ++common().size_; - growth_left() -= IsEmpty(control()[target.offset]); + common().set_size(common().size() + 1); + set_growth_left(growth_left() - IsEmpty(control()[target.offset])); SetCtrl(common(), target.offset, H2(hash), sizeof(slot_type)); common().maybe_increment_generation_on_insert(); infoz().RecordInsert(hash, target.probe_length); @@ -2507,10 +2704,10 @@ class raw_hash_set { "constructed value does not match the lookup key"); } - iterator iterator_at(size_t i) { + iterator iterator_at(size_t i) ABSL_ATTRIBUTE_LIFETIME_BOUND { return {control() + i, slot_array() + i, common().generation_ptr()}; } - const_iterator iterator_at(size_t i) const { + const_iterator iterator_at(size_t i) const ABSL_ATTRIBUTE_LIFETIME_BOUND { return {control() + i, slot_array() + i, common().generation_ptr()}; } @@ -2527,19 +2724,24 @@ class raw_hash_set { // side-effect. // // See `CapacityToGrowth()`. - size_t& growth_left() { return common().growth_left(); } - - // Prefetch the heap-allocated memory region to resolve potential TLB misses. - // This is intended to overlap with execution of calculating the hash for a - // key. - void prefetch_heap_block() const { base_internal::PrefetchT2(control()); } + size_t growth_left() const { return common().growth_left(); } + void set_growth_left(size_t gl) { return common().set_growth_left(gl); } + + // Prefetch the heap-allocated memory region to resolve potential TLB and + // cache misses. This is intended to overlap with execution of calculating the + // hash for a key. + void prefetch_heap_block() const { +#if ABSL_HAVE_BUILTIN(__builtin_prefetch) || defined(__GNUC__) + __builtin_prefetch(control(), 0, 1); +#endif + } CommonFields& common() { return settings_.template get<0>(); } const CommonFields& common() const { return settings_.template get<0>(); } - ctrl_t* control() const { return common().control_; } + ctrl_t* control() const { return common().control(); } slot_type* slot_array() const { - return static_cast<slot_type*>(common().slots_); + return static_cast<slot_type*>(common().slot_array()); } HashtablezInfoHandle& infoz() { return common().infoz(); } @@ -2565,16 +2767,16 @@ class raw_hash_set { static_cast<slot_type*>(src)); } // Note: dealloc_fn will only be used if we have a non-standard allocator. - static void dealloc_fn(void* set, const PolicyFunctions&, ctrl_t* ctrl, - void* slot_mem, size_t n) { - auto* h = static_cast<raw_hash_set*>(set); + static void dealloc_fn(CommonFields& common, const PolicyFunctions&) { + auto* set = reinterpret_cast<raw_hash_set*>(&common); // Unpoison before returning the memory to the allocator. - SanitizerUnpoisonMemoryRegion(slot_mem, sizeof(slot_type) * n); + SanitizerUnpoisonMemoryRegion(common.slot_array(), + sizeof(slot_type) * common.capacity()); - Deallocate<alignof(slot_type)>( - &h->alloc_ref(), ctrl, - AllocSize(n, sizeof(slot_type), alignof(slot_type))); + Deallocate<BackingArrayAlignment(alignof(slot_type))>( + &set->alloc_ref(), common.backing_array_start(), + common.alloc_size(sizeof(slot_type), alignof(slot_type))); } static const PolicyFunctions& GetPolicyFunctions() { @@ -2680,6 +2882,5 @@ ABSL_NAMESPACE_END } // namespace absl #undef ABSL_SWISSTABLE_ENABLE_GENERATIONS -#undef ABSL_INTERNAL_ASSERT_IS_FULL #endif // ABSL_CONTAINER_INTERNAL_RAW_HASH_SET_H_ |