aboutsummaryrefslogtreecommitdiff
path: root/absl/container/internal/raw_hash_set.h
diff options
context:
space:
mode:
authorEvan Brown <ezb@google.com>2023-10-03 10:01:17 -0700
committerCopybara-Service <copybara-worker@google.com>2023-10-03 10:02:13 -0700
commit22dc7911f9a4721f6dec0cc41a36fd204aa0e4f8 (patch)
treeab4a1e578b6e509612e47b208595bf8a4f790c5f /absl/container/internal/raw_hash_set.h
parent74a8f6faf075cd796885a77c5cbb9ef56f65f747 (diff)
downloadabseil-22dc7911f9a4721f6dec0cc41a36fd204aa0e4f8.tar.gz
abseil-22dc7911f9a4721f6dec0cc41a36fd204aa0e4f8.tar.bz2
abseil-22dc7911f9a4721f6dec0cc41a36fd204aa0e4f8.zip
Refactor swisstable copy/move assignment to fix issues with allocator propagation and improve performance.
Correctness: - We use swap to implement copy assignment and move assignment, which means that allocator propagation in copy/move assignment depends on `propagate_on_container_swap` in addition to `propagate_on_container_copy_assignment`/`propagate_on_container_move_assignment`. - In swap, if `propagate_on_container_swap` is `false` and `get_allocator() != other.get_allocator()`, the behavior is undefined (https://en.cppreference.com/w/cpp/container/unordered_set/swap) - we should assert that this UB case isn't happening. For this reason, we also delete the NoPropagateOn_Swap test case in raw_hash_set_allocator_test. Performance: - Don't rely on swap so we don't have to do unnecessary copying into the moved-from sets. - Don't use temp sets in move assignment. - Default the move constructor of CommonFields. - Avoid using exchange in raw_hash_set move constructor. - In `raw_hash_set(raw_hash_set&& that, const allocator_type& a)` with unequal allocators and in move assignment with non-propagating unequal allocators, move set keys instead of copying them. PiperOrigin-RevId: 570419290 Change-Id: I499e54f17d9cb0b0836601f5c06187d1f269a5b8
Diffstat (limited to 'absl/container/internal/raw_hash_set.h')
-rw-r--r--absl/container/internal/raw_hash_set.h141
1 files changed, 94 insertions, 47 deletions
diff --git a/absl/container/internal/raw_hash_set.h b/absl/container/internal/raw_hash_set.h
index 543e6f4c..8f48eb17 100644
--- a/absl/container/internal/raw_hash_set.h
+++ b/absl/container/internal/raw_hash_set.h
@@ -270,8 +270,21 @@ void SwapAlloc(AllocType& lhs, AllocType& rhs,
swap(lhs, rhs);
}
template <typename AllocType>
-void SwapAlloc(AllocType& /*lhs*/, AllocType& /*rhs*/,
- std::false_type /* propagate_on_container_swap */) {}
+void SwapAlloc(AllocType& lhs, AllocType& rhs,
+ std::false_type /* propagate_on_container_swap */) {
+ (void)lhs;
+ (void)rhs;
+ assert(lhs == rhs &&
+ "It's UB to call swap with unequal non-propagating allocators.");
+}
+
+template <typename AllocType>
+void CopyAlloc(AllocType& lhs, AllocType& rhs,
+ std::true_type /* propagate_alloc */) {
+ lhs = rhs;
+}
+template <typename AllocType>
+void CopyAlloc(AllocType&, AllocType&, std::false_type /* propagate_alloc */) {}
// The state for a probe sequence.
//
@@ -980,20 +993,7 @@ class CommonFields : public CommonFieldsGenerationInfo {
CommonFields& operator=(const CommonFields&) = delete;
// Movable
- CommonFields(CommonFields&& that)
- : 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.slot_array()),
- capacity_(that.capacity()),
- size_(that.size_) {
- that.set_control(EmptyGroup());
- that.set_slots(nullptr);
- that.set_capacity(0);
- that.size_ = 0;
- }
+ CommonFields(CommonFields&& that) = default;
CommonFields& operator=(CommonFields&&) = default;
ctrl_t* control() const { return control_; }
@@ -1918,28 +1918,33 @@ class raw_hash_set {
: // 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.
- settings_(absl::exchange(that.common(), CommonFields{}),
- that.hash_ref(), that.eq_ref(), that.alloc_ref()) {}
+ // TODO(b/296061262): move instead of copying hash/eq/alloc.
+ // Note: we avoid using exchange for better generated code.
+ settings_(std::move(that.common()), that.hash_ref(), that.eq_ref(),
+ that.alloc_ref()) {
+ that.common() = CommonFields{};
+ }
raw_hash_set(raw_hash_set&& that, const allocator_type& a)
: settings_(CommonFields{}, that.hash_ref(), that.eq_ref(), a) {
if (a == that.alloc_ref()) {
std::swap(common(), that.common());
} else {
- reserve(that.size());
- // Note: this will copy keys instead of moving them. This can be fixed if
- // it ever becomes an issue.
- for (auto& elem : that) insert(std::move(elem));
+ move_elements_allocs_unequal(std::move(that));
}
}
raw_hash_set& operator=(const raw_hash_set& that) {
- raw_hash_set tmp(that,
- AllocTraits::propagate_on_container_copy_assignment::value
- ? that.alloc_ref()
- : alloc_ref());
- swap(tmp);
- return *this;
+ if (ABSL_PREDICT_FALSE(this == &that)) return *this;
+ constexpr bool propagate_alloc =
+ AllocTraits::propagate_on_container_copy_assignment::value;
+ // TODO(ezb): maybe avoid allocating a new backing array if this->capacity()
+ // is an exact match for that.size(). If this->capacity() is too big, then
+ // it would make iteration very slow to reuse the allocation. Maybe we can
+ // do the same heuristic as clear() and reuse if it's small enough.
+ raw_hash_set tmp(that, propagate_alloc ? that.alloc_ref() : alloc_ref());
+ // NOLINTNEXTLINE: not returning *this for performance.
+ return assign_impl<propagate_alloc>(std::move(tmp));
}
raw_hash_set& operator=(raw_hash_set&& that) noexcept(
@@ -1954,18 +1959,7 @@ class raw_hash_set {
typename AllocTraits::propagate_on_container_move_assignment());
}
- ~raw_hash_set() {
- const size_t cap = capacity();
- if (!cap) return;
- destroy_slots();
-
- // Unpoison before returning the memory to the allocator.
- SanitizerUnpoisonMemoryRegion(slot_array(), sizeof(slot_type) * cap);
- infoz().Unregister();
- Deallocate<BackingArrayAlignment(alignof(slot_type))>(
- &alloc_ref(), common().backing_array_start(),
- common().alloc_size(sizeof(slot_type), alignof(slot_type)));
- }
+ ~raw_hash_set() { destructor_impl(); }
iterator begin() ABSL_ATTRIBUTE_LIFETIME_BOUND {
auto it = iterator_at(0);
@@ -2559,6 +2553,22 @@ class raw_hash_set {
}
}
+ inline void dealloc() {
+ assert(capacity() != 0);
+ // Unpoison before returning the memory to the allocator.
+ SanitizerUnpoisonMemoryRegion(slot_array(), sizeof(slot_type) * capacity());
+ infoz().Unregister();
+ Deallocate<BackingArrayAlignment(alignof(slot_type))>(
+ &alloc_ref(), common().backing_array_start(),
+ common().alloc_size(sizeof(slot_type), alignof(slot_type)));
+ }
+
+ inline void destructor_impl() {
+ if (capacity() == 0) return;
+ destroy_slots();
+ dealloc();
+ }
+
// Erases, but does not destroy, the value pointed to by `it`.
//
// This merely updates the pertinent control byte. This can be used in
@@ -2682,18 +2692,55 @@ class raw_hash_set {
}
}
- // TODO(alkis): Optimize this assuming *this and that don't overlap.
- raw_hash_set& move_assign(raw_hash_set&& that, std::true_type) {
- raw_hash_set tmp(std::move(that));
- swap(tmp);
+ template<bool propagate_alloc>
+ raw_hash_set& assign_impl(raw_hash_set&& that) {
+ // We don't bother checking for this/that aliasing. We just need to avoid
+ // breaking the invariants in that case.
+ destructor_impl();
+ common() = std::move(that.common());
+ // TODO(b/296061262): move instead of copying hash/eq/alloc.
+ hash_ref() = that.hash_ref();
+ eq_ref() = that.eq_ref();
+ CopyAlloc(alloc_ref(), that.alloc_ref(),
+ std::integral_constant<bool, propagate_alloc>());
+ that.common() = CommonFields{};
return *this;
}
- raw_hash_set& move_assign(raw_hash_set&& that, std::false_type) {
- raw_hash_set tmp(std::move(that), alloc_ref());
- swap(tmp);
+
+ raw_hash_set& move_elements_allocs_unequal(raw_hash_set&& that) {
+ const size_t size = that.size();
+ if (size == 0) return *this;
+ reserve(size);
+ for (iterator it = that.begin(); it != that.end(); ++it) {
+ insert(std::move(PolicyTraits::element(it.slot_)));
+ PolicyTraits::destroy(&that.alloc_ref(), it.slot_);
+ }
+ that.dealloc();
+ that.common() = CommonFields{};
return *this;
}
+ raw_hash_set& move_assign(raw_hash_set&& that,
+ std::true_type /*propagate_alloc*/) {
+ return assign_impl<true>(std::move(that));
+ }
+ raw_hash_set& move_assign(raw_hash_set&& that,
+ std::false_type /*propagate_alloc*/) {
+ if (alloc_ref() == that.alloc_ref()) {
+ return assign_impl<false>(std::move(that));
+ }
+ // Aliasing can't happen here because allocs would compare equal above.
+ assert(this != &that);
+ destructor_impl();
+ // We can't take over that's memory so we need to move each element.
+ // While moving elements, this should have that's hash/eq so copy hash/eq
+ // before moving elements.
+ // TODO(b/296061262): move instead of copying hash/eq.
+ hash_ref() = that.hash_ref();
+ eq_ref() = that.eq_ref();
+ return move_elements_allocs_unequal(std::move(that));
+ }
+
protected:
// Attempts to find `key` in the table; if it isn't found, returns a slot that
// the value can be inserted into, with the control byte already set to