New adaptation policy
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
@@ -25,7 +26,7 @@ namespace uc {
|
||||
|
||||
template <class T,
|
||||
bool HashIndexEnabled = false,
|
||||
class AdaptationPolicy = CostModelPolicy,
|
||||
class AdaptationPolicy = ResizePolicy,
|
||||
class Hash = std::hash<T>,
|
||||
class Equal = std::equal_to<T>>
|
||||
class AdaptiveSequence {
|
||||
@@ -54,8 +55,10 @@ private:
|
||||
struct IdMetadata {
|
||||
internal_id previous = invalid_internal_id;
|
||||
internal_id next = invalid_internal_id;
|
||||
internal_id free_next = invalid_internal_id;
|
||||
std::uint32_t primary = 0; // vector index or stable leaf id
|
||||
std::uint32_t secondary = 0; // local leaf index
|
||||
std::uint32_t generation = 0;
|
||||
std::uint8_t flags = 0;
|
||||
};
|
||||
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
struct IndexedState {
|
||||
detail::FlatDuplicateIndex<T, Hash, Equal> values;
|
||||
std::vector<IdMetadata> ids;
|
||||
internal_id free_head = invalid_internal_id;
|
||||
};
|
||||
|
||||
struct NoIndexState {};
|
||||
@@ -237,9 +241,53 @@ public:
|
||||
}
|
||||
|
||||
AdaptiveSequence(const AdaptiveSequence&) = default;
|
||||
AdaptiveSequence(AdaptiveSequence&&) noexcept = default;
|
||||
AdaptiveSequence& operator=(const AdaptiveSequence&) = default;
|
||||
AdaptiveSequence& operator=(AdaptiveSequence&&) noexcept = default;
|
||||
|
||||
AdaptiveSequence(AdaptiveSequence&& other) noexcept
|
||||
: tiered_config_(std::move(other.tiered_config_)),
|
||||
policy_(std::move(other.policy_)),
|
||||
storage_(std::move(other.storage_)),
|
||||
index_state_(std::move(other.index_state_)),
|
||||
equal_(std::move(other.equal_)),
|
||||
residency_(other.residency_),
|
||||
read_adaptation_(other.read_adaptation_),
|
||||
logical_capacity_(other.logical_capacity_),
|
||||
read_sample_rate_(other.read_sample_rate_),
|
||||
read_sample_countdown_(other.read_sample_countdown_),
|
||||
edit_sample_rate_(other.edit_sample_rate_),
|
||||
edit_sample_countdown_(other.edit_sample_countdown_),
|
||||
generation_(other.generation_) {
|
||||
other.reset_after_move();
|
||||
}
|
||||
|
||||
AdaptiveSequence& operator=(const AdaptiveSequence& other) {
|
||||
if (this != &other) {
|
||||
AdaptiveSequence replacement(other);
|
||||
*this = std::move(replacement);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AdaptiveSequence& operator=(AdaptiveSequence&& other) noexcept {
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
const auto invalidated_generation = generation_ + 1;
|
||||
tiered_config_ = std::move(other.tiered_config_);
|
||||
policy_ = std::move(other.policy_);
|
||||
storage_ = std::move(other.storage_);
|
||||
index_state_ = std::move(other.index_state_);
|
||||
equal_ = std::move(other.equal_);
|
||||
residency_ = other.residency_;
|
||||
read_adaptation_ = other.read_adaptation_;
|
||||
logical_capacity_ = other.logical_capacity_;
|
||||
read_sample_rate_ = other.read_sample_rate_;
|
||||
read_sample_countdown_ = other.read_sample_countdown_;
|
||||
edit_sample_rate_ = other.edit_sample_rate_;
|
||||
edit_sample_countdown_ = other.edit_sample_countdown_;
|
||||
generation_ = invalidated_generation;
|
||||
other.reset_after_move();
|
||||
return *this;
|
||||
}
|
||||
~AdaptiveSequence() = default;
|
||||
|
||||
[[nodiscard]] size_type size() const noexcept {
|
||||
@@ -249,6 +297,10 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept { return size() == 0; }
|
||||
// Logical capacity is shared by both backends. It is the only trigger for
|
||||
// automatic representation/geometry changes; TieredStorage's internal
|
||||
// leaf slack is intentionally not exposed as container capacity.
|
||||
[[nodiscard]] size_type capacity() const noexcept { return logical_capacity_; }
|
||||
[[nodiscard]] StorageMode mode() const noexcept {
|
||||
return std::holds_alternative<vector_storage>(storage_)
|
||||
? StorageMode::vector
|
||||
@@ -258,13 +310,7 @@ public:
|
||||
[[nodiscard]] const TieredConfig& tiered_config() const noexcept { return tiered_config_; }
|
||||
|
||||
reference operator[](size_type index) {
|
||||
if (read_adaptation_ == ReadAdaptationMode::eager_nonconst) {
|
||||
apply_pending_adaptation();
|
||||
}
|
||||
observe_random_read();
|
||||
if (read_adaptation_ == ReadAdaptationMode::eager_nonconst) {
|
||||
apply_pending_adaptation();
|
||||
}
|
||||
if constexpr (HashIndexEnabled) {
|
||||
return reference_proxy(*this, index);
|
||||
} else {
|
||||
@@ -295,7 +341,6 @@ public:
|
||||
|
||||
void erase(size_type index) {
|
||||
check_index(index);
|
||||
apply_pending_adaptation();
|
||||
const auto old_size = size();
|
||||
const auto id = record_id(record_at_unchecked(index));
|
||||
if constexpr (HashIndexEnabled) {
|
||||
@@ -311,13 +356,11 @@ public:
|
||||
(void)std::get<tiered_storage>(storage_).erase(index, relocate);
|
||||
}
|
||||
if constexpr (HashIndexEnabled) {
|
||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
metadata.flags &= static_cast<std::uint8_t>(~alive_flag);
|
||||
metadata.previous = invalid_internal_id;
|
||||
metadata.next = invalid_internal_id;
|
||||
release_id_slot(id);
|
||||
}
|
||||
++generation_;
|
||||
observe_structural_edit(OperationKind::erase, old_size, index);
|
||||
shrink_after_erase();
|
||||
}
|
||||
|
||||
void set(size_type index, const T& value) { set_impl(index, T(value)); }
|
||||
@@ -325,14 +368,11 @@ public:
|
||||
|
||||
void clear() {
|
||||
if constexpr (HashIndexEnabled) {
|
||||
for (auto& metadata : index_state_.ids) {
|
||||
metadata.flags &= static_cast<std::uint8_t>(~alive_flag);
|
||||
metadata.previous = invalid_internal_id;
|
||||
metadata.next = invalid_internal_id;
|
||||
}
|
||||
index_state_.values.clear();
|
||||
rebuild_free_id_list();
|
||||
}
|
||||
storage_.template emplace<vector_storage>();
|
||||
logical_capacity_ = 0;
|
||||
residency_ = ResidencyMode::automatic;
|
||||
policy_.reset();
|
||||
read_sample_countdown_ = read_sample_rate_;
|
||||
@@ -342,9 +382,12 @@ public:
|
||||
|
||||
void reserve(size_type capacity) {
|
||||
force_vector_mode();
|
||||
std::get<vector_storage>(storage_).reserve(capacity);
|
||||
const auto requested = std::max(capacity, size());
|
||||
std::get<vector_storage>(storage_).reserve(requested);
|
||||
logical_capacity_ = std::max(logical_capacity_, requested);
|
||||
if constexpr (HashIndexEnabled) {
|
||||
const auto additional = capacity > size() ? capacity - size() : 0;
|
||||
index_state_.values.reserve_for_elements(logical_capacity_);
|
||||
const auto additional = requested > size() ? requested - size() : 0;
|
||||
index_state_.ids.reserve(index_state_.ids.size() + additional);
|
||||
}
|
||||
++generation_;
|
||||
@@ -364,14 +407,23 @@ public:
|
||||
if (!entry || entry->head == invalid_internal_id) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return resolve_id(entry->head);
|
||||
return resolve_internal_id(entry->head);
|
||||
} else {
|
||||
for (size_type i = 0; i < size(); ++i) {
|
||||
if (equal_(value_at_unchecked(i), value)) {
|
||||
return i;
|
||||
if (mode() == StorageMode::vector) {
|
||||
const auto& values = std::get<vector_storage>(storage_);
|
||||
for (size_type i = 0; i < values.size(); ++i) {
|
||||
if (equal_(record_value(values[i]), value)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
const auto found = std::get<tiered_storage>(storage_).find_if(
|
||||
[&](const record_type& record) {
|
||||
return equal_(record_value(record), value);
|
||||
});
|
||||
return found == tiered_storage::npos
|
||||
? std::nullopt : std::optional<size_type>(found);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,15 +437,26 @@ public:
|
||||
result.reserve(entry->count);
|
||||
auto id = entry->head;
|
||||
while (id != invalid_internal_id) {
|
||||
result.push_back(resolve_id(id));
|
||||
result.push_back(resolve_internal_id(id));
|
||||
id = index_state_.ids[static_cast<size_type>(id)].next;
|
||||
}
|
||||
std::sort(result.begin(), result.end());
|
||||
} else {
|
||||
for (size_type i = 0; i < size(); ++i) {
|
||||
if (equal_(value_at_unchecked(i), value)) {
|
||||
result.push_back(i);
|
||||
if (mode() == StorageMode::vector) {
|
||||
const auto& values = std::get<vector_storage>(storage_);
|
||||
for (size_type i = 0; i < values.size(); ++i) {
|
||||
if (equal_(record_value(values[i]), value)) {
|
||||
result.push_back(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::get<tiered_storage>(storage_).for_each_match(
|
||||
[&](const record_type& record) {
|
||||
return equal_(record_value(record), value);
|
||||
},
|
||||
[&](size_type index, const record_type&) {
|
||||
result.push_back(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -411,7 +474,7 @@ public:
|
||||
result.reserve(entry->count);
|
||||
auto id = entry->head;
|
||||
while (id != invalid_internal_id) {
|
||||
result.push_back(static_cast<stable_id>(id));
|
||||
result.push_back(stable_id_for_slot(id));
|
||||
id = index_state_.ids[static_cast<size_type>(id)].next;
|
||||
}
|
||||
return result;
|
||||
@@ -432,11 +495,11 @@ public:
|
||||
if (!entry) {
|
||||
return 0;
|
||||
}
|
||||
std::vector<internal_id> ids;
|
||||
std::vector<stable_id> ids;
|
||||
ids.reserve(entry->count);
|
||||
auto id = entry->head;
|
||||
while (id != invalid_internal_id) {
|
||||
ids.push_back(id);
|
||||
ids.push_back(stable_id_for_slot(id));
|
||||
id = index_state_.ids[static_cast<size_type>(id)].next;
|
||||
}
|
||||
for (const auto current : ids) {
|
||||
@@ -460,14 +523,22 @@ public:
|
||||
[[nodiscard]] stable_id id_at(size_type index) const
|
||||
requires(HashIndexEnabled) {
|
||||
check_index(index);
|
||||
return static_cast<stable_id>(record_at_unchecked(index).id);
|
||||
return stable_id_for_slot(record_at_unchecked(index).id);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool id_alive(stable_id id) const noexcept
|
||||
requires(HashIndexEnabled) {
|
||||
return id <= static_cast<stable_id>(std::numeric_limits<internal_id>::max())
|
||||
&& id < index_state_.ids.size()
|
||||
&& (index_state_.ids[static_cast<size_type>(id)].flags & alive_flag) != 0;
|
||||
if (id == invalid_id) {
|
||||
return false;
|
||||
}
|
||||
const auto slot = slot_from_stable_id(id);
|
||||
if (slot == invalid_internal_id
|
||||
|| static_cast<size_type>(slot) >= index_state_.ids.size()) {
|
||||
return false;
|
||||
}
|
||||
const auto& metadata = index_state_.ids[static_cast<size_type>(slot)];
|
||||
return metadata.generation == generation_from_stable_id(id)
|
||||
&& (metadata.flags & alive_flag) != 0;
|
||||
}
|
||||
|
||||
void erase_by_id(stable_id id)
|
||||
@@ -532,12 +603,11 @@ public:
|
||||
return read_adaptation_;
|
||||
}
|
||||
|
||||
bool adapt_now() {
|
||||
if (residency_ != ResidencyMode::automatic) {
|
||||
return false;
|
||||
}
|
||||
return apply_pending_adaptation();
|
||||
}
|
||||
// Automatic rebuilds are capacity-boundary operations. This compatibility
|
||||
// hook therefore never changes the representation between two resize
|
||||
// events; explicit force_* calls remain available for callers that need an
|
||||
// immediate manual conversion.
|
||||
bool adapt_now() noexcept { return false; }
|
||||
|
||||
[[nodiscard]] T* data() noexcept
|
||||
requires(!HashIndexEnabled) {
|
||||
@@ -625,10 +695,29 @@ public:
|
||||
return bytes;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t hash_bucket_count() const noexcept
|
||||
requires(HashIndexEnabled) {
|
||||
return index_state_.values.bucket_count();
|
||||
}
|
||||
|
||||
[[nodiscard]] AdaptationPolicy& policy() noexcept { return policy_; }
|
||||
[[nodiscard]] const AdaptationPolicy& policy() const noexcept { return policy_; }
|
||||
|
||||
private:
|
||||
void reset_after_move() noexcept {
|
||||
storage_.template emplace<vector_storage>();
|
||||
if constexpr (HashIndexEnabled) {
|
||||
index_state_.values.reset_after_move();
|
||||
index_state_.ids.clear();
|
||||
index_state_.free_head = invalid_internal_id;
|
||||
}
|
||||
residency_ = ResidencyMode::automatic;
|
||||
logical_capacity_ = 0;
|
||||
read_sample_countdown_ = read_sample_rate_;
|
||||
edit_sample_countdown_ = edit_sample_rate_;
|
||||
++generation_;
|
||||
}
|
||||
|
||||
static void set_location(IdMetadata& metadata,
|
||||
bool tiered,
|
||||
std::uint32_t primary,
|
||||
@@ -667,14 +756,88 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr unsigned stable_id_slot_bits =
|
||||
std::numeric_limits<internal_id>::digits;
|
||||
static_assert(stable_id_slot_bits == 32);
|
||||
|
||||
[[nodiscard]] static internal_id slot_from_stable_id(stable_id id) noexcept {
|
||||
return static_cast<internal_id>(id);
|
||||
}
|
||||
|
||||
[[nodiscard]] static std::uint32_t generation_from_stable_id(
|
||||
stable_id id) noexcept {
|
||||
return static_cast<std::uint32_t>(id >> stable_id_slot_bits);
|
||||
}
|
||||
|
||||
[[nodiscard]] stable_id stable_id_for_slot(internal_id slot) const noexcept
|
||||
requires(HashIndexEnabled) {
|
||||
const auto generation =
|
||||
index_state_.ids[static_cast<size_type>(slot)].generation;
|
||||
return (static_cast<stable_id>(generation) << stable_id_slot_bits)
|
||||
| static_cast<stable_id>(slot);
|
||||
}
|
||||
|
||||
[[nodiscard]] internal_id acquire_id_slot()
|
||||
requires(HashIndexEnabled) {
|
||||
if (index_state_.free_head != invalid_internal_id) {
|
||||
const auto id = index_state_.free_head;
|
||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
index_state_.free_head = metadata.free_next;
|
||||
assert(metadata.generation
|
||||
< std::numeric_limits<std::uint32_t>::max());
|
||||
const auto next_generation = metadata.generation + 1;
|
||||
metadata = {};
|
||||
metadata.generation = next_generation;
|
||||
return id;
|
||||
}
|
||||
|
||||
if (index_state_.ids.size() >= static_cast<size_type>(invalid_internal_id)) {
|
||||
throw std::length_error("stable id slot space exhausted");
|
||||
}
|
||||
const auto id = static_cast<internal_id>(index_state_.ids.size());
|
||||
index_state_.ids.emplace_back();
|
||||
return id;
|
||||
}
|
||||
|
||||
void release_id_slot(internal_id id) noexcept
|
||||
requires(HashIndexEnabled) {
|
||||
assert(id != invalid_internal_id);
|
||||
assert(static_cast<size_type>(id) < index_state_.ids.size());
|
||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
const auto generation = metadata.generation;
|
||||
metadata = {};
|
||||
metadata.generation = generation;
|
||||
if (generation != std::numeric_limits<std::uint32_t>::max()) {
|
||||
metadata.free_next = index_state_.free_head;
|
||||
index_state_.free_head = id;
|
||||
}
|
||||
}
|
||||
|
||||
void rebuild_free_id_list() noexcept
|
||||
requires(HashIndexEnabled) {
|
||||
index_state_.free_head = invalid_internal_id;
|
||||
for (size_type index = index_state_.ids.size(); index != 0; --index) {
|
||||
const auto id = static_cast<internal_id>(index - 1);
|
||||
auto& metadata = index_state_.ids[index - 1];
|
||||
const auto generation = metadata.generation;
|
||||
metadata = {};
|
||||
metadata.generation = generation;
|
||||
if (generation != std::numeric_limits<std::uint32_t>::max()) {
|
||||
metadata.free_next = index_state_.free_head;
|
||||
index_state_.free_head = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record_type make_record(T value) {
|
||||
if constexpr (HashIndexEnabled) {
|
||||
if (index_state_.ids.size() >= static_cast<size_type>(invalid_internal_id)) {
|
||||
throw std::length_error("stable id space exhausted");
|
||||
const auto id = acquire_id_slot();
|
||||
try {
|
||||
return IndexedRecord{std::move(value), id};
|
||||
} catch (...) {
|
||||
release_id_slot(id);
|
||||
throw;
|
||||
}
|
||||
const auto id = static_cast<internal_id>(index_state_.ids.size());
|
||||
index_state_.ids.emplace_back();
|
||||
return IndexedRecord{std::move(value), id};
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
@@ -707,23 +870,40 @@ private:
|
||||
}
|
||||
|
||||
void push_back_impl(T value) {
|
||||
apply_pending_adaptation();
|
||||
const auto old_size = size();
|
||||
grow_before_insert(old_size + 1);
|
||||
auto record = make_record(std::move(value));
|
||||
const auto id = record_id(record);
|
||||
auto relocate = relocation_callback();
|
||||
if (mode() == StorageMode::vector) {
|
||||
auto& values = std::get<vector_storage>(storage_);
|
||||
values.push_back(std::move(record));
|
||||
refresh_vector_locations(old_size);
|
||||
if constexpr (HashIndexEnabled) {
|
||||
try {
|
||||
// Keep an unmoved lookup key so a failed storage insertion can
|
||||
// roll the duplicate chain back before the slot is recycled.
|
||||
T rollback_key(record.value);
|
||||
link_value(record.value, id);
|
||||
try {
|
||||
if (mode() == StorageMode::vector) {
|
||||
auto& values = std::get<vector_storage>(storage_);
|
||||
values.push_back(std::move(record));
|
||||
refresh_vector_locations(old_size);
|
||||
} else {
|
||||
std::get<tiered_storage>(storage_).push_back(
|
||||
std::move(record), relocate);
|
||||
}
|
||||
} catch (...) {
|
||||
unlink_value(rollback_key, id);
|
||||
throw;
|
||||
}
|
||||
index_state_.ids[static_cast<size_type>(id)].flags |= alive_flag;
|
||||
} catch (...) {
|
||||
release_id_slot(id);
|
||||
throw;
|
||||
}
|
||||
} else if (mode() == StorageMode::vector) {
|
||||
std::get<vector_storage>(storage_).push_back(std::move(record));
|
||||
} else {
|
||||
std::get<tiered_storage>(storage_).push_back(std::move(record), relocate);
|
||||
}
|
||||
if constexpr (HashIndexEnabled) {
|
||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
metadata.flags |= alive_flag;
|
||||
link_value(value_at_unchecked(old_size), id);
|
||||
}
|
||||
++generation_;
|
||||
policy_.observe({OperationKind::append, old_size, old_size, 1, sizeof(T)});
|
||||
}
|
||||
@@ -732,23 +912,41 @@ private:
|
||||
if (index > size()) {
|
||||
throw std::out_of_range("AdaptiveSequence insertion index out of range");
|
||||
}
|
||||
apply_pending_adaptation();
|
||||
const auto old_size = size();
|
||||
grow_before_insert(old_size + 1);
|
||||
auto record = make_record(std::move(value));
|
||||
const auto id = record_id(record);
|
||||
auto relocate = relocation_callback();
|
||||
if (mode() == StorageMode::vector) {
|
||||
if constexpr (HashIndexEnabled) {
|
||||
try {
|
||||
T rollback_key(record.value);
|
||||
link_value(record.value, id);
|
||||
try {
|
||||
if (mode() == StorageMode::vector) {
|
||||
auto& values = std::get<vector_storage>(storage_);
|
||||
values.insert(values.begin() + static_cast<difference_type>(index),
|
||||
std::move(record));
|
||||
refresh_vector_locations(index);
|
||||
} else {
|
||||
std::get<tiered_storage>(storage_).insert(
|
||||
index, std::move(record), relocate);
|
||||
}
|
||||
} catch (...) {
|
||||
unlink_value(rollback_key, id);
|
||||
throw;
|
||||
}
|
||||
index_state_.ids[static_cast<size_type>(id)].flags |= alive_flag;
|
||||
} catch (...) {
|
||||
release_id_slot(id);
|
||||
throw;
|
||||
}
|
||||
} else if (mode() == StorageMode::vector) {
|
||||
auto& values = std::get<vector_storage>(storage_);
|
||||
values.insert(values.begin() + static_cast<difference_type>(index), std::move(record));
|
||||
refresh_vector_locations(index);
|
||||
values.insert(values.begin() + static_cast<difference_type>(index),
|
||||
std::move(record));
|
||||
} else {
|
||||
std::get<tiered_storage>(storage_).insert(index, std::move(record), relocate);
|
||||
}
|
||||
if constexpr (HashIndexEnabled) {
|
||||
auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
metadata.flags |= alive_flag;
|
||||
link_value(value_at_unchecked(index), id);
|
||||
}
|
||||
++generation_;
|
||||
observe_structural_edit(OperationKind::insert, old_size, index);
|
||||
}
|
||||
@@ -837,13 +1035,28 @@ private:
|
||||
|
||||
[[nodiscard]] size_type resolve_id(stable_id id) const
|
||||
requires(HashIndexEnabled) {
|
||||
if (id >= index_state_.ids.size()) {
|
||||
if (id == invalid_id) {
|
||||
throw std::out_of_range("unknown stable id");
|
||||
}
|
||||
const auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
if ((metadata.flags & alive_flag) == 0) {
|
||||
const auto slot = slot_from_stable_id(id);
|
||||
if (slot == invalid_internal_id
|
||||
|| static_cast<size_type>(slot) >= index_state_.ids.size()) {
|
||||
throw std::out_of_range("unknown stable id");
|
||||
}
|
||||
const auto& metadata = index_state_.ids[static_cast<size_type>(slot)];
|
||||
if (metadata.generation != generation_from_stable_id(id)
|
||||
|| (metadata.flags & alive_flag) == 0) {
|
||||
throw std::out_of_range("stable id no longer refers to an element");
|
||||
}
|
||||
return resolve_internal_id(slot);
|
||||
}
|
||||
|
||||
[[nodiscard]] size_type resolve_internal_id(internal_id id) const noexcept
|
||||
requires(HashIndexEnabled) {
|
||||
assert(id != invalid_internal_id);
|
||||
assert(static_cast<size_type>(id) < index_state_.ids.size());
|
||||
const auto& metadata = index_state_.ids[static_cast<size_type>(id)];
|
||||
assert((metadata.flags & alive_flag) != 0);
|
||||
if ((metadata.flags & tiered_flag) == 0) {
|
||||
return metadata.primary;
|
||||
}
|
||||
@@ -852,51 +1065,171 @@ private:
|
||||
metadata.secondary);
|
||||
}
|
||||
|
||||
bool apply_pending_adaptation() {
|
||||
if (residency_ != ResidencyMode::automatic || !policy_.decision_ready()) {
|
||||
return false;
|
||||
[[nodiscard]] static size_type ceil_sqrt(size_type value) noexcept {
|
||||
if (value <= 1) {
|
||||
return value;
|
||||
}
|
||||
auto root = static_cast<size_type>(std::sqrt(static_cast<long double>(value)));
|
||||
while (root > value / root) {
|
||||
--root;
|
||||
}
|
||||
while (root < value / root
|
||||
|| (root <= value / root && root * root < value)) {
|
||||
++root;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
[[nodiscard]] size_type minimum_tiered_size() const noexcept {
|
||||
if constexpr (requires(const AdaptationPolicy& p) {
|
||||
p.config().minimum_tiered_size;
|
||||
}) {
|
||||
return std::max<size_type>(1, policy_.config().minimum_tiered_size);
|
||||
} else {
|
||||
return 4 * 1024;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] size_type shrink_denominator() const noexcept {
|
||||
if constexpr (requires(const AdaptationPolicy& p) {
|
||||
p.config().shrink_denominator;
|
||||
}) {
|
||||
return std::max<size_type>(2, policy_.config().shrink_denominator);
|
||||
} else {
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] StorageMode automatic_mode_for(size_type element_count) const noexcept {
|
||||
return element_count >= minimum_tiered_size()
|
||||
? StorageMode::tiered : StorageMode::vector;
|
||||
}
|
||||
|
||||
[[nodiscard]] TieredConfig geometry_for(size_type element_count) const noexcept {
|
||||
auto result = tiered_config_;
|
||||
result.leaf_capacity = std::max<size_type>(4, ceil_sqrt(element_count));
|
||||
return result;
|
||||
}
|
||||
|
||||
void grow_before_insert(size_type required) {
|
||||
if (required <= logical_capacity_) {
|
||||
return;
|
||||
}
|
||||
size_type next = logical_capacity_ == 0 ? 1 : logical_capacity_;
|
||||
while (next < required) {
|
||||
if (next > std::numeric_limits<size_type>::max() / 2) {
|
||||
next = required;
|
||||
break;
|
||||
}
|
||||
next *= 2;
|
||||
}
|
||||
rebuild_for_capacity(next, required);
|
||||
}
|
||||
|
||||
void shrink_after_erase() noexcept {
|
||||
if (logical_capacity_ <= 1
|
||||
|| size() > logical_capacity_ / shrink_denominator()) {
|
||||
return;
|
||||
}
|
||||
const auto next = std::max(size(), logical_capacity_ / 2);
|
||||
// The erase has already committed. Capacity shrink is an automatic
|
||||
// optimization and must not turn that successful erase into a thrown
|
||||
// operation. Copyable records keep their source intact on failure;
|
||||
// nothrow-movable records use preallocated rebuild paths below.
|
||||
if constexpr (std::is_copy_constructible_v<record_type>
|
||||
|| std::is_nothrow_move_constructible_v<record_type>) {
|
||||
try {
|
||||
rebuild_for_capacity(next, size());
|
||||
} catch (...) {
|
||||
// Keep the current (or already committed) capacity/layout and
|
||||
// retry at a later resize boundary.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rebuild_for_capacity(size_type target_capacity, size_type target_size) {
|
||||
target_capacity = std::max(target_capacity, target_size);
|
||||
const bool growing = target_capacity >= logical_capacity_;
|
||||
if constexpr (HashIndexEnabled) {
|
||||
if (growing) {
|
||||
index_state_.values.reserve_for_elements(target_capacity);
|
||||
index_state_.ids.reserve(
|
||||
std::max(index_state_.ids.size(), target_capacity));
|
||||
}
|
||||
}
|
||||
auto target_mode = mode();
|
||||
if (residency_ == ResidencyMode::forced_vector) {
|
||||
target_mode = StorageMode::vector;
|
||||
} else if (residency_ == ResidencyMode::forced_tiered) {
|
||||
target_mode = StorageMode::tiered;
|
||||
} else {
|
||||
target_mode = automatic_mode_for(target_size);
|
||||
}
|
||||
|
||||
if constexpr (requires(AdaptationPolicy& p, StorageMode current,
|
||||
size_type count, TieredConfig config) {
|
||||
p.recommended_decision(current, count, config);
|
||||
}) {
|
||||
const auto recommendation =
|
||||
policy_.recommended_decision(mode(), size(), tiered_config_);
|
||||
if (!recommendation) {
|
||||
return false;
|
||||
}
|
||||
if (recommendation->target == mode()) {
|
||||
if (mode() == StorageMode::tiered
|
||||
&& recommendation->tiered_config != tiered_config_) {
|
||||
reconfigure_tiered(recommendation->tiered_config);
|
||||
return true;
|
||||
if (target_mode == StorageMode::vector) {
|
||||
vector_storage rebuilt;
|
||||
rebuilt.reserve(target_capacity);
|
||||
if (mode() == StorageMode::vector) {
|
||||
auto& source = std::get<vector_storage>(storage_);
|
||||
if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||
rebuilt.insert(rebuilt.end(), source.begin(), source.end());
|
||||
} else {
|
||||
rebuilt.insert(rebuilt.end(),
|
||||
std::make_move_iterator(source.begin()),
|
||||
std::make_move_iterator(source.end()));
|
||||
}
|
||||
return false;
|
||||
} else if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||
const auto& source = std::get<tiered_storage>(storage_);
|
||||
source.for_each([&](const record_type& record) {
|
||||
rebuilt.push_back(record);
|
||||
});
|
||||
} else {
|
||||
auto& source = std::get<tiered_storage>(storage_);
|
||||
source.for_each([&](record_type& record) {
|
||||
rebuilt.push_back(std::move(record));
|
||||
});
|
||||
}
|
||||
if (recommendation->target == StorageMode::tiered) {
|
||||
const auto previous_config = tiered_config_;
|
||||
tiered_config_ = recommendation->tiered_config;
|
||||
try {
|
||||
convert_to(recommendation->target);
|
||||
} catch (...) {
|
||||
// The recommended shape is policy state, not committed
|
||||
// container state. A failed allocation/copy must not make
|
||||
// tiered_config() describe storage that was never built.
|
||||
tiered_config_ = previous_config;
|
||||
throw;
|
||||
}
|
||||
return true;
|
||||
|
||||
const auto source_mode = mode();
|
||||
storage_.template emplace<vector_storage>(std::move(rebuilt));
|
||||
logical_capacity_ = target_capacity;
|
||||
refresh_vector_locations(0);
|
||||
++generation_;
|
||||
if (source_mode != StorageMode::vector) {
|
||||
notify_policy_transition(source_mode, StorageMode::vector);
|
||||
}
|
||||
convert_to(recommendation->target);
|
||||
return true;
|
||||
} else {
|
||||
const auto recommendation = policy_.recommended_mode(mode(), size());
|
||||
if (!recommendation || *recommendation == mode()) {
|
||||
return false;
|
||||
const auto target_config = geometry_for(target_size);
|
||||
tiered_storage rebuilt;
|
||||
if (mode() == StorageMode::vector) {
|
||||
if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||
rebuilt = tiered_storage::from_vector_copy(
|
||||
std::get<vector_storage>(storage_), target_config);
|
||||
} else {
|
||||
rebuilt = tiered_storage::from_vector(
|
||||
std::move(std::get<vector_storage>(storage_)), target_config);
|
||||
}
|
||||
} else if constexpr (std::is_copy_constructible_v<record_type>) {
|
||||
rebuilt = tiered_storage::reconfigured_copy(
|
||||
std::get<tiered_storage>(storage_), target_config);
|
||||
} else {
|
||||
rebuilt = tiered_storage::reconfigured_move(
|
||||
std::get<tiered_storage>(storage_), target_config);
|
||||
}
|
||||
|
||||
const auto source_mode = mode();
|
||||
tiered_config_ = rebuilt.config();
|
||||
storage_.template emplace<tiered_storage>(std::move(rebuilt));
|
||||
logical_capacity_ = target_capacity;
|
||||
refresh_tiered_locations();
|
||||
++generation_;
|
||||
notify_policy_transition(source_mode, StorageMode::tiered);
|
||||
}
|
||||
|
||||
if constexpr (HashIndexEnabled) {
|
||||
if (!growing) {
|
||||
index_state_.values.reserve_for_elements(target_capacity);
|
||||
}
|
||||
convert_to(*recommendation);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,8 +1244,8 @@ private:
|
||||
tiered = tiered_storage::from_vector_copy(
|
||||
std::get<vector_storage>(storage_), tiered_config_);
|
||||
} else {
|
||||
auto values = std::move(std::get<vector_storage>(storage_));
|
||||
tiered = tiered_storage::from_vector(std::move(values), tiered_config_);
|
||||
tiered = tiered_storage::from_vector(
|
||||
std::move(std::get<vector_storage>(storage_)), tiered_config_);
|
||||
}
|
||||
tiered_config_ = tiered.config();
|
||||
storage_.template emplace<tiered_storage>(std::move(tiered));
|
||||
@@ -924,6 +1257,9 @@ private:
|
||||
} else {
|
||||
values = std::get<tiered_storage>(storage_).to_vector_move();
|
||||
}
|
||||
if (logical_capacity_ > values.capacity()) {
|
||||
values.reserve(logical_capacity_);
|
||||
}
|
||||
storage_.template emplace<vector_storage>(std::move(values));
|
||||
refresh_vector_locations(0);
|
||||
}
|
||||
@@ -955,8 +1291,8 @@ private:
|
||||
rebuilt = tiered_storage::reconfigured_copy(
|
||||
std::get<tiered_storage>(storage_), target_config);
|
||||
} else {
|
||||
auto values = std::get<tiered_storage>(storage_).to_vector_move();
|
||||
rebuilt = tiered_storage::from_vector(std::move(values), target_config);
|
||||
rebuilt = tiered_storage::reconfigured_move(
|
||||
std::get<tiered_storage>(storage_), target_config);
|
||||
}
|
||||
tiered_config_ = rebuilt.config();
|
||||
storage_.template emplace<tiered_storage>(std::move(rebuilt));
|
||||
@@ -1028,6 +1364,7 @@ private:
|
||||
[[no_unique_address]] Equal equal_{};
|
||||
ResidencyMode residency_ = ResidencyMode::automatic;
|
||||
ReadAdaptationMode read_adaptation_ = ReadAdaptationMode::deferred;
|
||||
size_type logical_capacity_ = 0;
|
||||
std::size_t read_sample_rate_ = 256;
|
||||
mutable std::size_t read_sample_countdown_ = 256;
|
||||
std::size_t edit_sample_rate_ = 1;
|
||||
|
||||
Reference in New Issue
Block a user