New adaptation policy
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -38,14 +39,39 @@ public:
|
||||
}
|
||||
|
||||
Entry& ensure(const Key& key) {
|
||||
maybe_grow();
|
||||
const auto [position, found] = find_insert_position(key);
|
||||
// A moved-from std::vector is allowed to be empty while the scalar
|
||||
// counters retain their old values. Rebuild lazily so a moved-from
|
||||
// index remains reusable and mask() is never evaluated for zero
|
||||
// buckets.
|
||||
if (buckets_.empty()) {
|
||||
rehash(16);
|
||||
}
|
||||
|
||||
auto [position, found] = find_insert_position(key);
|
||||
if (position == npos) {
|
||||
// Normal growth is performed by reserve_for_elements() at a
|
||||
// logical container-capacity change. Reaching a completely full
|
||||
// table means that invariant was unavailable (most notably after
|
||||
// reusing a moved-from standalone index), so grow only as a
|
||||
// recovery path rather than as a load-factor policy.
|
||||
if (buckets_.size() > std::numeric_limits<std::size_t>::max() / 2) {
|
||||
throw std::length_error("flat hash index capacity overflow");
|
||||
}
|
||||
rehash(buckets_.size() * 2);
|
||||
const auto retry = find_insert_position(key);
|
||||
position = retry.first;
|
||||
found = retry.second;
|
||||
if (position == npos) {
|
||||
throw std::length_error("flat hash index is full");
|
||||
}
|
||||
}
|
||||
|
||||
auto& bucket = buckets_[position];
|
||||
if (!found) {
|
||||
bucket.key.emplace(key);
|
||||
if (bucket.state == State::tombstone) {
|
||||
--tombstones_;
|
||||
}
|
||||
bucket.key.emplace(key);
|
||||
bucket.entry = {};
|
||||
bucket.state = State::occupied;
|
||||
++size_;
|
||||
@@ -58,27 +84,62 @@ public:
|
||||
if (position == npos) {
|
||||
return false;
|
||||
}
|
||||
auto& bucket = buckets_[position];
|
||||
bucket.key.reset();
|
||||
bucket.entry = {};
|
||||
bucket.state = State::tombstone;
|
||||
--size_;
|
||||
++tombstones_;
|
||||
if (tombstones_ > buckets_.size() / 4) {
|
||||
rehash(buckets_.size());
|
||||
if constexpr (std::is_nothrow_invocable_v<const Hash&, const Key&>
|
||||
&& std::is_nothrow_move_assignable_v<Bucket>) {
|
||||
erase_and_compact(position);
|
||||
} else {
|
||||
// Computing hashes or moving a key can throw. Rebuild a complete
|
||||
// replacement without the erased key, then commit with swap, so a
|
||||
// failed erase never opens a hole in the live probe chain.
|
||||
erase_transactional(position);
|
||||
}
|
||||
--size_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
// Allocate before committing so a failed reset leaves the live table
|
||||
// and its duplicate metadata untouched.
|
||||
std::vector<Bucket> replacement(16);
|
||||
buckets_.swap(replacement);
|
||||
size_ = 0;
|
||||
tombstones_ = 0;
|
||||
}
|
||||
|
||||
// Used by the owning container after moving the live table elsewhere.
|
||||
// Keeping zero buckets avoids an allocation in its noexcept move path;
|
||||
// ensure()/reserve_for_elements() restore the minimum table lazily.
|
||||
void reset_after_move() noexcept {
|
||||
buckets_.clear();
|
||||
size_ = 0;
|
||||
tombstones_ = 0;
|
||||
rehash(16);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t distinct_values() const noexcept { return size_; }
|
||||
[[nodiscard]] std::size_t bucket_count() const noexcept { return buckets_.size(); }
|
||||
[[nodiscard]] static std::size_t buckets_for_elements(
|
||||
std::size_t element_capacity) {
|
||||
// At most one distinct key per element. Keeping the worst-case load
|
||||
// below 70% guarantees that ensure() cannot trigger an independent
|
||||
// rehash between container capacity changes.
|
||||
const auto required = element_capacity > (std::numeric_limits<std::size_t>::max() - 6) / 10
|
||||
? std::numeric_limits<std::size_t>::max()
|
||||
: (element_capacity * 10 + 6) / 7;
|
||||
std::size_t buckets = 16;
|
||||
while (buckets < required) {
|
||||
if (buckets > std::numeric_limits<std::size_t>::max() / 2) {
|
||||
throw std::length_error("flat hash index capacity overflow");
|
||||
}
|
||||
buckets *= 2;
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
void reserve_for_elements(std::size_t element_capacity) {
|
||||
const auto requested = buckets_for_elements(element_capacity);
|
||||
if (requested != buckets_.size() || tombstones_ != 0) {
|
||||
rehash(requested);
|
||||
}
|
||||
}
|
||||
[[nodiscard]] std::size_t allocated_bytes() const noexcept {
|
||||
return buckets_.capacity() * sizeof(Bucket);
|
||||
}
|
||||
@@ -116,10 +177,20 @@ private:
|
||||
|
||||
[[nodiscard]] std::pair<std::size_t, bool>
|
||||
find_insert_position(const Key& key) const {
|
||||
auto position = hasher_(key) & mask();
|
||||
return find_insert_position_in(buckets_, key);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::pair<std::size_t, bool>
|
||||
find_insert_position_in(const std::vector<Bucket>& buckets,
|
||||
const Key& key) const {
|
||||
if (buckets.empty()) {
|
||||
return {npos, false};
|
||||
}
|
||||
const auto table_mask = buckets.size() - 1;
|
||||
auto position = hasher_(key) & table_mask;
|
||||
auto first_tombstone = npos;
|
||||
for (std::size_t probe = 0; probe < buckets_.size(); ++probe) {
|
||||
const auto& bucket = buckets_[position];
|
||||
for (std::size_t probe = 0; probe < buckets.size(); ++probe) {
|
||||
const auto& bucket = buckets[position];
|
||||
if (bucket.state == State::empty) {
|
||||
return {first_tombstone == npos ? position : first_tombstone, false};
|
||||
}
|
||||
@@ -130,41 +201,92 @@ private:
|
||||
} else if (equal_(*bucket.key, key)) {
|
||||
return {position, true};
|
||||
}
|
||||
position = (position + 1) & mask();
|
||||
position = (position + 1) & table_mask;
|
||||
}
|
||||
if (first_tombstone != npos) {
|
||||
return {first_tombstone, false};
|
||||
}
|
||||
throw std::length_error("flat hash index is full");
|
||||
return {npos, false};
|
||||
}
|
||||
|
||||
void maybe_grow() {
|
||||
if ((size_ + tombstones_ + 1) * 100 >= buckets_.size() * 82) {
|
||||
rehash(buckets_.size() * 2);
|
||||
void erase_and_compact(std::size_t position) {
|
||||
auto hole = position;
|
||||
buckets_[hole] = Bucket{};
|
||||
auto current = (hole + 1) & mask();
|
||||
while (buckets_[current].state != State::empty) {
|
||||
auto& bucket = buckets_[current];
|
||||
if (bucket.state == State::occupied) {
|
||||
const auto home = hasher_(*bucket.key) & mask();
|
||||
const auto current_distance = (current - home) & mask();
|
||||
const auto hole_distance = (hole - home) & mask();
|
||||
if (hole_distance < current_distance) {
|
||||
buckets_[hole] = std::move(bucket);
|
||||
bucket = Bucket{};
|
||||
hole = current;
|
||||
}
|
||||
}
|
||||
current = (current + 1) & mask();
|
||||
}
|
||||
}
|
||||
|
||||
void erase_transactional(std::size_t erased_position) {
|
||||
std::vector<Bucket> replacement(buckets_.size());
|
||||
for (std::size_t source = 0; source < buckets_.size(); ++source) {
|
||||
const auto& bucket = buckets_[source];
|
||||
if (source == erased_position || bucket.state != State::occupied) {
|
||||
continue;
|
||||
}
|
||||
const auto [position, found] =
|
||||
find_insert_position_in(replacement, *bucket.key);
|
||||
if (position == npos || found) {
|
||||
throw std::logic_error("flat hash index rebuild invariant violated");
|
||||
}
|
||||
auto& target = replacement[position];
|
||||
target.key.emplace(*bucket.key);
|
||||
target.entry = bucket.entry;
|
||||
target.state = State::occupied;
|
||||
}
|
||||
buckets_.swap(replacement);
|
||||
tombstones_ = 0;
|
||||
}
|
||||
|
||||
void rehash(std::size_t requested_capacity) {
|
||||
std::size_t capacity = 16;
|
||||
while (capacity < requested_capacity) {
|
||||
if (capacity > std::numeric_limits<std::size_t>::max() / 2) {
|
||||
throw std::length_error("flat hash index capacity overflow");
|
||||
}
|
||||
capacity *= 2;
|
||||
}
|
||||
auto old = std::move(buckets_);
|
||||
buckets_.assign(capacity, Bucket{});
|
||||
size_ = 0;
|
||||
tombstones_ = 0;
|
||||
for (auto& bucket : old) {
|
||||
|
||||
// Build the complete replacement before touching the live table. An
|
||||
// allocation, hash/equality call, or copy of a copyable Key may throw;
|
||||
// in all of those cases the original buckets and counters remain
|
||||
// unchanged. vector::swap is the no-throw commit for std::allocator.
|
||||
std::vector<Bucket> replacement(capacity);
|
||||
std::size_t replacement_size = 0;
|
||||
for (const auto& bucket : buckets_) {
|
||||
if (bucket.state != State::occupied) {
|
||||
continue;
|
||||
}
|
||||
const auto [position, found] = find_insert_position(*bucket.key);
|
||||
(void)found;
|
||||
auto& target = buckets_[position];
|
||||
target.key.emplace(std::move(*bucket.key));
|
||||
const auto [position, found] =
|
||||
find_insert_position_in(replacement, *bucket.key);
|
||||
if (position == npos) {
|
||||
throw std::length_error("flat hash index is full");
|
||||
}
|
||||
if (found) {
|
||||
throw std::logic_error("flat hash index contains duplicate keys");
|
||||
}
|
||||
auto& target = replacement[position];
|
||||
target.key.emplace(*bucket.key);
|
||||
target.entry = bucket.entry;
|
||||
target.state = State::occupied;
|
||||
++size_;
|
||||
++replacement_size;
|
||||
}
|
||||
|
||||
buckets_.swap(replacement);
|
||||
size_ = replacement_size;
|
||||
tombstones_ = 0;
|
||||
}
|
||||
|
||||
Hash hasher_{};
|
||||
|
||||
Reference in New Issue
Block a user