From d8678e02ecec9375bec1dcf1388c6fc8b4ce3ad2 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Tue, 9 Jun 2020 10:10:56 +0200 Subject: BLI: generally improve C++ data structures The main focus here was to improve the docs significantly. Furthermore, I reimplemented `Set`, `Map` and `VectorSet`. They are now (usually) faster, simpler and more customizable. I also rewrote `Stack` to make it more efficient by avoiding unnecessary copies. Thanks to everyone who helped with constructive feedback. Approved by brecht and sybren. Differential Revision: https://developer.blender.org/D7931 --- source/blender/blenlib/BLI_vector_set.hh | 814 ++++++++++++++++++++----------- 1 file changed, 542 insertions(+), 272 deletions(-) (limited to 'source/blender/blenlib/BLI_vector_set.hh') diff --git a/source/blender/blenlib/BLI_vector_set.hh b/source/blender/blenlib/BLI_vector_set.hh index f402f47c357..7e6a7de6ee6 100644 --- a/source/blender/blenlib/BLI_vector_set.hh +++ b/source/blender/blenlib/BLI_vector_set.hh @@ -20,138 +20,195 @@ /** \file * \ingroup bli * - * A VectorSet is a set built on top of a vector. The elements are stored in a continuous array, - * but every element exists at most once. The insertion order is maintained, as long as there are - * no deletes. The expected time to check if a value is in the VectorSet is O(1). + * A `BLI::VectorSet` is an ordered container for elements of type `Key`. It has the same + * interface as `BLI::Set` with the following extensions: + * - The insertion order of keys is maintained as long as no elements are removed. + * - The keys are stored in a contiguous array. + * + * All core operations (add, remove and contains) can be done in O(1) amortized expected time. + * + * Using a VectorSet instead of a normal Set can be benefitial in any of the following + * circumstances: + * - The insertion order is important. + * - Iteration over all keys has to be fast. + * - The keys in the set are supposed to be passed to a function that does not have to know that + * the keys are stored in a set. With a VectorSet, one can get an ArrayRef containing all keys + * without additional copies. + * + * BLI::VectorSet is implemented using open addressing in a slot array with a power-of-two size. + * Other than in BLI::Set, a slot does not contain the key though. Instead it only contains an + * index into an array of keys that is stored separately. + * + * Some noteworthy information: + * - Key must be a movable type. + * - Pointers to keys might be invalidated, when the vector set is changed or moved. + * - The hash function can be customized. See BLI_hash.hh for details. + * - The probing strategy can be customized. See BLI_probing_strategies.hh for details. + * - The slot type can be customized. See BLI_vector_set_slots.hh for details. + * - The methods `add_new` and `remove_contained` should be used instead of `add` and `remove` + * whenever appropriate. Assumptions and intention are described better this way. + * - Using a range-for loop over a vector set, is as efficient as iterating over an array (because + * it is the same thing). + * - Lookups can be performed using types other than Key without conversion. For that use the + * methods ending with `_as`. The template parameters Hash and IsEqual have to support the other + * key type. This can greatly improve performance when the strings are used as keys. + * - The default constructor is cheap. + * - The `print_stats` method can be used to get information about the distribution of keys and + * memory usage. + * + * Possible Improvements: + * - Small buffer optimization for the keys. */ +#include "BLI_array.hh" #include "BLI_hash.hh" -#include "BLI_open_addressing.hh" -#include "BLI_vector.hh" +#include "BLI_hash_tables.hh" +#include "BLI_probing_strategies.hh" +#include "BLI_vector_set_slots.hh" namespace BLI { -// clang-format off - -#define ITER_SLOTS_BEGIN(VALUE, ARRAY, OPTIONAL_CONST, R_SLOT) \ - uint32_t hash = DefaultHash{}(VALUE); \ - uint32_t perturb = hash; \ - while (true) { \ - for (uint i = 0; i < 4; i++) {\ - uint32_t slot_index = (hash + i) & ARRAY.slot_mask(); \ - OPTIONAL_CONST Slot &R_SLOT = ARRAY.item(slot_index); - -#define ITER_SLOTS_END \ - } \ - perturb >>= 5; \ - hash = hash * 5 + 1 + perturb; \ - } ((void)0) - -// clang-format on - -template class VectorSet { +template< + /** + * Type of the elements that are stored in this set. It has to be movable. Furthermore, the + * hash and is-equal functions have to support it. + */ + typename Key, + /** + * The strategy used to deal with collisions. They are defined in BLI_probing_strategies.hh. + */ + typename ProbingStrategy = DefaultProbingStrategy, + /** + * The hash function used to hash the keys. There is a default for many types. See BLI_hash.hh + * for examples on how to define a custom hash function. + */ + typename Hash = DefaultHash, + /** + * The equality operator used to compare keys. By default it will simply compare keys using the + * `==` operator. + */ + typename IsEqual = DefaultEquality, + /** + * This is what will actually be stored in the hash table array. At a minimum a slot has to be + * able to hold an array index and information about whether the slot is empty, occupied or + * removed. Using a non-standard slot type can improve performance for some types. + * Also see BLI_vector_set_slots.hh. + */ + typename Slot = typename DefaultVectorSetSlot::type, + /** + * The allocator used by this set. Should rarely be changed, except when you don't want that + * MEM_* etc. is used internally. + */ + typename Allocator = GuardedAllocator> +class VectorSet { private: - static constexpr int32_t IS_EMPTY = -1; - static constexpr int32_t IS_DUMMY = -2; - - class Slot { - private: - int32_t m_value = IS_EMPTY; - - public: - static constexpr uint slots_per_item = 1; - - bool is_set() const - { - return m_value >= 0; - } - - bool is_empty() const - { - return m_value == IS_EMPTY; - } - - bool is_dummy() const - { - return m_value == IS_DUMMY; - } + /** + * Slots are either empty, occupied or removed. The number of occupied slots can be computed by + * subtracting the removed slots from the occupied-and-removed slots. + */ + uint32_t m_removed_slots; + uint32_t m_occupied_and_removed_slots; - bool has_value(const T &value, const T *elements) const - { - return this->is_set() && elements[this->index()] == value; - } + /** + * The maximum number of slots that can be used (either occupied or removed) until the set has to + * grow. This is the total number of slots times the max load factor. + */ + uint32_t m_usable_slots; - bool has_index(uint index) const - { - return m_value == (int32_t)index; - } + /** + * The number of slots minus one. This is a bit mask that can be used to turn any integer into a + * valid slot index efficiently. + */ + uint32_t m_slot_mask; - uint index() const - { - BLI_assert(this->is_set()); - return (uint)m_value; - } + /** This is called to hash incoming keys. */ + Hash m_hash; - int32_t &index_ref() - { - return m_value; - } + /** This is called to check equality of two keys. */ + IsEqual m_is_equal; - void set_index(uint index) - { - BLI_assert(!this->is_set()); - m_value = (int32_t)index; - } + /** The max load factor is 1/2 = 50% by default. */ +#define LOAD_FACTOR 1, 2 + LoadFactor m_max_load_factor = LoadFactor(LOAD_FACTOR); + using SlotArray = Array; +#undef LOAD_FACTOR - void set_dummy() - { - BLI_assert(this->is_set()); - m_value = IS_DUMMY; - } - }; + /** + * This is the array that contains the actual slots. There is always at least one empty slot and + * the size of the array is a power of two. + */ + SlotArray m_slots; - using ArrayType = OpenAddressingArray; - ArrayType m_array; + /** + * Pointer to an array that contains all keys. The keys are sorted by insertion order as long as + * no keys are removed. The first set->size() elements in this array are initialized. The + * capacity of the array is m_usable_slots. + */ + Key *m_keys; - /* The capacity of the array should always be at least m_array.slots_usable(). */ - T *m_elements = nullptr; + /** Iterate over a slot index sequence for a given hash. */ +#define VECTOR_SET_SLOT_PROBING_BEGIN(HASH, R_SLOT) \ + SLOT_PROBING_BEGIN (ProbingStrategy, HASH, m_slot_mask, SLOT_INDEX) \ + auto &R_SLOT = m_slots[SLOT_INDEX]; +#define VECTOR_SET_SLOT_PROBING_END() SLOT_PROBING_END() public: + /** + * Initialize an empty vector set. This is a cheap operation and won't do an allocation. This is + * necessary to avoid a high cost when no elements are added at all. An optimized grow operation + * is performed on the first insertion. + */ VectorSet() + : m_removed_slots(0), + m_occupied_and_removed_slots(0), + m_usable_slots(0), + m_slot_mask(0), + m_slots(1), + m_keys(nullptr) { - m_elements = this->allocate_elements_array(m_array.slots_usable()); } - VectorSet(ArrayRef values) : VectorSet() - { - this->add_multiple(values); - } - - VectorSet(const std::initializer_list &values) : VectorSet() - { - this->add_multiple(values); - } - - VectorSet(const Vector &values) : VectorSet() + /** + * Construct a vector set that contains the given keys. Duplicates will be removed automatically. + */ + VectorSet(const std::initializer_list &keys) : VectorSet() { - this->add_multiple(values); + this->add_multiple(keys); } - VectorSet(const VectorSet &other) : m_array(other.m_array) + ~VectorSet() { - m_elements = this->allocate_elements_array(m_array.slots_usable()); - uninitialized_copy_n(other.m_elements, m_array.slots_set(), m_elements); + destruct_n(m_keys, this->size()); + if (m_keys != nullptr) { + this->deallocate_keys_array(m_keys); + } } - VectorSet(VectorSet &&other) : m_array(std::move(other.m_array)), m_elements(other.m_elements) + VectorSet(const VectorSet &other) + : m_removed_slots(other.m_removed_slots), + m_occupied_and_removed_slots(other.m_occupied_and_removed_slots), + m_usable_slots(other.m_usable_slots), + m_slot_mask(other.m_slot_mask), + m_slots(other.m_slots) { - other.m_elements = other.allocate_elements_array(other.m_array.slots_usable()); + m_keys = this->allocate_keys_array(m_usable_slots); + uninitialized_copy_n(other.m_keys, other.size(), m_keys); } - ~VectorSet() + VectorSet(VectorSet &&other) noexcept + : m_removed_slots(other.m_removed_slots), + m_occupied_and_removed_slots(other.m_occupied_and_removed_slots), + m_usable_slots(other.m_usable_slots), + m_slot_mask(other.m_slot_mask), + m_slots(std::move(other.m_slots)), + m_keys(other.m_keys) { - destruct_n(m_elements, this->size()); - this->deallocate_elements_array(m_elements); + other.m_removed_slots = 0; + other.m_occupied_and_removed_slots = 0; + other.m_usable_slots = 0; + other.m_slot_mask = 0; + other.m_slots = SlotArray(1); + other.m_keys = nullptr; } VectorSet &operator=(const VectorSet &other) @@ -159,8 +216,10 @@ template class VectorSet { if (this == &other) { return *this; } + this->~VectorSet(); new (this) VectorSet(other); + return *this; } @@ -169,318 +228,529 @@ template class VectorSet { if (this == &other) { return *this; } + this->~VectorSet(); new (this) VectorSet(std::move(other)); + return *this; } /** - * Allocate memory such that at least min_usable_slots can be added without having to grow again. + * Add a new key to the vector set. This invokes undefined behavior when the key is in the set + * already. When you know for certain that a key is not in the set yet, use this method for + * better performance. This also expresses the intent better. */ - void reserve(uint min_usable_slots) + void add_new(const Key &key) { - if (m_array.slots_usable() < min_usable_slots) { - this->grow(min_usable_slots); - } + this->add_new__impl(key, m_hash(key)); + } + void add_new(Key &&key) + { + this->add_new__impl(std::move(key), m_hash(key)); } /** - * Add a new element. The method assumes that the value did not exist before. + * Add a key to the vector set. If the key exists in the set already, nothing is done. The return + * value is true if the key was newly added. + * + * This is similar to std::unordered_set::insert. */ - void add_new(const T &value) + bool add(const Key &key) { - this->add_new__impl(value); + return this->add_as(key); } - void add_new(T &&value) + bool add(Key &&key) { - this->add_new__impl(std::move(value)); + return this->add_as(std::move(key)); } /** - * Add a new element if it does not exist yet. Does not add the value again if it exists already. + * Same as `add`, but accepts other key types that are supported by the hash function. */ - bool add(const T &value) + template bool add_as(ForwardKey &&key) { - return this->add__impl(value); + return this->add__impl(std::forward(key), m_hash(key)); } - bool add(T &&value) + + /** + * Convenience function to add many keys to the vector set at once. Duplicates are removed + * automatically. + * + * We might be able to make this faster than sequentially adding all keys, but that is not + * implemented yet. + */ + void add_multiple(ArrayRef keys) { - return this->add__impl(std::move(value)); + for (const Key &key : keys) { + this->add(key); + } } /** - * Add multiple values. Duplicates will not be inserted. + * Returns true if the key is in the vector set. + * + * This is similar to std::unordered_set::find() != std::unordered_set::end(). */ - void add_multiple(ArrayRef values) + bool contains(const Key &key) const { - for (const T &value : values) { - this->add(value); - } + return this->contains_as(key); } /** - * Returns true when the value is in the set-vector, otherwise false. + * Same as `contains`, but accepts other key types that are supported by the hash function. */ - bool contains(const T &value) const + template bool contains_as(const ForwardKey &key) const { - ITER_SLOTS_BEGIN (value, m_array, const, slot) { - if (slot.is_empty()) { - return false; - } - else if (slot.has_value(value, m_elements)) { - return true; - } - } - ITER_SLOTS_END; + return this->contains__impl(key, m_hash(key)); } /** - * Remove a value from the set-vector. The method assumes that the value exists. + * Deletes the key from the set. Returns true when the key existed in the set and is now removed. + * This might change the order of elements in the vector. + * + * This is similar to std::unordered_set::erase. */ - void remove(const T &value) + bool remove(const Key &key) { - BLI_assert(this->contains(value)); - ITER_SLOTS_BEGIN (value, m_array, , slot) { - if (slot.has_value(value, m_elements)) { - uint old_index = this->size() - 1; - uint new_index = slot.index(); + return this->remove_as(key); + } - if (new_index < old_index) { - m_elements[new_index] = std::move(m_elements[old_index]); - this->update_slot_index(m_elements[new_index], old_index, new_index); - } + /** + * Same as `remove`, but accepts other key types that are supported by the hash function. + */ + template bool remove_as(const ForwardKey &key) + { + return this->remove__impl(key, m_hash(key)); + } - destruct(m_elements + old_index); - slot.set_dummy(); - m_array.update__set_to_dummy(); - return; - } - } - ITER_SLOTS_END; + /** + * Deletes the key from the set. This invokes undefined behavior when the key is not in the set. + * It might change the order of elements in the vector. + */ + void remove_contained(const Key &key) + { + this->remove_contained_as(key); } /** - * Get and remove the last element of the vector. + * Same as `remove_contained`, but accepts other key types that are supported by the hash + * function. */ - T pop() + template void remove_contained_as(const ForwardKey &key) { - BLI_assert(this->size() > 0); - uint index_to_pop = this->size() - 1; - T value = std::move(m_elements[index_to_pop]); - destruct(m_elements + index_to_pop); + this->remove_contained__impl(key, m_hash(key)); + } - ITER_SLOTS_BEGIN (value, m_array, , slot) { - if (slot.has_index(index_to_pop)) { - slot.set_dummy(); - m_array.update__set_to_dummy(); - return value; - } - } - ITER_SLOTS_END; + /** + * Delete and return a key from the set. This will remove the last element in the vector. The + * order of the remaining elements in the set is not changed. + */ + Key pop() + { + return this->pop__impl(); } /** - * Get the index of the value in the vector. It is assumed that the value is in the vector. + * Return the location of the key in the vector. It is assumed, that the key is in the vector + * set. If this is not necessarily the case, use `index_of_try`. */ - uint index(const T &value) const + uint32_t index_of(const Key &key) const { - BLI_assert(this->contains(value)); - ITER_SLOTS_BEGIN (value, m_array, const, slot) { - if (slot.has_value(value, m_elements)) { - return slot.index(); - } - } - ITER_SLOTS_END; + return this->index_of_as(key); } /** - * Get the index of the value in the vector. If it does not exist return -1. + * Same as `index_of`, but accepts other key types that are supported by the hash function. */ - int index_try(const T &value) const + template uint32_t index_of_as(const ForwardKey &key) const { - ITER_SLOTS_BEGIN (value, m_array, const, slot) { - if (slot.has_value(value, m_elements)) { - return slot.index(); - } - else if (slot.is_empty()) { - return -1; - } - } - ITER_SLOTS_END; + return this->index_of__impl(key, m_hash(key)); } /** - * Get the number of elements in the set-vector. + * Return the location of the key in the vector. If the key is not in the set, -1 is returned. + * If you know for sure that the key is in the set, it is better to use `index_of` instead. */ - uint size() const + int32_t index_of_try(const Key &key) const { - return m_array.slots_set(); + return (int32_t)this->index_of_try_as(key); } - bool is_empty() const + /** + * Same as `index_of_try`, but accepts other key types that are supported by the hash function. + */ + template int32_t index_of_try_as(const ForwardKey &key) const { - return this->size() == 0; + return this->index_of_try__impl(key, m_hash(key)); } - const T *begin() const + /** + * Get a pointer to the beginning of the array containing all keys. + */ + const Key *data() const { - return m_elements; + return m_keys; } - const T *end() const + const Key *begin() const { - return m_elements + this->size(); + return m_keys; } - const T &operator[](uint index) const + const Key *end() const + { + return m_keys + this->size(); + } + + /** + * Get the key stored at the given position in the vector. + */ + const Key &operator[](uint32_t index) const { BLI_assert(index <= this->size()); - return m_elements[index]; + return m_keys[index]; } - ArrayRef as_ref() const + operator ArrayRef() const + { + return ArrayRef(m_keys, this->size()); + } + + /** + * Get an ArrayRef referencing the keys vector. The referenced memory buffer is only valid as + * long as the vector set is not changed. + * + * The keys must not be changed, because this would change their hash value. + */ + ArrayRef as_ref() const { return *this; } - operator ArrayRef() const + /** + * Print common statistics like size and collision count. This is useful for debugging purposes. + */ + void print_stats(StringRef name = "") const { - return ArrayRef(m_elements, this->size()); + HashTableStats stats(*this, this->as_ref()); + stats.print(); } - void print_stats() const + /** + * Returns the number of keys stored in the vector set. + */ + uint32_t size() const { - std::cout << "VectorSet at " << (void *)this << ":\n"; - std::cout << " Size: " << this->size() << "\n"; - std::cout << " Usable Slots: " << m_array.slots_usable() << "\n"; - std::cout << " Total Slots: " << m_array.slots_total() << "\n"; - std::cout << " Average Collisions: " << this->compute_average_collisions() << "\n"; + return m_occupied_and_removed_slots - m_removed_slots; } - private: - void update_slot_index(T &value, uint old_index, uint new_index) + /** + * Returns true if no keys are stored. + */ + bool is_empty() const { - ITER_SLOTS_BEGIN (value, m_array, , slot) { - int32_t &stored_index = slot.index_ref(); - if (stored_index == old_index) { - stored_index = new_index; - return; - } - } - ITER_SLOTS_END; + return m_occupied_and_removed_slots == m_removed_slots; } - template void add_new_in_slot(Slot &slot, ForwardT &&value) + /** + * Returns the number of available slots. This is mostly for debugging purposes. + */ + uint32_t capacity() const { - uint index = this->size(); - slot.set_index(index); - new (m_elements + index) T(std::forward(value)); - m_array.update__empty_to_set(); + return m_slots.size(); } - void ensure_can_add() + /** + * Returns the amount of removed slots in the set. This is mostly for debugging purposes. + */ + uint32_t removed_amount() const { - if (UNLIKELY(m_array.should_grow())) { - this->grow(this->size() + 1); + return m_removed_slots; + } + + /** + * Returns the bytes required per element. This is mostly for debugging purposes. + */ + uint32_t size_per_element() const + { + return sizeof(Slot) + sizeof(Key); + } + + /** + * Returns the approximate memory requirements of the set in bytes. This is more correct for + * larger sets. + */ + uint32_t size_in_bytes() const + { + return sizeof(Slot) * m_slots.size() + sizeof(Key) * m_usable_slots; + } + + /** + * Potentially resize the vector set such that it can hold n elements without doing another grow. + */ + void reserve(uint32_t n) + { + if (m_usable_slots < n) { + this->realloc_and_reinsert(n); } } - BLI_NOINLINE void grow(uint min_usable_slots) + /** + * Get the number of collisions that the probing strategy has to go through to find the key or + * determine that it is not in the set. + */ + uint32_t count_collisions(const Key &key) const { - uint size = this->size(); + return this->count_collisions__impl(key, m_hash(key)); + } + + private: + BLI_NOINLINE void realloc_and_reinsert(uint32_t min_usable_slots) + { + uint32_t total_slots, usable_slots; + m_max_load_factor.compute_total_and_usable_slots( + SlotArray::inline_buffer_capacity(), min_usable_slots, &total_slots, &usable_slots); + uint32_t new_slot_mask = total_slots - 1; + + /* Optimize the case when the set was empty beforehand. We can avoid some copies here. */ + if (this->size() == 0) { + m_slots.~Array(); + new (&m_slots) SlotArray(total_slots); + m_removed_slots = 0; + m_occupied_and_removed_slots = 0; + m_usable_slots = usable_slots; + m_slot_mask = new_slot_mask; + m_keys = this->allocate_keys_array(usable_slots); + return; + } - ArrayType new_array = m_array.init_reserved(min_usable_slots); - T *new_elements = this->allocate_elements_array(new_array.slots_usable()); + SlotArray new_slots(total_slots); - for (uint i : IndexRange(size)) { - this->add_after_grow(i, new_array); + for (Slot &slot : m_slots) { + if (slot.is_occupied()) { + this->add_after_grow_and_destruct_old(slot, new_slots, new_slot_mask); + } } - uninitialized_relocate_n(m_elements, size, new_elements); - this->deallocate_elements_array(m_elements); + Key *new_keys = this->allocate_keys_array(usable_slots); + uninitialized_relocate_n(m_keys, this->size(), new_keys); + this->deallocate_keys_array(m_keys); - m_array = std::move(new_array); - m_elements = new_elements; + /* All occupied slots have been destructed already and empty/removed slots are assumed to be + * trivially destructible. */ + m_slots.clear_without_destruct(); + m_slots = std::move(new_slots); + m_keys = new_keys; + m_occupied_and_removed_slots -= m_removed_slots; + m_usable_slots = usable_slots; + m_removed_slots = 0; + m_slot_mask = new_slot_mask; } - void add_after_grow(uint index, ArrayType &new_array) + void add_after_grow_and_destruct_old(Slot &old_slot, + SlotArray &new_slots, + uint32_t new_slot_mask) { - const T &value = m_elements[index]; - ITER_SLOTS_BEGIN (value, new_array, , slot) { + const Key &key = m_keys[old_slot.index()]; + uint32_t hash = old_slot.get_hash(key, Hash()); + + SLOT_PROBING_BEGIN (ProbingStrategy, hash, new_slot_mask, slot_index) { + Slot &slot = new_slots[slot_index]; if (slot.is_empty()) { - slot.set_index(index); + slot.relocate_occupied_here(old_slot, hash); return; } } - ITER_SLOTS_END; + SLOT_PROBING_END(); } - float compute_average_collisions() const + template bool contains__impl(const ForwardKey &key, uint32_t hash) const { - if (this->size() == 0) { - return 0.0f; - } - - uint collisions_sum = 0; - for (const T &value : this->as_ref()) { - collisions_sum += this->count_collisions(value); + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.is_empty()) { + return false; + } + if (slot.contains(key, m_is_equal, hash, m_keys)) { + return true; + } } - return (float)collisions_sum / (float)this->size(); + VECTOR_SET_SLOT_PROBING_END(); } - uint count_collisions(const T &value) const + template void add_new__impl(ForwardKey &&key, uint32_t hash) { - uint collisions = 0; - ITER_SLOTS_BEGIN (value, m_array, const, slot) { - if (slot.is_empty() || slot.has_value(value, m_elements)) { - return collisions; + BLI_assert(!this->contains_as(key)); + + this->ensure_can_add(); + + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.is_empty()) { + uint32_t index = this->size(); + new (m_keys + index) Key(std::forward(key)); + slot.occupy(index, hash); + m_occupied_and_removed_slots++; + return; } - collisions++; } - ITER_SLOTS_END; + VECTOR_SET_SLOT_PROBING_END(); } - template void add_new__impl(ForwardT &&value) + template bool add__impl(ForwardKey &&key, uint32_t hash) { - BLI_assert(!this->contains(value)); this->ensure_can_add(); - ITER_SLOTS_BEGIN (value, m_array, , slot) { + + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { if (slot.is_empty()) { - this->add_new_in_slot(slot, std::forward(value)); - return; + uint32_t index = this->size(); + new (m_keys + index) Key(std::forward(key)); + m_occupied_and_removed_slots++; + slot.occupy(index, hash); + return true; + } + if (slot.contains(key, m_is_equal, hash, m_keys)) { + return false; } } - ITER_SLOTS_END; + VECTOR_SET_SLOT_PROBING_END(); } - template bool add__impl(ForwardT &&value) + template uint32_t index_of__impl(const ForwardKey &key, uint32_t hash) const { - this->ensure_can_add(); - ITER_SLOTS_BEGIN (value, m_array, , slot) { + BLI_assert(this->contains_as(key)); + + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.contains(key, m_is_equal, hash, m_keys)) { + return slot.index(); + } + } + VECTOR_SET_SLOT_PROBING_END(); + } + + template + int32_t index_of_try__impl(const ForwardKey &key, uint32_t hash) const + { + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.contains(key, m_is_equal, hash, m_keys)) { + return (int32_t)slot.index(); + } if (slot.is_empty()) { - this->add_new_in_slot(slot, std::forward(value)); + return -1; + } + } + VECTOR_SET_SLOT_PROBING_END(); + } + + Key pop__impl() + { + BLI_assert(this->size() > 0); + + uint32_t index_to_pop = this->size() - 1; + Key key = std::move(m_keys[index_to_pop]); + m_keys[index_to_pop].~Key(); + uint32_t hash = m_hash(key); + + m_removed_slots++; + + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.has_index(index_to_pop)) { + slot.remove(); + return key; + } + } + VECTOR_SET_SLOT_PROBING_END(); + } + + template bool remove__impl(const ForwardKey &key, uint32_t hash) + { + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.contains(key, m_is_equal, hash, m_keys)) { + this->remove_key_internal(slot); return true; } - else if (slot.has_value(value, m_elements)) { + if (slot.is_empty()) { return false; } } - ITER_SLOTS_END; + VECTOR_SET_SLOT_PROBING_END(); } - T *allocate_elements_array(uint size) + template void remove_contained__impl(const ForwardKey &key, uint32_t hash) { - return (T *)m_array.allocator().allocate_aligned((uint)sizeof(T) * size, alignof(T), __func__); + BLI_assert(this->contains_as(key)); + + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.contains(key, m_is_equal, hash, m_keys)) { + this->remove_key_internal(slot); + return; + } + } + VECTOR_SET_SLOT_PROBING_END(); } - void deallocate_elements_array(T *elements) + void remove_key_internal(Slot &slot) { - m_array.allocator().deallocate(elements); + uint32_t index_to_remove = slot.index(); + uint32_t size = this->size(); + uint32_t last_element_index = size - 1; + + if (index_to_remove < last_element_index) { + m_keys[index_to_remove] = std::move(m_keys[last_element_index]); + this->update_slot_index(m_keys[index_to_remove], last_element_index, index_to_remove); + } + + m_keys[last_element_index].~Key(); + slot.remove(); + m_removed_slots++; + return; } -}; -#undef ITER_SLOTS_BEGIN -#undef ITER_SLOTS_END + void update_slot_index(const Key &key, uint32_t old_index, uint32_t new_index) + { + uint32_t hash = m_hash(key); + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.has_index(old_index)) { + slot.update_index(new_index); + return; + } + } + VECTOR_SET_SLOT_PROBING_END(); + } + + template + uint32_t count_collisions__impl(const ForwardKey &key, uint32_t hash) const + { + uint32_t collisions = 0; + + VECTOR_SET_SLOT_PROBING_BEGIN (hash, slot) { + if (slot.contains(key, m_is_equal, hash, m_keys)) { + return collisions; + } + if (slot.is_empty()) { + return collisions; + } + collisions++; + } + VECTOR_SET_SLOT_PROBING_END(); + } + + void ensure_can_add() + { + if (m_occupied_and_removed_slots >= m_usable_slots) { + this->realloc_and_reinsert(this->size() + 1); + BLI_assert(m_occupied_and_removed_slots < m_usable_slots); + } + } + + Key *allocate_keys_array(uint32_t size) + { + return (Key *)m_slots.allocator().allocate((uint32_t)sizeof(Key) * size, alignof(Key), AT); + } + + void deallocate_keys_array(Key *keys) + { + m_slots.allocator().deallocate(keys); + } +}; } // namespace BLI -- cgit v1.2.3