Files
UniversalContainer/include/universal_container/tiered_storage.hpp
T
2026-08-13 23:42:26 +03:00

615 lines
22 KiB
C++

#pragma once
#include "universal_container/hierarchical_size_index.hpp"
#include "universal_container/ring_block.hpp"
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
namespace uc {
struct TieredConfig {
std::size_t leaf_capacity = 512;
std::size_t directory_fanout = 64;
std::size_t directory_levels = 4;
friend bool operator==(const TieredConfig&, const TieredConfig&) = default;
};
namespace detail {
template <class T>
class TieredStorage {
public:
using value_type = T;
using leaf_id_type = std::uint32_t;
static constexpr std::size_t npos = std::numeric_limits<std::size_t>::max();
explicit TieredStorage(TieredConfig config = {})
: config_(normalize(config)),
directory_(config_.directory_fanout, config_.directory_levels) {}
TieredStorage(const TieredStorage& other)
: config_(other.config_),
directory_(config_.directory_fanout, config_.directory_levels),
size_(other.size_),
next_leaf_id_(other.next_leaf_id_),
leaf_positions_(other.leaf_positions_) {
leaves_.reserve(other.leaves_.size());
for (const auto& leaf : other.leaves_) {
leaves_.push_back(std::make_unique<Leaf>(*leaf));
}
rebuild_directory();
}
TieredStorage(TieredStorage&&) noexcept = default;
TieredStorage& operator=(const TieredStorage& other) {
if (this == &other) {
return *this;
}
TieredStorage copy(other);
swap(copy);
return *this;
}
TieredStorage& operator=(TieredStorage&&) noexcept = default;
~TieredStorage() = default;
void swap(TieredStorage& other) noexcept {
using std::swap;
swap(config_, other.config_);
swap(leaves_, other.leaves_);
swap(directory_, other.directory_);
swap(size_, other.size_);
swap(next_leaf_id_, other.next_leaf_id_);
swap(leaf_positions_, other.leaf_positions_);
}
[[nodiscard]] std::size_t size() const noexcept { return size_; }
[[nodiscard]] bool empty() const noexcept { return size_ == 0; }
[[nodiscard]] const TieredConfig& config() const noexcept { return config_; }
[[nodiscard]] std::size_t leaf_count() const noexcept { return leaves_.size(); }
[[nodiscard]] std::size_t actual_levels() const noexcept { return directory_.actual_levels(); }
// Changing only the directory geometry does not move elements or leaves.
// It is substantially cheaper than rebuilding with another leaf capacity.
void reconfigure_directory(std::size_t fanout, std::size_t maximum_levels) {
auto requested = config_;
requested.directory_fanout = fanout;
requested.directory_levels = maximum_levels;
requested = normalize(requested);
if (requested.directory_fanout == config_.directory_fanout
&& requested.directory_levels == config_.directory_levels) {
return;
}
// Build the complete replacement before changing either the active
// directory or the public configuration. HierarchicalSizeIndex::rebuild
// allocates several vectors and may throw; mutating directory_ first
// would otherwise leave locate() unusable after an allocation failure.
std::vector<std::size_t> sizes;
sizes.reserve(leaves_.size());
for (const auto& leaf : leaves_) {
sizes.push_back(leaf->values.size());
}
HierarchicalSizeIndex rebuilt(requested.directory_fanout,
requested.directory_levels);
rebuilt.rebuild(sizes);
static_assert(std::is_nothrow_move_assignable_v<HierarchicalSizeIndex>);
directory_ = std::move(rebuilt);
config_.directory_fanout = requested.directory_fanout;
config_.directory_levels = requested.directory_levels;
}
T& operator[](std::size_t index) noexcept {
const auto location = locate_unchecked(index);
return leaves_[location.leaf]->values[location.local];
}
const T& operator[](std::size_t index) const noexcept {
const auto location = locate_unchecked(index);
return leaves_[location.leaf]->values[location.local];
}
T& at(std::size_t index) {
check_index(index);
return (*this)[index];
}
const T& at(std::size_t index) const {
check_index(index);
return (*this)[index];
}
template <class Relocate>
void push_back(T value, Relocate&& relocate) {
if (leaves_.empty() || leaves_.back()->values.full()) {
leaves_.push_back(make_leaf());
rebuild_positions();
rebuild_directory();
}
auto& leaf = *leaves_.back();
leaf.values.push_back(std::move(value));
++size_;
directory_.update(leaves_.size() - 1, 1);
relocate(leaf.values[leaf.values.size() - 1], leaf.id, leaf.values.size() - 1);
}
void push_back(T value) {
push_back(std::move(value), NoRelocate{});
}
template <class Relocate>
void insert(std::size_t index, T value, Relocate&& relocate) {
if (index > size_) {
throw std::out_of_range("tiered insertion index out of range");
}
if (index == size_) {
push_back(std::move(value), std::forward<Relocate>(relocate));
return;
}
const auto location = directory_.locate(index);
auto& leaf = *leaves_[location.leaf];
if (!leaf.values.full()) {
leaf.values.insert(location.local, std::move(value));
++size_;
directory_.update(location.leaf, 1);
refresh_leaf(location.leaf, relocate);
return;
}
split_and_insert(location.leaf, location.local, std::move(value), relocate);
++size_;
rebuild_directory();
}
void insert(std::size_t index, T value) {
insert(index, std::move(value), NoRelocate{});
}
template <class Relocate>
T erase(std::size_t index, Relocate&& relocate) {
check_index(index);
const auto location = directory_.locate(index);
auto removed = leaves_[location.leaf]->values.erase(location.local);
--size_;
if (leaves_[location.leaf]->values.empty() && leaves_.size() > 1) {
retire_leaf(location.leaf);
rebuild_positions();
rebuild_directory();
return removed;
}
if (try_merge(location.leaf, relocate)) {
return removed;
}
directory_.update(location.leaf, -1);
refresh_leaf(location.leaf, relocate);
return removed;
}
T erase(std::size_t index) {
return erase(index, NoRelocate{});
}
void clear() noexcept {
leaves_.clear();
leaf_positions_.clear();
size_ = 0;
next_leaf_id_ = 0;
directory_.rebuild({});
}
template <class Function>
void for_each(Function&& function) {
for (auto& leaf : leaves_) {
for (std::size_t i = 0; i < leaf->values.size(); ++i) {
function(leaf->values[i]);
}
}
}
template <class Function>
void for_each(Function&& function) const {
for (const auto& leaf : leaves_) {
for (std::size_t i = 0; i < leaf->values.size(); ++i) {
function(leaf->values[i]);
}
}
}
template <class Predicate>
[[nodiscard]] std::size_t find_if(Predicate&& predicate) const {
std::size_t logical = 0;
for (const auto& leaf : leaves_) {
for (std::size_t i = 0; i < leaf->values.size(); ++i, ++logical) {
if (predicate(leaf->values[i])) {
return logical;
}
}
}
return npos;
}
template <class Predicate, class Function>
void for_each_match(Predicate&& predicate, Function&& function) const {
std::size_t logical = 0;
for (const auto& leaf : leaves_) {
for (std::size_t i = 0; i < leaf->values.size(); ++i, ++logical) {
if (predicate(leaf->values[i])) {
function(logical, leaf->values[i]);
}
}
}
}
template <class Function>
void for_each_with_location(Function&& function) const {
for (const auto& leaf : leaves_) {
for (std::size_t i = 0; i < leaf->values.size(); ++i) {
function(leaf->values[i], leaf->id, i);
}
}
}
[[nodiscard]] std::vector<T> to_vector_copy() const {
std::vector<T> result;
result.reserve(size_);
for_each([&](const T& value) { result.push_back(value); });
return result;
}
[[nodiscard]] std::vector<T> to_vector_move() {
std::vector<T> result;
result.reserve(size_);
for_each([&](T& value) { result.push_back(std::move(value)); });
return result;
}
template <class Relocate>
static TieredStorage from_vector(std::vector<T>&& source,
TieredConfig config,
Relocate&& relocate) {
TieredStorage result(config);
if (source.empty()) {
return result;
}
// Allocate the complete destination, including its directory, before
// moving the first value. For nothrow-move T the following loop and
// the final TieredStorage move cannot fail, so an allocation failure
// leaves source completely untouched.
const auto target_occupancy = result.prepare_bulk_destination(source.size());
std::size_t leaf_index = 0;
for (auto& value : source) {
auto& leaf = *result.leaves_[leaf_index];
leaf.values.push_back(std::move(value));
++result.size_;
if (leaf.values.size() == target_occupancy
&& leaf_index + 1 < result.leaves_.size()) {
++leaf_index;
}
}
assert(result.directory_.total() == result.size_);
for (std::size_t leaf = 0; leaf < result.leaves_.size(); ++leaf) {
result.refresh_leaf(leaf, relocate);
}
return result;
}
static TieredStorage from_vector(std::vector<T>&& source, TieredConfig config) {
return from_vector(std::move(source), config, NoRelocate{});
}
static TieredStorage from_vector_copy(const std::vector<T>& source,
TieredConfig config) {
TieredStorage result(config);
if (source.empty()) {
return result;
}
const auto target_occupancy = bulk_target_occupancy(
result.config_.leaf_capacity);
const auto count = (source.size() + target_occupancy - 1)
/ target_occupancy;
result.leaves_.reserve(count);
for (const auto& value : source) {
if (result.leaves_.empty()
|| result.leaves_.back()->values.size() == target_occupancy) {
result.leaves_.push_back(result.make_leaf());
}
result.leaves_.back()->values.push_back(value);
++result.size_;
}
result.rebuild_positions();
result.rebuild_directory();
return result;
}
static TieredStorage reconfigured_copy(const TieredStorage& source,
TieredConfig config) {
TieredStorage result(config);
if (source.empty()) {
return result;
}
const auto target_occupancy = bulk_target_occupancy(
result.config_.leaf_capacity);
const auto count = (source.size() + target_occupancy - 1)
/ target_occupancy;
result.leaves_.reserve(count);
source.for_each([&](const T& value) {
if (result.leaves_.empty()
|| result.leaves_.back()->values.size() == target_occupancy) {
result.leaves_.push_back(result.make_leaf());
}
result.leaves_.back()->values.push_back(value);
++result.size_;
});
result.rebuild_positions();
result.rebuild_directory();
return result;
}
// Rebuild a tiered layout without an intermediate vector. All leaves and
// directory arrays are allocated first; consequently this operation has a
// strong allocation-failure guarantee when T is nothrow-move-constructible.
static TieredStorage reconfigured_move(TieredStorage& source,
TieredConfig config) {
TieredStorage result(config);
if (source.empty()) {
return result;
}
const auto target_occupancy = result.prepare_bulk_destination(source.size());
std::size_t leaf_index = 0;
source.for_each([&](T& value) {
auto& leaf = *result.leaves_[leaf_index];
leaf.values.push_back(std::move(value));
++result.size_;
if (leaf.values.size() == target_occupancy
&& leaf_index + 1 < result.leaves_.size()) {
++leaf_index;
}
});
assert(result.directory_.total() == result.size_);
return result;
}
[[nodiscard]] std::size_t logical_index(leaf_id_type leaf_id,
std::size_t local) const {
if (leaf_id >= leaf_positions_.size()) {
throw std::out_of_range("unknown tiered leaf id");
}
const auto position = leaf_positions_[leaf_id];
if (position == npos || local >= leaves_[position]->values.size()) {
throw std::out_of_range("stale tiered location");
}
return directory_.prefix_before(position) + local;
}
[[nodiscard]] std::size_t allocated_bytes() const noexcept {
std::size_t bytes = leaves_.capacity() * sizeof(typename decltype(leaves_)::value_type)
+ leaf_positions_.capacity() * sizeof(std::size_t)
+ directory_.allocated_bytes();
for (const auto& leaf : leaves_) {
bytes += sizeof(Leaf) + leaf->values.allocated_bytes();
}
return bytes;
}
private:
struct NoRelocate {
void operator()(const T&, leaf_id_type, std::size_t) const noexcept {}
};
struct Leaf {
Leaf(leaf_id_type leaf_id, std::size_t capacity)
: id(leaf_id), values(capacity) {}
leaf_id_type id;
RingBlock<T> values;
};
static TieredConfig normalize(TieredConfig config) {
config.leaf_capacity = std::clamp<std::size_t>(config.leaf_capacity, 4, 1U << 20U);
config.directory_fanout = std::clamp<std::size_t>(config.directory_fanout, 2, 1U << 16U);
config.directory_levels = std::clamp<std::size_t>(config.directory_levels, 1, 8);
return config;
}
// Bulk-loading completely full leaves makes the first random insertion
// into almost every leaf pay for a split and a directory rebuild. Keep a
// small, deterministic reserve instead. Seven eighths preserves compact
// memory usage while giving each freshly built leaf enough room for the
// short edit bursts that motivate switching away from vector storage.
[[nodiscard]] static std::size_t
bulk_target_occupancy(std::size_t capacity) noexcept {
return std::max<std::size_t>(1, capacity - capacity / 8);
}
// Construct the entire shape needed by a bulk load while source elements
// are still untouched. RingBlock allocates all of its optional slots in
// its constructor, so inserting into these leaves does not allocate.
[[nodiscard]] std::size_t prepare_bulk_destination(std::size_t element_count) {
assert(element_count != 0);
const auto target_occupancy = bulk_target_occupancy(config_.leaf_capacity);
const auto count = 1 + (element_count - 1) / target_occupancy;
leaves_.reserve(count);
leaf_positions_.reserve(count);
std::vector<std::size_t> expected_sizes;
expected_sizes.reserve(count);
auto remaining = element_count;
for (std::size_t i = 0; i < count; ++i) {
leaves_.push_back(make_leaf());
const auto leaf_size = std::min(remaining, target_occupancy);
expected_sizes.push_back(leaf_size);
remaining -= leaf_size;
}
rebuild_positions();
directory_.rebuild(expected_sizes);
return target_occupancy;
}
[[nodiscard]] std::unique_ptr<Leaf> make_leaf() {
if (next_leaf_id_ == std::numeric_limits<leaf_id_type>::max()) {
throw std::length_error("tiered leaf id space exhausted");
}
const auto id = next_leaf_id_++;
if (leaf_positions_.size() <= id) {
leaf_positions_.resize(static_cast<std::size_t>(id) + 1, npos);
}
return std::make_unique<Leaf>(id, config_.leaf_capacity);
}
void check_index(std::size_t index) const {
if (index >= size_) {
throw std::out_of_range("tiered index out of range");
}
}
[[nodiscard]] typename HierarchicalSizeIndex::Location
locate_unchecked(std::size_t index) const noexcept {
assert(index < size_);
try {
return directory_.locate(index);
} catch (...) {
std::terminate();
}
}
void rebuild_positions() {
std::fill(leaf_positions_.begin(), leaf_positions_.end(), npos);
for (std::size_t i = 0; i < leaves_.size(); ++i) {
const auto id = leaves_[i]->id;
if (leaf_positions_.size() <= id) {
leaf_positions_.resize(static_cast<std::size_t>(id) + 1, npos);
}
leaf_positions_[id] = i;
}
}
void rebuild_directory() {
std::vector<std::size_t> sizes;
sizes.reserve(leaves_.size());
for (const auto& leaf : leaves_) {
sizes.push_back(leaf->values.size());
}
directory_.rebuild(sizes);
assert(directory_.total() == size_);
}
template <class Relocate>
void refresh_leaf(std::size_t leaf_index, Relocate& relocate) {
auto& leaf = *leaves_[leaf_index];
for (std::size_t i = 0; i < leaf.values.size(); ++i) {
relocate(leaf.values[i], leaf.id, i);
}
}
template <class Relocate>
void split_and_insert(std::size_t leaf_index,
std::size_t local,
T value,
Relocate& relocate) {
auto& old = *leaves_[leaf_index];
std::vector<T> combined;
combined.reserve(old.values.size() + 1);
for (std::size_t i = 0; i < old.values.size(); ++i) {
combined.push_back(std::move(old.values[i]));
}
combined.insert(combined.begin() + static_cast<std::ptrdiff_t>(local), std::move(value));
const auto left_id = old.id;
auto left = std::make_unique<Leaf>(left_id, config_.leaf_capacity);
auto right = make_leaf();
const auto middle = combined.size() / 2;
for (std::size_t i = 0; i < middle; ++i) {
left->values.push_back(std::move(combined[i]));
}
for (std::size_t i = middle; i < combined.size(); ++i) {
right->values.push_back(std::move(combined[i]));
}
leaves_[leaf_index] = std::move(left);
leaves_.insert(leaves_.begin() + static_cast<std::ptrdiff_t>(leaf_index + 1),
std::move(right));
rebuild_positions();
refresh_leaf(leaf_index, relocate);
refresh_leaf(leaf_index + 1, relocate);
}
void retire_leaf(std::size_t leaf_index) {
const auto id = leaves_[leaf_index]->id;
if (id < leaf_positions_.size()) {
leaf_positions_[id] = npos;
}
leaves_.erase(leaves_.begin() + static_cast<std::ptrdiff_t>(leaf_index));
}
template <class Relocate>
bool try_merge(std::size_t leaf_index, Relocate& relocate) {
if (leaves_.size() < 2) {
return false;
}
const auto threshold = std::max<std::size_t>(1, config_.leaf_capacity / 4);
if (leaves_[leaf_index]->values.size() >= threshold) {
return false;
}
std::size_t left_index = leaf_index;
std::size_t right_index = leaf_index + 1;
if (right_index >= leaves_.size()) {
left_index = leaf_index - 1;
right_index = leaf_index;
}
const auto combined_size = leaves_[left_index]->values.size()
+ leaves_[right_index]->values.size();
if (combined_size > config_.leaf_capacity) {
return false;
}
const auto survivor_id = leaves_[left_index]->id;
auto merged = std::make_unique<Leaf>(survivor_id, config_.leaf_capacity);
for (std::size_t i = 0; i < leaves_[left_index]->values.size(); ++i) {
merged->values.push_back(std::move(leaves_[left_index]->values[i]));
}
for (std::size_t i = 0; i < leaves_[right_index]->values.size(); ++i) {
merged->values.push_back(std::move(leaves_[right_index]->values[i]));
}
const auto retired_id = leaves_[right_index]->id;
leaves_[left_index] = std::move(merged);
leaves_.erase(leaves_.begin() + static_cast<std::ptrdiff_t>(right_index));
if (retired_id < leaf_positions_.size()) {
leaf_positions_[retired_id] = npos;
}
rebuild_positions();
rebuild_directory();
refresh_leaf(left_index, relocate);
return true;
}
TieredConfig config_;
std::vector<std::unique_ptr<Leaf>> leaves_;
HierarchicalSizeIndex directory_;
std::size_t size_ = 0;
leaf_id_type next_leaf_id_ = 0;
std::vector<std::size_t> leaf_positions_;
};
} // namespace detail
} // namespace uc