From 2d6915084a56e3d36a3f74cf260b62b01d1a1748 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 11 Oct 2026 18:10:01 -0400 Subject: [PATCH] Add opaque value get and set to headmap. --- .../database/impl/primitives/headmap.ipp | 108 ++++++++-- .../bitcoin/database/primitives/headmap.hpp | 67 ++++-- .../bitcoin/database/primitives/linkage.hpp | 17 ++ test/primitives/headmap.cpp | 203 ++++++++++++++++++ test/primitives/linkage.cpp | 19 ++ 5 files changed, 381 insertions(+), 33 deletions(-) diff --git a/include/bitcoin/database/impl/primitives/headmap.ipp b/include/bitcoin/database/impl/primitives/headmap.ipp index 5d55cd0e6..69f4568cb 100644 --- a/include/bitcoin/database/impl/primitives/headmap.ipp +++ b/include/bitcoin/database/impl/primitives/headmap.ipp @@ -132,7 +132,13 @@ code CLASS::reload() NOEXCEPT // ---------------------------------------------------------------------------- TEMPLATE -Link CLASS::at(size_t index) const NOEXCEPT +memory CLASS::get_memory() const NOEXCEPT +{ + return file_.get(); +} + +TEMPLATE +Value CLASS::at(size_t index) const NOEXCEPT { using namespace system; @@ -147,9 +153,9 @@ Link CLASS::at(size_t index) const NOEXCEPT const auto raw = ptr.data(); if constexpr (aligned) { - // Reads full padded word (masked by to_link). + // Reads full padded word (masked by to_value). const auto& head = *pointer_cast>(raw); - return to_link(head.load(std::memory_order_relaxed)); + return to_value(head.load(load_order)); } else { @@ -160,18 +166,12 @@ Link CLASS::at(size_t index) const NOEXCEPT cell_array(value) = head; mutex_.unlock_shared(); - return to_link(value); + return to_value(value); } } TEMPLATE -memory CLASS::get_memory() const NOEXCEPT -{ - return file_.get(); -} - -TEMPLATE -Link CLASS::at(const memory& ptr, size_t index) const NOEXCEPT +Value CLASS::at(const memory& ptr, size_t index) const NOEXCEPT { using namespace system; @@ -185,9 +185,9 @@ Link CLASS::at(const memory& ptr, size_t index) const NOEXCEPT if constexpr (aligned) { - // Reads full padded word (masked by to_link). + // Reads full padded word (masked by to_value). const auto& head = *pointer_cast>(raw); - return to_link(head.load(std::memory_order_relaxed)); + return to_value(head.load(load_order)); } else { @@ -198,7 +198,7 @@ Link CLASS::at(const memory& ptr, size_t index) const NOEXCEPT cell_array(value) = head; mutex_.unlock_shared(); - return to_link(value); + return to_value(value); } } @@ -221,7 +221,7 @@ Link CLASS::allocate(const Link& bytes) NOEXCEPT } TEMPLATE -template +template > bool CLASS::get(const Link& link, Element& element) const NOEXCEPT { using namespace system; @@ -250,7 +250,7 @@ bool CLASS::get(const Link& link, Element& element) const NOEXCEPT // NOT WRITER-WRITER THREAD SAFE (the element is read-write). TEMPLATE -template +template > bool CLASS::put(const Link& link, const Element& element) NOEXCEPT { using namespace system; @@ -317,6 +317,82 @@ bool CLASS::push(const Link& link) NOEXCEPT return file_.allocate(bucket_size) != storage::eof; } +// cell +// ---------------------------------------------------------------------------- + +TEMPLATE +template , + if_not_equal> +bool CLASS::get(size_t index, Element& element) const NOEXCEPT +{ + using namespace system; + static_assert(!is_slab); + + // Buckets at or above the logical size are unallocated (count publishes). + if (index >= count()) + return false; + + const auto ptr = file_.get(link_to_position(index)); + if (!ptr) + return false; + + const auto raw = ptr.data(); + cell value{}; + if constexpr (aligned) + { + const auto& head = *pointer_cast>(raw); + value = head.load(load_order); + } + else + { + const auto& head = cell_array(raw); + + mutex_.lock_shared(); + cell_array(value) = head; + mutex_.unlock_shared(); + } + + iostream stream{ pointer_cast(&value), cell_size }; + reader source{ stream }; + return element.from_data(source); +} + +TEMPLATE +template , + if_not_equal> +bool CLASS::set(size_t index, const Element& element) NOEXCEPT +{ + using namespace system; + static_assert(!is_slab); + static_assert(aligned, "set requires an aligned cell"); + + // Allocate as necessary and fill allocations. + const auto position = link_to_position(index); + const auto ptr = file_.get_filled(position, bucket_size, fill); + if (!ptr) + return false; + + file_.prepare(position, bucket_size); + auto& top = *pointer_cast>(ptr.data()); + auto head = top.load(std::memory_order_acquire); + auto written = true; + cell update{}; + do + { + auto copy = element; + iostream current{ pointer_cast(&head), cell_size }; + reader source{ current }; + iostream replacement{ pointer_cast(&update), cell_size }; + flipper sink{ replacement }; + written = copy.from_data(source) && copy.to_data(sink); + } + while (written && !top.compare_exchange_weak(head, update, + std::memory_order_release, std::memory_order_acquire)); + + file_.mark(position, bucket_size); + return written; +} + } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/primitives/headmap.hpp b/include/bitcoin/database/primitives/headmap.hpp index 0ac6ad47b..d61180323 100644 --- a/include/bitcoin/database/primitives/headmap.hpp +++ b/include/bitcoin/database/primitives/headmap.hpp @@ -29,13 +29,15 @@ namespace libbitcoin { namespace database { -/// A head-only array of links indexed by position (no body, no count cell). +/// A head-only array of values indexed by position (no body, no count cell). /// The storage logical size is the count, providing count atomicity and /// bucket publication as with bodies. Content restores with the heads. -/// A maximal cell is a slab, in which the link is a byte offset and elements -/// are serialized, as with bodies (the link accessors do not apply). -template = true> +/// A value is a link (masked to terminal) or an opaque integral, of no more +/// bytes than the cell or the link. +/// A cell size of max_size_t is a slab, in which the link is a byte offset +/// and elements are serialized as with bodies (cell accessors do not apply). +template (), std::min(Cell, Link::size)> = true> class headmap { public: @@ -88,16 +90,30 @@ class headmap /// Query interface. /// ----------------------------------------------------------------------- - /// Return link at index, terminal if at or above count. - Link at(size_t index) const NOEXCEPT; - /// Head accessor (shared lock), for at(ptr, index) over many indexes. memory get_memory() const NOEXCEPT; - Link at(const memory& ptr, size_t index) const NOEXCEPT; + + /// Return value at index, default (terminal link) if not allocated. + Value at(size_t index) const NOEXCEPT; + + /// Return value at index using accessor, default if not allocated. + Value at(const memory& ptr, size_t index) const NOEXCEPT; /// Append link at count into reserved capacity (single writer). bool push(const Link& link) NOEXCEPT; + /// Get element at index, false if not allocated or deserialize error. + template = true, + if_not_equal = true> + bool get(size_t index, Element& element) const NOEXCEPT; + + /// Set element at index, extending count as necessary (filled empty). + /// The element reads the current cell and writes its replacement, and is + /// rerun from a copy for each failed exchange. + template = true, + if_not_equal = true> + bool set(size_t index, const Element& element) NOEXCEPT; + /// Slab. /// ----------------------------------------------------------------------- @@ -105,11 +121,11 @@ class headmap Link allocate(const Link& bytes) NOEXCEPT; /// Get element at link, false if deserialize error. - template + template = true> bool get(const Link& link, Element& element) const NOEXCEPT; /// Put element at allocated link, false if serialize error. - template + template = true> bool put(const Link& link, const Element& element) NOEXCEPT; private: @@ -121,6 +137,16 @@ class headmap static_assert(is_nonzero(Link::size)); static_assert(std::atomic::is_always_lock_free); + // Links are ordered by count publication, opaque values by the cell. + static constexpr auto is_link = is_linkage; + static constexpr auto cell_size = system::possible_narrow_sign_cast< + ptrdiff_t>(bucket_size); + static constexpr auto fill = is_link ? system::bit_all : uint8_t{}; + static constexpr auto load_order = is_link ? std::memory_order_relaxed : + std::memory_order_acquire; + static constexpr auto store_order = is_link ? std::memory_order_relaxed : + std::memory_order_release; + INLINE static auto& cell_array(memory::iterator it) NOEXCEPT { return system::unsafe_array_cast(it); @@ -132,11 +158,14 @@ class headmap return cell_array(system::pointer_cast(&value)); } - INLINE static constexpr Link to_link(cell value) NOEXCEPT + INLINE static constexpr Value to_value(cell value) NOEXCEPT { using namespace system; - return possible_narrow_cast( - bit_and(Link::terminal, value)); + if constexpr (is_link) + return possible_narrow_cast( + bit_and(Value::terminal, value)); + else + return possible_narrow_cast(value); } // Byte offset of bucket index within head file. @@ -162,12 +191,16 @@ class headmap template using head_map = headmap; +template +using value_map = headmap; + } // namespace database } // namespace libbitcoin -#define TEMPLATE template If> -#define CLASS headmap +#define TEMPLATE template (), std::min(Cell, Link::size)> If> +#define CLASS headmap #include diff --git a/include/bitcoin/database/primitives/linkage.hpp b/include/bitcoin/database/primitives/linkage.hpp index 828866186..bf3b93577 100644 --- a/include/bitcoin/database/primitives/linkage.hpp +++ b/include/bitcoin/database/primitives/linkage.hpp @@ -88,6 +88,23 @@ bool operator!=(const linkage& left, return !(left == right); } +/// True if the type is a linkage. +template +constexpr bool is_linkage = false; + +template +constexpr bool is_linkage> = true; + +/// Serialized bytes of a linkage or integral value. +template +constexpr size_t value_size() NOEXCEPT +{ + if constexpr (is_linkage) + return Value::size; + else + return sizeof(Value); +} + } // namespace database } // namespace libbitcoin diff --git a/test/primitives/headmap.cpp b/test/primitives/headmap.cpp index b0d2d7d21..4baf0bc87 100644 --- a/test/primitives/headmap.cpp +++ b/test/primitives/headmap.cpp @@ -29,6 +29,8 @@ using small_link = linkage<3>; using unaligned_headmap = headmap; using slab_link = linkage<5>; using slab_headmap = headmap; +using value_link = linkage<3>; +using value_headmap = headmap; class little_slab { @@ -51,6 +53,101 @@ class little_slab uint32_t value{ 0 }; }; +// Opaque byte cell read. +class byte_get +{ +public: + static constexpr size_t size = one; + + bool from_data(database::reader& source) NOEXCEPT + { + value = source.read_byte(); + return source; + } + + uint8_t value{}; +}; + +// Opaque byte cell overwrite. +class byte_put +{ +public: + static constexpr size_t size = one; + + bool from_data(database::reader& source) NOEXCEPT + { + source.skip_byte(); + return source; + } + + bool to_data(database::flipper& sink) const NOEXCEPT + { + sink.write_byte(value); + return sink; + } + + uint8_t value{}; +}; + +// Opaque byte cell flag merge. +class flags_put +{ +public: + static constexpr size_t size = one; + + bool from_data(database::reader& source) NOEXCEPT + { + current = source.read_byte(); + return source; + } + + bool to_data(database::flipper& sink) const NOEXCEPT + { + sink.write_byte(bit_or(current, flags)); + return sink; + } + + uint8_t flags{}; + uint8_t current{}; +}; + +// Two byte link cell overwrite. +class link_put +{ +public: + static constexpr size_t size = link::size; + + bool from_data(database::reader& source) NOEXCEPT + { + source.skip_bytes(link::size); + return source; + } + + bool to_data(database::flipper& sink) const NOEXCEPT + { + sink.write_little_endian(value); + return sink; + } + + link::integer value{}; +}; + +// Three byte link cell read. +class small_link_get +{ +public: + static constexpr size_t size = small_link::size; + + bool from_data(database::reader& source) NOEXCEPT + { + using integer = small_link::integer; + value = source.read_little_endian(); + return source; + } + + small_link::integer value{}; +}; + BOOST_AUTO_TEST_CASE(headmap__create__empty__expected) { data_chunk head_file{}; @@ -264,4 +361,110 @@ BOOST_AUTO_TEST_CASE(headmap__slab_setup__lifecycle__expected) BOOST_REQUIRE_EQUAL(out.value, 0x11223344_u32); } +// cell get/set +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(headmap__get__beyond_count__false) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + value_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + + byte_get out{}; + BOOST_REQUIRE(!instance.get(0u, out)); +} + +BOOST_AUTO_TEST_CASE(headmap__set__byte_beyond_count__filled_empty) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + value_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.set(3u, byte_put{ 0x2a })); + BOOST_REQUIRE_EQUAL(instance.count(), 4u); + BOOST_REQUIRE_EQUAL(head_file, base16_chunk("0000002a")); + + byte_get out{}; + BOOST_REQUIRE(instance.get(2u, out)); + BOOST_REQUIRE_EQUAL(out.value, 0x00u); + BOOST_REQUIRE(instance.get(3u, out)); + BOOST_REQUIRE_EQUAL(out.value, 0x2au); + BOOST_REQUIRE(!instance.get(4u, out)); +} + +BOOST_AUTO_TEST_CASE(headmap__set__byte_overwrite__expected) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + value_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.set(1u, byte_put{ 0x01 })); + BOOST_REQUIRE(instance.set(1u, byte_put{ 0x02 })); + BOOST_REQUIRE_EQUAL(instance.count(), 2u); + BOOST_REQUIRE_EQUAL(instance.at(1), 0x02u); +} + +BOOST_AUTO_TEST_CASE(headmap__set__flags__merged) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + value_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.set(2u, flags_put{ 0x01 })); + BOOST_REQUIRE(instance.set(2u, flags_put{ 0x04 })); + BOOST_REQUIRE_EQUAL(instance.count(), 3u); + BOOST_REQUIRE_EQUAL(head_file, base16_chunk("000005")); + BOOST_REQUIRE_EQUAL(instance.at(2), 0x05u); +} + +BOOST_AUTO_TEST_CASE(headmap__set__concurrent_flags__all_merged) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + value_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.set(0u, byte_put{})); + + std::vector rounds(byte_bits * 1000u); + std::iota(rounds.begin(), rounds.end(), zero); + std::for_each(poolstl::execution::par, rounds.cbegin(), rounds.cend(), + [&](size_t round) NOEXCEPT + { + const flags_put flag{ shift_left(1u, round % byte_bits) }; + instance.set(0u, flag); + }); + + BOOST_REQUIRE_EQUAL(instance.count(), 1u); + BOOST_REQUIRE_EQUAL(instance.at(0), 0xffu); +} + +BOOST_AUTO_TEST_CASE(headmap__set__link_beyond_count__filled_terminal) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + test_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.set(2u, link_put{ 0x1122 })); + BOOST_REQUIRE_EQUAL(instance.count(), 3u); + BOOST_REQUIRE_EQUAL(head_file, base16_chunk("ffffffff2211")); + BOOST_REQUIRE(instance.at(0).is_terminal()); + BOOST_REQUIRE(instance.at(1).is_terminal()); + BOOST_REQUIRE_EQUAL(instance.at(2), 0x1122_u16); +} + +BOOST_AUTO_TEST_CASE(headmap__get__unaligned_link__expected) +{ + data_chunk head_file{}; + test::chunk_storage head_store{ head_file }; + unaligned_headmap instance{ head_store }; + BOOST_REQUIRE(instance.create()); + BOOST_REQUIRE(instance.reserve(1u)); + BOOST_REQUIRE(instance.push(0x112233_u32)); + + small_link_get out{}; + BOOST_REQUIRE(instance.get(0u, out)); + BOOST_REQUIRE_EQUAL(out.value, 0x112233_u32); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/primitives/linkage.cpp b/test/primitives/linkage.cpp index 71a7048c4..6ea4b108b 100644 --- a/test/primitives/linkage.cpp +++ b/test/primitives/linkage.cpp @@ -121,6 +121,25 @@ static_assert(linkage<6>{ 40 } + 2u == 42_u64); static_assert(linkage<7>{ 40 } + 2u == 42_u64); static_assert(linkage<8>{ 40 } + 2u == 42_u64); +// Linkage type trait. +static_assert(is_linkage>); +static_assert(is_linkage>); +static_assert(is_linkage>); +static_assert(is_linkage>); +static_assert(!is_linkage); +static_assert(!is_linkage); +static_assert(!is_linkage>); + +// Serialized value sizes (not sizeof for links). +static_assert(value_size>() == 1); +static_assert(value_size>() == 3); +static_assert(value_size>() == 5); +static_assert(value_size>() == 4); +static_assert(value_size() == 1); +static_assert(value_size() == 2); +static_assert(value_size() == 4); +static_assert(value_size() == 8); + // assign integer BOOST_AUTO_TEST_CASE(linkage__assign_integer__1__expected)