Files
scylladb/utils/intrusive-array.hh
Kefu Chai 7215d4bfe9 utils: do not include unused headers
these unused includes were identifier by clang-include-cleaner. after
auditing these source files, all of the reports have been confirmed.

please note, because quite a few source files relied on
`utils/to_string.hh` to pull in the specialization of
`fmt::formatter<std::optional<T>>`, after removing
`#include <fmt/std.h>` from `utils/to_string.hh`, we have to
include `fmt/std.h` directly.

Signed-off-by: Kefu Chai <kefu.chai@scylladb.com>
2025-01-14 07:56:39 -05:00

338 lines
11 KiB
C++

/*
* Copyright (C) 2020-present ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdlib>
#include <limits>
#include <utility>
#include "utils/assert.hh"
#include "utils/collection-concepts.hh"
template <typename T>
concept BoundsKeeper = requires (T val, bool bit) {
{ val.is_head() } noexcept -> std::same_as<bool>;
{ val.set_head(bit) } noexcept -> std::same_as<void>;
{ val.is_tail() } noexcept -> std::same_as<bool>;
{ val.set_tail(bit) } noexcept -> std::same_as<void>;
{ val.with_train() } noexcept -> std::same_as<bool>;
{ val.set_train(bit) } noexcept -> std::same_as<void>;
};
/*
* A plain array of T-s that grows and shrinks by constructing a new
* instances. Holds at least one element. Has facilities for sorting
* the elements and for doing "container_of" by the given element
* pointer. LSA-compactible.
*
* Important feature of the array is zero memory overhead -- it doesn't
* keep its size/capacity onboard. The size is calculated each time by
* walking the array of T-s and checking which one of them is the tail
* element. Respectively, the T must keep head/tail flags on itself.
*/
template <typename T>
requires BoundsKeeper<T> && std::is_nothrow_move_constructible_v<T>
class intrusive_array {
// Sanity constant to avoid infinite loops searching for tail
static constexpr int max_len = std::numeric_limits<short int>::max();
union maybe_constructed {
maybe_constructed() { }
~maybe_constructed() { }
T object;
/*
* Train is 1 or more allocated but unoccupied memory slots after
* the tail one. Being unused, this memory keeps the train length.
* An array with the train is marked with the respective flag on
* the 0th element. Train is created by the erase() call and can
* be up to 65535 elements long
*
* Train length is included into the storage_size() to make
* allocator and compaction work correctly, but is not included
* into the number_of_elements(), so the array behaves just like
* there's no train
*
* Respectively both grow and shrink constructors do not carry
* the train (and drop the bit from 0th element) and don't expect
* the memory for the new array to include one
*/
unsigned short train_len;
static_assert(sizeof(T) >= sizeof(unsigned short));
};
maybe_constructed _data[1];
size_t number_of_elements() const noexcept {
for (int i = 0; i < max_len; i++) {
if (_data[i].object.is_tail()) {
return i + 1;
}
}
std::abort();
}
public:
size_t storage_size() const noexcept {
size_t nr = number_of_elements();
if (_data[0].object.with_train()) {
nr += _data[nr].train_len;
}
return nr * sizeof(T);
}
using iterator = T*;
using const_iterator = const T*;
/*
* There are 3 constructing options for the array: initial, grow
* and shrink.
*
* * initial just creates a 1-element array
* * grow -- makes a new one moving all elements from the original
* array and inserting the one (only one) more element at the given
* position
* * shrink -- also makes a new array skipping the not needed
* element while moving them from the original one
*
* In all cases the enough big memory chunk must be provided by the
* caller!
*
* Note, that none of them calls destructors on T-s, unlike vector.
* This is because when the older array is destroyed it has no idea
* about whether or not it was grown/shrunk and thus it destroys
* T-s itself.
*/
// Initial
template <typename... Args>
intrusive_array(Args&&... args) {
new (&_data[0].object) T(std::forward<Args>(args)...);
_data[0].object.set_head(true);
_data[0].object.set_tail(true);
}
// Growing
struct grow_tag {
int add_pos;
};
template <typename... Args>
intrusive_array(intrusive_array& from, grow_tag grow, Args&&... args) {
// The add_pos is strongly _expected_ to be within bounds
int i, off = 0;
bool tail = false;
for (i = 0; !tail; i++) {
if (i == grow.add_pos) {
off = 1;
continue;
}
tail = from._data[i - off].object.is_tail();
new (&_data[i].object) T(std::move(from._data[i - off].object));
}
SCYLLA_ASSERT(grow.add_pos <= i && i < max_len);
new (&_data[grow.add_pos].object) T(std::forward<Args>(args)...);
_data[0].object.set_head(true);
_data[0].object.set_train(false);
if (grow.add_pos == 0) {
_data[1].object.set_head(false);
}
_data[i - off].object.set_tail(true);
if (off == 0) {
_data[i - 1].object.set_tail(false);
}
}
// Shrinking
struct shrink_tag {
int del_pos;
};
intrusive_array(intrusive_array& from, shrink_tag shrink) {
int i, off = 0;
bool tail = false;
for (i = 0; !tail; i++) {
tail = from._data[i].object.is_tail();
if (i == shrink.del_pos) {
off = 1;
} else {
new (&_data[i - off].object) T(std::move(from._data[i].object));
}
}
_data[0].object.set_head(true);
_data[0].object.set_train(false);
_data[i - off - 1].object.set_tail(true);
}
intrusive_array(const intrusive_array& other) = delete;
intrusive_array(intrusive_array&& other) noexcept {
bool tail = false;
int i;
for (i = 0; !tail; i++) {
tail = other._data[i].object.is_tail();
new (&_data[i].object) T(std::move(other._data[i].object));
}
if (_data[0].object.with_train()) {
_data[i].train_len = other._data[i].train_len;
}
}
~intrusive_array() {
bool tail = false;
for (int i = 0; !tail; i++) {
tail = _data[i].object.is_tail();
_data[i].object.~T();
}
}
/*
* Drops the element in-place at position @pos and grows the
* "train". To be used in places where reconstruction is not
* welcome (e.g. because it throws)
*
* Single-elemented array cannot be erased from, just drop it
* altogether if needed
*/
void erase(int pos) noexcept {
SCYLLA_ASSERT(!is_single_element());
SCYLLA_ASSERT(pos < max_len);
bool with_train = _data[0].object.with_train();
bool tail = _data[pos].object.is_tail();
_data[pos].object.~T();
if (tail) {
SCYLLA_ASSERT(pos > 0);
_data[pos - 1].object.set_tail(true);
} else {
while (!tail) {
new (&_data[pos].object) T(std::move(_data[pos + 1].object));
_data[pos + 1].object.~T();
tail = _data[pos++].object.is_tail();
}
_data[0].object.set_head(true);
}
_data[0].object.set_train(true);
unsigned short train_len = with_train ? _data[pos + 1].train_len : 0;
SCYLLA_ASSERT(train_len < max_len);
_data[pos].train_len = train_len + 1;
}
T& operator[](int pos) noexcept { return _data[pos].object; }
const T& operator[](int pos) const noexcept { return _data[pos].object; }
iterator begin() noexcept { return &_data[0].object; }
const_iterator begin() const noexcept { return &_data[0].object; }
const_iterator cbegin() const noexcept { return &_data[0].object; }
iterator end() noexcept { return &_data[number_of_elements()].object; }
const_iterator end() const noexcept { return &_data[number_of_elements()].object; }
const_iterator cend() const noexcept { return &_data[number_of_elements()].object; }
size_t index_of(iterator i) const noexcept { return i - &_data[0].object; }
size_t index_of(const_iterator i) const noexcept { return i - &_data[0].object; }
bool is_single_element() const noexcept { return _data[0].object.is_tail(); }
// A helper for keeping the array sorted
template <typename K, typename Compare>
requires Comparable<K, T, Compare>
const_iterator lower_bound(const K& val, Compare cmp, bool& match) const {
int i = 0;
do {
auto x = cmp(_data[i].object, val);
if (x >= 0) {
match = (x == 0);
break;
}
} while (!_data[i++].object.is_tail());
return &_data[i].object;
}
template <typename K, typename Compare>
requires Comparable<K, T, Compare>
iterator lower_bound(const K& val, Compare cmp, bool& match) {
return const_cast<iterator>(const_cast<const intrusive_array*>(this)->lower_bound(val, std::move(cmp), match));
}
template <typename K, typename Compare>
requires Comparable<K, T, Compare>
const_iterator lower_bound(const K& val, Compare cmp) const {
bool match = false;
return lower_bound(val, cmp, match);
}
template <typename K, typename Compare>
requires Comparable<K, T, Compare>
iterator lower_bound(const K& val, Compare cmp) {
return const_cast<iterator>(const_cast<const intrusive_array*>(this)->lower_bound(val, std::move(cmp)));
}
// And its peer ... just to be used
template <typename K, typename Compare>
requires Comparable<K, T, Compare>
const_iterator upper_bound(const K& val, Compare cmp) const {
int i = 0;
do {
if (cmp(_data[i].object, val) > 0) {
break;
}
} while (!_data[i++].object.is_tail());
return &_data[i].object;
}
template <typename K, typename Compare>
requires Comparable<K, T, Compare>
iterator upper_bound(const K& val, Compare cmp) {
return const_cast<iterator>(const_cast<const intrusive_array*>(this)->upper_bound(val, std::move(cmp)));
}
template <typename Func>
requires Disposer<Func, T>
void for_each(Func&& fn) noexcept {
bool tail = false;
for (int i = 0; !tail; i++) {
tail = _data[i].object.is_tail();
fn(&_data[i].object);
}
}
size_t size() const noexcept { return number_of_elements(); }
static intrusive_array& from_element(T* ptr, int& idx) noexcept {
idx = 0;
while (!ptr->is_head()) {
SCYLLA_ASSERT(idx < max_len); // may the force be with us...
idx++;
ptr--;
}
static_assert(offsetof(intrusive_array, _data[0].object) == 0);
return *reinterpret_cast<intrusive_array*>(ptr);
}
};