/* * Copyright (C) 2021-present ScyllaDB */ /* * SPDX-License-Identifier: AGPL-3.0-or-later */ #pragma once #include #include #include #include #include "utils/allocation_strategy.hh" #include "utils/array-search.hh" #include class size_calculator; namespace compact_radix_tree { template class printer; template inline unsigned find_in_array(uint8_t val, const uint8_t* arr); template <> inline unsigned find_in_array<4>(uint8_t val, const uint8_t* arr) { return utils::array_search_4_eq(val, arr); } template <> inline unsigned find_in_array<8>(uint8_t val, const uint8_t* arr) { return utils::array_search_8_eq(val, arr); } template <> inline unsigned find_in_array<16>(uint8_t val, const uint8_t* arr) { return utils::array_search_16_eq(val, arr); } template <> inline unsigned find_in_array<32>(uint8_t val, const uint8_t* arr) { return utils::array_search_32_eq(val, arr); } template <> inline unsigned find_in_array<64>(uint8_t val, const uint8_t* arr) { return utils::array_search_x32_eq(val, arr, 2); } // A union of any number of types. template struct variadic_union; template struct variadic_union { union { Tx _this; }; variadic_union() noexcept {} ~variadic_union() {} }; template struct variadic_union { union { Tx _this; variadic_union _other; }; variadic_union() noexcept {} ~variadic_union() {} }; /* * Radix tree implementation for the key being an integer type. * The search key is split into equal-size pieces to find the * next node in each level. The pieces are defined compile-time * so the tree is compile-time limited in ints depth. * * Uses 3 memory optimizations: * - a node dynamically grows in size depending on the range of * keys it carries * - additionally, if the set of keys on a node is very sparse the * node may become "indirect" thus keeping only the actual set * of keys * - if a node has 1 child it's removed from the tree and this * loneley kid is attached directly to its (former) grandfather */ template requires std::is_nothrow_move_constructible_v && std::is_integral_v class tree { friend class ::size_calculator; template friend class printer; class leaf_node; class inner_node; struct node_head; class node_head_ptr; public: /* * The search key in the tree is an integer, the whole * logic below is optimized for that. */ using key_t = std::make_unsigned_t; /* * The lookup uses 7-bit pieces from the key to search on * each level. Thus all levels but the last one keep pointers * on lower levels, the last one is the leaf node that keeps * values on board. * * The 8th bit in the node index byte is used to denote an * unused index which is quite helpful. */ using node_index_t = uint8_t; static constexpr unsigned radix_bits = 7; static constexpr key_t radix_mask = (1 << radix_bits) - 1; static constexpr unsigned leaf_depth = (8 * sizeof(key_t) + radix_bits - 1) / radix_bits - 1; static constexpr unsigned node_index_limit = 1 << radix_bits; static_assert(node_index_limit != 0); static constexpr node_index_t unused_node_index = node_index_limit; private: /* * Nodes can be of 2 kinds -- direct and indirect. * * Direct nodes are arrays of elements. Getting a value from * this node is simple indexing. There are 2 of them -- static * and dynamic. Static nodes have fixed size capable to keep all * the possible indices, dynamic work like a vector growing in * size. Former occupy more space, but work a bit faster because * of * missing boundary checks. * * Indirect nodes keep map of indices on board and perform lookup * rather than direct indexing to get a value. They also grow in * size, but unlike dynamic direct nodes by converting between each * other. * * When a node is tried to push a new index over its current * capacity it grows into some other node, that can fit all its * keys plus at least one. * * When a key is removed from an indirect node and it becomes * less that some threshold, it's shrunk into smaller node. * * The nil is a placeholder for non-existing empty node. */ enum class layout : uint8_t { nil, indirect_tiny, indirect_small, indirect_medium, indirect_large, direct_dynamic, direct_static, }; /* * When a node has only one child, the former is removed from * the tree and its parent is set up to directly point to this * only kid. The kid, in turn, carries a "prefix" on board * denoting the index that might have been skipped by this cut. * * The lower 7 bits are the prefix length, the rest is the * prefix itself. */ static constexpr key_t prefix_len_mask = radix_mask; static constexpr key_t prefix_mask = ~prefix_len_mask; static key_t make_prefix(key_t key, unsigned len) noexcept { return (key & prefix_mask) + len; } /* * Mask to check node's prefix (mis-)match */ static key_t prefix_mask_at(unsigned depth) noexcept { return prefix_mask << (radix_bits * (leaf_depth - depth)); } /* * Finds the number of leading elements that coinside for two * indices. Needed on insertion, when a short-cut node gets * expanded back. */ static unsigned common_prefix_len(key_t k1, key_t k2) noexcept { static constexpr unsigned trailing_bits = (8 * sizeof(key_t)) % radix_bits; static constexpr unsigned round_up_delta = trailing_bits == 0 ? 0 : radix_bits - trailing_bits; /* * This won't work if k1 == k2 (clz is undefined for full * zeroes value), but we don't get here in this case */ return (__builtin_clz(k1 ^ k2) + round_up_delta) / radix_bits; } /* * Gets the depth's radix_bits-len index from the whole key, that's * used in intra-node search. */ static node_index_t node_index(key_t key, unsigned depth) noexcept { return (key >> (radix_bits * (leaf_depth - depth))) & radix_mask; } enum class erase_mode { real, cleanup, }; /* * When removing an index from a node it may end-up in one of 4 * states: * * - empty -- the last index was removed, the parent node is * welcome to drop the slot and mark it as unused * (and maybe get shrunk/squashed after that) * - squash -- only one index left, the parent node is welcome * to remove this node and replace it with its only * child (tuning it's prefix respectively) * - shrink -- current layout contains few indices, so parent * node should shrink the slot into smaller node * - nothing - just nothing */ enum class erase_result { nothing, empty, shrink, squash, }; template static erase_result after_drop(unsigned count) noexcept { if (count == 0) { return erase_result::empty; } if (count == 1) { return erase_result::squash; } if constexpr (Threshold != 0) { if (count <= Threshold) { return erase_result::shrink; } } return erase_result::nothing; } /* * Lower-bound calls return back pointer on the value and the leaf * node_head on which the value was found. The latter is needed * for iterator's ++ optimization. */ struct lower_bound_res { const T* elem; const node_head* leaf; key_t key; lower_bound_res(const T* e, const node_head& l, key_t k) noexcept : elem(e), leaf(&l), key(k) {} lower_bound_res() noexcept : elem(nullptr), leaf(nullptr), key(0) {} }; /* * Allocation returns a slot pointer and a boolean denoting * if the allocation really took place (false if the slot * is aleady occupied) */ using allocate_res = std::pair; using clone_res = std::pair; /* * A header all nodes start with. The type of a node (inner/leaf) * is evaluated (fingers-crossed) from the depth argument, so the * header doesn't have this bit. */ struct node_head { node_head_ptr* _backref; // Prefix for squashed nodes key_t _prefix; const layout _base_layout; // Number of keys on the node uint8_t _size; // How many slots are there. Used only by direct dynamic nodes const uint8_t _capacity; node_head() noexcept : _backref(nullptr), _prefix(0), _base_layout(layout::nil), _size(0), _capacity(0) {} node_head(key_t prefix, layout lt, uint8_t capacity) noexcept : _backref(nullptr) , _prefix(prefix) , _base_layout(lt) , _size(0) , _capacity(capacity) {} node_head(node_head&& o) noexcept : _backref(std::exchange(o._backref, nullptr)) , _prefix(o._prefix) , _base_layout(o._base_layout) , _size(std::exchange(o._size, 0)) , _capacity(o._capacity) { if (_backref != nullptr) { *_backref = this; } } node_head(const node_head&) = delete; ~node_head() { assert(_size == 0); } /* * Helpers to cast header to the actual node class or to the * node's base class (see below). */ template NBT& as_base() noexcept { return *boost::intrusive::get_parent_from_member(this, &NBT::_head); } template const NBT& as_base() const noexcept { return *boost::intrusive::get_parent_from_member(this, &NBT::_head); } template typename NT::node_type& as_base_of() noexcept { return as_base(); } template const typename NT::node_type& as_base_of() const noexcept { return as_base(); } template NT& as_node() noexcept { return *boost::intrusive::get_parent_from_member(&as_base_of(), &NT::_base); } template const NT& as_node() const noexcept { return *boost::intrusive::get_parent_from_member(&as_base_of(), &NT::_base); } // Construct a key from leaf node prefix and index key_t key_of(node_index_t ni) const noexcept { return (_prefix & prefix_mask) + ni; } // Prefix manipulations unsigned prefix_len() const noexcept { return _prefix & prefix_len_mask; } void trim_prefix(unsigned v) noexcept { _prefix -= v; } void bump_prefix(unsigned v) noexcept { _prefix += v; } bool check_prefix(key_t key, unsigned& depth) const noexcept { unsigned real_depth = depth + prefix_len(); key_t mask = prefix_mask_at(real_depth); if ((key & mask) != (_prefix & mask)) { return false; } depth = real_depth; return true; } /* * A bunch of "polymorphic" API wrappers that selects leaf/inner * node to call the method on. * * The node_base below provides the same set, but ploymorphs * the calls into the actual node layout. */ /* * Finds the element by the given key */ const T* get(key_t key, unsigned depth) const noexcept { if (depth == leaf_depth) { return as_base_of().get(key, depth); } else { return as_base_of().get(key, depth); } } /* * Finds the element whose key is not greater than the given one */ lower_bound_res lower_bound(key_t key, unsigned depth) const noexcept { unsigned real_depth = depth + prefix_len(); key_t mask = prefix_mask_at(real_depth); if ((key & mask) > (_prefix & mask)) { return lower_bound_res(); } depth = real_depth; if (depth == leaf_depth) { return as_base_of().lower_bound(key, depth); } else { return as_base_of().lower_bound(key, depth); } } /* * Allocates a new slot for the value. The caller is given the * pointer to the slot and the sign if it's now busy or not, * so that it can destruct it and construct a new element. */ allocate_res alloc(key_t key, unsigned depth) { if (depth == leaf_depth) { return as_base_of().alloc(key, depth); } else { return as_base_of().alloc(key, depth); } } /* * Erase the element with the given key, if present. */ erase_result erase(key_t key, unsigned depth, erase_mode erm) noexcept { if (depth == leaf_depth) { return as_base_of().erase(key, depth, erm); } else { return as_base_of().erase(key, depth, erm); } } /* * Weed walks the tree and removes the elements for which * the filter() returns true. */ template erase_result weed(Fn&& filter, unsigned depth) { if (depth == leaf_depth) { return as_base_of().weed(filter, depth); } else { return as_base_of().weed(filter, depth); } } /* * Grow the current node and return the new one */ node_head* grow(key_t key, unsigned depth) { node_index_t ni = node_index(key, depth); if (depth == leaf_depth) { return as_base_of().template grow(ni); } else { return as_base_of().template grow(ni); } } /* * Shrink the current node and return the new one */ node_head* shrink(unsigned depth) { if (depth == leaf_depth) { return as_base_of().template shrink(); } else { return as_base_of().template shrink(); } } /* * Walk the tree without modifying it (however, the elements * themselves can be modified) */ template bool visit(Visitor&& v, unsigned depth) const { bool ret = true; depth += prefix_len(); if (v(*this, depth, true)) { if (depth == leaf_depth) { ret = as_base_of().visit(v, depth); } else { ret = as_base_of().visit(v, depth); } v(*this, depth, false); } return ret; } template clone_res clone(Fn&& cloner, unsigned depth) const noexcept { depth += prefix_len(); if (depth == leaf_depth) { return as_base_of().template clone(cloner, depth); } else { return as_base_of().template clone(cloner, depth); } } void free(unsigned depth) noexcept { if (depth == leaf_depth) { leaf_node::free(as_node()); } else { inner_node::free(as_node()); } } size_t node_size(unsigned depth) const noexcept { if (depth == leaf_depth) { return as_base_of().node_size(); } else { return as_base_of().node_size(); } } /* * A leaf-node specific helper for iterator */ lower_bound_res lower_bound(key_t key) const noexcept { return as_base_of().lower_bound(key, leaf_depth); } /* * And two inner-node specific calls for nodes * squashing/expanding */ void set_lower(node_index_t ni, node_head* n) noexcept { as_node().set_lower(ni, n); } node_head_ptr pop_lower() noexcept { return as_node().pop_lower(); } }; /* * Pointer to node head. Inner nodes keep these, tree root pointer * is the one as well. */ class node_head_ptr { node_head* _v; public: node_head_ptr(node_head* v) noexcept : _v(v) {} node_head_ptr(const node_head_ptr&) = delete; node_head_ptr(node_head_ptr&& o) noexcept : _v(std::exchange(o._v, nullptr)) { if (_v != nullptr) { _v->_backref = this; } } node_head& operator*() const noexcept { return *_v; } node_head* operator->() const noexcept { return _v; } node_head* raw() const noexcept { return _v; } operator bool() const noexcept { return _v != nullptr; } bool is(const node_head& n) const noexcept { return _v == &n; } node_head_ptr& operator=(node_head* v) noexcept { _v = v; // Checking (_v != &nil_root) is not needed for correctness, since // nil_root's _backref is never read anyway. But we do this check for // performance reasons: since nil_root is shared between shards, // writing to it would cause serious cache contention. if (_v != nullptr && _v != &nil_root) { _v->_backref = this; } return *this; } }; /* * This helper wraps several layouts into one and preceeds them with * the header. It does nothing but provides a polymorphic calls to the * lower/inner layouts depending on the head.base_layout value. */ template struct node_base { node_head _head; variadic_union _layouts; template static size_t node_size(layout lt, uint8_t capacity) noexcept { return sizeof(node_head) + Tx::layout_size(capacity); } template static size_t node_size(layout lt, uint8_t capacity) noexcept { return lt == Tx::this_layout ? sizeof(node_head) + Tx::layout_size(capacity) : node_size(lt, capacity); } static size_t node_size(layout lt, uint8_t capacity) noexcept { return node_size(lt, capacity); } size_t node_size() const noexcept { return node_size(_head._base_layout, _head._capacity); } // construct template void construct(variadic_union& cur) noexcept { new (&cur._this) Tx(_head); } template void construct(variadic_union& cur) noexcept { if (_head._base_layout == Tx::this_layout) { new (&cur._this) Tx(_head); return; } construct(cur._other); } node_base(key_t prefix, layout lt, uint8_t capacity) noexcept : _head(prefix, lt, capacity) { construct(_layouts); } node_base(const node_base&) = delete; template void move_construct(variadic_union& cur, variadic_union&& o) noexcept { new (&cur._this) Tx(std::move(o._this), _head); } template void move_construct(variadic_union& cur, variadic_union&& o) noexcept { if (_head._base_layout == Tx::this_layout) { new (&cur._this) Tx(std::move(o._this), _head); return; } move_construct(cur._other, std::move(o._other)); } node_base(node_base&& o) noexcept : _head(std::move(o._head)) { move_construct(_layouts, std::move(o._layouts)); } ~node_base() { } // get value by key template const T* get(const variadic_union& cur, key_t key, unsigned depth) const noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.get(_head, key, depth); } return (const T*)nullptr; } template const T* get(const variadic_union& cur, key_t key, unsigned depth) const noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.get(_head, key, depth); } return get(cur._other, key, depth); } const T* get(key_t key, unsigned depth) const noexcept { return get(_layouts, key, depth); } // finds a lowed-bound element template lower_bound_res lower_bound(const variadic_union& cur, key_t key, unsigned depth) const noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.lower_bound(_head, key, depth); } return lower_bound_res(); } template lower_bound_res lower_bound(const variadic_union& cur, key_t key, unsigned depth) const noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.lower_bound(_head, key, depth); } return lower_bound(cur._other, key, depth); } lower_bound_res lower_bound(key_t key, unsigned depth) const noexcept { return lower_bound(_layouts, key, depth); } // erase by key template erase_result erase(variadic_union& cur, key_t key, unsigned depth, erase_mode erm) noexcept { return cur._this.erase(_head, key, depth, erm); } template erase_result erase(variadic_union& cur, key_t key, unsigned depth, erase_mode erm) noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.erase(_head, key, depth, erm); } return erase(cur._other, key, depth, erm); } erase_result erase(key_t key, unsigned depth, erase_mode erm) noexcept { return erase(_layouts, key, depth, erm); } // weed values with filter template erase_result weed(variadic_union& cur, Fn&& filter, unsigned depth) { return cur._this.weed(_head, filter, _head._prefix, depth); } template erase_result weed(variadic_union& cur, Fn&& filter, unsigned depth) { if (_head._base_layout == Tx::this_layout) { return cur._this.weed(_head, filter, _head._prefix, depth); } return weed(cur._other, filter, depth); } template erase_result weed(Fn&& filter, unsigned depth) { return weed(_layouts, filter, depth); } // allocate new slot template allocate_res alloc(variadic_union& cur, key_t key, unsigned depth) { return cur._this.alloc(_head, key, depth); } template allocate_res alloc(variadic_union& cur, key_t key, unsigned depth) { if (_head._base_layout == Tx::this_layout) { return cur._this.alloc(_head, key, depth); } return alloc(cur._other, key, depth); } allocate_res alloc(key_t key, unsigned depth) { return alloc(_layouts, key, depth); } // append slot to node template void append(variadic_union& cur, node_index_t ni, Slot&& val) noexcept { cur._this.append(_head, ni, std::move(val)); } template void append(variadic_union& cur, node_index_t ni, Slot&& val) noexcept { if (_head._base_layout == Tx::this_layout) { cur._this.append(_head, ni, std::move(val)); return; } append(cur._other, ni, std::move(val)); } void append(node_index_t ni, Slot&& val) noexcept { return append(_layouts, ni, std::move(val)); } // find and remove some element (usually the last one) template Slot pop(variadic_union& cur) noexcept { return cur._this.pop(_head); } template Slot pop(variadic_union& cur) noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.pop(_head); } return pop(cur._other); } Slot pop() noexcept { return pop(_layouts); } // visiting template bool visit(const variadic_union& cur, Visitor&& v, unsigned depth) const { return cur._this.visit(_head, v, depth); } template bool visit(const variadic_union& cur, Visitor&& v, unsigned depth) const { if (_head._base_layout == Tx::this_layout) { return cur._this.visit(_head, v, depth); } return visit(cur._other, v, depth); } template bool visit(Visitor&& v, unsigned depth) const { return visit(_layouts, v, depth); } // cloning template clone_res clone(const variadic_union& cur, Fn&& cloner, unsigned depth) const noexcept { return cur._this.template clone(_head, cloner, depth); } template clone_res clone(const variadic_union& cur, Fn&& cloner, unsigned depth) const noexcept { if (_head._base_layout == Tx::this_layout) { return cur._this.template clone(_head, cloner, depth); } return clone(cur._other, cloner, depth); } template clone_res clone(Fn&& cloner, unsigned depth) const noexcept { return clone(_layouts, cloner, depth); } // growing into larger layout template node_head* grow(variadic_union& cur, node_index_t want_ni) { if constexpr (Tx::growable) { return cur._this.template grow(_head, want_ni); } std::abort(); } template node_head* grow(variadic_union& cur, node_index_t want_ni) { if constexpr (Tx::growable) { if (_head._base_layout == Tx::this_layout) { return cur._this.template grow(_head, want_ni); } } return grow(cur._other, want_ni); } template node_head* grow(node_index_t want_ni) { return grow(_layouts, want_ni); } // shrinking into smaller layout template node_head* shrink(variadic_union& cur) { if constexpr (Tx::shrinkable) { return cur._this.template shrink(_head); } std::abort(); } template node_head* shrink(variadic_union& cur) { if constexpr (Tx::shrinkable) { if (_head._base_layout == Tx::this_layout) { return cur._this.template shrink(_head); } } return shrink(cur._other); } template node_head* shrink() { return shrink(_layouts); } }; /* * Node layouts. Define the way indices and payloads are stored on the node */ /* * Direct layout is just an array of data. * * It makes a difference between inner slots, that are pointers to other nodes, * and leaf slots, which are of user type. The former can be nullptr denoting * the missing slot, while the latter may not have this sign, so the layout * uses a bitmask to check if a slot is occupiued or not. */ template struct direct_layout { static constexpr bool shrinkable = ShrinkInto != layout::nil; static constexpr bool growable = GrowInto != layout::nil; static constexpr layout this_layout = Layout; static bool check_capacity(const node_head& head, node_index_t ni) noexcept { if constexpr (this_layout == layout::direct_static) { return true; } else { return ni < head._capacity; } } static unsigned capacity(const node_head& head) noexcept { if constexpr (this_layout == layout::direct_static) { return node_index_limit; } else { return head._capacity; } } struct array_of_non_node_head_ptr { /* * This bismask is the maximum possible, while the array of slots * is dynamic. This is to make sure all direct layouts have the * slots at the same offset, so we may not introduce new layouts * for it, and to avoid some capacity if-s in the code below */ std::bitset _present; Slot _slots[0]; array_of_non_node_head_ptr(const node_head& head) noexcept { _present.reset(); } array_of_non_node_head_ptr(array_of_non_node_head_ptr&& o, const node_head& head) noexcept : _present(std::move(o._present)) { for (unsigned i = 0; i < capacity(head); i++) { if (o.has(i)) { new (&_slots[i]) Slot(std::move(o._slots[i])); o._slots[i].~Slot(); } } } array_of_non_node_head_ptr(const array_of_non_node_head_ptr&) = delete; bool has(unsigned i) const noexcept { return _present.test(i); } bool has(const node_head& h, unsigned i) const noexcept { return has(i); } void add(node_head& head, unsigned i) noexcept { _present.set(i); } void del(node_head& head, unsigned i) noexcept { _present.set(i, false); } unsigned count(const node_head& head) const noexcept { return _present.count(); } }; struct array_of_node_head_ptr { Slot _slots[0]; array_of_node_head_ptr(const node_head& head) noexcept { for (unsigned i = 0; i < capacity(head); i++) { new (&_slots[i]) node_head_ptr(nullptr); } } array_of_node_head_ptr(array_of_node_head_ptr&& o, const node_head& head) noexcept { for (unsigned i = 0; i < capacity(head); i++) { new (&_slots[i]) Slot(std::move(o._slots[i])); o._slots[i].~Slot(); } } array_of_node_head_ptr(const array_of_node_head_ptr&) = delete; bool has(unsigned i) const noexcept { return _slots[i]; } bool has(const node_head& h, unsigned i) const noexcept { return check_capacity(h, i) && _slots[i]; } void add(node_head& head, unsigned i) noexcept { head._size++; } void del(node_head& head, unsigned i) noexcept { head._size--; } unsigned count(const node_head& head) const noexcept { return head._size; } }; using array_of_slot = std::conditional_t, array_of_node_head_ptr, array_of_non_node_head_ptr>; array_of_slot _data; direct_layout(const node_head& head) noexcept : _data(head) {} direct_layout(direct_layout&& o, const node_head& head) noexcept : _data(std::move(o._data), head) {} direct_layout(const direct_layout&) = delete; const T* get(const node_head& head, key_t key, unsigned depth) const noexcept { node_index_t ni = node_index(key, depth); if (!_data.has(head, ni)) { return nullptr; } return get_at(_data._slots[ni], key, depth + 1); } Slot pop(node_head& head) noexcept { for (unsigned i = 0; i < capacity(head); i++) { if (_data.has(i)) { Slot ret = std::move(_data._slots[i]); _data.del(head, i); _data._slots[i].~Slot(); return ret; } } return nullptr; } allocate_res alloc(node_head& head, key_t key, unsigned depth) { node_index_t ni = node_index(key, depth); if (!check_capacity(head, ni)) { return allocate_res(nullptr, false); } bool exists = _data.has(ni); if (!exists) { populate_slot(_data._slots[ni], key, depth + 1); _data.add(head, ni); } return allocate_on(_data._slots[ni], key, depth + 1, !exists); } void append(node_head& head, node_index_t ni, Slot&& val) noexcept { assert(check_capacity(head, ni)); assert(!_data.has(ni)); _data.add(head, ni); new (&_data._slots[ni]) Slot(std::move(val)); } erase_result erase(node_head& head, key_t key, unsigned depth, erase_mode erm) noexcept { node_index_t ni = node_index(key, depth); if (_data.has(head, ni)) { if (erase_from_slot(&_data._slots[ni], key, depth + 1, erm)) { _data.del(head, ni); return after_drop(_data.count(head)); } } return erase_result::nothing; } template erase_result weed(node_head& head, Fn&& filter, key_t pfx, unsigned depth) { bool removed_something = false; for (unsigned i = 0; i < capacity(head); i++) { if (_data.has(i)) { if (weed_from_slot(head, i, &_data._slots[i], filter, depth + 1)) { _data.del(head, i); removed_something = true; } } } return removed_something ? after_drop(_data.count(head)) : erase_result::nothing; } template clone_res clone(const node_head& head, Cloner&& clone, unsigned depth) const noexcept { NT* nn; try { nn = NT::allocate(head._prefix, head._base_layout, head._capacity); } catch (...) { return clone_res(nullptr, std::current_exception()); } auto ex = copy_slots(head, _data._slots, capacity(head), depth, nn->_base, [this] (unsigned i) noexcept { return _data.has(i) ? i : unused_node_index; }, clone); return std::make_pair(&nn->_base._head, std::move(ex)); } template node_head* grow(node_head& head, node_index_t want_ni) { static_assert(GrowInto == layout::direct_dynamic && GrowThreshold == 0); uint8_t next_cap = head._capacity << 1; while (want_ni >= next_cap) { next_cap <<= 1; } assert(next_cap > head._capacity); NT* nn = NT::allocate(head._prefix, layout::direct_dynamic, next_cap); move_slots(_data._slots, head._capacity, head._capacity + 1, nn->_base, [this] (unsigned i) noexcept { return _data.has(i) ? i : unused_node_index; }); head._size = 0; return &nn->_base._head; } template node_head* shrink(node_head& head) { static_assert(shrinkable && ShrinkThreshold != 0); NT* nn = NT::allocate(head._prefix, ShrinkInto); move_slots(_data._slots, node_index_limit, ShrinkThreshold, nn->_base, [this] (unsigned i) noexcept { return _data.has(i) ? i : unused_node_index; }); head._size = 0; return &nn->_base._head; } lower_bound_res lower_bound(const node_head& head, key_t key, unsigned depth) const noexcept { node_index_t ni = node_index(key, depth); if (_data.has(head, ni)) { lower_bound_res ret = lower_bound_at(&_data._slots[ni], head, ni, key, depth); if (ret.elem != nullptr) { return ret; } } for (unsigned i = ni + 1; i < capacity(head); i++) { if (_data.has(i)) { /* * Nothing was found on the slot, that matches the * given index. We need to move to the next one, but * zero-out all key's bits related to lower levels. * * Fortunately, leaf nodes will rewrite the whole * thing on match, so put 0 into the whole key. * * Also note, that short-cut iterator++ assumes that * index is NOT 0-ed in case of mismatch! */ return lower_bound_at(&_data._slots[i], head, i, 0, depth); } } return lower_bound_res(); } template bool visit(const node_head& head, Visitor&& v, unsigned depth) const { for (unsigned i = 0; i < capacity(head); i++) { if (_data.has(i)) { if (!visit_slot(v, head, i, &_data._slots[i], depth)) { return false; } } } return true; } static size_t layout_size(uint8_t capacity) noexcept { if constexpr (this_layout == layout::direct_static) { return sizeof(direct_layout) + node_index_limit * sizeof(Slot); } else { assert(capacity != 0); return sizeof(direct_layout) + capacity * sizeof(Slot); } } }; /* * The indirect layout is used to keep small number of sparse keys on * small node. To do that it keeps an array of indices and when is * asked to get an element searches in this array. This map additionally * works as a presense bitmask from direct layout. * * Since indirect layouts of different sizes have slots starting at * different addresses in memory, they cannot grow dynamically, but are * converted (by moving data) into each other. */ template struct indirect_layout { static constexpr bool shrinkable = ShrinkInto != layout::nil; static constexpr bool growable = GrowInto != layout::nil; static constexpr unsigned size = Size; static constexpr layout this_layout = Layout; node_index_t _idx[Size]; Slot _slots[0]; bool has(unsigned i) const noexcept { return _idx[i] != unused_node_index; } void unset(unsigned i) noexcept { _idx[i] = unused_node_index; } indirect_layout(const node_head& head) noexcept { for (unsigned i = 0; i < Size; i++) { _idx[i] = unused_node_index; } } indirect_layout(indirect_layout&& o, const node_head& head) noexcept { for (unsigned i = 0; i < Size; i++) { _idx[i] = o._idx[i]; if (o.has(i)) { new (&_slots[i]) Slot(std::move(o._slots[i])); o._slots[i].~Slot(); } } } indirect_layout(const indirect_layout&) = delete; const T* get(const node_head& head, key_t key, unsigned depth) const noexcept { node_index_t ni = node_index(key, depth); unsigned i = find_in_array(ni, _idx); if (i >= Size) { return nullptr; } return get_at(_slots[i], key, depth + 1); } Slot pop(node_head& head) noexcept { for (unsigned i = 0; i < Size; i++) { if (has(i)) { Slot ret = std::move(_slots[i]); head._size--; _slots[i].~Slot(); return ret; } } return nullptr; } allocate_res alloc(node_head& head, key_t key, unsigned depth) { node_index_t ni = node_index(key, depth); bool new_slot = false; unsigned i = find_in_array(ni, _idx); if (i >= Size) { i = find_in_array(unused_node_index, _idx); if (i >= Size) { return allocate_res(nullptr, false); } populate_slot(_slots[i], key, depth + 1); _idx[i] = ni; head._size++; new_slot = true; } return allocate_on(_slots[i], key, depth + 1, new_slot); } void append(node_head& head, node_index_t ni, Slot&& val) noexcept { unsigned i = head._size++; assert(i < Size); assert(_idx[i] == unused_node_index); _idx[i] = ni; new (&_slots[i]) Slot(std::move(val)); } erase_result erase(node_head& head, key_t key, unsigned depth, erase_mode erm) noexcept { node_index_t ni = node_index(key, depth); unsigned i = find_in_array(ni, _idx); if (i < Size) { if (erase_from_slot(&_slots[i], key, depth + 1, erm)) { unset(i); head._size--; return after_drop(head._size); } } return erase_result::nothing; } template erase_result weed(node_head& head, Fn&& filter, key_t pfx, unsigned depth) { bool removed_something = false; for (unsigned i = 0; i < Size; i++) { if (has(i)) { if (weed_from_slot(head, _idx[i], &_slots[i], filter, depth + 1)) { unset(i); head._size--; removed_something = true; } } } return removed_something ? after_drop(head._size) : erase_result::nothing; } template clone_res clone(const node_head& head, Cloner&& clone, unsigned depth) const noexcept { NT* nn; try { nn = NT::allocate(head._prefix, head._base_layout, head._capacity); } catch (...) { return clone_res(nullptr, std::current_exception()); } auto ex = copy_slots(head, _slots, Size, depth, nn->_base, [this] (unsigned i) noexcept { return _idx[i]; }, clone); return std::make_pair(&nn->_base._head, std::move(ex)); } template node_head* grow(node_head& head, node_index_t want_ni) { static_assert(growable && GrowThreshold == 0); NT* nn = NT::allocate(head._prefix, GrowInto); move_slots(_slots, Size, Size + 1, nn->_base, [this] (unsigned i) noexcept { return _idx[i]; }); head._size = 0; return &nn->_base._head; } template node_head* shrink(node_head& head) { static_assert(shrinkable && ShrinkThreshold != 0); NT* nn = NT::allocate(head._prefix, ShrinkInto); move_slots(_slots, Size, ShrinkThreshold, nn->_base, [this] (unsigned i) noexcept { return _idx[i]; }); head._size = 0; return &nn->_base._head; } lower_bound_res lower_bound(const node_head& head, key_t key, unsigned depth) const noexcept { node_index_t ni = node_index(key, depth); unsigned i = find_in_array(ni, _idx); if (i < Size) { lower_bound_res ret = lower_bound_at(&_slots[i], head, _idx[i], key, depth); if (ret.elem != nullptr) { return ret; } } unsigned ui = Size; for (unsigned i = 0; i < Size; i++) { if (has(i) && _idx[i] > ni && (ui == Size || _idx[i] < _idx[ui])) { ui = i; } } if (ui == Size) { return lower_bound_res(); } // See comment in direct_layout about the zero key argument return lower_bound_at(&_slots[ui], head, _idx[ui], 0, depth); } template bool visit(const node_head& head, Visitor&& v, unsigned depth) const { /* * Two common-case fast paths that save notable amount * of instructions from below. */ if (head._size == 0) { return true; } if (head._size == 1 && has(0)) { return visit_slot(v, head, _idx[0], &_slots[0], depth); } unsigned indices[Size]; unsigned sz = 0; for (unsigned i = 0; i < Size; i++) { if (has(i)) { indices[sz++] = i; } } if (v.sorted) { std::sort(indices, indices + sz, [this] (int a, int b) { return _idx[a] < _idx[b]; }); } for (unsigned i = 0; i < sz; i++) { unsigned pos = indices[i]; if (!visit_slot(v, head, _idx[pos], &_slots[pos], depth)) { return false; } } return true; } static size_t layout_size(uint8_t capacity) noexcept { return sizeof(indirect_layout) + size * sizeof(Slot); } }; template static void move_slots(SlotType* slots, unsigned nr, unsigned thresh, auto& into, FN&& node_index_of) noexcept { unsigned count = 0; for (unsigned i = 0; i < nr; i++) { node_index_t ni = node_index_of(i); if (ni != unused_node_index) { into.append(ni, std::move(slots[i])); slots[i].~SlotType(); if (++count >= thresh) { break; } } } } template static std::exception_ptr copy_slots(const node_head& h, const T* slots, unsigned nr, unsigned depth, auto& into, FN&& node_index_of, Cloner&& cloner) noexcept { unsigned count = 0; for (unsigned i = 0; i < nr; i++) { node_index_t ni = node_index_of(i); if (ni != unused_node_index) { try { into.append(ni, cloner(h.key_of(ni), slots[i])); } catch (...) { return std::current_exception(); } } } return nullptr; } template static std::exception_ptr copy_slots(const node_head& h, const node_head_ptr* slots, unsigned nr, unsigned depth, auto& into, FN&& node_index_of, Cloner&& cloner) noexcept { unsigned count = 0; for (unsigned i = 0; i < nr; i++) { node_index_t ni = node_index_of(i); if (ni != unused_node_index) { clone_res res = slots[i]->clone(cloner, depth + 1); if (res.first != nullptr) { /* * Append the slot anyway. It may happen that this inner node * has some entries on board, so rather than clearing them * here -- propagate the exception back and let the top-level * code call clear() on everything, including this half-cloned * node. */ into.append(ni, std::move(res.first)); } if (res.second) { return res.second; } } } return nullptr; } /* * Expand a node that failed prefix check. * Turns a node with non-zero prefix on which parent tries to allocate * an index beyond its limits. For this: * - the inner node is allocated on the level, that's enough to fit * both -- current node and the desired index * - the given node is placed into this new inner one at the index it's * expected to be found there (the prefix value) * - the allocation continues on this new inner (with fixed depth) */ static node_head* expand(node_head& n, key_t key, unsigned& depth) { key_t n_prefix = n._prefix; /* * The plen is the level at which current node and desired * index still coinside */ unsigned plen = common_prefix_len(key, n_prefix); assert(plen >= depth); plen -= depth; depth += plen; assert(n.prefix_len() > plen); node_index_t ni = node_index(n_prefix, depth); node_head* nn = inner_node::allocate_initial(make_prefix(key, plen), ni); // Trim all common nodes + nn one from n n.trim_prefix(plen + 1); nn->set_lower(ni, &n); return nn; } /* * Pop one the single lower node and prepare it to replace the * current one. This preparation is purely increasing its prefix * len, as the prefix value itself is already correct */ static node_head* squash(node_head* n, unsigned depth) noexcept { const node_head_ptr np = n->pop_lower(); node_head* kid = np.raw(); assert(kid != nullptr); // Kid has n and it's prefix squashed kid->bump_prefix(n->prefix_len() + 1); return kid; } static bool maybe_drop_from(node_head_ptr* np, erase_result res, unsigned depth) noexcept { node_head* n = np->raw(); switch (res) { case erase_result::empty: n->free(depth); *np = nullptr; return true; case erase_result::squash: if (depth != leaf_depth) { *np = squash(n, depth); n->free(depth); } break; case erase_result::shrink: try { *np = n->shrink(depth); n->free(depth); } catch(...) { /* * The node tried to shrink but failed to * allocate memory for the new layout. This * is not that bad, it can survive in current * layout and be shrunk (or squashed or even * dropped) later. */ } break; case erase_result::nothing: ; // make compiler happy } return false; } static const T* get_at(const T& val, key_t key, unsigned depth) noexcept { return &val; } static const T* get_at(const node_head_ptr& np, key_t key, unsigned depth = 0) noexcept { if (!np->check_prefix(key, depth)) { return nullptr; } return np->get(key, depth); } static allocate_res allocate_on(T& val, key_t key, unsigned depth, bool allocated) noexcept { return allocate_res(&val, allocated); } static allocate_res allocate_on(node_head_ptr& n, key_t key, unsigned depth = 0, bool _ = false) { if (!n->check_prefix(key, depth)) { n = expand(*n, key, depth); } allocate_res ret = n->alloc(key, depth); if (ret.first == nullptr) { /* * The nullptr ret means the n has run out of * free slots. Grow one into bigger layout and * try again */ node_head* nn = n->grow(key, depth); n->free(depth); n = nn; ret = nn->alloc(key, depth); assert(ret.first != nullptr); } return ret; } // Populating value slot happens in tree::emplace static void populate_slot(T& val, key_t key, unsigned depth) noexcept { } static void populate_slot(node_head_ptr& np, key_t key, unsigned depth) { /* * Allocate leaf immediatelly with the prefix * len big enough to cover all skipped node * up to the current depth */ assert(leaf_depth >= depth); np = leaf_node::allocate_initial(make_prefix(key, leaf_depth - depth)); } template static bool visit_slot(Visitor&& v, const node_head& n, node_index_t ni, const T* val, unsigned depth) { return v(n.key_of(ni), *val); } template static bool visit_slot(Visitor&& v, const node_head& n, node_index_t, const node_head_ptr* ptr, unsigned depth) { return (*ptr)->visit(v, depth + 1); } static lower_bound_res lower_bound_at(const T* val, const node_head& n, node_index_t ni, key_t, unsigned) noexcept { return lower_bound_res(val, n, n.key_of(ni)); } static lower_bound_res lower_bound_at(const node_head_ptr* ptr, const node_head&, node_index_t, key_t key, unsigned depth) noexcept { return (*ptr)->lower_bound(key, depth + 1); } template static bool weed_from_slot(node_head& n, node_index_t ni, T* val, Fn&& filter, unsigned depth) { if (!filter(n.key_of(ni), *val)) { return false; } val->~T(); return true; } template static bool weed_from_slot(node_head&, node_index_t, node_head_ptr* np, Fn&& filter, unsigned depth) { return weed_from_slot(np, filter, depth); } template static bool weed_from_slot(node_head_ptr* np, Fn&& filter, unsigned depth) { node_head* n = np->raw(); depth += n->prefix_len(); erase_result er = n->weed(filter, depth); // FIXME -- after weed the node might want to shrink into // even smaller, than just previous, layout return maybe_drop_from(np, er, depth); } static bool erase_from_slot(T* val, key_t key, unsigned depth, erase_mode erm) noexcept { if (erm == erase_mode::real) { val->~T(); } return true; } static bool erase_from_slot(node_head_ptr* np, key_t key, unsigned depth, erase_mode erm) noexcept { node_head* n = np->raw(); assert(n->check_prefix(key, depth)); erase_result er = n->erase(key, depth, erm); if (erm == erase_mode::cleanup) { return false; } return maybe_drop_from(np, er, depth); } template void visit(Visitor&& v) const { if (!_root.is(nil_root)) { _root->visit(std::move(v), 0); } } template void visit(Visitor&& v) { struct adaptor { Visitor&& v; bool sorted; bool operator()(key_t key, const T& val) { return v(key, const_cast(val)); } bool operator()(const node_head& n, unsigned depth, bool enter) { return v(const_cast(n), depth, enter); } }; const_cast(this)->visit(adaptor{std::move(v), v.sorted}); } /* * Actual types that define inner and leaf nodes. * * Leaf nodes are the most numerous inhabitant of the tree and * they must be as small as possible for the tree to be memory * efficient. Opposite to this, inner nodes are ~1% of the tree, * so it's not that important to keep them small. * * At the same time ... * * When looking up a value leaf node is the last in a row and * makes a single lookup, while there can be several inner ones, * on which several lookups will be done. * * Said that ... * * Leaf nodes are optimized for size, but not for speed, so they * have several layouts to grow (and shrink) strictly on demand. * * Inner nodes are optimized for speed, and just a little-but for * size, so they are of direct dynamic size only. */ class leaf_node { template friend class printer; friend class tree; friend class node_head; template friend class indirect_layout; template friend class direct_layout; using tiny_node = indirect_layout; using small_node = indirect_layout; using medium_node = indirect_layout; using large_node = indirect_layout; using direct_node = direct_layout; public: using node_type = node_base; leaf_node(leaf_node&& other) noexcept : _base(std::move(other._base)) {} ~leaf_node() { } size_t storage_size() const noexcept { return _base.node_size(); } private: node_type _base; leaf_node(key_t prefix, layout lt, uint8_t capacity) noexcept : _base(prefix, lt, capacity) { } leaf_node(const leaf_node&) = delete; static node_head* allocate_initial(key_t prefix) { return &allocate(prefix, layout::indirect_tiny)->_base._head; } static leaf_node* allocate(key_t prefix, layout lt, uint8_t capacity = 0) { void* mem = current_allocator().alloc(node_type::node_size(lt, capacity)); return new (mem) leaf_node(prefix, lt, capacity); } static void free(leaf_node& node) noexcept { node.~leaf_node(); current_allocator().free(&node, node._base.node_size()); } }; class inner_node { template friend class printer; friend class tree; friend class node_head; template friend class indirect_layout; template friend class direct_layout; static constexpr uint8_t initial_capacity = 4; using dynamic_node = direct_layout; public: using node_type = node_base; inner_node(inner_node&& other) noexcept : _base(std::move(other._base)) {} ~inner_node() {} size_t storage_size() const noexcept { return _base.node_size(); } private: node_type _base; inner_node(key_t prefix, layout lt, uint8_t capacity) noexcept : _base(prefix, lt, capacity) {} inner_node(const inner_node&) = delete; static node_head* allocate_initial(key_t prefix, node_index_t want_ni) { uint8_t capacity = initial_capacity; while (want_ni >= capacity) { capacity <<= 1; } return &allocate(prefix, layout::direct_dynamic, capacity)->_base._head; } static inner_node* allocate(key_t prefix, layout lt, uint8_t capacity = 0) { void* mem = current_allocator().alloc(node_type::node_size(lt, capacity)); return new (mem) inner_node(prefix, lt, capacity); } static void free(inner_node& node) noexcept { node.~inner_node(); current_allocator().free(&node, node._base.node_size()); } node_head_ptr pop_lower() noexcept { return _base.pop(); } void set_lower(node_index_t ni, node_head* n) noexcept { _base.append(ni, node_head_ptr(n)); } }; node_head_ptr _root; static inline node_head nil_root; public: tree() noexcept : _root(&nil_root) {} ~tree() { clear(); } tree(const tree&) = delete; tree(tree&& o) noexcept : _root(std::exchange(o._root, &nil_root)) {} const T* get(key_t key) const noexcept { return get_at(_root, key); } T* get(key_t key) noexcept { return const_cast(get_at(_root, key)); } const T* lower_bound(key_t key) const noexcept { return _root->lower_bound(key, 0).elem; } T* lower_bound(key_t key) noexcept { return const_cast(const_cast(this)->lower_bound(key)); } template void emplace(key_t key, Args&&... args) { if (_root.is(nil_root)) { populate_slot(_root, key, 0); } allocate_res v = allocate_on(_root, key); if (!v.second) { v.first->~T(); } try { new (v.first) T(std::forward(args)...); } catch (...) { erase_from_slot(&_root, key, 0, erase_mode::cleanup); throw; } } void erase(key_t key) noexcept { if (!_root.is(nil_root)) { erase_from_slot(&_root, key, 0, erase_mode::real); if (!_root) { _root = &nil_root; } } } void clear() noexcept { struct clearing_visitor { bool sorted = false; bool operator()(key_t key, T& val) noexcept { val.~T(); return true; } bool operator()(node_head& n, unsigned depth, bool enter) noexcept { if (!enter) { n._size = 0; n.free(depth); } return true; } }; visit(clearing_visitor{}); _root = &nil_root; } template requires std::is_invocable_r::value void clone_from(const tree& tree, Cloner&& cloner) { assert(_root.is(nil_root)); if (!tree._root.is(nil_root)) { clone_res cres = tree._root->clone(cloner, 0); if (cres.first != nullptr) { _root = cres.first; } if (cres.second) { clear(); std::rethrow_exception(cres.second); } } } /* * Weed walks the tree and removes the elements for which the * fn returns true. */ template requires std::is_invocable_r::value void weed(Fn&& filter) { if (!_root.is(nil_root)) { weed_from_slot(&_root, filter, 0); if (!_root) { _root = &nil_root; } } } private: template struct walking_visitor { Fn&& fn; bool sorted; using value_t = std::conditional_t; using node_t = std::conditional_t; bool operator()(key_t key, value_t& val) { return fn(key, val); } bool operator()(node_t& n, unsigned depth, bool enter) noexcept { return true; } }; public: /* * Walking the tree element-by-element. The called function Fn * may return false to stop the walking and return. * * The \sorted value specifies if the walk should call Fn on * keys in ascending order. If it's false keys will be called * randomly, because indirect nodes store the slots without * sorting */ template requires std::is_invocable_r::value void walk(Fn&& fn, bool sorted = true) const { visit(walking_visitor{std::move(fn), sorted}); } template requires std::is_invocable_r::value void walk(Fn&& fn, bool sorted = true) { visit(walking_visitor{std::move(fn), sorted}); } template class iterator_base { public: using iterator_category = std::forward_iterator_tag; using value_type = std::conditional_t; using difference_type = ssize_t; using pointer = value_type*; using reference = value_type&; private: key_t _key = 0; pointer _value = nullptr; const tree* _tree = nullptr; const node_head* _leaf = nullptr; public: key_t key() const noexcept { return _key; } iterator_base() noexcept = default; iterator_base(const tree* t) noexcept : _tree(t) { lower_bound_res res = _tree->_root->lower_bound(_key, 0); _leaf = res.leaf; _value = const_cast(res.elem); _key = res.key; } iterator_base& operator++() noexcept { if (_value == nullptr) { _value = nullptr; return *this; } _key++; if (node_index(_key, leaf_depth) != 0) { /* * Short-cut. If we're still inside the leaf, * then it's worth trying to shift forward on * it without messing with upper levels */ lower_bound_res res = _leaf->lower_bound(_key); _value = const_cast(res.elem); if (_value != nullptr) { _key = res.key; return *this; } /* * No luck. Go ahead and scan the tree from top * again. It's only leaf_depth levels though. Also * not to make the call below visit this leaf one * more time, bump up the index to move out of the * current leaf and keep the leaf's part zero. */ _key += node_index_limit; _key &= ~radix_mask; } lower_bound_res res = _tree->_root->lower_bound(_key, 0); _leaf = res.leaf; _value = const_cast(res.elem); _key = res.key; return *this; } iterator_base operator++(int) noexcept { iterator_base cur = *this; operator++(); return cur; } pointer operator->() const noexcept { return _value; } reference operator*() const noexcept { return *_value; } bool operator==(const iterator_base& o) const noexcept { return _value == o._value; } bool operator!=(const iterator_base& o) const noexcept { return !(*this == o); } }; using iterator = iterator_base; using const_iterator = iterator_base; iterator begin() noexcept { return iterator(this); } iterator end() noexcept { return iterator(); } const_iterator cbegin() const noexcept { return const_iterator(this); } const_iterator cend() const noexcept { return const_iterator(); } const_iterator begin() const noexcept { return cbegin(); } const_iterator end() const noexcept { return cend(); } bool empty() const noexcept { return _root.is(nil_root); } template requires std::is_nothrow_invocable_r::value size_t memory_usage(Fn&& entry_mem_usage) const noexcept { struct counting_visitor { Fn&& entry_mem_usage; bool sorted = false; size_t mem = 0; bool operator()(key_t key, const T& val) { mem += entry_mem_usage(key, val); return true; } bool operator()(const node_head& n, unsigned depth, bool enter) noexcept { if (enter) { mem += n.node_size(depth); } return true; } }; counting_visitor v{std::move(entry_mem_usage)}; visit(v); return v.mem; } struct stats { struct node_stats { unsigned long indirect_tiny = 0; unsigned long indirect_small = 0; unsigned long indirect_medium = 0; unsigned long indirect_large = 0; unsigned long direct_static = 0; unsigned long direct_dynamic = 0; }; node_stats inners; node_stats leaves; }; stats get_stats() const noexcept { struct counting_visitor { bool sorted = false; stats st; bool operator()(key_t key, const T& val) noexcept { std::abort(); } void update(typename stats::node_stats& ns, const node_head& n) const noexcept { switch (n._base_layout) { case layout::indirect_tiny: ns.indirect_tiny++; break; case layout::indirect_small: ns.indirect_small++; break; case layout::indirect_medium: ns.indirect_medium++; break; case layout::indirect_large: ns.indirect_large++; break; case layout::direct_static: ns.direct_static++; break; case layout::direct_dynamic: ns.direct_dynamic++; break; default: break; } } bool operator()(const node_head& n, unsigned depth, bool enter) noexcept { if (!enter) { return true; } if (depth == leaf_depth) { update(st.leaves, n); return false; // don't visit elements } else { update(st.inners, n); return true; } } }; counting_visitor v; visit(v); return v.st; } }; } // namespace