Pools¶
template<typename Row, typename... Properties>
using Interned = Store<pool<Row>, Properties...>; // rows, each stored once, by handle
template<typename Row, typename Handle, typename... Properties>
using Named = Store<pool<Row, Handle>, Properties...>; // the same, handing out the index Handle
template<typename T, typename... Properties>
using Slots = Store<slots<T>, Properties...>; // elements in slots that never move
-
class Pool¶
A pool:
Store<slots<Row>, Properties...>, interning withunique. The namePoolstands for the resolved Store type on this page.
A pool is one kind (pool, handles with unique) whatever it holds: rows (a Store or a registered
container of elements) are interned as rows, on this page; values that are not rows are interned whole, on
Records, repositories, views. Slots (handles without unique) are a kind of their own.
A pool is reached by handle, not iterated. A handle is an index<Row>, or the index the
request names, pool<V, H> (an urd::index of any type: a pool of urd::data<T> handing out index<T>,
rows handing out a repository’s own row index): a pool of rows kept in full numbers them 0..size()-1, so a pool without structural_sharing is counted and walked by its handles. A pool
with structural_sharing (a tree pool) shares nodes between rows and keeps no list of them: it has a handle of its
own, and no size. Every handle type has a none value (is_none()), which find and erase give where there is
no row to name, as a pool of records does.
Request |
Representation |
|---|---|
|
|
|
|
|
|
any of them with |
the same, thread-safe |
|
the bit row interner: rows of indices, each element in the |
|
the schema row interner: rows of unsigned columns stored by the |
|
|
Template parameters¶
RowA Store (
Store<sequence<T>, frozen>,Store<set<I>, dense, frozen>), or any range constructible from its elements. The Row’s own rules decide which rows are equal: a set’s in any order, once.HThe handle,
index<Row>by default: anyurd::index. Pools that number their rows hand it out; a tree pool, whose handle is its own, takes only the default (astatic_assert).
Member types¶
|
|
|
|
|
|
Member functions: interning pools (handles, unique)¶
-
template<std::ranges::input_range Elements>
std::pair<handle_type, bool> insert(const Elements &elements)¶
-
std::pair<handle_type, bool> insert(std::initializer_list<element> elements)¶
The handle of the row with these elements, and whether the insert stored anything:
falsewhere an equal row is held (withstructural_sharing, where every node of it is), exactly wherefindwould have given the handle. A tree pool of dense sets takes a row that keeps its bits (significant_words(), Sets) by its 64-bit words, read as its blocks in place: no element listed, nothing allocated.findlikewise.
-
handle_type find(std::initializer_list<element> elements) const¶
The handle
insert(elements)would give, where inserting would store nothing; else none. Withoutstructural_sharing, an equal row inserted before. A tree pool also finds a row it can build from the nodes it holds, such as a part of a longer row.
-
std::size_t size() const¶
-
bool empty() const¶
Without
structural_sharing: the distinct rows, whose handles are 0..size()-1.
-
handle_type erase(handle_type handle)¶
Without
structural_sharing,monotone,concurrentorlayered: erases the row by moving the last row into its place, which then hashandle; gives the handle the moved row had, or none where the erased row was the last. The moved row’s views stay valid. Whatever keeps handles (postings over the rows, counts) follows the move.
-
std::pair<handle_type, bool> insert(handle_type base, std::span<const write_type> writes)¶
Sequence rows: the row
basewithwritesapplied (sorted by position, each once and within the row). A pool that shares structure stores only what changed.
-
std::pair<handle_type, bool> insert(handle_type base, std::span<const element> added, std::span<const element> removed)¶
Rows that are sets:
basewithaddedput in and thenremovedtaken out.
-
bool contains(handle_type handle, const element &value) const¶
Rows that are sets; a tree pool reads one path, not the row.
-
value_type get(handle_type handle) const¶
-
std::size_t row_size(handle_type handle) const¶
-
void copy(handle_type handle, std::span<element> out) const¶
The row’s elements into
out, which must holdrow_size(handle)of them; allocates nothing (a tree pool reads them, and counts them forrow_size, along the tree).
-
void copy(handle_type handle, value_type &out) const¶
A tree pool of dense sets: the row into
outword by word (assign_words, Sets), its storage reused; allocates nothing whereoutholds the row’s words already.
-
auto at(handle_type handle) const¶
-
auto operator[](handle_type handle) const¶
The stored row without copying: a span of the elements, or for a dense set its bits (
BitsetSpan). Valid until the pool is cleared. Every pool has it that keeps rows in full; one withstructural_sharingkeeps a row as a tree of shared nodes and has neither this nor lookups byprehashedrows.
-
void clear()¶
Forgets every row; the handles given so far mean nothing after; a layered pool its own layer’s. Shrinkable or
monotone_until_clearpools.
Member functions: slots (handles without unique)¶
-
class Slots¶
A pool of slots:
Store<slots<T>, Properties...>. The nameSlotsstands for the resolved Store type below.
-
handle_type insert(const value_type &element)¶
-
handle_type insert(value_type &&element)¶
-
template<typename ...Args>
handle_type emplace(Args&&... args)¶ A slot holding the element: an erased one where there is one, else a new one.
-
const value_type &operator[](handle_type handle) const¶
-
value_type &operator[](handle_type handle)¶
-
std::size_t size() const¶
-
bool empty() const¶
-
void erase(handle_type handle)¶
Frees the slot for reuse; the handle and references to the element mean nothing after.
-
void clear()¶
make_unique() and make_shared() own an acquired slot as std::unique_ptr and std::shared_ptr do:
the slot goes back with its (last) owner, and once as many owners have lived at once nothing allocates.
With keyed<K> (single-threaded), slots are acquired only by key: make_shared(key, args...) hands out a slot
given back under an equal key (urd::Hash, urd::EqualTo) as it is, else a new T(key, args...). Each slot
keeps its key and goes back to its key’s free list, so a slot is never reused for another key (a buffer shaped by its
column widths keeps its shape); once each key has had as many owners at once, nothing allocates.
using Buffers = Store<slots<Rows>, keyed<column_widths>>;
auto buffers = Buffers();
auto rows = buffers.make_shared(column_widths { 8, 16 }); // Rows(widths), or one given back under equal widths
Complexity¶
Representation |
access |
insert row |
find row |
memory |
holds |
|---|---|---|---|---|---|
|
as its part |
as its part |
as its part |
O(n), 0.7 words |
its own rows, indices after the parent’s |
|
as its part |
as its part |
as its part |
O(n), 4.5 words |
its own rows, indices after the parent’s |
|
as its part |
as its part |
as its part |
O(n), 1.1 words |
its own rows, indices after the parent’s |
|
as its part |
as its part |
as its part |
O(n), 4.1 words |
its own rows, indices after the parent’s |
|
O(1) |
O(r)~ |
O(r)~ |
O(n), 4.5 words |
each distinct row once, in full |
|
O(r) |
O(r)~ |
O(r)~ |
O(n), 1 words |
nodes shared across rows: a delta adds about k log r nodes |
|
O(1) |
O(r)~ |
O(r)~ |
O(n), 4.1 words |
each distinct row once, in full; inserts under a lock |
|
O(r) |
O(r)~ |
O(r)~ |
O(n), 1.8 words |
nodes shared across rows: a delta adds about k log r nodes |
|
O(r) |
O(r) |
O(r) |
O(n), 0.2 words |
nodes shared across rows; all-zero subtrees take nothing |
|
O(r) |
O(r) |
O(r) |
O(n), 0.8 words |
nodes shared across rows; all-zero subtrees take nothing |
|
O(1) |
O(1)* |
— |
O(n), 1.5 words |
stable slots; freed slots reused first (with keyed<K>, under an equal key) |
|
O(1) |
O(1)* |
— |
O(n), 1.6 words |
stable slots; freed slots reused first, under a lock |
|
O(1) |
O(r) |
O(r) |
O(n), 2.5 words |
each distinct record once, as one block of bytes |
|
O(1) |
O(r) |
O(r) |
O(n), 2.6 words |
each distinct record once, in segments that never move |
|
O(1) |
O(r) |
O(r) |
O(n), 2.7 words |
each distinct record once; inserts under a lock |
|
as its part |
as its part |
as its part |
O(n), 2.6 words |
its own rows, indices after the parent’s |
|
as its part |
as its part |
as its part |
O(n), 2.7 words |
its own rows, indices after the parent’s |
|
O(1) |
O(r) |
O(r) |
O(n), 2.5 words |
each distinct record once, serialized into an arena |
|
O(1) |
O(r) |
O(r) |
O(n), 2.7 words |
each distinct record once, serialized; inserts under a lock |
|
as its part |
as its part |
as its part |
O(n), 2.5 words |
its own rows, indices after the parent’s |
|
as its part |
as its part |
as its part |
O(n), 2.7 words |
its own rows, indices after the parent’s |
|
O(1) |
O(r) |
O(r) |
O(n), 2.5 words |
each distinct value once |
|
O(1) |
O(r) |
O(r) |
O(n), 2.6 words |
each distinct value once, in segments that never move |
|
O(1) |
O(r) |
O(r) |
O(n), 2.7 words |
each distinct value once; inserts under a lock |
* amortized, ~ expected; n elements, U the key range, r a row’s length, k a delta’s changes. Memory is the heap it holds (urd::memory_usage): what it grows in, and per element (or key, or slot) at most so many words, each the size of an element and at least a machine word. The declared costs; what they measure to is in Benchmarks.
Measured, not only bounded: for 20,000 versions of a row that each differ from an earlier one in 3 positions, a tree pool takes 136–271 bytes per version against 447–4,409 for rows stored in full (Benchmarks).
Example¶
using States = Store<pool<Store<set<Atom>, dense, frozen>>, structural_sharing>;
auto states = States();
const auto initial = states.insert(facts); // index<...>
const auto next = states.insert(initial, added, removed); // a delta: only the changed path is new
states.contains(next, atom);
Rows by column widths¶
With by_column_widths a pool is built with an urd::column_widths (urd/repr/containers/schema_rows.hpp):
its columns, each the bits (1 to 64) of the unsigned value it holds. urd knows no types: what a column’s value means
(an index, a number’s bits, a label) is the user’s. Rows lie back to back, each the widths’ bits().
using Objects = Store<pool<Store<sequence<index<Object>>, frozen>>, by_column_widths>;
auto rows = Objects(column_widths(3, 10)); // three objects of 10 bits per row
rows.insert(std::vector { a, b, c }); // any range of values, one per column
rows.view(h).get<index<Object>>(0); // a row_type (SchemaRow): get(i), get<T>(i), as<T>(), size()
auto row = Objects::row_builder_type(rows.column_widths()); // an EncodedRow
row.set(0, 7); // false where 7 does not fit the column
rows.find(row.row());
A row goes in as any range of values, one per column: unsigned integers or indices (index_of); or as a row of
equal widths, taken as it is. A value that does not fit its column throws std::out_of_range, a count other than the
columns’ std::invalid_argument, before anything is stored. view(h) hands out the Store’s row_type (a
SchemaRow): get(i) (the column’s unsigned value), get<T>(i) (as an unsigned integer or index type),
as<T>() (the columns as a random-access range of T), size(). The Store’s row_builder_type (an
EncodedRow) builds a row column by column (set, copy from another row) to look up or insert.
A row’s padding is zero. A row hashes and compares by its values (urd::hash_range), whatever the bits of its
columns, so a layered pool finds a parent’s row through wider columns, and a key hashed once (prehashed, with
hash_range of the values) serves the whole chain.
Some rows of a pool¶
urd::subset_view<Pool> (urd/elements/subset_view.hpp) is some rows of a pool, by their sorted handles: it
borrows the pool and the handles, which must outlive it. size(), empty(), operator[](i) (the pool’s
view of the i-th handle) and begin()/end() (a borrowed random-access range of those views) read them;
contains(key) finds the row in the pool (by values, or a stored row) and then its handle among these by binary
search. Over a layered pool, the handles may be a parent’s.
const auto subset = urd::subset_view(rows, std::span(sorted_handles));
subset.contains(std::array<std::uint64_t, 2> { 3, 300 });