/* * Copyright (C) 2020-present ScyllaDB */ /* * SPDX-License-Identifier: AGPL-3.0-or-later */ #pragma once #include #include #include "utils/bptree.hh" #include "utils/intrusive-array.hh" #include "utils/collection-concepts.hh" #include /* * The double-decker is the ordered keeper of key:value pairs having * the pairs sorted by both key and value (key first). * * The keys collisions are expected to be rare enough to afford holding * the values in a sorted array with the help of linear algorithms. */ template requires Comparable && std::is_nothrow_move_constructible_v class double_decker { public: using inner_array = intrusive_array; using outer_tree = bplus::tree; using outer_iterator = typename outer_tree::iterator; using outer_const_iterator = typename outer_tree::const_iterator; private: outer_tree _tree; public: template class iterator_base { friend class double_decker; using outer_iterator = std::conditional_t; protected: outer_iterator _bucket; int _idx; public: iterator_base() = default; iterator_base(outer_iterator bkt, int idx) noexcept : _bucket(bkt), _idx(idx) {} using iterator_category = std::bidirectional_iterator_tag; using difference_type = ssize_t; using value_type = std::conditional_t; using pointer = value_type*; using reference = value_type&; reference operator*() const noexcept { return (*_bucket)[_idx]; } pointer operator->() const noexcept { return &((*_bucket)[_idx]); } iterator_base& operator++() noexcept { if ((*_bucket)[_idx++].is_tail()) { _bucket++; _idx = 0; } return *this; } iterator_base operator++(int) noexcept { iterator_base cur = *this; operator++(); return cur; } iterator_base& operator--() noexcept { if (_idx-- == 0) { _bucket--; _idx = _bucket->index_of(_bucket->end()) - 1; } return *this; } iterator_base operator--(int) noexcept { iterator_base cur = *this; operator--(); return cur; } bool operator==(const iterator_base& o) const noexcept = default; }; using const_iterator = iterator_base; class iterator final : public iterator_base { friend class double_decker; using super = iterator_base; iterator(const const_iterator&& other) noexcept : super(std::move(other._bucket), other._idx) {} public: iterator() noexcept : super() {} iterator(outer_iterator bkt, int idx) noexcept : super(bkt, idx) {} iterator(T* ptr) noexcept { inner_array& arr = inner_array::from_element(ptr, super::_idx); super::_bucket = outer_iterator(&arr); } template requires Disposer iterator erase_and_dispose(Less less, Func&& disp) noexcept { disp(&**this); // * to deref this, * to call operator*, & to get addr from ref if (super::_bucket->is_single_element()) { outer_iterator bkt = super::_bucket.erase(less); return iterator(bkt, 0); } bool tail = (*super::_bucket)[super::_idx].is_tail(); super::_bucket->erase(super::_idx); if (tail) { super::_bucket++; super::_idx = 0; } return *this; } iterator erase(Less less) noexcept { return erase_and_dispose(less, bplus::default_dispose); } }; /* * Structure that shed some more light on how the lower_bound * actually found the bounding elements. */ struct bound_hint { /* * Set to true if the element fully matched to the key * according to Compare */ bool match; /* * Set to true if the bucket for the given key exists */ bool key_match; /* * Set to true if the given key is more than anything * on the bucket and iterator was switched to the next * one (or when the key_match is false) */ bool key_tail; /* * This helper says whether the emplace will invalidate (some) * iterators or not. Emplacing with !key_match will go and create * new node in B+ which doesn't invalidate iterators. In another * case some existing B+ data node will be reconstructed, so the * iterators on those nodes will become invalid. */ bool emplace_keeps_iterators() const noexcept { return !key_match; } }; iterator begin() noexcept { return iterator(_tree.begin(), 0); } const_iterator begin() const noexcept { return const_iterator(_tree.begin(), 0); } const_iterator cbegin() const noexcept { return const_iterator(_tree.begin(), 0); } iterator end() noexcept { return iterator(_tree.end(), 0); } const_iterator end() const noexcept { return const_iterator(_tree.end(), 0); } const_iterator cend() const noexcept { return const_iterator(_tree.end(), 0); } explicit double_decker(Less less) noexcept : _tree(less) { } double_decker(const double_decker& other) = delete; double_decker(double_decker&& other) noexcept : _tree(std::move(other._tree)) {} iterator insert(Key k, T value, Compare cmp) { std::pair oip = _tree.emplace(std::move(k), std::move(value)); outer_iterator& bkt = oip.first; int idx = 0; if (!oip.second) { /* * Unlikely, but in this case we reconstruct the array. The value * must not have been moved by emplace() above. */ idx = bkt->index_of(bkt->lower_bound(value, cmp)); size_t new_size = (bkt->size() + 1) * sizeof(T); bkt.reconstruct(new_size, *bkt, typename inner_array::grow_tag{idx}, std::move(value)); } return iterator(bkt, idx); } template iterator emplace_before(iterator i, Key k, const bound_hint& hint, Args&&... args) { assert(!hint.match); outer_iterator& bucket = i._bucket; if (!hint.key_match) { /* * The most expected case -- no key conflict, respectively the * bucket is not found, and i points to the next one. Just go * ahead and emplace the new bucket before the i and push the * 0th element into it. */ outer_iterator nb = bucket.emplace_before(std::move(k), _tree.less(), std::forward(args)...); return iterator(nb, 0); } /* * Key conflict, need to expand some inner vector, but still there * are two cases -- whether the bounding element is on k's bucket * or the bound search overflew and switched to the next one. */ int idx = i._idx; if (hint.key_tail) { /* * The latter case -- i points to the next one. Need to shift * back and append the new element to its tail. */ bucket--; idx = bucket->index_of(bucket->end()); } size_t new_size = (bucket->size() + 1) * sizeof(T); bucket.reconstruct(new_size, *bucket, typename inner_array::grow_tag{idx}, std::forward(args)...); return iterator(bucket, idx); } template requires Comparable const_iterator find(const K& key, Compare cmp) const { outer_const_iterator bkt = _tree.find(key); int idx = 0; if (bkt != _tree.end()) { bool match = false; idx = bkt->index_of(bkt->lower_bound(key, cmp, match)); if (!match) { bkt = _tree.end(); idx = 0; } } return const_iterator(bkt, idx); } template requires Comparable iterator find(const K& k, Compare cmp) { return iterator(const_cast(this)->find(k, std::move(cmp))); } template requires Comparable const_iterator lower_bound(const K& key, Compare cmp, bound_hint& hint) const { outer_const_iterator bkt = _tree.lower_bound(key, hint.key_match); hint.key_tail = false; hint.match = false; if (bkt == _tree.end() || !hint.key_match) { return const_iterator(bkt, 0); } int i = bkt->index_of(bkt->lower_bound(key, cmp, hint.match)); if (i != 0 && (*bkt)[i - 1].is_tail()) { /* * The lower_bound is after the last element -- shift * to the net bucket's 0'th one. */ bkt++; i = 0; hint.key_tail = true; } return const_iterator(bkt, i); } template requires Comparable iterator lower_bound(const K& key, Compare cmp, bound_hint& hint) { return iterator(const_cast(this)->lower_bound(key, std::move(cmp), hint)); } template requires Comparable const_iterator lower_bound(const K& key, Compare cmp) const { bound_hint hint; return lower_bound(key, cmp, hint); } template requires Comparable iterator lower_bound(const K& key, Compare cmp) { return iterator(const_cast(this)->lower_bound(key, std::move(cmp))); } template requires Comparable const_iterator upper_bound(const K& key, Compare cmp) const { bool key_match; outer_const_iterator bkt = _tree.lower_bound(key, key_match); if (bkt == _tree.end() || !key_match) { return const_iterator(bkt, 0); } int i = bkt->index_of(bkt->upper_bound(key, cmp)); if (i != 0 && (*bkt)[i - 1].is_tail()) { // Beyond the end() boundary bkt++; i = 0; } return const_iterator(bkt, i); } template requires Comparable iterator upper_bound(const K& key, Compare cmp) { return iterator(const_cast(this)->upper_bound(key, std::move(cmp))); } template requires Disposer void clear_and_dispose(Func&& disp) noexcept { _tree.clear_and_dispose([&disp] (inner_array* arr) noexcept { arr->for_each(disp); }); } void clear() noexcept { clear_and_dispose(bplus::default_dispose); } template requires Disposer iterator erase_and_dispose(iterator begin, iterator end, Func&& disp) noexcept { bool same_bucket = begin._bucket == end._bucket; // Drop the tail of the starting bucket if it's not fully erased while (begin._idx != 0) { if (same_bucket) { if (begin == end) { return begin; } end._idx--; } begin = begin.erase_and_dispose(_tree.less(), disp); } // Drop all the buckets in between outer_iterator nb = _tree.erase_and_dispose(begin._bucket, end._bucket, [&disp] (inner_array* arr) noexcept { arr->for_each(disp); }); assert(nb == end._bucket); /* * Drop the head of the ending bucket. Every erased element is the 0th * one, when erased it will shift the rest left and reconstruct the array, * thus we cannot rely on the end to keep neither _bucket not _idx. * * Said that -- just erase the required number of elements. A corner case * when end points to the tree end is handled, _idx is 0 in this case. */ iterator next(nb, 0); while (end._idx-- != 0) { next = next.erase_and_dispose(_tree.less(), disp); } return next; } iterator erase(iterator begin, iterator end) noexcept { return erase_and_dispose(begin, end, bplus::default_dispose); } bool empty() const noexcept { return _tree.empty(); } static size_t estimated_object_memory_size_in_allocator(allocation_strategy& allocator, const T* obj) noexcept { /* * The T-s are merged together in array, so getting any run-time * value of a pointer would be wrong. So here's some guessing of * how much memory would this thing occupy in memory */ return sizeof(typename outer_tree::data); } };