From cc260ee8d95c3ffdbb3185c6f1317be7096643a1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 5 Jan 2026 22:15:33 -0500 Subject: [PATCH 001/111] xo-arena: empty scaffold (builds, but empty!) [WIP] --- CMakeLists.txt | 41 +++++++++++++++++++++++++++++++++ cmake/xo-bootstrap-macros.cmake | 33 ++++++++++++++++++++++++++ cmake/xo_arenaConfig.cmake.in | 12 ++++++++++ include/xo/arena/.gitkeep | 0 4 files changed, 86 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 cmake/xo-bootstrap-macros.cmake create mode 100644 cmake/xo_arenaConfig.cmake.in create mode 100644 include/xo/arena/.gitkeep diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..70e8d6d9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,41 @@ +# xo-arena/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_arena VERSION 1.0) +enable_language(CXX) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +# one-time project-specific c++ flags. usually empty +set(PROJECT_CXX_FLAGS "") +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- +# output targets + +#add_subdirectory(utest) + +# ---------------------------------------------------------------- +# header-only library + +set(SELF_LIB xo_arena) +xo_add_headeronly_library(${SELF_LIB}) +xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# input dependencies +# +# NOTE: dependency set here must be kept consistent with +# xo-arena/cmake/xo_arenaConfig.cmake.in + +#xo_headeronly_dependency(${SELF_LIB} xo_flatstring) + +# end CMakeLists.txt diff --git a/cmake/xo-bootstrap-macros.cmake b/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..2cf387e5 --- /dev/null +++ b/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,33 @@ +# ---------------------------------------------------------------- +# for example: +# $ PREFIX=/usr/local # for example +# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build +# +# will get +# CMAKE_MODULE_PATH +# from xo-cmake-config --cmake-module-path +# +# and expect .cmake macros in +# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake +# ---------------------------------------------------------------- + +find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED) + +if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL "prefix")) + message(FATAL "could not find xo-cmake-config executable") +endif() + +if (NOT XO_SUBMODULE_BUILD) + if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix)) + # default to typical install location for xo-project-macros + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH) + message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") + endif() +endif() + +# needs to have been installed somewhere on CMAKE_MODULE_PATH, +# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX) +# +include(xo_macros/xo_cxx) + +xo_cxx_bootstrap_message() diff --git a/cmake/xo_arenaConfig.cmake.in b/cmake/xo_arenaConfig.cmake.in new file mode 100644 index 00000000..b5c3cd5c --- /dev/null +++ b/cmake/xo_arenaConfig.cmake.in @@ -0,0 +1,12 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# note: changes to find_dependency() calls here +# must coordinate with xo_dependency() calls +# in CMakeLists.txt +# +#find_dependency(xo_flatstring) + +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/xo/arena/.gitkeep b/include/xo/arena/.gitkeep new file mode 100644 index 00000000..e69de29b From 513c1c8b8ae77c75ad794ec7913bd050819fbb25 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:01:38 -0500 Subject: [PATCH 002/111] xo-arena xo-facet xo-alloc2: build restored --- CMakeLists.txt | 14 +----- README.md | 1 - cmake/xo_arenaConfig.cmake.in | 2 +- include/xo/arena/AllocError.hpp | 79 +++++++++++++++++++++++++++++++++ src/arena/AllocError.cpp | 45 +++++++++++++++++++ src/arena/CMakeLists.txt | 19 ++++++++ 6 files changed, 146 insertions(+), 14 deletions(-) delete mode 100644 README.md create mode 100644 include/xo/arena/AllocError.hpp create mode 100644 src/arena/AllocError.cpp create mode 100644 src/arena/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 70e8d6d9..24d544ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,22 +20,12 @@ add_definitions(${PROJECT_CXX_FLAGS}) # ---------------------------------------------------------------- # output targets +add_subdirectory(src/arena) #add_subdirectory(utest) # ---------------------------------------------------------------- -# header-only library +# cmake export -set(SELF_LIB xo_arena) -xo_add_headeronly_library(${SELF_LIB}) -xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) -# ---------------------------------------------------------------- -# input dependencies -# -# NOTE: dependency set here must be kept consistent with -# xo-arena/cmake/xo_arenaConfig.cmake.in - -#xo_headeronly_dependency(${SELF_LIB} xo_flatstring) - # end CMakeLists.txt diff --git a/README.md b/README.md deleted file mode 100644 index 6c14c75c..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# xo-arena diff --git a/cmake/xo_arenaConfig.cmake.in b/cmake/xo_arenaConfig.cmake.in index b5c3cd5c..3b32fb0e 100644 --- a/cmake/xo_arenaConfig.cmake.in +++ b/cmake/xo_arenaConfig.cmake.in @@ -6,7 +6,7 @@ include(CMakeFindDependencyMacro) # must coordinate with xo_dependency() calls # in CMakeLists.txt # -#find_dependency(xo_flatstring) +find_dependency(xo_reflectutil) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") check_required_components("@PROJECT_NAME@") diff --git a/include/xo/arena/AllocError.hpp b/include/xo/arena/AllocError.hpp new file mode 100644 index 00000000..ca98b367 --- /dev/null +++ b/include/xo/arena/AllocError.hpp @@ -0,0 +1,79 @@ +/** @file AllocError.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + enum class error : int32_t { + /** sentinel **/ + invalid = -1, + /** not an error **/ + ok, + /** reserved size exhauged **/ + reserve_exhausted, + /** unable to commit (i.e. mprotect failure) **/ + commit_failed, + /** allocation size too big (See @ref ArenaConfig::header_size_mask_) **/ + header_size_mask, + /** sub_alloc not preceded by super alloc (or another sub_alloc) **/ + orphan_sub_alloc, + /** attempt to call alloc_info for allocator with alloc header feature disabled + * (e.g. @ref see ArenaConfig::store_header_flag_) + **/ + alloc_info_disabled, + /** attempt to call alloc_info for address not owned by allocator **/ + alloc_info_address, + /** for example: alloc iteration not supported in arenas with + * AllocConfig.store_header_flag_ = false + **/ + alloc_iterator_not_supported, + /** attempt to deref an iterator that does not refer to an alloc **/ + alloc_iterator_deref, + /** attempt to advance an iterator that does not refer to an alloc **/ + alloc_iterator_next, + }; + + struct AllocError { + using size_type = std::size_t; + using value_type = std::byte*; + + AllocError() = default; + explicit AllocError(error err, + uint32_t seq) : error_{err}, + error_seq_{seq} {} + AllocError(error err, + uint32_t seq, + size_type req_z, + size_type com_z, + size_type rsv_z) : error_{err}, + error_seq_{seq}, + request_z_{req_z}, + committed_z_{com_z}, + reserved_z_{rsv_z} {} + + static const char * error_description(error x); + + /** error code **/ + error error_ = error::ok; + + /** sequence# of this error. + * Each error event within an allocator gets next sequence number + **/ + uint32_t error_seq_ = 0; + /** reqeust size assoc'd with errror **/ + size_type request_z_ = 0; + /** committed allocator memory at time of error **/ + size_type committed_z_ = 0; + /** reserved allocator memory at time of error **/ + size_type reserved_z_ = 0; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AllocError.hpp */ diff --git a/src/arena/AllocError.cpp b/src/arena/AllocError.cpp new file mode 100644 index 00000000..43c16783 --- /dev/null +++ b/src/arena/AllocError.cpp @@ -0,0 +1,45 @@ +/** @file AllocError.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "AllocError.hpp" + +namespace xo { + namespace mm { + + const char * + AllocError::error_description(error x) + { + switch (x) { + case error::invalid: + break; + case error::ok: + return "ok"; + case error::reserve_exhausted: + return "reserve-exhausted"; + case error::commit_failed: + return "commit-failed"; + case error::header_size_mask: + return "header-size-mask"; + case error::orphan_sub_alloc: + return "orphan-sub-alloc"; + case error::alloc_info_disabled: + return "alloc-info-disabled"; + case error::alloc_info_address: + return "alloc-info-address"; + case error::alloc_iterator_not_supported: + return "alloc-iterator-not-supported"; + case error::alloc_iterator_deref: + return "alloc-iterator-deref"; + case error::alloc_iterator_next: + return "alloc-iterator-next"; + } + + return "?error"; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AllocError.cpp */ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt new file mode 100644 index 00000000..bc969b7f --- /dev/null +++ b/src/arena/CMakeLists.txt @@ -0,0 +1,19 @@ +# xo-arena/src/CMakeLists.txt + +set(SELF_LIB xo_arena) +set(SELF_SRCS + AllocError.cpp +) + +xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) +xo_install_include_tree3(include/xo/arena) + +# ---------------------------------------------------------------- +# input dependencies +# +# NOTE: dependency set here must be kept consistent with +# xo-arena/cmake/xo_arenaConfig.cmake.in + +xo_dependency(${SELF_LIB} xo_reflectutil) + +# end src/CMakeLists.txt From a7ad98e7482dde65ea3807b1acde298f33ea91ad Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:05:10 -0500 Subject: [PATCH 003/111] xo-alloc2 xo-arena: adopt AllocHeader.hpp --- include/xo/arena/AllocHeader.hpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 include/xo/arena/AllocHeader.hpp diff --git a/include/xo/arena/AllocHeader.hpp b/include/xo/arena/AllocHeader.hpp new file mode 100644 index 00000000..ec99f599 --- /dev/null +++ b/include/xo/arena/AllocHeader.hpp @@ -0,0 +1,28 @@ +/** @file AllocHeader.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include +#include + +namespace xo { + namespace mm { + struct AllocHeader { + using repr_type = std::uintptr_t; + using size_type = std::size_t; + + explicit AllocHeader(repr_type x) : repr_{x} {} + + repr_type repr_; + }; + + static_assert(sizeof(AllocHeader) == sizeof(AllocHeader::repr_type)); + static_assert(std::is_standard_layout_v); + } +} + +/* end AllocHeader.hpp */ From bedec1a938797f75512a0541d9d8b1a0cda2c2cb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:08:50 -0500 Subject: [PATCH 004/111] xo-arena: annex padding from xo-alloc2 --- include/xo/arena/padding.hpp | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 include/xo/arena/padding.hpp diff --git a/include/xo/arena/padding.hpp b/include/xo/arena/padding.hpp new file mode 100644 index 00000000..10d03b82 --- /dev/null +++ b/include/xo/arena/padding.hpp @@ -0,0 +1,60 @@ +/** @file padding.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + + struct padding { + /** word size for alignment**/ + static constexpr std::size_t c_alloc_alignment = sizeof(std::uintptr_t); + + static inline std::size_t is_aligned(std::size_t n, + std::size_t align = c_alloc_alignment) { + return n % align == 0; + } + + /** how much to add to @p z to get a multiple of + * @ref c_alloc_alignment + **/ + static inline std::size_t alloc_padding(std::size_t z, + std::size_t align = c_alloc_alignment) + { + + /* round up to multiple of c_bpw, but map 0 -> 0 + * (table assuming c_bpw==8) + * + * z%c_bpw dz + * ------------ + * 0 0 + * 1 7 + * 2 6 + * .. .. + * 7 1 + */ + std::size_t dz = (align - (z % align)) % align; + + return dz; + } + + /** @p z rounded up to an exact multiple + * of @ref c_alloc_alignment + **/ + static inline + std::size_t with_padding(std::size_t z, + std::size_t align = c_alloc_alignment) + { + return z + alloc_padding(z, align); + } + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end padding.hpp */ From c4d7bc8bd2d4f48e70594cb0cf614971921433bb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:11:29 -0500 Subject: [PATCH 005/111] xo-arena: annex AllocHeaderConfig.hpp from xo-alloc2 --- include/xo/arena/AllocHeaderConfig.hpp | 168 +++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 include/xo/arena/AllocHeaderConfig.hpp diff --git a/include/xo/arena/AllocHeaderConfig.hpp b/include/xo/arena/AllocHeaderConfig.hpp new file mode 100644 index 00000000..ab560886 --- /dev/null +++ b/include/xo/arena/AllocHeaderConfig.hpp @@ -0,0 +1,168 @@ +/** @file AllocHeaderConfig.hpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocHeader.hpp" +#include "padding.hpp" +#include + +namespace xo { + namespace mm { + /* + * Each allocation is preceded by a 64-bit header. + * Header is split into 3 configurable-width bit fields, + * labelled (from hi to lo bit order) {tseq, age, size}. + * + * 1. tseq. seq# identifying object types; needed for gc. + * 2. gen. age cohort; increases when alloc survives gc. + * 3. size. alloc size. + * + * Arena allocator only uses size. + * X1 collector uses {tseq, gen, size} + * + * alloc header + * + * TTTTTTTTTTTTGGGGGZZZZZZZZZZZZ + * < tseq >< size > + * + * masking + * + * ..432107654321076543210 bit + * + * > < .gen_bits + * 0..............01111111 gen_mask_unshifted + * 0..011111110..........0 gen_mask_shifted + * > < gen_shift + */ + struct AllocHeaderConfig { + using repr_type = AllocHeader; + using span_type = std::pair; + + AllocHeaderConfig() = default; + AllocHeaderConfig(std::uint32_t gz, + std::uint8_t guard_byte, + std::uint8_t t, + std::uint8_t a, + std::uint8_t z) noexcept : guard_z_{gz}, + guard_byte_{guard_byte}, + tseq_bits_{t}, + age_bits_{a}, + size_bits_{z} {} + + /** create header tuple (@p t, @p a, @p z) + * with typeseq @p t, age @p a, size @p z + **/ + std::uint64_t mkheader(std::uint64_t t, + std::uint64_t a, + std::uint64_t z) const noexcept { + uint64_t tseq_bits = (t << (age_bits_ + size_bits_)) & tseq_mask(); + uint64_t age_bits = (a << size_bits_) & age_mask(); + uint64_t size_bits = z & size_mask();; + + return (tseq_bits | age_bits | size_bits); + } + + std::uint64_t tseq_mask() const noexcept { + // e.g. + // FF FF FF 00 00 00 00 00 + // with tseq_bits=24, age_bits=8, size_bits=32 + // + return ((1ul << tseq_bits_) - 1) << (age_bits_ + size_bits_); + } + + std::uint64_t age_mask() const noexcept { + // e.g. + // 00 00 00 FF 00 00 00 00 + // with age_bits=8, size_bits=32 + // + return ((1ul << age_bits_) - 1) << size_bits_; + } + + std::uint64_t size_mask() const noexcept { + // e.g. + // 00 00 00 00 FF FF FF FF + // with size_bits=32 + // + return ((1ul << size_bits_) - 1); + } + + /** extract type id from alloc header @p hdr **/ + std::uint32_t tseq(repr_type hdr) const noexcept { + // e.g. + // 0x302010 + // for header + // 30 20 10 -- -- -- -- -- + // with tseq_bits_ = 24, age_bits_ + size_bits_ = 40 + // + return (hdr.repr_ & tseq_mask()) >> (age_bits_ + size_bits_); + } + + /** extract age from alloc header @p hdr **/ + std::uint32_t age(repr_type hdr) const noexcept { + // e.g. + // 0xa0 + // for header + // -- -- -- a0 -- -- -- -- + // with age_bits_ = 8, size_bits_ = 32 + // + return (hdr.repr_ & age_mask()) >> size_bits_; + } + + /** extract size from alloc header @p hdr **/ + std::size_t size(repr_type hdr) const noexcept { + // e.g. + // 0x01020300 + // for header + // -- -- -- -- 01 02 03 00 + // with size_bits_ = 32 + // + return (hdr.repr_ & size_mask()); + } + + /** extract padded size from alloc header @p hdr **/ + std::size_t size_with_padding(repr_type hdr) const noexcept { + return padding::with_padding(this->size(hdr)); + } + + /** true iff sentinel tseq, flagging a forwarding pointer **/ + bool is_forwarding_tseq(repr_type hdr) const noexcept { + // e.g. + // 0xFFFFFF + // i.e. header + // FF FF FF -- -- -- -- -- + // with tseq_bits_ = 24, age_bits + size_bits_ = 40 + // + return (hdr.repr_ & tseq_mask()) == tseq_mask(); + } + + bool is_size_enabled() const noexcept { return size_bits_ > 0; } + + /** construct alloc header for a forwarding object **/ + AllocHeader mark_forwarding_tseq(AllocHeader hdr) const noexcept { + return AllocHeader((hdr.repr_ & ~tseq_mask()) | tseq_mask()); + } + + /** if non-zero, allocate extra space between allocs, and fill + * with fixed test-pattern contents. Allows for simple + * runtime arena sanitizing checks. + * Will be rounded up to multiple of @ref padding::c_alloc_alignment + **/ + std::uint32_t guard_z_ = 0; + /** if guard_z_ > 0, write at least that many copies + * of this guard byte following each complete allocation + **/ + std::uint8_t guard_byte_ = 0xfd; + /** number of bits for tseq **/ + std::uint8_t tseq_bits_ = 24; + /** number of bits for age **/ + std::uint8_t age_bits_ = 8; + /** number of bits for size **/ + std::uint8_t size_bits_ = 32; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AllocHeaderConfig.hpp */ From 1e878d24f8e7b5d038f6ae9ae0880f92a790066f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:14:13 -0500 Subject: [PATCH 006/111] xo-arena: annex AllocInfo.*pp from xo-alloc2 --- include/xo/arena/AllocInfo.hpp | 97 ++++++++++++++++++++++++++++++++++ src/arena/AllocInfo.cpp | 45 ++++++++++++++++ src/arena/CMakeLists.txt | 1 + 3 files changed, 143 insertions(+) create mode 100644 include/xo/arena/AllocInfo.hpp create mode 100644 src/arena/AllocInfo.cpp diff --git a/include/xo/arena/AllocInfo.hpp b/include/xo/arena/AllocInfo.hpp new file mode 100644 index 00000000..30113e46 --- /dev/null +++ b/include/xo/arena/AllocInfo.hpp @@ -0,0 +1,97 @@ +/** @file AllocInfo.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocHeaderConfig.hpp" +#include + +namespace xo { + namespace mm { + /** @class AllocInfo + * @brief bookkeeping information for an allocation + * + * AllocInfo instances are 1:1 with sum of calls to + * {@ref AAllocator::alloc, @ref AAllocator::alloc_super} + * + **/ + struct AllocInfo { + /** @defgroup mm-allocinfo-traits **/ + ///@{ + + using size_type = AllocHeader::size_type; + using byte = std::byte; + using span_type = std::pair; + + ///@} + + /** @defgroup mm-allocinfo-ctors **/ + ///@{ + + AllocInfo(const AllocHeaderConfig * p_cfg, + const byte * p_guard_lo, + const AllocHeader * p_hdr, + const byte * p_guard_hi) : p_config_{p_cfg}, + p_guard_lo_{p_guard_lo}, + p_header_{p_hdr}, + p_guard_hi_{p_guard_hi} {} + + /** error when alloc-header not configured **/ + static AllocInfo error_not_configured(const AllocHeaderConfig * p_cfg) { + return AllocInfo(p_cfg, nullptr, nullptr, nullptr); + } + /** error on deref empty iterator **/ + static AllocInfo error_invalid_iterator(const AllocHeaderConfig * p_cfg) { + return AllocInfo(p_cfg, nullptr, nullptr, nullptr); + } + + ///@} + + /** @defgroup mm-allocinfo-methods **/ + ///@{ + + AllocHeader header() const noexcept { return *p_header_; } + + /** true for non-sentinel AllocInfo instance **/ + bool is_valid() const noexcept { return ((p_config_ != nullptr) + && (p_header_ != nullptr)); } + /** true iff sentinel tseq, flagging a forwarding pointer **/ + bool is_forwarding_tseq() const noexcept { + return p_config_->is_forwarding_tseq(*p_header_); + } + + /** Guard bytes preceding allocation-header **/ + span_type guard_lo() const noexcept; + /** Type sequence number in garbage collector **/ + std::uint32_t tseq() const noexcept { return p_config_->tseq(*p_header_); } + /** Allocation age in garbage collector **/ + std::uint32_t age() const noexcept { return p_config_->age (*p_header_); } + /** Allocation size (including allocator-supplied padding, excluding alloc header) **/ + size_type size() const noexcept { return p_config_->size(*p_header_); } + /** Payload for this allocation. This is the memory available to application **/ + span_type payload() const noexcept; + /** Guard bytes immediately following allocation **/ + span_type guard_hi() const noexcept; + /** Number of guard bytes **/ + size_type guard_z() const noexcept { return p_config_->guard_z_; } + /** Value (fixed test pattern) of guard byte **/ + char guard_byte() const noexcept { return p_config_->guard_byte_; } + + ///@} + + /** @defgroup mm-allocinfo-instance-vars **/ + ///@{ + + const AllocHeaderConfig * p_config_ = nullptr; + const byte * p_guard_lo_ = nullptr; + const AllocHeader * p_header_ = nullptr; + const byte * p_guard_hi_ = nullptr; + + ///@} + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AllocInfo.hpp */ diff --git a/src/arena/AllocInfo.cpp b/src/arena/AllocInfo.cpp new file mode 100644 index 00000000..9c4c8e7c --- /dev/null +++ b/src/arena/AllocInfo.cpp @@ -0,0 +1,45 @@ +/** @file AllocInfo.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "AllocInfo.hpp" + +namespace xo { + namespace mm { + auto + AllocInfo::guard_lo() const noexcept -> span_type + { + if (!p_guard_lo_) + return span_type(nullptr, nullptr); + + return span_type(p_guard_lo_, + p_guard_lo_ + p_config_->guard_z_); + } + + auto + AllocInfo::payload() const noexcept -> span_type + { + if (!p_header_) + return span_type(nullptr, nullptr); + + byte * lo = (byte *)(p_header_ + 1); + size_type z = this->size(); + + return span_type(lo, lo+z); + } + + auto + AllocInfo::guard_hi() const noexcept -> span_type + { + if (!p_guard_hi_) + return span_type(nullptr, nullptr); + + return span_type(p_guard_hi_, + p_guard_hi_ + p_config_->guard_z_); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AllocInfo.cpp */ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index bc969b7f..9a12405c 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -3,6 +3,7 @@ set(SELF_LIB xo_arena) set(SELF_SRCS AllocError.cpp + AllocInfo.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) From d7e7f90d93cc754d8ff0181297ae6cd39ffae74f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:17:13 -0500 Subject: [PATCH 007/111] xo-arena: annex ArenaConfig.*pp from xo-alloc2/ --- include/xo/arena/ArenaConfig.hpp | 70 ++++++++++++++++++++++++++++++++ src/arena/ArenaConfig.cpp | 18 ++++++++ src/arena/CMakeLists.txt | 1 + 3 files changed, 89 insertions(+) create mode 100644 include/xo/arena/ArenaConfig.hpp create mode 100644 src/arena/ArenaConfig.cpp diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp new file mode 100644 index 00000000..7d3f3478 --- /dev/null +++ b/include/xo/arena/ArenaConfig.hpp @@ -0,0 +1,70 @@ +/** @file ArenaConfig.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + + /** @class ArenaConfig + * @brief configuration for a @ref DArena instance + **/ + struct ArenaConfig { + /** @defgroup mm-arenaconfig-ctors ArenaConfig ctors **/ + ///@{ + + /** create anonymous arena with size @p z **/ + static ArenaConfig simple(std::size_t z); + + ///@} + + /** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/ + ///@{ + + /** optional name, for diagnostics **/ + std::string name_; + /** desired arena size -- hard max = reserved virtual memory **/ + std::size_t size_; + /** hugepage size -- using huge pages relieves some TLB pressure + * (provided you use their full extent :) + **/ + std::size_t hugepage_z_ = 2 * 1024 * 1024; + /** if non-zero, allocate extra space between allocs, and fill + * with fixed test-pattern contents. Allows for simple + * runtime arena sanitizing checks. + * Will be rounded up to multiple of @ref padding::c_alloc_alignment + **/ + std::size_t guard_z_ = 0; + /** if guard_z_ > 0, write at least that many copies + * of this guard byte following each complete allocation + **/ + std::uint8_t guard_byte_ = 0xfd; + /** if store_header_flag_ is true: mask bits for allocation size. + * remaining bits can be stolen for other purposes + * otherwise ignored + **/ + /** true to store header (8 bytes) at the beginning of each allocation. + * necessary and sufficient to allows iterating over allocs + * present in arena + **/ + bool store_header_flag_ = false; + /** mask applied to 8-byte alloc header. + * bits set to 1 store alloc size; remaining + * bits in alloc header can be used for other purposes. + **/ + std::uint64_t header_size_mask_ = 0xffffffff; + /** true to enable debug logging **/ + bool debug_flag_ = false; + + ///@} + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ArenaConfig.hpp */ diff --git a/src/arena/ArenaConfig.cpp b/src/arena/ArenaConfig.cpp new file mode 100644 index 00000000..d08feb46 --- /dev/null +++ b/src/arena/ArenaConfig.cpp @@ -0,0 +1,18 @@ +/** @file ArenaConfig.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "ArenaConfig.hpp" + +namespace xo { + namespace mm { + ArenaConfig + ArenaConfig::simple(std::size_t z) + { + return ArenaConfig { .name_ = "anonymous", .size_ = z }; + } + } +} + +/* end ArenaConfig.cpp */ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 9a12405c..345e9cc9 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -4,6 +4,7 @@ set(SELF_LIB xo_arena) set(SELF_SRCS AllocError.cpp AllocInfo.cpp + ArenaConfig.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) From eeaf52ca86b9422d146e112bc62dc944fc63d899 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:30:40 -0500 Subject: [PATCH 008/111] xo-arena: annex cmpresult.*pp from xo-alloc2 --- cmake/xo_arenaConfig.cmake.in | 1 + include/xo/arena/cmpresult.hpp | 87 ++++++++++++++++++++++++++++++++++ src/arena/CMakeLists.txt | 2 + src/arena/cmpresult.cpp | 38 +++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 include/xo/arena/cmpresult.hpp create mode 100644 src/arena/cmpresult.cpp diff --git a/cmake/xo_arenaConfig.cmake.in b/cmake/xo_arenaConfig.cmake.in index 3b32fb0e..1700fb8a 100644 --- a/cmake/xo_arenaConfig.cmake.in +++ b/cmake/xo_arenaConfig.cmake.in @@ -7,6 +7,7 @@ include(CMakeFindDependencyMacro) # in CMakeLists.txt # find_dependency(xo_reflectutil) +find_dependency(indentlog) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") check_required_components("@PROJECT_NAME@") diff --git a/include/xo/arena/cmpresult.hpp b/include/xo/arena/cmpresult.hpp new file mode 100644 index 00000000..0a9a9110 --- /dev/null +++ b/include/xo/arena/cmpresult.hpp @@ -0,0 +1,87 @@ +/** @file cmpresult.hpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + enum class comparison : int32_t { + invalid = -1, + comparable = 0, + incomparable = +1, + }; + + extern const char * comparison2str(comparison x); + + inline std::ostream & + operator<<(std::ostream & os, comparison x) { + os << comparison2str(x); + return os; + } + + /** Result of a generic comparison operation + **/ + struct cmpresult { + /** @defgroup mm-cmpresult-ctors cmpresult ctors **/ + ///@{ + cmpresult() : err_{comparison::invalid}, cmp_{0} {} + cmpresult(comparison err, std::int16_t cmp) : err_{err}, cmp_{cmp} {} + + static cmpresult incomparable() { return cmpresult(comparison::incomparable, 0); } + static cmpresult lesser() { return cmpresult(comparison::comparable, -1); } + static cmpresult equal() { return cmpresult(comparison::comparable, 0); } + static cmpresult greater() { return cmpresult(comparison::comparable, +1); } + template + static cmpresult from_cmp(T && x, T && y) { + if (x < y) + return cmpresult::lesser(); + else if (x == y) + return cmpresult::equal(); + else + return cmpresult::greater(); + } + + ///@} + + /** @defgroup mm-cmpresult-methods cmpresult methods **/ + ///@{ + + /** print to stream **/ + void display(std::ostream & os) const; + + bool is_lesser() const { + return (err_ == comparison::comparable) && (cmp_ < 0); + } + bool is_equal() const { + return (err_ == comparison::comparable) && (cmp_ == 0); + } + ///@} + + /** @defgroup mm-cmpresult-instance-vars cmpresult instance vars **/ + ///@{ + /** -1 -> invalid (sentinel) + * 0 -> comparable + * +1 -> incomparable (e.g. iterators from different arenas) + **/ + comparison err_ = comparison::invalid; + /** <0 -> lesser; 0 -> equal, >0 -> greater **/ + std::int16_t cmp_ = 0; + ///@} + }; + + inline std::ostream & operator<<(std::ostream & os, + const cmpresult & x) + { + x.display(os); + return os; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end cmpresult.hpp */ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 345e9cc9..40977231 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -5,6 +5,7 @@ set(SELF_SRCS AllocError.cpp AllocInfo.cpp ArenaConfig.cpp + cmpresult.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) @@ -17,5 +18,6 @@ xo_install_include_tree3(include/xo/arena) # xo-arena/cmake/xo_arenaConfig.cmake.in xo_dependency(${SELF_LIB} xo_reflectutil) +xo_dependency(${SELF_LIB} indentlog) # end src/CMakeLists.txt diff --git a/src/arena/cmpresult.cpp b/src/arena/cmpresult.cpp new file mode 100644 index 00000000..cd7c2998 --- /dev/null +++ b/src/arena/cmpresult.cpp @@ -0,0 +1,38 @@ +/** @file cmpresult.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "cmpresult.hpp" +#include +#include + +namespace xo { + namespace mm { + const char * + comparison2str(comparison x) + { + switch (x) { + case comparison::invalid: + break; + case comparison::comparable: + return "cmp"; + case comparison::incomparable: + return "!cmp"; + } + + return "?comparison"; + } + + void + cmpresult::display(std::ostream & os) const + { + os << ""; + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end cmpresult.cpp */ From 3fad95bed069ea46b02a8c06a99f3170211c9485 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:39:23 -0500 Subject: [PATCH 009/111] xo-arena: retire old+stale ArenaConfig --- include/xo/arena/ArenaConfig.hpp | 70 -------------------------------- src/arena/ArenaConfig.cpp | 18 -------- src/arena/CMakeLists.txt | 1 - 3 files changed, 89 deletions(-) delete mode 100644 include/xo/arena/ArenaConfig.hpp delete mode 100644 src/arena/ArenaConfig.cpp diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp deleted file mode 100644 index 7d3f3478..00000000 --- a/include/xo/arena/ArenaConfig.hpp +++ /dev/null @@ -1,70 +0,0 @@ -/** @file ArenaConfig.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -#include -#include - -namespace xo { - namespace mm { - - /** @class ArenaConfig - * @brief configuration for a @ref DArena instance - **/ - struct ArenaConfig { - /** @defgroup mm-arenaconfig-ctors ArenaConfig ctors **/ - ///@{ - - /** create anonymous arena with size @p z **/ - static ArenaConfig simple(std::size_t z); - - ///@} - - /** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/ - ///@{ - - /** optional name, for diagnostics **/ - std::string name_; - /** desired arena size -- hard max = reserved virtual memory **/ - std::size_t size_; - /** hugepage size -- using huge pages relieves some TLB pressure - * (provided you use their full extent :) - **/ - std::size_t hugepage_z_ = 2 * 1024 * 1024; - /** if non-zero, allocate extra space between allocs, and fill - * with fixed test-pattern contents. Allows for simple - * runtime arena sanitizing checks. - * Will be rounded up to multiple of @ref padding::c_alloc_alignment - **/ - std::size_t guard_z_ = 0; - /** if guard_z_ > 0, write at least that many copies - * of this guard byte following each complete allocation - **/ - std::uint8_t guard_byte_ = 0xfd; - /** if store_header_flag_ is true: mask bits for allocation size. - * remaining bits can be stolen for other purposes - * otherwise ignored - **/ - /** true to store header (8 bytes) at the beginning of each allocation. - * necessary and sufficient to allows iterating over allocs - * present in arena - **/ - bool store_header_flag_ = false; - /** mask applied to 8-byte alloc header. - * bits set to 1 store alloc size; remaining - * bits in alloc header can be used for other purposes. - **/ - std::uint64_t header_size_mask_ = 0xffffffff; - /** true to enable debug logging **/ - bool debug_flag_ = false; - - ///@} - }; - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end ArenaConfig.hpp */ diff --git a/src/arena/ArenaConfig.cpp b/src/arena/ArenaConfig.cpp deleted file mode 100644 index d08feb46..00000000 --- a/src/arena/ArenaConfig.cpp +++ /dev/null @@ -1,18 +0,0 @@ -/** @file ArenaConfig.cpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#include "ArenaConfig.hpp" - -namespace xo { - namespace mm { - ArenaConfig - ArenaConfig::simple(std::size_t z) - { - return ArenaConfig { .name_ = "anonymous", .size_ = z }; - } - } -} - -/* end ArenaConfig.cpp */ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 40977231..806a1fb3 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -4,7 +4,6 @@ set(SELF_LIB xo_arena) set(SELF_SRCS AllocError.cpp AllocInfo.cpp - ArenaConfig.cpp cmpresult.cpp ) From e601a94f4020d1b8c3112c48a1c039fa1b16eaa2 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:41:37 -0500 Subject: [PATCH 010/111] xo-arena: annex ArenaConfig.hpp from xo-alloc2/ --- include/xo/arena/ArenaConfig.hpp | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 include/xo/arena/ArenaConfig.hpp diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp new file mode 100644 index 00000000..4d79637b --- /dev/null +++ b/include/xo/arena/ArenaConfig.hpp @@ -0,0 +1,47 @@ +/** @file ArenaConfig.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocHeaderConfig.hpp" +#include +#include + +namespace xo { + namespace mm { + + /** @class ArenaConfig + * + * @brief configuration for a @ref DArena instance + **/ + struct ArenaConfig { + /** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/ + ///@{ + + /** optional name, for diagnostics **/ + std::string name_; + /** desired arena size -- hard max = reserved virtual memory **/ + std::size_t size_ = 0; + /** hugepage size -- using huge pages relieves some TLB pressure + * (provided you use their full extent :) + **/ + std::size_t hugepage_z_ = 2 * 1024 * 1024; + /** true to store header (8 bytes) at the beginning of each allocation. + * necessary and sufficient to allows iterating over allocs + * present in arena + **/ + bool store_header_flag_ = false; + /** configuration for per-alloc header **/ + AllocHeaderConfig header_{}; + /** true to enable debug logging **/ + bool debug_flag_ = false; + + ///@} + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ArenaConfig.hpp */ From 9e3831c67ab90d75d160cd9b853389a0992772e4 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:49:41 -0500 Subject: [PATCH 011/111] xo-arena: annex DArena.* DArenaIterator.* from xo-alloc2 --- include/xo/arena/DArena.hpp | 297 +++++++++++++ include/xo/arena/DArenaIterator.hpp | 127 ++++++ src/arena/CMakeLists.txt | 4 +- src/arena/DArena.cpp | 622 ++++++++++++++++++++++++++++ src/arena/DArenaIterator.cpp | 144 +++++++ 5 files changed, 1193 insertions(+), 1 deletion(-) create mode 100644 include/xo/arena/DArena.hpp create mode 100644 include/xo/arena/DArenaIterator.hpp create mode 100644 src/arena/DArena.cpp create mode 100644 src/arena/DArenaIterator.cpp diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp new file mode 100644 index 00000000..f9c2e3c6 --- /dev/null +++ b/include/xo/arena/DArena.hpp @@ -0,0 +1,297 @@ +/** @file DArena.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "ArenaConfig.hpp" +#include "AllocError.hpp" +#include "AllocInfo.hpp" +#include + +namespace xo { + namespace mm { + struct DArenaIterator; // see DArenaIterator.hpp + + /** @class DArena + * + * @brief represent arena allocator state + * + * Provides minimal RAII functionality around memory mapping. + * For allocation implementation see @ref IAllocator_DArena + **/ + struct DArena { + /* + * <----------------------------size--------------------------> + * <------------committed-----------><-------uncommitted------> + * <--allocated--> + * + * XXXXXXXXXXXXXXX___________________.......................... + * + * [X] allocated: in use + * [_] committed: physical memory obtained + * [.] uncommitted: mapped in virtual memory, not backed by memory + */ + + /** @defgroup mm-arena-traits arena type traits **/ + ///@{ + + /** @brief an amount of memory **/ + using size_type = std::size_t; + /** @brief allocation pointer; use for allocation results **/ + using value_type = std::byte*; + /** @brief a contiguous memory range **/ + using range_type = std::pair; + /** @brief type for allocation header (if enabled) **/ + using header_type = AllocHeader; + /** integer identifying a type (see xo::facet::typeid()) **/ + using typeseq = xo::reflect::typeseq; + + /** @brief mode argument for @ref _alloc **/ + enum class alloc_mode : uint8_t { + /** ordinary alloc. Most common mode **/ + standard, + /** begin a sequence of suballocs that share a single alloc header **/ + super, + /** make a subsidiary allocation on behalf of a preceding super alloc. + * Will be followed by at least one more suballoc call. + **/ + sub_incomplete, + /** make a subsidiary allocation that completes preceding super alloc. **/ + sub_complete, + }; + + ///@} + + /** @defgroup mm-arena-ctors arena constructors and destructors **/ + ///@{ + + /** create arena per configuration @p cfg. **/ + static DArena map(const ArenaConfig & cfg); + + /** null ctor **/ + DArena() = default; + /** ctor from already-mapped (but not committed) address range **/ + DArena(const ArenaConfig & cfg, + size_type page_z, + size_type arena_align_z, + value_type lo, + value_type hi); + /** DArena is not copyable **/ + DArena(const DArena & other) = delete; + /** move ctor **/ + DArena(DArena && other); + /** dtor releases mapped memory **/ + ~DArena(); + + /** move-assignment **/ + DArena & operator=(DArena && other); + + ///@} + + /** @defgroup mm-arena-methods **/ + ///@{ + + /** Reserved memory, in bytes. This is the maximum size of this arena. **/ + size_type reserved() const noexcept { return hi_ - lo_; } + /** Allocated memory in bytes: memory consumed by allocs from this arena, + * including administrative overhead (alloc headers + guard bytes) + **/ + size_type allocated() const noexcept { return free_ - lo_; } + /** Committed memory in bytes: amount of memory actually backed by physical memory **/ + size_type committed() const noexcept { return committed_z_; } + /** Available committed memory. + * This is the amount of memory guaranteed to be usable for future allocs from this arena. + **/ + size_type available() const noexcept { return limit_ - free_; } + + /** True iff address @p addr is owned by this arena, + * i.e. falls within [@ref lo_, @ref hi_) + **/ + bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); } + + /** obtain uncommitted contiguous memory range comprising + * a whole multiple of @p align_z bytes, of at least size @p req_z, + * aligned on a @p align_z boundary. Uncommitted memory is not (yet) + * backed by physical memory. + * + * If @p enable_hugepage_flag is true and THP + * (transparent huge pages) are available, use THP for arena memory. + * This relieves TLB and page table memory when @p req_z is a lot larger than + * page size (likely 4KB). Cost is that arena will consum physical memory in unit + * of @p align_z. Arena may waste up to @p align_z bytes of memory as a result. + * + * If @p enable_hugepage_flag is true, @p align_z should be huge page size + * (probably 2MB) for optimal performance. + * + * At present the THP feature is not supported on OSX. + * May be supportable through mach_vm_allocate(). + * + * Note that we reject MAP_HUGETLB|MAP_HUGE_2MB flags to mmap here, + * since requires previously-reserved memory in /proc/sys/vm/nr_hugepages. + * + * @return pair giving reserved memory address range [lo,hi) + **/ + static range_type map_aligned_range(size_type req_z, + size_type align_z, + bool enable_hugepage_flag); + + /** true if arena is mapped i.e. has a reserved address range **/ + bool is_mapped() const noexcept { return (lo_ != nullptr) && (hi_ != nullptr); } + + /** @ret iterator pointing to the first allocation in this arena **/ + DArenaIterator begin() const noexcept; + /** @ret iterator pointing to just after the last allocation in this arena **/ + DArenaIterator end() const noexcept; + + /** @ret header for first allocation in this arena **/ + AllocHeader * begin_header() const noexcept; + /** @ret location of header for next (not yet performed!) + * allocation in this arena + **/ + AllocHeader * end_header() const noexcept; + + /** get header from allocated object address **/ + header_type * obj2hdr(void * obj) noexcept; + + /** report alloc book-keeping info for allocation at @p mem + * + * Require: + * 1. @p mem is address returned by allocation on this arena + * i.e. by @ref IAllocator_DArena::alloc() or @ref IAllocator_DArena::alloc_super() + * 2. @p mem has not been invalidated since it was allocated + * i.e. by call to @ref DArena::clear + * + * Note: non-const, may stash error details + **/ + AllocInfo alloc_info(value_type mem) const noexcept; + + /** allocate at least @p z bytes of memory. + * Return nullptr and capture error if unable to satisfy request. + * May expand committed memory, as long as resulting committed size + * is no larger than reserved size + **/ + value_type alloc(typeseq t, size_type z); + + /** when store_header_flag enabled: + * like alloc(), but combine memory consumed by this alloc + * plus following consecutive sub_alloc()'s into a single header. + * otherwise equivalent to alloc() + **/ + value_type super_alloc(typeseq t, size_type z); + + /** when store_header_flag enabled: + * follow preceding super_alloc() by one or more sub_allocs(). + * accumulate total allocated size (including padding) into + * single header. All sub_allocs() except the last must set + * @p complete_flag to false. The last sub_alloc() must set + * @p complete_flag to true. + **/ + value_type sub_alloc(size_type z, bool complete_flag); + + /** alloc copy of @p src **/ + value_type alloc_copy(value_type src); + + /** capture error information: advance error count + set last_error **/ + void capture_error(error err, + size_type target_z = 0) const; + + /** alloc driver. shared by alloc(), super_alloc(), sub_alloc() **/ + value_type _alloc(std::size_t req_z, + alloc_mode mode, + typeseq tseq, + uint32_t age); + + /** expand committed space in arena @p d + * to size at least @p z + * In practice will round up to a multiple of @ref page_z_. + **/ + bool expand(size_type z) noexcept; + + /** create initial guard **/ + void establish_initial_guard() noexcept; + + /** discard all allocated memory, return to empty state + * Promise: + * - committed memory unchanged + * - available memory = committed memory + **/ + void clear() noexcept; + + ///@} + + /** @defgroup mm-arena-instance-vars **/ + ///@{ + + /** arena configuration **/ + ArenaConfig config_; + + /** size of a VM page (obtained automatically via getpagesize()). Likely 4k **/ + size_type page_z_ = 0; + + /** alignment for this arena. In practice will be either page_z_ or cfg.hugepage_z_ **/ + size_type arena_align_z_ = 0; + + /** arena owns memory in range [@ref lo_, @ref hi_) + **/ + std::byte * lo_ = nullptr; + + /** prefix of this size is committed. + * Remainder mapped but uncommitted. + **/ + size_type committed_z_ = 0; + + /** if config_.store_header_flag_: + * Pointer to header for last allocation. + **/ + header_type * last_header_ = nullptr; + + /** free pointer. + * Memory in range [@ref lo_, @ref free_) current in use + **/ + std::byte * free_ = nullptr; + + /** soft limit; end of committed virtual memory + * Memory in range [@ref lo_, @ref limit_) is committed + * (backed by physical memory) + **/ + std::byte * limit_ = nullptr; + + /** hard limit; end of reserved virtual memory + * Memory in range [@ref limit_, @ref hi_) is uncommitted + **/ + std::byte * hi_ = nullptr; + + /** count runtime errors. Each error updates @ref last_error_ **/ + uint32_t error_count_ = 0; + + /** capture some error details if/when error **/ + AllocError last_error_; + + ///@} + }; + + /** construct a @tparam T instance from arguments @p args + * using memory obtained from arena @p ialloc + **/ + template + static T * + construct_with(DArena & ialloc, Args&&... args) + { + using xo::reflect::typeseq; + + typeseq t = typeseq::id(); + std::byte * mem = ialloc.alloc(t, sizeof(T)); + + if (mem) + return new (mem) T(std::forward(args)...); + + return nullptr; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DArena.hpp */ diff --git a/include/xo/arena/DArenaIterator.hpp b/include/xo/arena/DArenaIterator.hpp new file mode 100644 index 00000000..cb277d6c --- /dev/null +++ b/include/xo/arena/DArenaIterator.hpp @@ -0,0 +1,127 @@ +/** @file DArenaIterator.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocInfo.hpp" +#include "AllocHeader.hpp" +#include "cmpresult.hpp" + +namespace xo { + namespace mm { + struct DArena; + + /** @class DArenaIterator + * @brief Representation for alloc iterator over arena + * + * Map showing an arena allocation: + * + * @verbatim + * + * <-------------z1---------------> + * < guard >< hz >< req_z >< dz >< guard > + * + * +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ + * + * ^ ^ ^ + * header mem header + * ^ (next alloc) + * DArenaIterator::pos_ + * + * guard [+] guard before+after each allocation, for simple sanitize checks + * header [0] alloc header (non-size bits) + * [z] alloc header (size bits) + * mem [@] app-requested memory, including padding [p] + * dz [p] padding (to uintptr_t alignment. req_z+dz recorded in header) + * free_ DArena::free_ just after guard bytes for last allocation + * + * @endverbatim + **/ + struct DArenaIterator { + /** @defgroup mm-arenaiterator-ctors DArenaIterator instance vars **/ + ///@{ + DArenaIterator() = default; + DArenaIterator(const DArena * arena, + AllocHeader * pos) : arena_{arena}, + pos_{pos} {} + + /** Create iterator in invalid state **/ + static DArenaIterator invalid() { return DArenaIterator(); } + + /** Create iterator pointing to the beginning of @p arena + * Iterator cannot modify memory, but can capture + * an iterator error in @p *arena + **/ + static DArenaIterator begin(const DArena * arena); + /** Create iterator pointing to the end of @p arena + * Iterator cannot modify memory, but can capture + * an iterator error in @p *arena + **/ + static DArenaIterator end(const DArena * arena); + ///@} + + /** @defgroup mm-arenaiterator-methods DArenaIterator methods **/ + ///@{ + /** Address of allocation header for beginning of alloc range in @p arena **/ + static AllocHeader * begin_header(const DArena * arena); + /** Address of allocation header for end of alloc range. + * This is the address of header for _next_ allocation in @p arena + * i.e. free pointer + **/ + static AllocHeader * end_header(const DArena * arena); + + /** A valid iterator can be compared, at least with itself + * It can be dereferenced if is also non-empty + **/ + bool is_valid() const noexcept { return (arena_ != nullptr) && (pos_ != nullptr); } + /** An invalid (or sentinel) iterator is incomparable with all + * iterators including itself + **/ + bool is_invalid() const noexcept { return !is_valid(); } + + /** fetch contents at current iterator position **/ + AllocInfo deref() const noexcept; + /** compare two iterators. To be comparable, + * iterators must refer to the same arena + **/ + cmpresult compare(const DArenaIterator & other) const noexcept; + /** advance iterator to next allocation **/ + void next() noexcept; + + /** cast iterator position to byte* */ + std::byte * pos_as_byte() const { return (std::byte *)pos_; } + + /** *ix synonym for ix.deref() **/ + AllocInfo operator*() const noexcept { return this->deref(); } + /** ++ix synonym for ix.next() **/ + DArenaIterator & operator++() noexcept { this->next(); return *this; } + ///@} + + /** @defgroup mm-arenaiterator-instance-vars **/ + ///@{ + /** iterator visits allocations from this arena **/ + const DArena * arena_ = nullptr; + /** current iterator position **/ + AllocHeader * pos_ = nullptr; + ///@} + }; + + inline bool + operator==(const DArenaIterator & x, + const DArenaIterator & y) + { + return x.compare(y).is_equal(); + } + + inline bool + operator!=(const DArenaIterator & x, + const DArenaIterator & y) + { + return !x.compare(y).is_equal(); + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DArenaIterator.hpp */ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 806a1fb3..111bbe15 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -2,9 +2,11 @@ set(SELF_LIB xo_arena) set(SELF_SRCS + cmpresult.cpp AllocError.cpp AllocInfo.cpp - cmpresult.cpp + DArena.cpp + DArenaIterator.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp new file mode 100644 index 00000000..5c0e3641 --- /dev/null +++ b/src/arena/DArena.cpp @@ -0,0 +1,622 @@ +/** @file DArena.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +//#include "alloc/AAllocator.hpp" +#include "DArena.hpp" +#include "DArenaIterator.hpp" +#include +#include +#include +#include +#include // for ::munmap() +#include // for ::getpagesize() +#include // for ::memset() + +namespace xo { + using xo::reflect::typeseq; + using std::byte; + using std::cerr; + using std::endl; + using std::size_t; + + namespace mm { + auto + DArena::map_aligned_range(size_t req_z, + size_t align_z, + bool enable_hugepage_flag) -> range_type + { + scope log(XO_DEBUG(true), + xtag("req_z", req_z), xtag("align_z", align_z)); + + // 1. round up to multiple of align_z + size_t target_z = padding::with_padding(req_z, align_z); // 4. + + // 2. mmap() will give us page-aligned memory, + // but not hugepage-aligned. + // + // Over-request by align_z to ensure + // aligned subrange of size target_z + // + byte * base = (byte *)(::mmap(nullptr, + target_z + align_z, + PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0)); + + // on mmap success: upper limit of mapped address range + byte * hi = base + (target_z + align_z); + // lowest hugepage-aligned address in [base, hi) + byte * aligned_base = (byte *)(padding::with_padding((size_t)base, align_z)); + // end of hugeppage-aligned range starting at aligned_base + byte * aligned_hi = aligned_base + target_z; + + log && log("acquired memory [lo,hi) using mmap", + xtag("lo", base), + xtag("aligned_lo", aligned_base), + xtag("req_z", req_z), + xtag("target_z", target_z), + xtag("aligned_hi", aligned_hi), + xtag("hi", hi)); + + // 3. assess mmap success + { + if (base == MAP_FAILED) { + throw std::runtime_error(tostr("ArenaAlloc: uncommitted allocation failed", + xtag("size", req_z))); + } + + assert((size_t)aligned_base % align_z == 0); + assert(aligned_base >= base); + assert(aligned_base < base + align_z); + } + + // 4. release unaligned prefix + if (base < aligned_base) { + size_t ua_prefix = aligned_base - base; + + ::munmap(base, ua_prefix); + } + + // 5. release unaligned suffix + if (aligned_hi < hi) { + size_t suffix = hi - aligned_hi; + + ::munmap(aligned_hi, suffix); + } + + if (enable_hugepage_flag) { +#ifdef __linux__ + /** linux: + * opt-in to transparent huge pages (THP) + * provided OS configured to support them. + * otherwise fallback gracefully. + * + * Huge pages -> use fewer TLB entries + faster + * shorter path through page table. + * + * When we commit (i.e. obtain physical memory on page fault), + * typically expect to pay ~1us per superpage. + * Much better than ~500us to commit 512 4k VM pages. + * + * But wasted if we don't use the memory. + * + * Page table has a handful of levels + **/ + ::madvise(aligned_base, target_z, MADV_HUGEPAGE); // 8. +#endif + } + + return std::make_pair(aligned_base, aligned_hi); + } + + DArena + DArena::map(const ArenaConfig & cfg) + { + scope log(XO_DEBUG(true)); + + /* vm page size. 4KB, probably */ + size_t page_z = getpagesize(); + + bool enable_hugepage_flag = (cfg.size_ >= cfg.hugepage_z_); + + /* Align start of arena memory on this boundary. + * Will use THP (transparent huge pages) if available + * and arena size is at least as large as hugepage size (2MB, probably) + */ + size_t align_z = (enable_hugepage_flag ? cfg.hugepage_z_ : page_z); + + log && log(xtag("page_z", page_z), + xtag("align_z", align_z)); + + auto [lo, hi] = map_aligned_range(cfg.size_, + align_z, + enable_hugepage_flag); + + if (!lo) { + // control here implies mmap() failed silently + + throw std::runtime_error(tostr("ArenaAlloc: allocation failed", + xtag("size", cfg.size_))); + } + + +#ifdef NOPE + log && log(xtag("lo", (void*)lo_), + xtag("page_z", page_z_), + xtag("hugepage_z", hugepage_z_)); +#endif + + return DArena(cfg, page_z, align_z, lo, hi); + } /*map*/ + + DArena::DArena(const ArenaConfig & cfg, + size_type page_z, + size_type arena_align_z, + byte * lo, + byte * hi) : config_{cfg}, + page_z_{page_z}, + arena_align_z_{arena_align_z}, + lo_{lo}, + committed_z_{0}, + free_{lo}, + limit_{lo}, + hi_{hi}, + error_count_{0}, + last_error_{} + { + //retval.checkpoint_ = lo_; + + /** make sure guard size is aligned **/ + config_.header_.guard_z_ + = padding::with_padding(config_.header_.guard_z_); + } + + DArena::DArena(DArena && other) { + config_ = other.config_; + page_z_ = other.page_z_; + arena_align_z_ = other.arena_align_z_; + lo_ = other.lo_; + committed_z_ = other.committed_z_; + free_ = other.free_; + limit_ = other.limit_; + hi_ = other.hi_; + error_count_ = other.error_count_; + last_error_ = other.last_error_; + + other.config_ = ArenaConfig(); + other.lo_ = nullptr; + other.committed_z_ = 0; + other.free_ = nullptr; + other.limit_ = nullptr; + other.hi_ = nullptr; + other.error_count_ = 0; + other.last_error_ = AllocError(); + } + + DArena & + DArena::operator=(DArena && other) + { + config_ = other.config_; + page_z_ = other.page_z_; + arena_align_z_ = other.arena_align_z_; + lo_ = other.lo_; + committed_z_ = other.committed_z_; + free_ = other.free_; + limit_ = other.limit_; + hi_ = other.hi_; + error_count_ = other.error_count_; + last_error_ = other.last_error_; + + other.config_ = ArenaConfig(); + other.lo_ = nullptr; + other.committed_z_ = 0; + other.free_ = nullptr; + other.limit_ = nullptr; + other.hi_ = nullptr; + other.error_count_ = 0; + other.last_error_ = AllocError(); + + return *this; + } + + DArena::~DArena() + { + if (lo_) { + //log && log("unmap [lo,hi)", + // xtag("lo", lo_), + // xtag("z", hi_ - lo_), + // xtag("hi", hi_)); + + ::munmap(lo_, hi_ - lo_); + } + + // hygiene + lo_ = nullptr; + committed_z_ = 0; + // checkpoint_ = nullptr; + free_ = nullptr; + limit_ = nullptr; + hi_ = nullptr; + error_count_ = 0; + last_error_ = AllocError(); + } + + auto + DArena::obj2hdr(void * obj) noexcept -> header_type * + { + assert(config_.store_header_flag_); + + return (header_type *)((byte *)obj - sizeof(header_type)); + } + + AllocInfo + DArena::alloc_info(value_type mem) const noexcept + { + if (!config_.store_header_flag_) [[unlikely]] { + this->capture_error(error::alloc_info_disabled); + + return AllocInfo::error_not_configured(&config_.header_); + } + + byte * header_mem = mem - sizeof(AllocHeader); + +#ifdef OBSOLETE // relying on cross-alloc header shenanigans in DX1Collector + if (!this->contains(header_mem)) { + this->capture_error(error::alloc_info_address); + } +#endif + + AllocHeader * header = (AllocHeader *)header_mem; + + const byte * guard_lo + = header_mem - config_.header_.guard_z_; + const byte * guard_hi + = mem + config_.header_.size(*header); + + return AllocInfo(&config_.header_, + guard_lo, + (AllocHeader *)header_mem, + guard_hi); + } + + DArenaIterator + DArena::begin() const noexcept + { + return DArenaIterator::begin(this); + } + + DArenaIterator + DArena::end() const noexcept + { + return DArenaIterator::end(this); + } + + AllocHeader * + DArena::begin_header() const noexcept + { + if (config_.store_header_flag_ == false) { + this->capture_error(error::alloc_iterator_not_supported); + + return nullptr; + } + + return (AllocHeader *)(lo_ + config_.header_.guard_z_); + } + + AllocHeader * + DArena::end_header() const noexcept + { + if (config_.store_header_flag_ == false) { + this->capture_error(error::alloc_iterator_not_supported); + + return nullptr; + } + + return (AllocHeader *)free_; + } + + std::byte * + DArena::alloc(typeseq t, std::size_t req_z) + { + /* - primary allocation path: + * exactly 1 header per alloc() call. + * - store_header_flag follows configuration + */ + return _alloc(req_z, alloc_mode::standard, t, 0 /*age*/); + } + + std::byte * + DArena::super_alloc(typeseq t, std::size_t req_z) + { + /* - (uncommon) pattern for parent alloc immediately followed by + * zero-or-more susidiary allocs, all sharing a single header. + * - collapses into alloc() behavior when + * ArenaConfig.store_header_flag_ disabled + */ + + (void)t; + + return _alloc(req_z, + alloc_mode::super, + t, + 0 /*age*/); + } + + std::byte * + DArena::sub_alloc(std::size_t req_z, + bool complete_flag) + { + /* - (uncommon) pattern for subsidiary allocs: + * that piggyback onto preceding super_alloc() + * - collapses into alloc() behavior when + * ArenaConfig.store_header_flag_ disabled + */ + + return _alloc(req_z, + (complete_flag + ? alloc_mode::sub_complete + : alloc_mode::sub_incomplete), + typeseq::anon() /*typeseq: ignored*/, + 0 /*age - ignored */); + } + + std::byte * + DArena::alloc_copy(std::byte * src) + { + /* NOTE: allocator that owns src must have the same header configuration */ + + assert(config_.store_header_flag_); + + /* src will come from an allocator other than this one; + * we rely on header layout from destination + * allocator -> assumes compatible header config + */ + AllocInfo src_info = alloc_info(src); + + size_t req_z = src_info.size(); + typeseq tseq = typeseq(src_info.tseq()); + uint32_t age = src_info.age(); + + return _alloc(req_z, alloc_mode::standard, tseq, age + 1); + } + + void + DArena::capture_error(error err, + size_type target_z) const + { + DArena * self = const_cast(this); + + ++(self->error_count_); + self->last_error_ = AllocError(err, + error_count_, + target_z, + committed_z_, + reserved()); + } + + byte * + DArena::_alloc(std::size_t req_z, + alloc_mode mode, + typeseq tseq, + uint32_t age) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + /* + * sub_complete + * sub_incomplete | + * standard super | | + * v v v v + */ + std::array store_header_v = {{ true, true, false, false }}; + std::array retain_header_v = {{ false, true, false, false }}; + std::array store_guard_v = {{ true, false, false, true }}; + + /* -> write header at free_ */ + bool store_header_flag = false; + /* -> stash last_header_*/ + bool retain_header_flag = false; + /* -> write guard bytes */ + bool store_guard = false; + + if (config_.store_header_flag_) { + store_header_flag = store_header_v[(int)mode]; + retain_header_flag = retain_header_v[(int)mode]; + store_guard = store_guard_v[(int)mode]; + } + + assert(padding::is_aligned((size_t)free_)); + + /* + * free_(pre) + * v + * + * <-------------z1---------------> + * < guard >< hz >< req_z >< dz >< guard > + * + * used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail + * + * ^ ^ ^ + * header mem | + * ^ | + * last_header_ free_(post) + * + * [+] guard after each allocation, for simple sanitize checks + * [0] unused header bits (avail to application) + * [z] record allocation size + * [@] new allocated memory + * [p] padding (to uintptr_t alignment) + */ + + /* non-zero if header feature enabled */ + size_t hz = 0; + /* dz: pad req_z to alignment size (multiple of 8 bytes, probably) */ + size_t dz = padding::alloc_padding(req_z); + size_t z0 = req_z + dz; + /* if non-zero: + * will store padded alloc size at the beginning of each allocation + * reminder: + * important to store padded size for correct arena iteration + */ + uint64_t header = (req_z + dz); + + if (store_header_flag) + { + if (config_.header_.is_size_enabled()) [[likely]] { + header = this->config_.header_.mkheader(tseq.seqno(), age, req_z + dz); + hz = sizeof(header); + } else { + /* req_z doesn't fit in configured header_size_mask bits */ + capture_error(error::header_size_mask); + return nullptr; + } + } + + size_t z1 = hz + z0; + + assert(padding::is_aligned(z1)); + + if (!this->expand(this->allocated() + z1)) [[unlikely]] { + /* (error state already captured) */ + return nullptr; + } + + if (store_header_flag) { + /* capturing header */ + *(uint64_t *)free_ = header; + + if (retain_header_flag) { + /* and rembering for subsequent + * sub_alloc() + */ + last_header_ = (AllocHeader *)free_; + } + } + + byte * mem = free_ + hz; + + this->free_ += z1; + + if (store_guard) { + /* write guard bytes for overrun detection */ + ::memset(free_, + config_.header_.guard_byte_, + config_.header_.guard_z_); + + this->free_ += config_.header_.guard_z_; + } + + log && log(xtag("self", config_.name_), + xtag("hz", hz), + xtag("z0", req_z), + xtag("+pad", dz), + xtag("z1", z1), + xtag("size", this->committed()), + xtag("avail", this->available())); + log && log(xtag("mem", mem), + xtag("free", free_)); + + return mem; + } + + void + DArena::establish_initial_guard() noexcept + { + assert(free_ == lo_); + + ::memset(this->free_, + config_.header_.guard_byte_, + config_.header_.guard_z_); + + this->free_ += config_.header_.guard_z_; + } + + bool + DArena::expand(size_t target_z) noexcept + { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("target_z", target_z), + xtag("committed_z", committed_z_)); + + if (target_z <= committed_z_) [[likely]] { + log && log("trivial success, offset within committed range", + xtag("target_z", target_z), + xtag("committed_z", committed_z_)); + return true; + } + + if (lo_ + target_z > hi_) [[unlikely]] { + this->capture_error(error::reserve_exhausted, target_z); + return false; + } + + /* + * pre: + * + * _______________................................... + * ^ ^ ^ + * lo limit hi + * + * < committed_z > + * <----------target_z-----------> + * > <- z: 0 <= z < hugepage_z + * <---------aligned_target_z---------> + * <--- add_commit_z --> + * + * post: + * ____________________________________.............. + * ^ ^ ^ + * lo limit hi + * + */ + + std::size_t aligned_target_z = padding::with_padding(target_z, arena_align_z_); + std::byte * commit_start = limit_; // = lo_ + committed_z_; + std::size_t add_commit_z = aligned_target_z - committed_z_; + + assert(limit_ == lo_ + committed_z_); + + if (::mprotect(commit_start, + add_commit_z, + PROT_READ | PROT_WRITE) != 0) [[unlikely]] + { + if (log) { + log("commit failed!"); + log(xtag("aligned_target_z", aligned_target_z), + xtag("commit_start", commit_start), + xtag("add_commit_z", add_commit_z), + xtag("commit_end", commit_start + add_commit_z) + ); + } + + capture_error(error::commit_failed, add_commit_z); + return false; + } + + committed_z_ = aligned_target_z; + limit_ = lo_ + committed_z_; + + if (commit_start == lo_) [[unlikely]] { + /* first expand() for this allocator - start with guard_z_ bytes */ + + this->establish_initial_guard(); + } + + assert(committed_z_ % arena_align_z_ == 0); + assert(reinterpret_cast(limit_) % arena_align_z_ == 0); + + return true; + } /*expand*/ + + void + DArena::clear() noexcept + { + this->free_ = lo_; + this->establish_initial_guard(); + } + } +} /*namespace xo*/ + +/* end DArena.cpp */ diff --git a/src/arena/DArenaIterator.cpp b/src/arena/DArenaIterator.cpp new file mode 100644 index 00000000..931108bb --- /dev/null +++ b/src/arena/DArenaIterator.cpp @@ -0,0 +1,144 @@ +/** @file DArenaIterator.cpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#include "DArenaIterator.hpp" +#include "DArena.hpp" +#include +#include +#include + +namespace xo { + using std::byte; + + namespace mm { + DArenaIterator + DArenaIterator::begin(const DArena * arena) + { + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag)); + + AllocHeader * begin_hdr = begin_header(arena); + + if (!begin_hdr) + return DArenaIterator::invalid(); + + log && log(xtag("begin_hdr", begin_hdr)); + + return DArenaIterator(arena, begin_hdr); + } + + DArenaIterator + DArenaIterator::end(const DArena * arena) + { + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag)); + + AllocHeader * end_hdr = end_header(arena); + + if (!end_hdr) + return DArenaIterator::invalid(); + + log && log(xtag("end_hdr", end_hdr)); + + return DArenaIterator(arena, end_hdr); + } + + AllocHeader * + DArenaIterator::begin_header(const DArena * arena) + { + assert(arena); + + return arena->begin_header(); + } + + AllocHeader * + DArenaIterator::end_header(const DArena * arena) + { + assert(arena); + + return arena->end_header(); + } + + AllocInfo + DArenaIterator::deref() const noexcept + { + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag)); + + bool contains_flag = arena_->contains(this->pos_as_byte()); + bool bounds_flag = (this->pos_as_byte() < arena_->free_); + + log && log(xtag("contains_flag", contains_flag), + xtag("bounds_flag", bounds_flag)); + + if (!contains_flag || !bounds_flag) { + arena_->capture_error(error::alloc_iterator_deref); + + return AllocInfo::error_invalid_iterator(&(arena_->config_.header_)); + } + + /* iterator points to beginning of header. + * memory given to application start immediately followed header + */ + + byte * mem = (byte *)(pos_ + 1); + + return arena_->alloc_info(mem); + } + + cmpresult + DArenaIterator::compare(const DArenaIterator & other_ix) const noexcept + { + scope log(XO_DEBUG(false), + xtag("arena", arena_), + xtag("pos", pos_), + xtag("other.arena", other_ix.arena_), + xtag("other.pos", other_ix.pos_)); + + if (is_invalid() || (arena_ != other_ix.arena_)) + return cmpresult::incomparable(); + + if (pos_ < other_ix.pos_) { + return cmpresult::lesser(); + } else if(pos_ == other_ix.pos_) { + return cmpresult::equal(); + } else { + return cmpresult::greater(); + } + } + + void + DArenaIterator::next() noexcept + { + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag)); + + bool contains_flag = arena_->contains(this->pos_as_byte()); + bool bounds_flag = (this->pos_as_byte() < arena_->free_); + + log && log(xtag("contains_flag", contains_flag), + xtag("bounds_flag", bounds_flag)); + + if (!contains_flag || !bounds_flag) { + arena_->capture_error(error::alloc_iterator_next); + return; + } + + size_t mem_z = arena_->config_.header_.size(*pos_); + size_t guard_z = arena_->config_.header_.guard_z_; + + byte * next_as_byte = ((byte *)pos_ + sizeof(AllocHeader) + mem_z + guard_z); + /* next == ix.arena_free_ --> iterator is at end of allocator */ + assert(next_as_byte <= arena_->free_); + + AllocHeader * next = (AllocHeader *)next_as_byte; + + this->pos_ = next; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DArenaIterator.cpp */ From 312542b1742302201c505306445b6be820ba093a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 12:10:05 -0500 Subject: [PATCH 012/111] xo-arena: + docs --- docs/ArenaConfig-reference.rst | 55 +++++++++++ docs/CMakeLists.txt | 22 +++++ docs/DArena-reference.rst | 96 +++++++++++++++++++ docs/DArenaIterator-reference.rst | 50 ++++++++++ docs/README | 72 ++++++++++++++ docs/_static/README | 1 + docs/_static/img/favicon.ico | Bin 0 -> 309936 bytes docs/cmpresult-reference.rst | 50 ++++++++++ docs/conf.py | 39 ++++++++ docs/examples.rst | 125 ++++++++++++++++++++++++ docs/glossary.rst | 24 +++++ docs/implementation.rst | 153 ++++++++++++++++++++++++++++++ docs/index.rst | 31 ++++++ 13 files changed, 718 insertions(+) create mode 100644 docs/ArenaConfig-reference.rst create mode 100644 docs/CMakeLists.txt create mode 100644 docs/DArena-reference.rst create mode 100644 docs/DArenaIterator-reference.rst create mode 100644 docs/README create mode 100644 docs/_static/README create mode 100644 docs/_static/img/favicon.ico create mode 100644 docs/cmpresult-reference.rst create mode 100644 docs/conf.py create mode 100644 docs/examples.rst create mode 100644 docs/glossary.rst create mode 100644 docs/implementation.rst create mode 100644 docs/index.rst diff --git a/docs/ArenaConfig-reference.rst b/docs/ArenaConfig-reference.rst new file mode 100644 index 00000000..48a38ac1 --- /dev/null +++ b/docs/ArenaConfig-reference.rst @@ -0,0 +1,55 @@ +.. _ArenaConfig-reference: + +ArenaConfig Reference +===================== + +Configuration for an arena allocator + +Context +------- + +.. ditaa:: + :--scale: 0.99 + + +-----------------------------------------------------+ + | DArena | + | DArenaIterator | + +-----------------------------------------------------+ + | ArenaConfig cBLU| + +--------------+------------------------+-------------+ + | | AllocInfo | | + | +------------------------+ | + | AllocError | AllocHeaderConfig | cmpresult | + | +------------------------+ | + | | AllocHeader | | + +--------------+------------------------+-------------+ + + +.. uml:: + :caption: example arena config + :scale: 99% + :align: center + + object cfg<> + cfg : name = "tmp" + cfg : size = 128MB + cfg : hugepage_z = 2MB + cfg : guard_z = 8 + cfg : guard_byte = 0xfd + cfg : store_header_flag = true + cfg : header_size_mask = 0xffffffff + cfg : debug_flag = false + +.. code-block:: cpp + + #include + +Class +----- + +.. doxygenclass:: xo::mm::ArenaConfig + +Instance Variables +------------------ + +.. doxygengroup:: mm-arenaconfig-instance-vars diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt new file mode 100644 index 00000000..7fc9ee26 --- /dev/null +++ b/docs/CMakeLists.txt @@ -0,0 +1,22 @@ +# xo-arena/docs/CMakeLists.txt + +xo_doxygen_collect_deps() +xo_docdir_doxygen_config() +xo_docdir_sphinx_config( + index.rst + glossary.rst + examples.rst + implementation.rst + #AAllocator-reference.rst + #IAllocator_Xfer-reference.rst + #AAllocIterator-reference.rst + ArenaConfig-reference.rst + DArena-reference.rst + AllocInfo-reference.rst + cmpresult-reference.rst + #install.rst + #introduction.rst +) + +# see xo-reader/doc or xo-unit/doc for working examples +# example.rst install.rst implementation.rst diff --git a/docs/DArena-reference.rst b/docs/DArena-reference.rst new file mode 100644 index 00000000..50ad44f5 --- /dev/null +++ b/docs/DArena-reference.rst @@ -0,0 +1,96 @@ +.. _DArena-reference: + +DArena +====== + +Native arena allocator + +Context +------- + +.. ditaa:: + :--scale: 0.99 + + +-----------------------------------------------------+ + | DArena cBLU| + | DArenaIterator | + +-----------------------------------------------------+ + | ArenaConfig | + +--------------+------------------------+-------------+ + | | AllocInfo | | + | +------------------------+ | + | AllocError | AllocHeaderConfig | cmpresult | + | +------------------------+ | + | | AllocHeader | | + +--------------+------------------------+-------------+ + +.. code-block:: cpp + + #include + +Arena memory layout +~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + <------------------------reserved--------------------------> + <------------committed-----------><-------uncommitted------> + <--allocated--><----available----> + + XXXXXXXXXXXXXXX___________________.......................... + ^ ^ ^ ^ + lo free limit hi + + [X] allocated: in use + [_] committed: physical memory obtained + [.] uncommitted: mapped in virtual memory, not backed by memory + + +Representation for a single allocation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + free_(pre) + v + + <-------------z1---------------> + < guard >< hz >< req_z >< dz >< guard > + + used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail + + ^ ^ ^ + header mem | + ^ | + last_header_ free_(post) + + [+] guard surrounding each allocation, for simple sanitize checks + [0] unused header bits (available for application metadata) + [z] record allocation size + [@] new allocated memory + [p] padding (to uintptr_t alignment) + +Class +----- + +.. doxygenclass:: xo::mm::DArena + +Member Variables +---------------- + +.. doxygengroup:: mm-arena-instance-vars + +Type Traits +----------- + +.. doxygengroup:: mm-arena-traits + +Constructors +------------ + +.. doxygengroup:: mm-arena-ctors + +Methods +------- + +.. doxygengroup:: mm-arena-methods diff --git a/docs/DArenaIterator-reference.rst b/docs/DArenaIterator-reference.rst new file mode 100644 index 00000000..d41a1590 --- /dev/null +++ b/docs/DArenaIterator-reference.rst @@ -0,0 +1,50 @@ +.. _DArenaIterator-reference: + +DArenaIterator +============== + +Iterator for allocs obtained from a :cpp:class:`xo::mm::DArena`. + +Context +------- + +.. ditaa:: + :--scale: 0.99 + + +-----------------------------------------------------+ + | DArena | + | DArenaIterator cBLU| + +-----------------------------------------------------+ + | ArenaConfig | + +--------------+------------------------+-------------+ + | | AllocInfo | | + | +------------------------+ | + | AllocError | AllocHeaderConfig | cmpresult | + | +------------------------+ | + | | AllocHeader | | + +--------------+------------------------+-------------+ + + +.. code-block:: cpp + + #include + +Class +----- + +.. doxygenclass:: xo::mm::DArenaIterator + +Member Variables +---------------- + +.. doxygengroup:: mm-arenaiterator-instance-vars + +Constructors +------------ + +.. doxygengroup:: mm-arenaiterator-ctors + +Methods +------- + +.. doxygengroup:: mm-arenaiterator-methods diff --git a/docs/README b/docs/README new file mode 100644 index 00000000..bdd13316 --- /dev/null +++ b/docs/README @@ -0,0 +1,72 @@ +build + + +-----------------------------------------------+ + | cmake | + | CMakeLists.txt | + | $PREFIX/share/cmake/xo_macros/xo_cxx.cmake | + +-----------------------------------------------+ + | + | +----------------------+ + +------------------------------------------------->| .build/docs/Doxyfile | + | +----------------------+ + | | + | /------------/ + | | + | v + | +---------------------------------------+ +-----------------+ + +---->| doxygen |--->| .build/docs/dox | + | | $PREFIX/share/xo-macros/Doxyfile.in | | +- html/ | + | +---------------------------------------+ | +- xml/ | + | +-----------------+ + | | + | /------------/ + | | + | v + | +---------------------------------------+ +--------------------+ + \---->| sphinx |--->| .build/docs/sphinx | + | +- conf.py | | +- html/ | + | +- _static/ | +--------------------+ + | +- *.rst | + +---------------------------------------+ + +files + + README this file + CMakeLists.txt build entry point + conf.py sphinx config + _static static files for sphinx + +map + + index.rst + +- examples.rst + +- ArenaConfig-reference.rst + +- DArena-reference.rst + +- DArenaIterator-reference.rst + +- AllocInfo-reference.rst + +- cmpresult-reference.rst + +- glossary.rst + ... + +examples + +.. doxygenclass:: ${c++ class name} + :project: + :path: + :members: + :protected-members: + :private-members: + :undoc-members: + :member-groups: + :members-only: + :outline: + :no-link: + :allow-dot-graphs: + +.. doxygendefine:: ${c preprocessor define} + +.. doxygenconcept:: ${c++ concept definition} + +.. doxygenenum:: ${c++ enum definition} + +.. doxygenfunction:: ${c++ function name} diff --git a/docs/_static/README b/docs/_static/README new file mode 100644 index 00000000..7297d046 --- /dev/null +++ b/docs/_static/README @@ -0,0 +1 @@ +add any static {.html, .js, ..} files for sphinx to pickup here diff --git a/docs/_static/img/favicon.ico b/docs/_static/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4163dd69c734f8186cf1ce5e726213cebd231c31 GIT binary patch literal 309936 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 + +Class +----- + +.. doxygenclass:: xo::mm::cmpresult + +Constructors +------------ + +.. doxgyengroup:: mm-cmpresult-ctors + +Methods +------- + +.. doxygengroup:: mm-cmpresult-methods + +Member Variables +---------------- + +.. doxygengroup:: mm-cmpresult-instance-vars diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..72dafcf4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,39 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'xo arena documentation' +copyright = '2025, Roland Conybeare' +author = 'Roland Conybeare' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +#extensions = [] +extensions = [ "breathe", + "sphinx.ext.mathjax", # inline math + "sphinx.ext.autodoc", # generate info from docstrings + "sphinxcontrib.ditaa", # diagrams-through-ascii-art + "sphinxcontrib.plantuml" # text -> uml diagrams + ] + +# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in +# match project name in Doxyfile.in +breathe_default_project = "xodoxxml" + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +pygments_style = 'sphinx' + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_favicon = '_static/img/favicon.ico' diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 00000000..5e78bbba --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,125 @@ +.. _examples: + +.. toctree + :maxdepth: 2 + +Examples +======== + +Arena allocation +----------------- + +.. code-block:: cpp + + #include + + using namespace xo::mm; + using namespace std; + + +Create an arena: + +.. code-block:: cpp + + // create arena, size 64k + DArena arena = DArena::map(ArenaConfig { .size_ = 64*1024; }); + + cout << arena.lo() << ".." << arena.hi(); + +This determines a VM memory address range. +Actually address range is rounded up to a whole number of VM pages. +Size here is a hard maximum. It cannot be changed for this arena instance. + +.. code-block:: cpp + + arena.reserved(); // 64k + arena.committed(); // 0k + arena.allocated(); // ok + arena.available(); // 0k + +Although we know the address range for arena, it doesn't own any physical +memory yet. Two ways to commit memory: + +1. Attempt allocation: + + .. code-block:: cpp + + std::byte * mem = arena.alloc(5*1024); + if (!mem) + throw std::runtime_error("alloc failed"); + + arena.reserved(); // 64k + arena.committed(); // 8k - 2 pages + arena.allocateed(); // 5k + arena.available(); // 3k + +2. Expand committed memory explicitly: + + .. code-block:: cpp + + bool ok = arena.expand(5*1024); + assert(ok); + + arena.reserved(); // 64k + arena.committed(); // 8k - 2 pages + arena.allocated(); // 0k + arena.available(); // 8k + +Examining alloc metadata +------------------------ + +Given a successful allocation: + +.. code-block:: cpp + + std::size_t req_z = 5*1024; + std::byte * mem = arena.alloc(req_z); + if (!mem) + throw std::runtime_error("alloc failed"); + + AllocInfo info = arena.alloc_info(mem); + + info.payload(); // [mem, mem + req_z (+ up to 7 bytes padding)] + info.is_valid(); // true + info.guard_lo(); // guard bytes preceding alloc + info.guard_hi(); // guard bytes following alloc + +Can alternatively scan all live allocs in arena: + +.. code-block:: cpp + + for (AllocInfo info : arena) { + info.payload(); // allocated memory range + info.is_valid(); // true + info.guard_lo(); // guard bytes preceding alloc + info.guard_hi(); // guard bytes following alloc + } + +Recycling memory +---------------- + +.. code-block:: cpp + + // arena in non-empty state + arena.reserved(); // 64k + arena.committed(); // 8k - 2 pages + arena.allocateed(); // 5k + arena.available(); // 3k + + arena.clear(); + + arena.reserved() // 64k + arena.committed(); // 8k - 2 pages + arena.allocated(); // 0k + arena.available(); // 8k + +Memory recycled by :cpp:func:`DArena::clear()` +is available for reuse by application; it's still owned by arena. +We're just resetting the free pointer back to the beginning of arena +memory. + +To release memory to the operating system, destroy arena: + +.. code-block:: cpp + + arena.~DArena(); // or just let arena go out of scope diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 00000000..5878417c --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,24 @@ +.. _glossary: + +Glossary +-------- + +.. glossary:: + FOMO + | faceted object model + + page + | a (4k) page of virtual memory. + | O/S manages virtual memory in chunks of this size. + + hugepage + | large (2MB) VM page; use to reduce page fault expense and TLB pressure. + + THP + | transparent huge pages + + TLB + | translation lookaside buffer + + VM + | virtual memory diff --git a/docs/implementation.rst b/docs/implementation.rst new file mode 100644 index 00000000..69f92de6 --- /dev/null +++ b/docs/implementation.rst @@ -0,0 +1,153 @@ +.. _implementation: + +Implementation +============== + +Library dependency tower for *xo-arena* + +.. ditaa:: + + +------------------------------------+ + | xo_arena | + +-----------------+------------------+ + | xo_indentlog | xo_reflectutil | + +-----------------+------------------+ + | xo_cmake | + +------------------------------------+ + +Abstraction tower for *xo-arena* components (simplified) + +.. ditaa:: + :--scale: 0.99 + + +-------------------+ + | DArena | + | DArenaIterator | + +-------------------+ + | ArenaConfig | + +-------------------+ + | auxiliary types | + +-------------------+ + + +Abstraction tower for *xo-arena* components (detailed) + +.. ditaa:: + :--scale: 0.99 + + +-----------------------------------------------------+ + | DArena | + | DArenaIterator | + +-----------------------------------------------------+ + | ArenaConfig | + +--------------+------------------------+-------------+ + | | AllocInfo | | + | +------------------------+ | + | AllocError | AllocHeaderConfig | cmpresult | + | +------------------------+ | + | | AllocHeader | | + +--------------+------------------------+-------------+ + +.. list-table:: Native Arena Allocator + :header-rows: 1 + :widths: 20 90 + + * - Class + - Description + * - ``ArenaConfig`` + - Configuration for a ``DArena`` instance + * - ``DArena`` + - VM-aware arena allocator + * - ``DArenaIterator`` + - Iterator over ``DArena`` allocations + +.. list-table:: Auxiliary/Support Types + :header-rows: 1 + :widths: 20 90 + + * - Class + - Description + * - ``AllocError`` + - Return type for an alloc request, with error details. + * - ``AllocInfo`` + - An opaque allocation. Value of an alloc-iterator. + * - ``AllocHeaderConfig`` + - Per-allocator configuration of alloc headers + * - ``AllocHeader`` + - Per-allocation header (8 bytes) + * - ``cmpresult`` + - Result of alloc-iterator comparison + +Example Object Diagram + +.. uml:: + :caption: representation for an arena allocator + :scale: 99% + :align: center + + object darena1<> + darena1 : config + darena1 : lo + darena1 : hi + darena1 : free + darena1 : limit + darena1 : last_error + + object header1<> + header1 : size + header1 : header + + object hconfig1<> + hconfig1 : guard_z + hconfig1 : guard_byte + hconfig1 : tseq_bits + hconfig1 : age_bits + hconfig1 : size_bits + + darena1 o-- header1 + header1 o-- hconfig1 + +.. uml:: + :caption: memory layout + :scale: 99% + :align: center + + object darena1<> + darena1 : config + darena1 : lo + darena1 : hi + darena1 : free + darena1 : limit + darena1 : last_error + + rectangle "allocated" #90EE90 { + note as n1 + lo -> free + objects here + end note + } + + rectangle "available" #FFFFE0 { + note as n2 + free -> limit + alloc from here + end note + } + + rectangle "uncommitted" #D3D3D3 { + note as n3 + limit -> hi + not mapped yet + end note + } + + darena1 -[hidden]down- n1 + n1 -[hidden]down- n2 + n2 -[hidden]down- n3 + +Remarks: + +* See xo-alloc2 for abstract allocator trait *AAllocator* + along with its application to *DArena*. +* We split these because in *xo-facet* we rely on *DArena* to implement + double-dispatch (two-dimensional vtables, as seen for example in CLOS, Julia, Mathematica). diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..b5aeafbc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,31 @@ +# xo-arena documentation master file + +xo-arena documentation +====================== + +xo-arena provides: + +* Fast vm-aware arena allocation. +* Allocates uncommitted virtual memory, and commits on demand. +* When available, uses THP (Transparent Huge Pages) to mitigate pagetable pressure. +* Optional GC support, with per-alloc header. + +Diagnostic features: + +* with alloc headers: forward iterators over individual allocations +* configurable guard memory between allocations. + +.. toctree:: + :maxdepth: 2 + :caption: xo-arena contents + + examples + implementation + ArenaConfig-reference + DArena-reference + DArenaIterator-reference + AllocInfo-reference + cmpresult-reference + glossary + genindex + search From 17753316f82622b6c93055f6c5e907df0dd6731d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 15:34:51 -0500 Subject: [PATCH 013/111] xo-arena: DArena unit test --- CMakeLists.txt | 2 +- include/xo/arena/DArena.hpp | 3 ++ src/arena/DArena.cpp | 16 ++++++++++ utest/CMakeLists.txt | 23 ++++++++++++++ utest/DArena.test.cpp | 61 +++++++++++++++++++++++++++++++++++++ utest/arena_utest_main.cpp | 6 ++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 utest/CMakeLists.txt create mode 100644 utest/DArena.test.cpp create mode 100644 utest/arena_utest_main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 24d544ec..36fbfcb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,7 @@ add_definitions(${PROJECT_CXX_FLAGS}) # output targets add_subdirectory(src/arena) -#add_subdirectory(utest) +add_subdirectory(utest) # ---------------------------------------------------------------- # cmake export diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index f9c2e3c6..3ad10db6 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -219,6 +219,9 @@ namespace xo { **/ void clear() noexcept; + /** swap contents (including configuration) with another arena **/ + void swap(DArena & other) noexcept; + ///@} /** @defgroup mm-arena-instance-vars **/ diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index 5c0e3641..ecd698ae 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -616,6 +616,22 @@ namespace xo { this->free_ = lo_; this->establish_initial_guard(); } + + void + DArena::swap(DArena & other) noexcept + { + std::swap(config_, other.config_); + std::swap(page_z_, other.page_z_); + std::swap(arena_align_z_, other.arena_align_z_); + std::swap(lo_, other.lo_); + std::swap(committed_z_, other.committed_z_); + std::swap(last_header_, other.last_header_); + std::swap(free_, other.free_); + std::swap(limit_, other.limit_); + std::swap(hi_, other.hi_); + std::swap(error_count_, other.error_count_); + std::swap(last_error_, other.last_error_); + } } } /*namespace xo*/ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt new file mode 100644 index 00000000..34bf3caf --- /dev/null +++ b/utest/CMakeLists.txt @@ -0,0 +1,23 @@ +# xo-alloc2/utest/CMakeLists.txt +# + +set(UTEST_EXE utest.arena) +set(UTEST_SRCS + arena_utest_main.cpp +# objectmodel.test.cpp + DArena.test.cpp +# DArenaIterator.test.cpp +# Collector.test.cpp +# DX1CollectorIterator.test.cpp +# random_allocs.cpp +) + +if (ENABLE_TESTING) + xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) + xo_self_dependency(${UTEST_EXE} xo_arena) +# xo_headeronly_dependency(${UTEST_EXE} randomgen) +# xo_headeronly_dependency(${UTEST_EXE} indentlog) + xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) +endif() + +# end CMakeLists.txt diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp new file mode 100644 index 00000000..27870472 --- /dev/null +++ b/utest/DArena.test.cpp @@ -0,0 +1,61 @@ +/** @file DArena.test.cpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#include "xo/arena/DArena.hpp" +#include + +namespace xo { + using xo::mm::DArena; + using xo::mm::ArenaConfig; + using std::byte; + + namespace ut { + TEST_CASE("DArena-tiny", "[arena][DArena]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 1 }; + DArena arena = DArena::map(cfg); + + REQUIRE(arena.config_.name_ == cfg.name_); + REQUIRE(arena.lo_ != nullptr); + REQUIRE(arena.free_ == arena.lo_); + REQUIRE(arena.limit_ == arena.lo_); + REQUIRE(arena.hi_ != nullptr); + REQUIRE(arena.hi_ > arena.lo_); + REQUIRE(((size_t)arena.hi_ - (size_t)arena.lo_) % arena.page_z_ == 0); + REQUIRE(arena.lo_ + cfg.size_ <= arena.hi_); + + /* verify arena.lo_ is aligned on a page boundary */ + REQUIRE(((size_t)(arena.lo_) & (arena.page_z_ - 1)) == 0); + + /* verify arena.hi_ is aligned on a hugepage boundary */ + REQUIRE(((size_t)(arena.hi_) & (arena.page_z_ - 1)) == 0); + + byte * lo = arena.lo_; + byte * free = arena.free_; + byte * limit = arena.limit_; + byte * hi = arena.hi_; + size_t committed_z = arena.committed_z_; + + DArena arena2 = std::move(arena); + + REQUIRE(arena.lo_ == nullptr); + REQUIRE(arena.free_ == nullptr); + REQUIRE(arena.limit_ == nullptr); + REQUIRE(arena.hi_ == nullptr); + REQUIRE(arena.committed_z_ == 0); + + REQUIRE(arena.lo_ == nullptr); + REQUIRE(arena2.lo_ == lo); + REQUIRE(arena2.free_ == free); + REQUIRE(arena2.limit_ == limit); + REQUIRE(arena2.hi_ == hi); + REQUIRE(arena2.committed_z_ == committed_z); + } + + } +} + +/* end DArena.test.cpp */ diff --git a/utest/arena_utest_main.cpp b/utest/arena_utest_main.cpp new file mode 100644 index 00000000..e0fa95fa --- /dev/null +++ b/utest/arena_utest_main.cpp @@ -0,0 +1,6 @@ +/* file arena_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end arena_utest_main.cpp */ From 6b826e3e67213611ce265911f1f4e66ef73997fb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:10:58 -0500 Subject: [PATCH 014/111] xo-arena: + DArenaVector template + unit test --- include/xo/arena/DArenaVector.hpp | 229 ++++++++++++++++++++++++++++++ utest/CMakeLists.txt | 1 + utest/DArenaVector.test.cpp | 26 ++++ 3 files changed, 256 insertions(+) create mode 100644 include/xo/arena/DArenaVector.hpp create mode 100644 utest/DArenaVector.test.cpp diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp new file mode 100644 index 00000000..1babfd12 --- /dev/null +++ b/include/xo/arena/DArenaVector.hpp @@ -0,0 +1,229 @@ +/** @file DArenaVector.hpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#include "DArena.hpp" +#include + +namespace xo { + namespace mm { + /** @brief vector of T using dedicated DArena for storage + * + * Replicate (to the extent feasible) std::vector + * behavior, but using a dedicated DArena to provide storage + * + * Unlike std::vector: + * 1. does not support copying + * 2. capacity fixed at construction time + * + * @tparam T element type. Must be Erasable + **/ + template + struct DArenaVector { + public: + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = value_type &; + using const_reference = const value_type &; + using iterator = value_type *; + using const_iterator = const value_type *; + + /** null ctor **/ + DArenaVector() = default; + /** ctor from already-mapped (but not committed) address range_type + * vector has size zero + **/ + DArenaVector(const ArenaConfig & cfg, + size_type page_z, + size_type arena_align_z, + DArena::value_type lo, + DArena::value_type hi); + /** not intended to be copyable **/ + DArenaVector(const DArenaVector &) = delete; + /** move ctor **/ + DArenaVector(DArenaVector && other); + /** releases mapped memory **/ + ~DArenaVector(); + + /** create empty vector using @p cfg to configure backing store **/ + static DArenaVector map(const ArenaConfig & cfg); + + bool empty() const { return size_ == 0; } + size_type size() const { return size_; } + size_type max_size() const { return capacity(); } + size_type capacity() const { return store_.reserved() / sizeof(T); } + + T & operator[](size_t i) { return *(this->_address_of(i)); } + const T & operator[](size_t i) const { return *(this->_address_of(i)); } + + T & at(size_type i) { _check_valid_index(i); return *(this->_address_of(i)); } + const T & at(size_type i) const { _check_valid_index(i); return *(this->_address_of(i)); } + + iterator begin() noexcept { return this->_address_of(0); } + iterator end() noexcept { return this->_address_of(size_); } + const_iterator cbegin() const noexcept { return this->_address_of(0); } + const_iterator begin() const noexcept { return this->cbegin(); } + const_iterator cend() const noexcept { return this->_address_of(size_); } + const_iterator end() const noexcept { return this->cend(); } + + constexpr T * data() { return store_.lo_; } + constexpr const T * data() const { return store_.lo_; } + + void reserve(size_type z); + void resize(size_type z); + void shrink_to_fit(); + void clear(); + + void push_back(T && x); + void push_back(const T & x); + + void swap(DArenaVector & other) noexcept; + + private: + T * _address_of(size_type i) { return *((T *)store_.lo_) + i; } + const T * _address_of(size_type i) const { return *((const T *)store_.lo_) + i; } + + void _check_valid_index(size_type i) const; + + private: + size_type size_ = 0; + DArena store_; + }; + + template + DArenaVector::DArenaVector(const ArenaConfig & cfg, + size_type page_z, + size_type arena_align_z, + DArena::value_type lo, + DArena::value_type hi) + : store_{cfg, page_z, arena_align_z, lo, hi} + {} + + template + DArenaVector::DArenaVector(DArenaVector && other) + : size_{other.size_}, store_{std::move(other.store_)} + {} + + template + DArenaVector::~DArenaVector() + { + if constexpr (std::is_trivially_destructible_v) { + // nothing to do + } else { + // invoke destructor for each element + for (size_type i = 0, n = size(); i < n; ++i) { + T & x = (*this)[i]; + + x.~T(); + } + } + } + + template + DArenaVector + DArenaVector::map(const ArenaConfig & cfg) + { + DArenaVector retval; + + retval.store_ = std::move(DArena::map(cfg)); + + return retval; + } + + template + void + DArenaVector::reserve(size_type z) { + store_.expand(z * sizeof(T)); + } + + template + void + DArenaVector::resize(size_type z) { + if (z > size_) { + // expand arena to accomodate + size_t req_z = z * sizeof(T); + + store_.expand(req_z); + + // run ctors + if constexpr (std::is_trivially_constructible_v) { + // nothing to do + ; + } else { + for (size_type i = size_; i < z; ++i) { + void * addr = &(*this)[i]; + + new (addr) T(); + } + } + } else { + if constexpr (std::is_trivially_destructible_v) { + // nothing to do + } else { + // invoke destructor for each discarded element + for (size_type i = z; i < size_; ++i) { + T & x = (*this)[i]; + + x.~T(); + } + } + } + + this->size_ = z; + } + + template + void + DArenaVector::shrink_to_fit() { + // could in principle release unused mapped pages here + } + + template + void + DArenaVector::clear() { + this->resize(0); + } + + template + void + DArenaVector::_check_valid_index(size_type i) const { + if (size_ <= i) + throw std::out_of_range("DArenaVector index out of bounds"); + } + + template + void + DArenaVector::push_back(T && x) { + size_type z = size_ + 1; + size_type req_z = z * sizeof(T); + + store_.expand(req_z); + + T * addr = this->address_of(size_); + + new (addr) T{std::move(x)}; + + size_ = z; + } + + template + void + DArenaVector::push_back(const T & x) { + size_type z = size_; + this->resize(z + 1); + (*this)[z] = x; + } + + template + void + DArenaVector::swap(DArenaVector & other) noexcept { + std::swap(size_, other.size_); + std::swap(store_, other.store_); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DArenaVector.hpp */ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 34bf3caf..26b4e3d7 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -6,6 +6,7 @@ set(UTEST_SRCS arena_utest_main.cpp # objectmodel.test.cpp DArena.test.cpp + DArenaVector.test.cpp # DArenaIterator.test.cpp # Collector.test.cpp # DX1CollectorIterator.test.cpp diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp new file mode 100644 index 00000000..684e920c --- /dev/null +++ b/utest/DArenaVector.test.cpp @@ -0,0 +1,26 @@ +/** @file DArenaVector.test.cpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#include "xo/arena/DArenaVector.hpp" +#include + +namespace xo { + using xo::mm::DArenaVector; + using xo::mm::ArenaConfig; + using std::byte; + + namespace ut { + TEST_CASE("DArenaVector-tiny", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 1 }; + DArenaVector arenavec = DArenaVector::map(cfg); + + REQUIRE(arenavec.empty()); + } + } +} + +/* end DArenaVector.test.cpp */ From 8d34672a3bfe77594e452abc3bf7b926b4b1f0db Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:15:41 -0500 Subject: [PATCH 015/111] xo-arena: DArenaVector bugfixes --- include/xo/arena/DArenaVector.hpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index 1babfd12..32eb0119 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -68,8 +68,8 @@ namespace xo { const_iterator cend() const noexcept { return this->_address_of(size_); } const_iterator end() const noexcept { return this->cend(); } - constexpr T * data() { return store_.lo_; } - constexpr const T * data() const { return store_.lo_; } + constexpr T * data() { return reinterpret_cast(store_.lo_); } + constexpr const T * data() const { return reinterpret_cast(store_.lo_); } void reserve(size_type z); void resize(size_type z); @@ -82,8 +82,8 @@ namespace xo { void swap(DArenaVector & other) noexcept; private: - T * _address_of(size_type i) { return *((T *)store_.lo_) + i; } - const T * _address_of(size_type i) const { return *((const T *)store_.lo_) + i; } + T * _address_of(size_type i) { return ((T *)store_.lo_) + i; } + const T * _address_of(size_type i) const { return ((const T *)store_.lo_) + i; } void _check_valid_index(size_type i) const; @@ -201,7 +201,7 @@ namespace xo { store_.expand(req_z); - T * addr = this->address_of(size_); + T * addr = this->_address_of(size_); new (addr) T{std::move(x)}; @@ -211,9 +211,13 @@ namespace xo { template void DArenaVector::push_back(const T & x) { - size_type z = size_; - this->resize(z + 1); - (*this)[z] = x; + size_type z = size_ + 1; + store_.expand(z * sizeof(T)); + + T * addr = this->_address_of(size_); + new (addr) T{x}; + + size_ = z; } template From ddc313f3db37dd2e9c59aca630c3fb3920451c30 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:30:00 -0500 Subject: [PATCH 016/111] xo-arena: + DArenaVector push_back unit tests --- utest/DArenaVector.test.cpp | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index 684e920c..dc6a32c9 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -20,6 +20,54 @@ namespace xo { REQUIRE(arenavec.empty()); } + + TEST_CASE("DArenaVector-push_back-rvalue", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + REQUIRE(vec.empty()); + REQUIRE(vec.size() == 0); + + vec.push_back(1.5); + + REQUIRE(!vec.empty()); + REQUIRE(vec.size() == 1); + REQUIRE(vec[0] == 1.5); + + vec.push_back(2.5); + vec.push_back(3.5); + + REQUIRE(vec.size() == 3); + REQUIRE(vec[0] == 1.5); + REQUIRE(vec[1] == 2.5); + REQUIRE(vec[2] == 3.5); + } + + TEST_CASE("DArenaVector-push_back-lvalue", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + double a = 10.0; + double b = 20.0; + double c = 30.0; + + vec.push_back(a); + + REQUIRE(vec.size() == 1); + REQUIRE(vec[0] == 10.0); + + vec.push_back(b); + vec.push_back(c); + + REQUIRE(vec.size() == 3); + REQUIRE(vec[0] == 10.0); + REQUIRE(vec[1] == 20.0); + REQUIRE(vec[2] == 30.0); + } } } From 0d55ab27129ac7d0182009cf3513588222ecde1e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:36:24 -0500 Subject: [PATCH 017/111] xo-arena: tidy DArena debug logging --- src/arena/DArena.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index ecd698ae..ee03ddea 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -27,7 +27,7 @@ namespace xo { size_t align_z, bool enable_hugepage_flag) -> range_type { - scope log(XO_DEBUG(true), + scope log(XO_DEBUG(false), xtag("req_z", req_z), xtag("align_z", align_z)); // 1. round up to multiple of align_z @@ -114,7 +114,7 @@ namespace xo { DArena DArena::map(const ArenaConfig & cfg) { - scope log(XO_DEBUG(true)); + scope log(XO_DEBUG(cfg.debug_flag_)); /* vm page size. 4KB, probably */ size_t page_z = getpagesize(); From 3a4219972ed6443263f094e478a19fec1a011127 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:36:58 -0500 Subject: [PATCH 018/111] xo-arena: + DArenaVector.at unit test --- utest/DArenaVector.test.cpp | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index dc6a32c9..2deca944 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -68,6 +68,48 @@ namespace xo { REQUIRE(vec[1] == 20.0); REQUIRE(vec[2] == 30.0); } + + TEST_CASE("DArenaVector-at-valid", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(100.0); + vec.push_back(200.0); + vec.push_back(300.0); + + REQUIRE(vec.at(0) == 100.0); + REQUIRE(vec.at(1) == 200.0); + REQUIRE(vec.at(2) == 300.0); + + // test mutability via at() + vec.at(1) = 250.0; + REQUIRE(vec.at(1) == 250.0); + } + + TEST_CASE("DArenaVector-at-throws", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + // empty vector - any index is invalid + REQUIRE_THROWS_AS(vec.at(0), std::out_of_range); + + vec.push_back(1.0); + vec.push_back(2.0); + + // valid indices work + REQUIRE_NOTHROW(vec.at(0)); + REQUIRE_NOTHROW(vec.at(1)); + + // index == size is invalid + REQUIRE_THROWS_AS(vec.at(2), std::out_of_range); + + // index > size is invalid + REQUIRE_THROWS_AS(vec.at(100), std::out_of_range); + } } } From 24e027cb355f36e6f89e32b06d8df0086ec1e9c2 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:41:12 -0500 Subject: [PATCH 019/111] xo-arena: add DArenaVector utests for resize+clear --- utest/DArenaVector.test.cpp | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index 2deca944..79794450 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -110,6 +110,117 @@ namespace xo { // index > size is invalid REQUIRE_THROWS_AS(vec.at(100), std::out_of_range); } + + TEST_CASE("DArenaVector-resize-expand", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + REQUIRE(vec.size() == 0); + + // resize from 0 to 5 + vec.resize(5); + REQUIRE(vec.size() == 5); + + // can write to all indices + for (size_t i = 0; i < 5; ++i) { + vec[i] = static_cast(i * 10); + } + + REQUIRE(vec[0] == 0.0); + REQUIRE(vec[1] == 10.0); + REQUIRE(vec[2] == 20.0); + REQUIRE(vec[3] == 30.0); + REQUIRE(vec[4] == 40.0); + + // resize to larger + vec.resize(8); + REQUIRE(vec.size() == 8); + + // original values preserved + REQUIRE(vec[0] == 0.0); + REQUIRE(vec[1] == 10.0); + REQUIRE(vec[4] == 40.0); + } + + TEST_CASE("DArenaVector-resize-shrink", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(1.0); + vec.push_back(2.0); + vec.push_back(3.0); + vec.push_back(4.0); + vec.push_back(5.0); + + REQUIRE(vec.size() == 5); + + // shrink to 3 + vec.resize(3); + REQUIRE(vec.size() == 3); + + // first 3 elements preserved + REQUIRE(vec[0] == 1.0); + REQUIRE(vec[1] == 2.0); + REQUIRE(vec[2] == 3.0); + + // index 3 now out of bounds + REQUIRE_THROWS_AS(vec.at(3), std::out_of_range); + + // shrink to 0 + vec.resize(0); + REQUIRE(vec.size() == 0); + REQUIRE(vec.empty()); + } + + TEST_CASE("DArenaVector-resize-same", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(10.0); + vec.push_back(20.0); + vec.push_back(30.0); + + REQUIRE(vec.size() == 3); + + // resize to same size + vec.resize(3); + REQUIRE(vec.size() == 3); + + // values unchanged + REQUIRE(vec[0] == 10.0); + REQUIRE(vec[1] == 20.0); + REQUIRE(vec[2] == 30.0); + } + + TEST_CASE("DArenaVector-clear", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(1.0); + vec.push_back(2.0); + vec.push_back(3.0); + + REQUIRE(vec.size() == 3); + REQUIRE(!vec.empty()); + + vec.clear(); + + REQUIRE(vec.size() == 0); + REQUIRE(vec.empty()); + + // can still push after clear + vec.push_back(99.0); + REQUIRE(vec.size() == 1); + REQUIRE(vec[0] == 99.0); + } } } From 8e90afcded55a83b58b7c3a2b38e7db5fa4b3c97 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 16:47:00 -0500 Subject: [PATCH 020/111] xo-arena: DArenaVector: add iterator unit tests --- utest/DArenaVector.test.cpp | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index 79794450..f90630ba 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -221,6 +221,83 @@ namespace xo { REQUIRE(vec.size() == 1); REQUIRE(vec[0] == 99.0); } + + TEST_CASE("DArenaVector-iterators", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(10.0); + vec.push_back(20.0); + vec.push_back(30.0); + + // begin/end + REQUIRE(vec.begin() != vec.end()); + REQUIRE(vec.end() - vec.begin() == 3); + + // iterate with pointer arithmetic + auto it = vec.begin(); + REQUIRE(*it == 10.0); + ++it; + REQUIRE(*it == 20.0); + ++it; + REQUIRE(*it == 30.0); + ++it; + REQUIRE(it == vec.end()); + + // modify through iterator + *vec.begin() = 15.0; + REQUIRE(vec[0] == 15.0); + } + + TEST_CASE("DArenaVector-const-iterators", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(1.0); + vec.push_back(2.0); + vec.push_back(3.0); + + const DArenaVector & cvec = vec; + + REQUIRE(cvec.cbegin() != cvec.cend()); + REQUIRE(cvec.begin() == cvec.cbegin()); + REQUIRE(cvec.end() == cvec.cend()); + + auto it = cvec.cbegin(); + REQUIRE(*it == 1.0); + ++it; + REQUIRE(*it == 2.0); + } + + TEST_CASE("DArenaVector-range-for", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(1.0); + vec.push_back(2.0); + vec.push_back(3.0); + + // read via range-for + double sum = 0.0; + for (double x : vec) { + sum += x; + } + REQUIRE(sum == 6.0); + + // modify via range-for + for (double & x : vec) { + x *= 2.0; + } + REQUIRE(vec[0] == 2.0); + REQUIRE(vec[1] == 4.0); + REQUIRE(vec[2] == 6.0); + } } } From b06ee414c5c56cfeac9194b538bc02eac9956556 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:02:23 -0500 Subject: [PATCH 021/111] xo-arena: + DAreanVector.reserve utest --- utest/DArenaVector.test.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index f90630ba..06b2337f 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -298,6 +298,41 @@ namespace xo { REQUIRE(vec[1] == 4.0); REQUIRE(vec[2] == 6.0); } + + TEST_CASE("DArenaVector-reserve", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + REQUIRE(vec.size() == 0); + REQUIRE(vec.capacity() > 0); + + size_t initial_capacity = vec.capacity(); + + // reserve doesn't change size + vec.reserve(100); + REQUIRE(vec.size() == 0); + REQUIRE(vec.capacity() >= 100); + + // add some elements + vec.push_back(1.0); + vec.push_back(2.0); + vec.push_back(3.0); + + REQUIRE(vec.size() == 3); + size_t cap_after_push = vec.capacity(); + + // reserve more space + vec.reserve(200); + REQUIRE(vec.size() == 3); + REQUIRE(vec.capacity() >= 200); + + // values still intact + REQUIRE(vec[0] == 1.0); + REQUIRE(vec[1] == 2.0); + REQUIRE(vec[2] == 3.0); + } } } From 3b15825769edb26eb1d196ab02b800637a6ddf0a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:09:07 -0500 Subject: [PATCH 022/111] xo-arena: + DArenaVector.swap utest --- utest/DArenaVector.test.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index 06b2337f..ddbab301 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -333,6 +333,41 @@ namespace xo { REQUIRE(vec[1] == 2.0); REQUIRE(vec[2] == 3.0); } + + TEST_CASE("DArenaVector-swap", "[arena][DArenaVector]") + { + ArenaConfig cfg1 { .name_ = "testarena1", + .size_ = 4096 }; + ArenaConfig cfg2 { .name_ = "testarena2", + .size_ = 4096 }; + + DArenaVector vec1 = DArenaVector::map(cfg1); + DArenaVector vec2 = DArenaVector::map(cfg2); + + vec1.push_back(1.0); + vec1.push_back(2.0); + + vec2.push_back(10.0); + vec2.push_back(20.0); + vec2.push_back(30.0); + + REQUIRE(vec1.size() == 2); + REQUIRE(vec2.size() == 3); + + vec1.swap(vec2); + + // sizes swapped + REQUIRE(vec1.size() == 3); + REQUIRE(vec2.size() == 2); + + // contents swapped + REQUIRE(vec1[0] == 10.0); + REQUIRE(vec1[1] == 20.0); + REQUIRE(vec1[2] == 30.0); + + REQUIRE(vec2[0] == 1.0); + REQUIRE(vec2[1] == 2.0); + } } } From 6b9a5471c6c87e7b9ffdd230fa22c25f67b2ec61 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:14:32 -0500 Subject: [PATCH 023/111] xo-arena: + DArenaVector.data utest --- utest/DArenaVector.test.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index ddbab301..a3d9667e 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -368,6 +368,38 @@ namespace xo { REQUIRE(vec2[0] == 1.0); REQUIRE(vec2[1] == 2.0); } + + TEST_CASE("DArenaVector-data", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(1.0); + vec.push_back(2.0); + vec.push_back(3.0); + + double * ptr = vec.data(); + + // data() points to first element + REQUIRE(ptr == &vec[0]); + + // can read via pointer + REQUIRE(ptr[0] == 1.0); + REQUIRE(ptr[1] == 2.0); + REQUIRE(ptr[2] == 3.0); + + // can write via pointer + ptr[1] = 99.0; + REQUIRE(vec[1] == 99.0); + + // const version + const DArenaVector & cvec = vec; + const double * cptr = cvec.data(); + REQUIRE(cptr[0] == 1.0); + REQUIRE(cptr[1] == 99.0); + REQUIRE(cptr[2] == 3.0); + } } } From 3415e62988ba4b645ae510a70f40e8aa392526a3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:19:35 -0500 Subject: [PATCH 024/111] xo-arena: bugfix in DArenaVector move ctor --- include/xo/arena/DArenaVector.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index 32eb0119..a85f8158 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -104,7 +104,9 @@ namespace xo { template DArenaVector::DArenaVector(DArenaVector && other) : size_{other.size_}, store_{std::move(other.store_)} - {} + { + other.size_ = 0; + } template DArenaVector::~DArenaVector() From f084a67b19a7660dad29b0d4e28707a12a54a7c1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:20:03 -0500 Subject: [PATCH 025/111] xo-arena: + utest for DArenaVector move ctor --- utest/DArenaVector.test.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index a3d9667e..c563a89a 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -400,6 +400,34 @@ namespace xo { REQUIRE(cptr[1] == 99.0); REQUIRE(cptr[2] == 3.0); } + + TEST_CASE("DArenaVector-move-ctor", "[arena][DArenaVector]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec1 = DArenaVector::map(cfg); + + vec1.push_back(10.0); + vec1.push_back(20.0); + vec1.push_back(30.0); + + double * original_data = vec1.data(); + size_t original_size = vec1.size(); + + // move construct vec2 from vec1 + DArenaVector vec2(std::move(vec1)); + + // vec2 has the data + REQUIRE(vec2.size() == original_size); + REQUIRE(vec2.data() == original_data); + REQUIRE(vec2[0] == 10.0); + REQUIRE(vec2[1] == 20.0); + REQUIRE(vec2[2] == 30.0); + + // vec1 is in valid but moved-from state + REQUIRE(vec1.size() == 0); + REQUIRE(vec1.empty()); + } } } From 1cbe9c0091920147abdc2d797856f3c5931851a6 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:23:47 -0500 Subject: [PATCH 026/111] xo-arena: add DArenaVector utest with non-trivial element ctor/dtor --- utest/DArenaVector.test.cpp | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index c563a89a..4366cdf7 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -428,6 +428,111 @@ namespace xo { REQUIRE(vec1.size() == 0); REQUIRE(vec1.empty()); } + + // Helper class to track ctor/dtor calls + struct LifetimeTracker { + static int ctor_count; + static int dtor_count; + + static void reset() { + ctor_count = 0; + dtor_count = 0; + } + + int value; + + LifetimeTracker() : value{0} { ++ctor_count; } + LifetimeTracker(int v) : value{v} { ++ctor_count; } + LifetimeTracker(const LifetimeTracker & other) : value{other.value} { ++ctor_count; } + LifetimeTracker(LifetimeTracker && other) : value{other.value} { ++ctor_count; other.value = 0; } + ~LifetimeTracker() { ++dtor_count; } + + LifetimeTracker & operator=(const LifetimeTracker &) = default; + LifetimeTracker & operator=(LifetimeTracker &&) = default; + }; + + int LifetimeTracker::ctor_count = 0; + int LifetimeTracker::dtor_count = 0; + + TEST_CASE("DArenaVector-nontrivial-dtor", "[arena][DArenaVector]") + { + LifetimeTracker::reset(); + + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(LifetimeTracker{1}); + vec.push_back(LifetimeTracker{2}); + vec.push_back(LifetimeTracker{3}); + + // 3 temp objects created, 3 moved into vector + // temps destroyed after push_back + REQUIRE(vec.size() == 3); + REQUIRE(LifetimeTracker::ctor_count == 6); // 3 temps + 3 moves + REQUIRE(LifetimeTracker::dtor_count == 3); // 3 temps destroyed + + // verify values + REQUIRE(vec[0].value == 1); + REQUIRE(vec[1].value == 2); + REQUIRE(vec[2].value == 3); + } + // vec destroyed, should call dtor for all 3 elements + REQUIRE(LifetimeTracker::dtor_count == 6); + } + + TEST_CASE("DArenaVector-nontrivial-resize-shrink", "[arena][DArenaVector]") + { + LifetimeTracker::reset(); + + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(LifetimeTracker{1}); + vec.push_back(LifetimeTracker{2}); + vec.push_back(LifetimeTracker{3}); + vec.push_back(LifetimeTracker{4}); + vec.push_back(LifetimeTracker{5}); + + REQUIRE(vec.size() == 5); + int dtors_before_shrink = LifetimeTracker::dtor_count; + + // shrink from 5 to 2 + vec.resize(2); + + REQUIRE(vec.size() == 2); + // should have called dtor for 3 elements (indices 2,3,4) + REQUIRE(LifetimeTracker::dtor_count == dtors_before_shrink + 3); + + // remaining elements intact + REQUIRE(vec[0].value == 1); + REQUIRE(vec[1].value == 2); + } + + TEST_CASE("DArenaVector-nontrivial-clear", "[arena][DArenaVector]") + { + LifetimeTracker::reset(); + + ArenaConfig cfg { .name_ = "testarena", + .size_ = 4096 }; + DArenaVector vec = DArenaVector::map(cfg); + + vec.push_back(LifetimeTracker{1}); + vec.push_back(LifetimeTracker{2}); + vec.push_back(LifetimeTracker{3}); + + REQUIRE(vec.size() == 3); + int dtors_before_clear = LifetimeTracker::dtor_count; + + vec.clear(); + + REQUIRE(vec.size() == 0); + REQUIRE(vec.empty()); + // should have called dtor for all 3 elements + REQUIRE(LifetimeTracker::dtor_count == dtors_before_clear + 3); + } } } From b5b83dcf1f706340ccaddb4ad40e758dade153da Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:27:34 -0500 Subject: [PATCH 027/111] xo-arena: move DArena utest from xo-alloc2/ --- utest/DArena.test.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp index 27870472..6293c7a2 100644 --- a/utest/DArena.test.cpp +++ b/utest/DArena.test.cpp @@ -55,6 +55,49 @@ namespace xo { REQUIRE(arena2.committed_z_ == committed_z); } + TEST_CASE("DArena-medium", "[arena][DArena]") + { + ArenaConfig cfg { .name_ = "testarena", + .size_ = 10*1024*1024 }; + DArena arena = DArena::map(cfg); + + REQUIRE(arena.config_.name_ == cfg.name_); + REQUIRE(arena.lo_ != nullptr); + REQUIRE(arena.free_ == arena.lo_); + REQUIRE(arena.limit_ == arena.lo_); + REQUIRE(arena.hi_ != nullptr); + REQUIRE(arena.hi_ > arena.lo_); + REQUIRE(((size_t)arena.hi_ - (size_t)arena.lo_) % cfg.hugepage_z_ == 0); + REQUIRE(arena.lo_ + cfg.size_ <= arena.hi_); + + /* verify arena.lo_ is aligned on a page boundary */ + REQUIRE(((size_t)(arena.lo_) & (cfg.hugepage_z_ - 1)) == 0); + + /* verify arena.hi_ is aligned on a hugepage boundary */ + REQUIRE(((size_t)(arena.hi_) & (cfg.hugepage_z_ - 1)) == 0); + + byte * lo = arena.lo_; + byte * free = arena.free_; + byte * limit = arena.limit_; + byte * hi = arena.hi_; + size_t committed_z = arena.committed_z_; + + DArena arena2 = std::move(arena); + + REQUIRE(arena.lo_ == nullptr); + REQUIRE(arena.free_ == nullptr); + REQUIRE(arena.limit_ == nullptr); + REQUIRE(arena.hi_ == nullptr); + REQUIRE(arena.committed_z_ == 0); + + REQUIRE(arena.lo_ == nullptr); + REQUIRE(arena2.lo_ == lo); + REQUIRE(arena2.free_ == free); + REQUIRE(arena2.limit_ == limit); + REQUIRE(arena2.hi_ == hi); + REQUIRE(arena2.committed_z_ == committed_z); + } + } } From d7491bfe8eced5ef4da93c948b97be2540cc15b3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:39:10 -0500 Subject: [PATCH 028/111] xo-arena: + DArena.expand utest --- include/xo/arena/DArena.hpp | 4 ++++ include/xo/arena/print.hpp | 34 ++++++++++++++++++++++++++++++++++ utest/DArena.test.cpp | 32 +++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 include/xo/arena/print.hpp diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 3ad10db6..a3253220 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -105,6 +105,10 @@ namespace xo { * This is the amount of memory guaranteed to be usable for future allocs from this arena. **/ size_type available() const noexcept { return limit_ - free_; } + /** VM page size for this arena (likely 4KB) **/ + size_type page_z() const noexcept { return page_z_; } + /** Last error encountered by this arena **/ + const AllocError & last_error() const noexcept { return last_error_; } /** True iff address @p addr is owned by this arena, * i.e. falls within [@ref lo_, @ref hi_) diff --git a/include/xo/arena/print.hpp b/include/xo/arena/print.hpp new file mode 100644 index 00000000..5c474762 --- /dev/null +++ b/include/xo/arena/print.hpp @@ -0,0 +1,34 @@ +/** @file print.hpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocError.hpp" +#include +#include + +namespace xo { + namespace mm { + inline std::ostream & + operator<<(std::ostream & os, const error & x) { + os << AllocError::error_description(x); + return os; + } + + inline std::ostream & + operator<<(std::ostream & os, const AllocError & x) { + os << ""; + return os; + } + } +} + +/* end print.hpp */ diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp index 6293c7a2..a94b5b09 100644 --- a/utest/DArena.test.cpp +++ b/utest/DArena.test.cpp @@ -3,12 +3,15 @@ * @author Roland Conybeare, Jan 2026 **/ -#include "xo/arena/DArena.hpp" +#include "DArena.hpp" +#include "print.hpp" +#include #include namespace xo { using xo::mm::DArena; using xo::mm::ArenaConfig; + using xo::xtag; using std::byte; namespace ut { @@ -98,6 +101,33 @@ namespace xo { REQUIRE(arena2.committed_z_ == committed_z); } + TEST_CASE("arena-expand-1", "[arena][DArena]") + { + /* typed allocator a1o */ + ArenaConfig cfg { .name_ = "testarena", + .size_ = 1, + .debug_flag_ = false }; + DArena arena = DArena::map(cfg); + + REQUIRE(arena.available() == 0); + REQUIRE(arena.allocated() == 0); + + size_t z2 = 512; + bool ok = arena.expand(z2); + + INFO(xtag("last_error", arena.last_error())); + + REQUIRE(ok); + + REQUIRE(arena.reserved() % arena.page_z() == 0); + REQUIRE(arena.committed() >= z2); + REQUIRE(arena.committed() % arena.page_z() == 0); + REQUIRE(arena.available() >= z2); + REQUIRE(arena.available() == arena.committed()); + REQUIRE(arena.allocated() == 0); + + } + } } From da88ac08daefd724d92377caff870a1b2cc5de1c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:41:30 -0500 Subject: [PATCH 029/111] xo-arena: add DArena.alloc utest --- utest/DArena.test.cpp | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp index a94b5b09..ae76226b 100644 --- a/utest/DArena.test.cpp +++ b/utest/DArena.test.cpp @@ -11,6 +11,9 @@ namespace xo { using xo::mm::DArena; using xo::mm::ArenaConfig; + using xo::mm::padding; + using xo::mm::error; + using xo::reflect::typeseq; using xo::xtag; using std::byte; @@ -101,7 +104,7 @@ namespace xo { REQUIRE(arena2.committed_z_ == committed_z); } - TEST_CASE("arena-expand-1", "[arena][DArena]") + TEST_CASE("DArena-expand-1", "[arena][DArena]") { /* typed allocator a1o */ ArenaConfig cfg { .name_ = "testarena", @@ -128,6 +131,44 @@ namespace xo { } + TEST_CASE("arena-alloc-1", "[arena][DArena]") + { + /* typed allocator a1o */ + ArenaConfig cfg { .name_ = "testarena", + .size_ = 64*1024, + .debug_flag_ = false }; + DArena arena = DArena::map(cfg); + + REQUIRE(arena.reserved() >= cfg.size_); + REQUIRE(arena.committed() == 0); + REQUIRE(arena.available() == 0); + REQUIRE(arena.allocated() == 0); + + size_t z0 = 1; + byte * m0 = arena.alloc(typeseq::anon(), 1); + + REQUIRE(m0); + REQUIRE(arena.last_error().error_ == error::ok); + REQUIRE(arena.last_error().error_seq_ == 0); + REQUIRE(arena.allocated() >= z0); + REQUIRE(arena.allocated() < z0 + padding::c_alloc_alignment ); + REQUIRE(arena.allocated() <= arena.committed()); + REQUIRE(arena.allocated() + arena.available() == arena.committed()); + REQUIRE(arena.committed() <= arena.reserved()); + + size_t z1 = 16; + byte * m1 = arena.alloc(typeseq::anon(), z1); + + REQUIRE(m1); + REQUIRE(arena.last_error().error_ == error::ok); + REQUIRE(arena.last_error().error_seq_ == 0); + REQUIRE(arena.allocated() >= z0 + z1); + REQUIRE(arena.allocated() < z0 + z1 + 2 * padding::c_alloc_alignment ); + REQUIRE(arena.allocated() <= arena.committed()); + REQUIRE(arena.allocated() + arena.available() == arena.committed()); + REQUIRE(arena.committed() <= arena.reserved()); + } + } } From 924670c6e3afb4ec29c166c6a78c98772e6039ab Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 17:43:26 -0500 Subject: [PATCH 030/111] xo-arena: add 2nd DArena.alloc utest --- utest/DArena.test.cpp | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp index ae76226b..e999c35f 100644 --- a/utest/DArena.test.cpp +++ b/utest/DArena.test.cpp @@ -10,6 +10,8 @@ namespace xo { using xo::mm::DArena; + using xo::mm::AllocHeader; + using xo::mm::AllocHeaderConfig; using xo::mm::ArenaConfig; using xo::mm::padding; using xo::mm::error; @@ -169,6 +171,48 @@ namespace xo { REQUIRE(arena.committed() <= arena.reserved()); } + TEST_CASE("arena-alloc-2", "[arena][DArena]") + { + using header_type = AllocHeader; + + /* typed allocator a1o, with object header */ + ArenaConfig cfg { .name_ = "testarena", + .size_ = 64*1024, + .store_header_flag_ = true, + /* up to 4GB */ + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 32 /*size-bits*/), + .debug_flag_ = false, + }; + DArena arena = DArena::map(cfg); + + REQUIRE(arena.reserved() >= cfg.size_); + REQUIRE(arena.committed() == 0); + REQUIRE(arena.available() == 0); + REQUIRE(arena.allocated() == 0); + + size_t z0 = 1; + byte * m0 = arena.alloc(typeseq::anon(), 1); + + REQUIRE(m0); + + header_type* header = (header_type*)(m0 - sizeof(header_type)); + + REQUIRE(arena.contains(header)); + REQUIRE(cfg.header_.size(*header) == padding::with_padding(z0)); + //REQUIRE(((*header) & cfg.header_size_mask_) == padding::with_padding(z0)); + REQUIRE(arena.last_error().error_ == error::ok); + REQUIRE(arena.last_error().error_seq_ == 0); + REQUIRE(arena.allocated() >= z0); + REQUIRE(arena.allocated() < sizeof(DArena::header_type) + z0 + padding::c_alloc_alignment ); + REQUIRE(arena.allocated() <= arena.committed()); + REQUIRE(arena.allocated() + arena.available() == arena.committed()); + REQUIRE(arena.committed() <= arena.reserved()); + } + } } From 94647287e0686608683b7fe909cce8f0f6e2488d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 23:55:33 -0500 Subject: [PATCH 031/111] xo-arena: DArenaHashMap [WIP] --- include/xo/arena/DArenaHashMap.hpp | 150 +++++++++++++++++++++++++++++ include/xo/arena/DArenaVector.hpp | 2 + utest/CMakeLists.txt | 1 + utest/DArenaHashMap.test.cpp | 26 +++++ utest/DArenaVector.test.cpp | 2 +- 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 include/xo/arena/DArenaHashMap.hpp create mode 100644 utest/DArenaHashMap.test.cpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp new file mode 100644 index 00000000..bc454e19 --- /dev/null +++ b/include/xo/arena/DArenaHashMap.hpp @@ -0,0 +1,150 @@ +/** @file DArenaHashMap.hpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "DArenaVector.hpp" +#include + +namespace xo { + namespace mm { + /** @brief flat hash map of key-value pairs using dedicated DArenas for storage + * + * Replicates (to the extent feasible) std::unordered_map + * + * @tparam K key type. + * @tparam V value type. + **/ + template , + typename Equal = std::equal_to> + struct DArenaHashMap { + public: + using size_type = std::size_t; + using key_type = Key; + using mapped_type = Value; + using value_type = std::pair; + using key_hash = Hash; + using key_equal = Equal; + using byte = std::byte; + + /** create hash map **/ + DArenaHashMap(size_type hint_max_capacity, + bool debug_flag = false); + DArenaHashMap(Hash && hash = Hash(), + Equal && eq = Equal(), + size_type hint_max_capacity = 0, + bool debug_flag = false); + + /** find smallest x such that 2^x >= n. Return {x, 2^x} **/ + static std::pair lub_exp2(size_t n); + static constexpr size_type group_size() { return c_group_size; } +#ifdef NOT_YET + static size_type min_groups(); + static size_type min_size() { return min_groups() * c_group_size; } +#endif + + size_type empty() const noexcept { return size_ == 0; } + size_type capacity() const noexcept { return n_group_ * c_group_size; } + +#ifdef NOT_YET + // TODO: std::pair + void + insert(std::pair & kv_pair) { + uint64_t h = hash_(kv_pair.first); + } +#endif + + private: + /** group size **/ + static constexpr std::size_t c_group_size = 16; + + /** hash function **/ + key_hash hash_; + /** key equal **/ + key_equal equal_; + /** number of pairs in this table **/ + std::size_t size_ = 0; + /** base-2 logarithm of n_group_ **/ + std::size_t n_group_exponent_ = 0; + /** table has capacity for this number of groups. always an exact power of two. + * number of slots is n_group_ * c_group_size + **/ + std::size_t n_group_ = 1 << n_group_exponent_; + /** control_[] partitioned into groups of c_group_size (16) consecutive elements **/ + DArenaVector control_; + /** slots_[] holds {key,value} pairs **/ + DArenaVector slots_; + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + template + DArenaHashMap::DArenaHashMap(size_type hint_max_capacity, + bool debug_flag) + : DArenaHashMap(Hash(), Equal(), hint_max_capacity, debug_flag) + { + } + + template + DArenaHashMap::DArenaHashMap(Hash && hash, + Equal && eq, + size_type hint_max_capacity, + bool debug_flag) + : hash_{std::move(hash)}, + equal_{std::move(eq)}, + size_{0}, + n_group_exponent_{lub_exp2(hint_max_capacity).first}, + n_group_{lub_exp2(hint_max_capacity).second}, + control_{DArenaVector::map(ArenaConfig{.size_ = n_group_})}, + slots_{DArenaVector::map(ArenaConfig{.size_ = n_group_ * sizeof(value_type)})}, + debug_flag_{debug_flag} + { + } + + template + auto + DArenaHashMap::lub_exp2(size_t n) -> std::pair + + { + size_type ngx = 0; + size_type ng = 1; + + while (ng < n) { + ++ngx; + ng *= 2; + } + + return std::make_pair(ngx, ng);; + } + +#ifdef NOT_YET + template + auto + DArenaHashMap::min_groups() -> size_type + { + size_type page_z = getpagesize(); + + // 1 page of slots + size_type n_slot = page_z / sizeof(value_type); + + // 1 page of groups + size_type n_group = n_slot / c_group_size; + + // glb power of 2, but at least 1 + size_type ng = 1; + + while (2 * ng < n_group) + ng *= 2; + + return ng; + } +#endif + + } +} /*namespace xo*/ + +/* end DArenaHashMap.hpp */ diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index a85f8158..5b2e38f1 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -3,6 +3,8 @@ * @author Roland Conybeare, Jan 2026 **/ +#pragma once + #include "DArena.hpp" #include diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 26b4e3d7..5b45103f 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -7,6 +7,7 @@ set(UTEST_SRCS # objectmodel.test.cpp DArena.test.cpp DArenaVector.test.cpp + DArenaHashMap.test.cpp # DArenaIterator.test.cpp # Collector.test.cpp # DX1CollectorIterator.test.cpp diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp new file mode 100644 index 00000000..675ba987 --- /dev/null +++ b/utest/DArenaHashMap.test.cpp @@ -0,0 +1,26 @@ +/** @file DArenaHashMap.test.cpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#include "DArenaHashMap.hpp" +#include + +namespace xo { + using xo::mm::DArenaHashMap; + //using xo::mM::ArenaConfig; + + namespace ut { + TEST_CASE("DArenaHashMap-ctor", "[arena][DArenaHashMap]") + { + using HashMap = DArenaHashMap; + + HashMap map; + + REQUIRE(map.empty()); + REQUIRE(map.capacity() == HashMap::group_size()); + } + } +} + +/* end DArenaHashMap.test.cpp */ diff --git a/utest/DArenaVector.test.cpp b/utest/DArenaVector.test.cpp index 4366cdf7..963c9513 100644 --- a/utest/DArenaVector.test.cpp +++ b/utest/DArenaVector.test.cpp @@ -3,7 +3,7 @@ * @author Roland Conybeare, Jan 2026 **/ -#include "xo/arena/DArenaVector.hpp" +#include "DArenaVector.hpp" #include namespace xo { From dd39ffc8d9a49f0de3b42b7b50a7085ad2654808 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 12:27:15 -0500 Subject: [PATCH 032/111] xo-array: + DArenaHashMap.insert --- include/xo/arena/DArenaHashMap.hpp | 275 +++++++++++++++++++++++------ utest/DArenaHashMap.test.cpp | 18 +- 2 files changed, 242 insertions(+), 51 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index bc454e19..f021236a 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -10,6 +10,101 @@ namespace xo { namespace mm { + struct DArenaHashMapUtil { + using size_type = std::size_t; + using control_type = std::uint8_t; + + /** control: sentinel for empty slot **/ + static constexpr uint8_t c_empty_slot = 0xFF; + /** control: tombstone for deleted slot **/ + static constexpr uint8_t c_tombstone = 0xFE; + + /** group size **/ + static constexpr size_type c_group_size = 16; + + /** find smallest multiple k : k * c_group_size >= n **/ + static size_type lub_group_mult(size_t n) { + return (n + c_group_size - 1) / c_group_size; + } + + /** find smallest x such that 2^x >= n. Return {x, 2^x} **/ + static std::pair lub_exp2(size_t n) { + size_type ngx = 0; + size_type ng = 1; + + while (ng < n) { + ++ngx; + ng *= 2; + } + + return std::make_pair(ngx, ng);; + } + }; + + namespace detail { + /** @brief 16x 8-bit control bytes. + * + * Support optimization using SIMD operations + **/ + struct Group { + std::array ctrl_; + + explicit Group(uint8_t * lo) { + std::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); + } + + /** find all exact matches in ctrl_[0..15] for @p h2. + * for each match set corresponding bit in return value. + * Bits {0x1, 0x2, 0x4, ...} set iff exact match on + * {ctrl_[0], ctrl_[1], ctrl_2[], ...} respectively + **/ + uint16_t all_matches(uint8_t h2) const { + uint16_t retval = 0; + uint16_t bit = 1; + for (auto xi : ctrl_) { + if (xi == h2) + retval |= bit; + bit = bit << 1; + } + + return retval; + } + + /** find all empty sentinels in ctrl_[0..15]. + * for each empty, set corresponding bit in return value. + * Bits {0x1, 0x2, 0x4, ...} set iff empty spot + * {ctrl_[0], ctrl_[1], ctrl_[2], ...} respectively + **/ + uint16_t empty_matches() const { + uint16_t retval = 0; + uint16_t bit = 1; + for (auto xi : ctrl_) { + if (xi == DArenaHashMapUtil::c_empty_slot) + retval |= bit; + bit = bit << 1; + } + + return retval; + } + +#ifdef NOT_YET + __m128i ctrl; // 16 bytes loaded via SSE2 + + // Find all slots matching h2 + uint16_t Match(uint8_t h2) const { + __m128i pattern = _mm_set1_epi8(h2); + __m128i result = _mm_cmpeq_epi8(ctrl, pattern); + return _mm_movemask_epi8(result); // 16-bit mask + } + + // Find all empty slots (0xFF) + uint16_t MatchEmpty() const { + return _mm_movemask_epi8(_mm_cmpeq_epi8(ctrl, _mm_set1_epi8(0xFF))); + } +#endif + }; + } + /** @brief flat hash map of key-value pairs using dedicated DArenas for storage * * Replicates (to the extent feasible) std::unordered_map @@ -21,15 +116,16 @@ namespace xo { typename Value, typename Hash = std::hash, typename Equal = std::equal_to> - struct DArenaHashMap { + struct DArenaHashMap : DArenaHashMapUtil { public: - using size_type = std::size_t; + using size_type = DArenaHashMapUtil::size_type; using key_type = Key; using mapped_type = Value; using value_type = std::pair; using key_hash = Hash; using key_equal = Equal; using byte = std::byte; + using group_type = detail::Group; /** create hash map **/ DArenaHashMap(size_type hint_max_capacity, @@ -39,29 +135,31 @@ namespace xo { size_type hint_max_capacity = 0, bool debug_flag = false); - /** find smallest x such that 2^x >= n. Return {x, 2^x} **/ - static std::pair lub_exp2(size_t n); - static constexpr size_type group_size() { return c_group_size; } -#ifdef NOT_YET - static size_type min_groups(); - static size_type min_size() { return min_groups() * c_group_size; } -#endif - size_type empty() const noexcept { return size_ == 0; } + size_type groups() const noexcept { return n_group_; } + size_type size() const noexcept { return size_; } size_type capacity() const noexcept { return n_group_ * c_group_size; } -#ifdef NOT_YET - // TODO: std::pair - void - insert(std::pair & kv_pair) { - uint64_t h = hash_(kv_pair.first); - } -#endif + /** insert @p kv_pair into hash map. replaces any previous value + * stored under the same key. + * + * Return true if size incremented; false if value updated + * for existing key + **/ + bool insert(const std::pair & kv_pair); private: - /** group size **/ - static constexpr std::size_t c_group_size = 16; + /** load group abstraction from control bytes starting at @p ix **/ + group_type _load_group(size_type ix) const { + return group_type(&control_[ix]); + } + /** like ctrl_[ix] = h2, but maintain overflow copy + * at end of ctrl_[] array + **/ + void _update_control(size_type ix, uint8_t h2); + + private: /** hash function **/ key_hash hash_; /** key equal **/ @@ -74,8 +172,11 @@ namespace xo { * number of slots is n_group_ * c_group_size **/ std::size_t n_group_ = 1 << n_group_exponent_; - /** control_[] partitioned into groups of c_group_size (16) consecutive elements **/ - DArenaVector control_; + /** table has capacity for this number of {key,value} pairs **/ + std::size_t n_slot_ = n_group_ * c_group_size; + /** control_[] partitioned into groups of c_group_size (16) consecutive elements + **/ + DArenaVector control_; /** slots_[] holds {key,value} pairs **/ DArenaVector slots_; /** true to enable debug logging **/ @@ -89,6 +190,10 @@ namespace xo { { } + /* remarks: + * - control: extra 16 slots for safe wraparound. + * last 16 bytes will be copy of first 16 bytes + */ template DArenaHashMap::DArenaHashMap(Hash && hash, Equal && eq, @@ -97,53 +202,123 @@ namespace xo { : hash_{std::move(hash)}, equal_{std::move(eq)}, size_{0}, - n_group_exponent_{lub_exp2(hint_max_capacity).first}, - n_group_{lub_exp2(hint_max_capacity).second}, - control_{DArenaVector::map(ArenaConfig{.size_ = n_group_})}, - slots_{DArenaVector::map(ArenaConfig{.size_ = n_group_ * sizeof(value_type)})}, + n_group_exponent_{lub_exp2(lub_group_mult(hint_max_capacity)).first}, + n_group_{lub_exp2(lub_group_mult(hint_max_capacity)).second}, + n_slot_{n_group_ * c_group_size}, + control_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ + c_group_size})}, + slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})}, debug_flag_{debug_flag} { } + template - auto - DArenaHashMap::lub_exp2(size_t n) -> std::pair - + void + DArenaHashMap::_update_control(size_type ix, uint8_t h2) { - size_type ngx = 0; - size_type ng = 1; + this->control_[ix] = h2; - while (ng < n) { - ++ngx; - ng *= 2; + if (ix < c_group_size) { + size_type N = this->capacity(); + + // refresh end-of-array copy + std::memcpy(&(control_[N]), &(control_[0]), c_group_size); } - - return std::make_pair(ngx, ng);; } -#ifdef NOT_YET template - auto - DArenaHashMap::min_groups() -> size_type + bool + DArenaHashMap::insert(const std::pair & kv_pair) { - size_type page_z = getpagesize(); + size_type h = hash_(kv_pair.first); + // h1: hi bits: probe sequence + size_type h1 = h >> 7; + // h2: lo bits: store in control byte + uint8_t h2 = h & 0x7f; - // 1 page of slots - size_type n_slot = page_z / sizeof(value_type); + size_type N = this->capacity(); - // 1 page of groups - size_type n_group = n_slot / c_group_size; + // same as: + // ix = h1 % N + // since N is power of 2 + size_type ix = h1 & (N - 1); - // glb power of 2, but at least 1 - size_type ng = 1; + // will make series of probes + for (;;) { + auto grp = _load_group(ix); - while (2 * ng < n_group) - ng *= 2; + { + // look for matching slot to update + uint16_t m = grp.all_matches(h2); - return ng; + // process each match. + // matches are encountered in the same order they + // appear in ctrl_[] + while (m) { + // zeroes: #of 0 before least-significant 1 bit + int skip = __builtin_ctz(m); + size_type slot_ix = (ix + skip) & (N - 1); + + // invariant: slot_ix in [0 .. N) + + auto & slot = slots_[slot_ix]; + + if (slot.first == kv_pair.first) { + // we have match on existing key; + // replace associated value + slot.second = kv_pair.second; + + // false: did not change table size + return false; + } + + // e.g: + // /-- lowest 1 bit gets cleared + // v + // m = b01101000 + // m-1 = b01100111 + // & = b01100000 + + m &= (m - 1); + } + } + + { + // look for empty slot to insert + uint16_t e = grp.empty_matches(); + + // process each empty slot + if (e) { + // zeroes: #of 0 before least significant 1 bit + int skip = __builtin_ctz(e); + size_type slot_ix = (ix + skip) & (N - 1); + + // invariant: slot_ix in [0 .. N) + + auto & slot = slots_[slot_ix]; + + // mark slot occupied in control space; + // maintain copy-at-end for overflow + this->update_control(slot_ix, h2); + new (&slot) value_type(kv_pair); + + ++(this->size_); + + // true: increased table size + return true; + } + } + + // slot range associated with grp + // has no room, and does not contain target key + // -> move on to next group. + // + // note: relying on c_group_size overflow bytes here + // when ix is close to N + + ix = (ix + c_group_size) & (N - 1); + } } -#endif - } } /*namespace xo*/ diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 675ba987..c7ebc4e9 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -7,6 +7,7 @@ #include namespace xo { + using xo::mm::DArenaHashMapUtil; using xo::mm::DArenaHashMap; //using xo::mM::ArenaConfig; @@ -18,7 +19,22 @@ namespace xo { HashMap map; REQUIRE(map.empty()); - REQUIRE(map.capacity() == HashMap::group_size()); + REQUIRE(map.size() == 0); + REQUIRE(map.groups() == 1); + REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); + } + + TEST_CASE("DArenaHashMap-ctor2", "[arena][DArenaHashMap]") + { + using HashMap = DArenaHashMap; + + HashMap map(257); + + REQUIRE(map.empty()); + REQUIRE(map.size() == 0); + REQUIRE(map.capacity() == map.groups() * DArenaHashMapUtil::c_group_size); + REQUIRE(map.capacity() == std::max(512ul, + DArenaHashMapUtil::c_group_size)); } } } From aff5a252a0fb398025994a96ac605694a6e15d16 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 16:52:50 -0500 Subject: [PATCH 033/111] xo-arena: DArenaHashMap: generative test + check load factor --- include/xo/arena/DArenaHashMap.hpp | 99 ++++- include/xo/arena/DArenaVector.hpp | 4 +- utest/CMakeLists.txt | 2 +- utest/DArenaHashMap.test.cpp | 101 ++++- utest/random_hash_ops.hpp | 631 +++++++++++++++++++++++++++++ 5 files changed, 817 insertions(+), 20 deletions(-) create mode 100644 utest/random_hash_ops.hpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index f021236a..b1659e67 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -6,10 +6,22 @@ #pragma once #include "DArenaVector.hpp" +#include +#include #include +#include namespace xo { namespace mm { +#ifdef NOT_YET + enum class insert_error : int32_t { + /** sentinel **/ + invalid = -1, + /** not an error **/ + ok, + }; +#endif + struct DArenaHashMapUtil { using size_type = std::size_t; using control_type = std::uint8_t; @@ -22,6 +34,9 @@ namespace xo { /** group size **/ static constexpr size_type c_group_size = 16; + /** max load factor **/ + static constexpr float c_max_load_factor = 0.875; + /** find smallest multiple k : k * c_group_size >= n **/ static size_type lub_group_mult(size_t n) { return (n + c_group_size - 1) / c_group_size; @@ -50,7 +65,7 @@ namespace xo { std::array ctrl_; explicit Group(uint8_t * lo) { - std::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); + ::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); } /** find all exact matches in ctrl_[0..15] for @p h2. @@ -140,18 +155,26 @@ namespace xo { size_type size() const noexcept { return size_; } size_type capacity() const noexcept { return n_group_ * c_group_size; } + float load_factor() const noexcept { return size_ / static_cast(n_slot_); } + /** insert @p kv_pair into hash map. replaces any previous value * stored under the same key. * - * Return true if size incremented; false if value updated - * for existing key + * Return pair retval with: + * reval.first: true if size incremented; + * retval.second: address of slots_[p] at which pair inserted/updated + * + * When table is full retval.second will be nullptr, + * with error captured in last_error_ **/ - bool insert(const std::pair & kv_pair); + std::pair try_insert(const std::pair & kv_pair); + + bool verify_ok(bool /*throw_flag_not_implemented*/ = true) const; private: /** load group abstraction from control bytes starting at @p ix **/ - group_type _load_group(size_type ix) const { - return group_type(&control_[ix]); + group_type _load_group(size_type ix) { + return group_type(&(control_[ix])); } /** like ctrl_[ix] = h2, but maintain overflow copy @@ -165,15 +188,15 @@ namespace xo { /** key equal **/ key_equal equal_; /** number of pairs in this table **/ - std::size_t size_ = 0; + size_type size_ = 0; /** base-2 logarithm of n_group_ **/ - std::size_t n_group_exponent_ = 0; + size_type n_group_exponent_ = 0; /** table has capacity for this number of groups. always an exact power of two. * number of slots is n_group_ * c_group_size **/ - std::size_t n_group_ = 1 << n_group_exponent_; + size_type n_group_ = (1 << n_group_exponent_); /** table has capacity for this number of {key,value} pairs **/ - std::size_t n_slot_ = n_group_ * c_group_size; + size_type n_slot_ = n_group_ * c_group_size; /** control_[] partitioned into groups of c_group_size (16) consecutive elements **/ DArenaVector control_; @@ -209,8 +232,14 @@ namespace xo { slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})}, debug_flag_{debug_flag} { - } + /* invariant: arenas have allocated address range, but no committed memory yet */ + this->control_.resize(n_slot_ + c_group_size); + /* all slots marked empty initially */ + std::fill(this->control_.begin(), this->control_.end(), c_empty_slot); + + this->slots_.resize(n_slot_); + } template void @@ -227,8 +256,8 @@ namespace xo { } template - bool - DArenaHashMap::insert(const std::pair & kv_pair) + auto + DArenaHashMap::try_insert(const std::pair & kv_pair) -> std::pair { size_type h = hash_(kv_pair.first); // h1: hi bits: probe sequence @@ -269,7 +298,7 @@ namespace xo { slot.second = kv_pair.second; // false: did not change table size - return false; + return std::make_pair(&slot, false); } // e.g: @@ -289,6 +318,14 @@ namespace xo { // process each empty slot if (e) { + // check that table is below max load factor (0.875). + // Check here so that table can stay at max load factor + // indefinitely as long as updates only + // + if (load_factor() >= c_max_load_factor) { + return std::make_pair(nullptr, false); + } + // zeroes: #of 0 before least significant 1 bit int skip = __builtin_ctz(e); size_type slot_ix = (ix + skip) & (N - 1); @@ -299,13 +336,13 @@ namespace xo { // mark slot occupied in control space; // maintain copy-at-end for overflow - this->update_control(slot_ix, h2); + this->_update_control(slot_ix, h2); new (&slot) value_type(kv_pair); ++(this->size_); // true: increased table size - return true; + return std::make_pair(&slot, true); } } @@ -319,6 +356,36 @@ namespace xo { ix = (ix + c_group_size) & (N - 1); } } + + /** + * Verify DArenaHashMap class invariants. + * + * SM1. size consistency + * - SM1.1 size_ <= n_slot_ + * - SM1.2 control_[] size consistent with slots_[] size + * - SM1.3 n_group_ consistent with n_group_exponent_ + * - SM1.4 n_slot_ consistent with n_group_ + * - SM1.5 n_slot_ a power of 2 + * SM2. load factor + * - SM2.1 load_factor() <= c_max_load_factor + * SM3. control_ + * - SM3.1 control_[N+i] = control_[i] for i in [0, c_group_size) + * - SM3.2 {number of control_[i] spots with non-sentinel values} = size_ + * SM4. slots_ + * - SM4.1 if control_[i] is non-sentinel: + * - SM4.1.1 control_[i] = hash_(slots_[i].first) & 0x7f + * - SM4.1.2 all slots in range [h .. i] are non-empty, + * where h is hash_(slots_[i].first >> 7 + * - SM4.2 if control_[i] is empty or tombstone: + * - slots_[i].first = key_type() + * + **/ + template + bool + DArenaHashMap::verify_ok(bool /*throw_flag_not_implemented*/) const + { + return true; + } } } /*namespace xo*/ diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index 5b2e38f1..d1d66343 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -7,6 +7,7 @@ #include "DArena.hpp" #include +#include // for ::memset() namespace xo { namespace mm { @@ -153,8 +154,7 @@ namespace xo { // run ctors if constexpr (std::is_trivially_constructible_v) { - // nothing to do - ; + ::memset(this->_address_of(size_), 0, req_z - (size_ * sizeof(T))); } else { for (size_type i = size_; i < z; ++i) { void * addr = &(*this)[i]; diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 5b45103f..c45a9710 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -17,7 +17,7 @@ set(UTEST_SRCS if (ENABLE_TESTING) xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) xo_self_dependency(${UTEST_EXE} xo_arena) -# xo_headeronly_dependency(${UTEST_EXE} randomgen) + xo_headeronly_dependency(${UTEST_EXE} randomgen) # xo_headeronly_dependency(${UTEST_EXE} indentlog) xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) endif() diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index c7ebc4e9..3f4a24fd 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -4,12 +4,19 @@ **/ #include "DArenaHashMap.hpp" +#include "random_hash_ops.hpp" +#include +#include +#include #include namespace xo { using xo::mm::DArenaHashMapUtil; using xo::mm::DArenaHashMap; - //using xo::mM::ArenaConfig; + using xo::rng::random_seed; + using xo::rng::xoshiro256ss; + using utest::UtestTools; + using utest::HashMapUtil; namespace ut { TEST_CASE("DArenaHashMap-ctor", "[arena][DArenaHashMap]") @@ -36,6 +43,98 @@ namespace xo { REQUIRE(map.capacity() == std::max(512ul, DArenaHashMapUtil::c_group_size)); } + + TEST_CASE("DArenaHashMap-try-insert", "[arena][DArenaHashMap]") + { + using HashMap = DArenaHashMap; + + HashMap map; + + REQUIRE(map.empty()); + REQUIRE(map.size() == 0); + REQUIRE(map.groups() == 1); + REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); + + { + auto x = map.try_insert(std::make_pair(1, 11)); + + REQUIRE(x.first); + REQUIRE(x.second); + REQUIRE(!map.empty()); + REQUIRE(map.size() == 1); + REQUIRE(map.groups() == 1); + REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); + REQUIRE(map.load_factor() == 1/16.0); + } + + { + auto x = map.try_insert(std::make_pair(2, 9)); + + REQUIRE(x.first); + REQUIRE(x.second); + REQUIRE(!map.empty()); + REQUIRE(map.size() == 2); + REQUIRE(map.groups() == 1); + REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); + REQUIRE(map.load_factor() == 2/16.0); + } + + { + auto x = map.try_insert(std::make_pair(259, 12)); + + REQUIRE(x.first); + REQUIRE(x.second); + REQUIRE(!map.empty()); + REQUIRE(map.size() == 3); + REQUIRE(map.groups() == 1); + REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); + REQUIRE(map.load_factor() == 3/16.0); + } + } + + TEST_CASE("DArenaHashMap-try-insert2", "[arena][DArenaHashMap]") + { + using HashMap = DArenaHashMap; + + std::uint64_t seed = 17747889312058974961ul; + //random_seed(&seed); // to get new random seed + //log && log(xtag("seed", seed)); + + auto rgen = xoshiro256ss(seed); + + /* 1. Perform series of tests with increasing scale + * 2. Each test may run in two modes: + * a. silent fast fail. just report success. + * In this mode avoid catch2 REQUIRE + * b. noisy. run with logging enabled + * This mode automatically invoked when silent mode + * observes test failure + */ + + for (std::uint32_t n = 0; n <= 2; ) { + HashMap hash_map; + + auto test_fn = [&rgen, &hash_map](bool dbg_flag, + std::uint32_t n) + { + bool ok_flag = true; + + ok_flag &= HashMapUtil::random_inserts(n, dbg_flag, &rgen, &hash_map); + + return ok_flag; + }; + + bool ok_flag = UtestTools::bimodal_test("DArenaHashMap-try-insert2", test_fn, n); + + if (n == 0) + n = 1; + else + n = 2*n; + } + } + + // TODO: + // - let's try getting lcov to work in xo-umbrella2 } } diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp new file mode 100644 index 00000000..6216a457 --- /dev/null +++ b/utest/random_hash_ops.hpp @@ -0,0 +1,631 @@ +/* @file random_hash_ops.hpp **/ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace utest { + struct Util { + /* generate vector with integers [0.. n-1] */ + static std::vector vector_upto(std::uint32_t n) { + std::vector u(n); + for (std::uint32_t i = 0; i < n; ++i) + u[i] = i; + + return u; + } /*vector_upto*/ + + static std::map + map_upto(std::uint32_t n) + { + std::map m; + for(std::uint32_t i=0; i + random_permutation(uint32_t n, xo::rng::xoshiro256ss *p_rgen) { + /* vector [0 .. n-1] */ + std::vector u = vector_upto(n); + + /* shuffle to get unpredictable permutation */ + std::shuffle(u.begin(), u.end(), *p_rgen); + + return u; + } /*random_permutation*/ + }; /*Util*/ + + // TODO: move REQUIRE_OR_CAPTURE(), REQUIRE_ORFAIL() to new subsystem xo-utestutil + +/* note: trivial REQUIRE() call in else branch bc we still want + * catch2 to count assertions when verification succeeds + */ +# define REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr) \ + if (catch_flag) { \ + REQUIRE((expr)); \ + } else { \ + REQUIRE(true); \ + ok_flag &= (expr); \ + } + +# define REQUIRE_ORFAIL(ok_flag, catch_flag, expr) \ + REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr); \ + if (!ok_flag) \ + return ok_flag + + /** UtestTools + **/ + struct UtestTools { + /** bimodal may run twice: + * - first mode is silent, only determines success or failure. + * - second mode skipped when first mode succeeds. + * when first mode fails, second mode runs noisily with debug logging enabled + * + * goal is to get detailed information from failing test; + * more detailed than feasible from catch2 INFO() + * + * test function should use REQUIRE_ORCAPTURE() / REQUIRE_ORFAIL(). + * It should *not* use REQUIRE() or CHECK(). + **/ + static inline bool bimodal_test(std::string test_name, + std::function test_fn, + std::uint32_t n) + { + bool ok_flag = false; + + for (std::uint32_t attention = 0; !ok_flag && (attention < 2); ++attention) { + bool debug_flag = (attention == 1); + + xo::scope log(XO_DEBUG2(debug_flag, test_name)); + + ok_flag = test_fn(debug_flag, n); + } + + return ok_flag; + } + }; + + /* compare xo-ordinaltree/utest/random_tree_ops.hpp */ + template + struct HashMapUtil : public Util { +#ifdef NOT_YET + static bool + test_clear(bool catch_flag, + Tree * p_tree) + { + bool ok_flag = true; + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag)); + + p_tree->clear(); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag)); + REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->empty()); + REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->size() == 0); + + return ok_flag; + } /*test_clear*/ +#endif + + static bool + random_inserts(const std::vector & keys, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen, + HashMap * p_map) + { + using xo::xtag; + + bool ok_flag = true; + + xo::scope log(XO_DEBUG(catch_flag), xtag("n-keys", keys.size())); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + + /* n keys */ + std::size_t n = keys.size(); + /* permute keys, remembering original position */ + std::vector> permuted_keys(n); + { + uint32_t i = 0; + for (const auto & x : keys) { + permuted_keys[i] = std::make_pair(i, x); + } + } + /* shuffle to get unpredictable insert order */ + std::shuffle(keys.begin(), keys.end(), *p_rgen); + + size_t tree_z0 = p_map->size(); + + /* insert keys in permuted order */ + { + uint32_t i = 1; + for(const auto & pr_i : permuted_keys) { + log && log(xtag("i", i), xtag("ord", pr_i.first), xtag("n", n), xtag("key", pr_i.second)); + + /* .first: iterator @ insert position + * .second: true if insert occurred (ịẹ tree size incremented) + */ + auto insert_result = p_map->insert(typename HashMap::value_type(pr_i.second, 10.0 * i)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second); + + /* verify: iterator returned by Treẹinsert(), refers to inserted key,value pair */ + log && log(xtag("iter.node", insert_result.first.node())); + + REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->first == pr_i.second); + REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->second == 10.0 * i); + + ++i; + } + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == tree_z0 + n); + + return ok_flag; + } + + /* do + * n = (hi - lo) / k + * random inserts (taken from *p_rgen) into *p_rbtreẹ + * inserted keys will comprise the distinct values + * {lo, lo+k, lo+2k, ..., lo+n.k} + */ + static bool + random_inserts(std::uint32_t lo, + std::uint32_t hi, + std::uint32_t k, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen, + HashMap * p_map) + { + // TODO: rewrite in terms of 'random_inserts with explicit vector'. + + using xo::xtag; + + bool ok_flag = true; + + xo::scope log(XO_DEBUG(catch_flag), xtag("lo", lo), xtag("hi", hi), xtag("k", k)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + + if ((hi <= lo) || (k == 0)) + return true; + + uint32_t n = (hi - lo) / k; + + /* n keys 0..n-1 */ + std::vector u(n); + for(std::uint32_t i=0; isize(); + + /* insert keys according to permutation u */ + uint32_t i = 1; + for(uint32_t x : u) { + log && log(xtag("i", i), xtag("n", n), xtag("key", x)); + /* .first: iterator @ insert position + * .second: true if insert occurred (ịẹ tree size incremented) + */ + auto insert_result = p_map->try_insert(typename HashMap::value_type(x, 10 * x)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second); + + /* verify: iterator returned by Treẹinsert(), refers to inserted key,value pair */ + //log && log(xtag("iter.node", insert_result.first.node())); + REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->first == x); + REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->second == 10 * x); + + ++i; + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == tree_z0 + n); + + return ok_flag; + } /*random_inserts*/ + + static bool + random_inserts(std::uint32_t n, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen, + HashMap * p_map) + { + return random_inserts(0, n, 1, catch_flag, p_rgen, p_map); + } + +#ifdef NOT_YET + /* do n random removes (taken from *p_rgen) from *p_rbtree; + * assumes *p_rbtree has keys [0 .. n-1] where n=p_rbtreẹsize + */ + static bool + random_removes(bool catch_flag, // dbg_flag + xo::rng::xoshiro256ss * p_rgen, + Tree * p_map) + { + using xo::scope; + using xo::xtag; + + bool ok_flag = true; + + xo::scope log(XO_DEBUG(catch_flag)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + + uint32_t n = p_map->size(); + + /* random permutation of keys in *p_map */ + std::vector u + = random_permutation(n, p_rgen); + + log && log(xtag("remove-order", u)); + + /* will keep track of which keys remain as we move them */ + std::map m = Util::map_upto(n); + + /* remove keys in permutation order */ + std::uint32_t i = 1; + for (std::uint32_t x : u) { + log && log("iter i: removing key from n-node tree", + xtag("i", i), xtag("key", x), xtag("n", n)); + + /* remove x from tracking map m also */ + m.erase(x); + + log && log("remove key :iter ", i, "/", n, xtag("key", x)); + + p_map->erase(x); + // rbtreẹdisplay(); + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == n-i); + /* amongst other things, this guarantees that keys in *p_map + * appear in increasing order + */ + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + +#ifdef NOT_YET + /* 1. rbtree should now contain all the keys in [0..n-1], + * with u[0]..u[i-1] excluded; this is the same as the + * contents of m. + */ + auto m_ix = m.begin(); + auto m_end_ix = m.end(); + auto visitor_fn = + ([&m_ix, m_end_ix] + (std::pair const & contents) + { + REQUIRE(m_ix != m_end_ix); + REQUIRE(contents.first == m_ix->second); + ++m_ix; + }); + p_map->visit_inorder(visitor_fn); +#endif + ++i; + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, m.empty()); + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == 0); + + log.end_scope(); + + return ok_flag; + } /*random_removes*/ +#endif + +#ifdef NOT_YET + /* Require: + * - tree has keys [0..n-1], where n=treẹsize() + * - for each key k, associated value is 10*k + */ + static bool + random_lookups(bool catch_flag, + Tree const & tree, + xo::rng::xoshiro256ss * p_rgen) + { + using xo::scope; + using xo::xtag; + + xo::scope log(XO_DEBUG(catch_flag)); + + /* -> false if/when verification fails */ + bool ok_flag = true; + + REQUIRE_ORFAIL(ok_flag, catch_flag, tree.verify_ok(catch_flag)); + + size_t n = tree.size(); + std::vector u + = random_permutation(n, p_rgen); + + /* lookup keys in permutation order */ + std::uint32_t i = 1; + for (std::uint32_t x : u) { + INFO(tostr(xtag("i", i), xtag("n", n), xtag("x", x))); + + REQUIRE_ORFAIL(ok_flag, catch_flag, tree[x] == x*10); + REQUIRE_ORFAIL(ok_flag, catch_flag, tree.verify_ok(catch_flag)); + REQUIRE_ORFAIL(ok_flag, catch_flag, tree.size() == n); + + /* also test treẹfind() */ + auto find_ix = tree.find(x); + + REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix != tree.end()); + REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->first == x); + REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->second == x*10); + + ++i; + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, tree.size() == n); + + log.end_scope(); + + return ok_flag; + } /*random_lookups*/ +#endif + +#ifdef NOT_YET + /* Require: + * - tree has keys [0..n-1], where n=treẹsize() + * - tree values at key k is dvalue+10*k + * + * catch_flag. true -> log to console + interact with catch2 + * false -> verify iteration behavior for return code + */ + static bool + check_bidirectional_iterator(uint32_t dvalue, + bool catch_flag, + Tree const & tree) + { + using xo::scope; + using xo::xtag; + + /* -> false if/when verification fails */ + bool ok_flag = true; + + std::size_t const n = tree.size(); + + xo::scope log(XO_DEBUG(catch_flag)); + + log && log("tree with size n", xtag("n", n)); + + { + std::size_t i = 0; + + auto end_ix = tree.end(); + + log && log(xtag("end_ix", end_ix)); + + auto begin_ix = tree.begin(); + auto ix = begin_ix; + + int last_key = -1; + + while (ix != end_ix) { + log && log("forward loop top", + xtag("i", i), + xtag("ix", ix)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first == i); + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10*i); + if(i > 0) { + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first > last_key); + } + last_key = ix->first; + ++i; + ++ix; + + log && log("forward loop bottom", + xtag("last_key", last_key), + xtag("next ix", ix)); + } + + /* should have visited exactly n locations */ + REQUIRE_ORFAIL(ok_flag, catch_flag, i == n); + REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix); + + log && log(xtag("ix", ix), xtag("begin_ix", begin_ix)); + + /* now run iterator backwards, + * starting from "one past the end" + */ + if(ix != begin_ix) { + do { + --i; + --ix; + + log && log("forward backup", + xtag("i", i), + xtag("ix", ix)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, ix.is_dereferenceable()); + + log && log(xtag("ix.first", (*ix).first)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, (*ix).first == i); + } while (ix != begin_ix); + } + + /* should have visited exactly n locations in reverse */ + REQUIRE_ORFAIL(ok_flag, catch_flag, i == 0); + } + + /* ----- reverse iterators ----- */ + + { + std::int64_t i = n - 1; + + auto rbegin_ix = tree.rbegin(); + auto rend_ix = tree.rend(); + + auto rix = rbegin_ix; + + int last_key = -1; + + while (rix != rend_ix) { + log && log("reverse loop top", + xtag("i", i), + xtag("rix", rix)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, rix->first == i); + REQUIRE_ORFAIL(ok_flag, catch_flag, rix->second == dvalue + 10*i); + if (i < n-1) { + REQUIRE_ORFAIL(ok_flag, catch_flag, rix->first < last_key); + } + last_key = rix->first; + --i; + ++rix; + + log && log("reverse loop bottom", + xtag("last_key", last_key), + xtag("next ix", rix)); + } + + /* should have visited exactly n locations */ + REQUIRE_ORFAIL(ok_flag, catch_flag, i == -1); + + log && log(xtag("rbegin_ix", rbegin_ix)); + + /* now run reverse iterator backwrds, + * starting from "one before the beginning" + */ + if (rix != rbegin_ix) { + do { + ++i; + --rix; + + log && log("reverse backup", + xtag("i", i), + xtag("rix", rix), + xtag("rix.first", rix->first)); + + REQUIRE_ORFAIL(ok_flag, catch_flag, (*rix).first == i); + } while (rix != rbegin_ix); + } + + /* should have visited exactly n locations in reversê2 */ + REQUIRE_ORFAIL(ok_flag, catch_flag, i == n - 1); + } + + log.end_scope(); + + return ok_flag; + } /*check_bidirectional_iterator*/ +#endif + +#ifdef NOT_YET + /** Require: + * - tree has keys [0..n-1], where n=treesize() + * - tree valu at key k is dvalue+10*k + * + * @p catch_flag. control behavior at each test assertion. + * true -> log to console + interact with catch2 + * false -> verify iteration behavior for return code. + * + **/ + static bool + check_reduced_sum(uint32_t dvalue, + bool catch_flag, + Tree const & rbtree) + { + using xo::scope; + using xo::xtag; + + scope log(XO_DEBUG(catch_flag)); + + /* -> false if/when check fails */ + bool ok_flag = true; + + size_t const n = rbtree.size(); + + for(size_t i = 0; i < n; ++i) { + /* compute reduction up to key=i */ + double reduced_upto + = rbtree.reduce_lub(i /*key*/, + true /*is_closed*/); + + double reduced = (i+1) * (5*i + dvalue); + + INFO(tostr(xtag("i", i), xtag("n", n), + xtag("tree.reduced_upto", reduced_upto), + xtag("reduced", reduced), + xtag("dvalue", dvalue))); + + auto glb_ix = rbtree.cfind_sum_glb(reduced); + + REQUIRE_ORFAIL(ok_flag, catch_flag, reduced_upto == reduced); + REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix.is_dereferenceable()); + /* glb_ix is truth-y */ + REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix); + + REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix->first == i); + } + + return ok_flag; + } /*check_reduced_sum*/ +#endif + +#ifdef NOT_YET + /* Require: + * - *p_rbtree has keys [0..n-1], where n=rbtree.size() + * - for each key k, associated value is 10*k + * + * Promise: + * - for each key k, associated value is dvalue + 10*k + */ + static bool + random_updates(uint32_t dvalue, + bool catch_flag, + Tree * p_rbtree, + xo::rng::xoshiro256ss * p_rgen) + { + using xo::scope; + using xo::xtag; + + scope log(XO_DEBUG(catch_flag)); + + /* -> false if/when check fails */ + bool ok_flag = true; + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok()); + + std::size_t n = p_rbtree->size(); + std::vector u + = Util::random_permutation(n, p_rgen); + + /* update key/value pairs in permutation order */ + uint32_t i = 1; + for (uint32_t x : u) { + REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == x*10); + + (*p_rbtree)[x] = dvalue + 10*x; + + REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == dvalue + 10*x); + REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok()); + /* assignment to existing key does not change tree size */ + REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->size() == n); + ++i; + } + + REQUIRE(p_rbtree->size() == n); + + return ok_flag; + } /*random_updates*/ +#endif + }; /*TreeUtil*/ +} /*namespace utest*/ + +/* end random_tree_ops.hpp */ From 1252196ab43a04cd909f4af264b3e20224c03ed4 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 17:56:38 -0500 Subject: [PATCH 034/111] xo-indentlog xo-arena: improve verify_ok logging workflow + scope.retroactively_enable() --- include/xo/arena/DArenaHashMap.hpp | 57 ++++++++++++++++++++++++++++-- utest/random_hash_ops.hpp | 6 ++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index b1659e67..960c3a2d 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -6,12 +6,50 @@ #pragma once #include "DArenaVector.hpp" +#include #include #include #include #include namespace xo { + struct verify_policy { + static verify_policy log_only() { + return verify_policy{.flags_ = 0x01}; + } + static verify_policy throw_only() { + return verify_policy{.flags_ = 0x02}; + } + static verify_policy chatty() { + return verify_policy{.flags_ = 0x03}; + } + + bool is_silent() const noexcept { return flags_ == 0; } + bool log_flag() const noexcept { return flags_ & 0x01; } + bool throw_flag() const noexcept { return flags_ & 0x02; } + + template + bool report_error(scope & log, Tn&&... args) + { + if (!this->is_silent()) { + // TODO: consider global arena here for string + std::string msg = tostr(std::forward(args)...); + + if (this->log_flag()) { + log.retroactively_enable(); + log(msg); + } + if (this->throw_flag()) { + throw std::runtime_error(msg); + } + } + return false; + } + + const char * c_self_ = "anonymous"; + uint8_t flags_; + }; + namespace mm { #ifdef NOT_YET enum class insert_error : int32_t { @@ -169,7 +207,7 @@ namespace xo { **/ std::pair try_insert(const std::pair & kv_pair); - bool verify_ok(bool /*throw_flag_not_implemented*/ = true) const; + bool verify_ok(verify_policy p = verify_policy::throw_only()) const; private: /** load group abstraction from control bytes starting at @p ix **/ @@ -382,8 +420,23 @@ namespace xo { **/ template bool - DArenaHashMap::verify_ok(bool /*throw_flag_not_implemented*/) const + DArenaHashMap::verify_ok(verify_policy policy) const { + using xo::scope; + using xo::tostr; + using xo::xtag; + + constexpr const char * c_self = "DArenaHashMap::verify_ok"; + scope log(XO_DEBUG(debug_flag_), xtag("size", size_)); + + /* SM1.1: size_ <= n_slot_ */ + if (size_ > n_slot_) { + return policy.report_error(log, + c_self, ": expect .size < .n_slot", + xtag("size", size_), + xtag("n_slot", n_slot_)); + } + return true; } } diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index 6216a457..548f72c8 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -197,7 +197,9 @@ namespace utest { xo::scope log(XO_DEBUG(catch_flag), xtag("lo", lo), xtag("hi", hi), xtag("k", k)); - REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + auto policy = xo::verify_policy::chatty(); + + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(policy)); if ((hi <= lo) || (k == 0)) return true; @@ -223,7 +225,7 @@ namespace utest { */ auto insert_result = p_map->try_insert(typename HashMap::value_type(x, 10 * x)); - REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag)); + REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(policy)); REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second); From 33bfd67cca2f3a479673c5548f35c0ba4ed16240 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 17:59:46 -0500 Subject: [PATCH 035/111] xo-arena: verify SM1.2 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 960c3a2d..a39420be 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -432,11 +432,26 @@ namespace xo { /* SM1.1: size_ <= n_slot_ */ if (size_ > n_slot_) { return policy.report_error(log, - c_self, ": expect .size < .n_slot", + c_self, ": expect .size <= .n_slot", xtag("size", size_), xtag("n_slot", n_slot_)); } + /* SM1.2: control_[] size consistent with slots_[] size */ + if (control_.size() != n_slot_ + c_group_size) { + return policy.report_error(log, + c_self, ": expect .control_.size = .n_slot + c_group_size", + xtag("control_.size", control_.size()), + xtag("n_slot", n_slot_), + xtag("c_group_size", c_group_size)); + } + if (slots_.size() != n_slot_) { + return policy.report_error(log, + c_self, ": expect .slots_.size = .n_slot", + xtag("slots_.size", slots_.size()), + xtag("n_slot", n_slot_)); + } + return true; } } From ff7a73fca5cf7a19688d4ab5b7b762d6822a8a48 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:02:42 -0500 Subject: [PATCH 036/111] xo-arena: verify SM1.3 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index a39420be..4475bc24 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -452,6 +452,14 @@ namespace xo { xtag("n_slot", n_slot_)); } + /* SM1.3: n_group_ consistent with n_group_exponent_ */ + if (n_group_ != (size_type{1} << n_group_exponent_)) { + return policy.report_error(log, + c_self, ": expect .n_group = 2^.n_group_exponent", + xtag("n_group", n_group_), + xtag("n_group_exponent", n_group_exponent_)); + } + return true; } } From eb6b0c3a63212e977f68a8cdd69996a26bbb29f7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:04:51 -0500 Subject: [PATCH 037/111] xo-arena: verify SM1.4 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 4475bc24..15e101de 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -460,6 +460,15 @@ namespace xo { xtag("n_group_exponent", n_group_exponent_)); } + /* SM1.4: n_slot_ consistent with n_group_ */ + if (n_slot_ != n_group_ * c_group_size) { + return policy.report_error(log, + c_self, ": expect .n_slot = .n_group * c_group_size", + xtag("n_slot", n_slot_), + xtag("n_group", n_group_), + xtag("c_group_size", c_group_size)); + } + return true; } } From 7bf7ce9f3d24c8d3f6439ed730f8d1d8c8d7528b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:08:54 -0500 Subject: [PATCH 038/111] xo-arena: verify SM1.5 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 15e101de..6e7fd7a5 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -469,6 +469,13 @@ namespace xo { xtag("c_group_size", c_group_size)); } + /* SM1.5: n_slot_ a power of 2 */ + if ((n_slot_ & (n_slot_ - 1)) != 0) { + return policy.report_error(log, + c_self, ": expect .n_slot is power of 2", + xtag("n_slot", n_slot_)); + } + return true; } } From f10be5a41861867b14d843b9bbba8186e90d1f49 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:10:36 -0500 Subject: [PATCH 039/111] xo-arena: verify SM2.1 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 6e7fd7a5..153d6394 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -476,6 +476,14 @@ namespace xo { xtag("n_slot", n_slot_)); } + /* SM2.1: load_factor() <= c_max_load_factor */ + if (load_factor() > c_max_load_factor) { + return policy.report_error(log, + c_self, ": expect .load_factor <= c_max_load_factor", + xtag("load_factor", load_factor()), + xtag("c_max_load_factor", c_max_load_factor)); + } + return true; } } From 605e5638a1fbbc1baa1913f2788bdd73c712bc57 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:11:55 -0500 Subject: [PATCH 040/111] xo-arena: verify SM3.1 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 153d6394..981e4e01 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -484,6 +484,17 @@ namespace xo { xtag("c_max_load_factor", c_max_load_factor)); } + /* SM3.1: control_[N+i] = control_[i] for i in [0, c_group_size) */ + for (size_type i = 0; i < c_group_size; ++i) { + if (control_[n_slot_ + i] != control_[i]) { + return policy.report_error(log, + c_self, ": expect control_[N+i] = control_[i]", + xtag("i", i), + xtag("control_[i]", control_[i]), + xtag("control_[N+i]", control_[n_slot_ + i])); + } + } + return true; } } From 6a1d5ba3b03942d25ddd86a37327b2f28e7a286c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:14:40 -0500 Subject: [PATCH 041/111] xo-arena: verify SM3.2 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 981e4e01..7c1dfbc0 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -495,6 +495,23 @@ namespace xo { } } + /* SM3.2: {number of control_[i] spots with non-sentinel values} = size_ */ + { + size_type occupied_count = 0; + for (size_type i = 0; i < n_slot_; ++i) { + uint8_t c = control_[i]; + if ((c != c_empty_slot) && (c != c_tombstone)) { + ++occupied_count; + } + } + if (occupied_count != size_) { + return policy.report_error(log, + c_self, ": expect occupied control count = size", + xtag("occupied_count", occupied_count), + xtag("size", size_)); + } + } + return true; } } From a7fec616cdbb4ff8bb3cee6767dea862766c321d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:16:38 -0500 Subject: [PATCH 042/111] xo-arena: verify SM4.1.1 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 7c1dfbc0..a37c228b 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -512,6 +512,21 @@ namespace xo { } } + /* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */ + for (size_type i = 0; i < n_slot_; ++i) { + uint8_t c = control_[i]; + if ((c != c_empty_slot) && (c != c_tombstone)) { + uint8_t expected_h2 = hash_(slots_[i].first) & 0x7f; + if (c != expected_h2) { + return policy.report_error(log, + c_self, ": expect control[i] = hash(key) & 0x7f", + xtag("i", i), + xtag("control[i]", c), + xtag("expected_h2", expected_h2)); + } + } + } + return true; } } From fb5216ff9811001b9e2b5d200f4c266afb17705a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:18:21 -0500 Subject: [PATCH 043/111] xo-arena: verify SM4.1.2 for DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index a37c228b..7e03a1e8 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -527,6 +527,29 @@ namespace xo { } } + /* SM4.1.2: if control_[i] is non-sentinel, all slots in range [h .. i] are non-empty, + * where h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1) + */ + for (size_type i = 0; i < n_slot_; ++i) { + uint8_t c = control_[i]; + if ((c != c_empty_slot) && (c != c_tombstone)) { + size_type h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1); + size_type j = h; + while (j != i) { + uint8_t cj = control_[j]; + if ((cj == c_empty_slot) || (cj == c_tombstone)) { + return policy.report_error(log, + c_self, ": expect non-empty slot in probe range [h..i]", + xtag("i", i), + xtag("h", h), + xtag("j", j), + xtag("control[j]", cj)); + } + j = (j + 1) & (n_slot_ - 1); + } + } + } + return true; } } From d90712708a176ce052cd5bfe434fb350bc306f00 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 18:19:55 -0500 Subject: [PATCH 044/111] xo-arena: verify SM4.2 in DArenaHashMap.verify_ok --- include/xo/arena/DArenaHashMap.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 7e03a1e8..c003dc07 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -550,6 +550,19 @@ namespace xo { } } + /* SM4.2: if control_[i] is empty or tombstone, slots_[i].first = key_type() */ + for (size_type i = 0; i < n_slot_; ++i) { + uint8_t c = control_[i]; + if ((c == c_empty_slot) || (c == c_tombstone)) { + if (!(slots_[i].first == key_type())) { + return policy.report_error(log, + c_self, ": expect empty/tombstone slot has default key", + xtag("i", i), + xtag("control[i]", c)); + } + } + } + return true; } } From 344ced1159432a82aae8b143192ee74cfa13a550 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 7 Jan 2026 19:07:29 -0500 Subject: [PATCH 045/111] xo-arena: refactor: migrate DArenaHashMap state for rehash [WIP] --- include/xo/arena/DArenaHashMap.hpp | 203 ++++++++++++++++++++--------- 1 file changed, 144 insertions(+), 59 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index c003dc07..510f26a4 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -156,6 +156,42 @@ namespace xo { } #endif }; + + template + struct HashMapStore : DArenaHashMapUtil { + public: + using value_type = std::pair; + + public: + /** group_exp2: number of groups {x, 2^x} **/ + explicit HashMapStore(const std::pair & group_exp2) + : size_{0}, + n_group_exponent_{group_exp2.first}, + n_group_{group_exp2.second}, + n_slot_{group_exp2.second * c_group_size}, + control_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ + c_group_size})}, + slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} + {} + + public: + /** number of pairs in this table **/ + size_type size_ = 0; + /** base-2 logarithm of n_group_ **/ + size_type n_group_exponent_ = 0; + /** table has capacity for this number of groups. always an exact power of two. + * number of slots is n_group_ * c_group_size + **/ + size_type n_group_ = (1 << n_group_exponent_); + /** table has capacity for this number of {key,value} pairs **/ + size_type n_slot_ = n_group_ * c_group_size; + /** control_[] partitioned into groups of c_group_size (16) consecutive elements + **/ + DArenaVector control_; + /** slots_[] holds {key,value} pairs **/ + DArenaVector slots_; + }; } /** @brief flat hash map of key-value pairs using dedicated DArenas for storage @@ -188,15 +224,15 @@ namespace xo { size_type hint_max_capacity = 0, bool debug_flag = false); - size_type empty() const noexcept { return size_ == 0; } - size_type groups() const noexcept { return n_group_; } - size_type size() const noexcept { return size_; } - size_type capacity() const noexcept { return n_group_ * c_group_size; } + size_type empty() const noexcept { return store_.size_ == 0; } + size_type groups() const noexcept { return store_.n_group_; } + size_type size() const noexcept { return store_.size_; } + size_type capacity() const noexcept { return store_.n_group_ * c_group_size; } - float load_factor() const noexcept { return size_ / static_cast(n_slot_); } + float load_factor() const noexcept { return store_.size_ / static_cast(store_.n_slot_); } - /** insert @p kv_pair into hash map. replaces any previous value - * stored under the same key. + /** insert @p kv_pair into hash map. + * Replaces any previous value stored under the same key. * * Return pair retval with: * reval.first: true if size incremented; @@ -207,12 +243,20 @@ namespace xo { **/ std::pair try_insert(const std::pair & kv_pair); + /** insert @p kv_pair into hash map. + * Increase table size if necessary + **/ + bool insert(const std::pair & kv_pair); + bool verify_ok(verify_policy p = verify_policy::throw_only()) const; private: + /** increase hash table size (invoke when max load factor reached) **/ + bool _try_grow(); + /** load group abstraction from control bytes starting at @p ix **/ group_type _load_group(size_type ix) { - return group_type(&(control_[ix])); + return group_type(&(store_.control_[ix])); } /** like ctrl_[ix] = h2, but maintain overflow copy @@ -225,6 +269,10 @@ namespace xo { key_hash hash_; /** key equal **/ key_equal equal_; + + /** hash table state contents + size-related attributes **/ + detail::HashMapStore store_; +#ifdef OBSOLETE /** number of pairs in this table **/ size_type size_ = 0; /** base-2 logarithm of n_group_ **/ @@ -240,6 +288,7 @@ namespace xo { DArenaVector control_; /** slots_[] holds {key,value} pairs **/ DArenaVector slots_; +#endif /** true to enable debug logging **/ bool debug_flag_ = false; }; @@ -262,34 +311,29 @@ namespace xo { bool debug_flag) : hash_{std::move(hash)}, equal_{std::move(eq)}, - size_{0}, - n_group_exponent_{lub_exp2(lub_group_mult(hint_max_capacity)).first}, - n_group_{lub_exp2(lub_group_mult(hint_max_capacity)).second}, - n_slot_{n_group_ * c_group_size}, - control_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ + c_group_size})}, - slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})}, + store_{lub_exp2(lub_group_mult(hint_max_capacity))}, debug_flag_{debug_flag} { /* invariant: arenas have allocated address range, but no committed memory yet */ - this->control_.resize(n_slot_ + c_group_size); + this->store_.control_.resize(store_.n_slot_ + c_group_size); /* all slots marked empty initially */ - std::fill(this->control_.begin(), this->control_.end(), c_empty_slot); + std::fill(this->store_.control_.begin(), this->store_.control_.end(), c_empty_slot); - this->slots_.resize(n_slot_); + this->store_.slots_.resize(store_.n_slot_); } template void DArenaHashMap::_update_control(size_type ix, uint8_t h2) { - this->control_[ix] = h2; + this->store_.control_[ix] = h2; if (ix < c_group_size) { size_type N = this->capacity(); // refresh end-of-array copy - std::memcpy(&(control_[N]), &(control_[0]), c_group_size); + std::memcpy(&(store_.control_[N]), &(store_.control_[0]), c_group_size); } } @@ -328,9 +372,9 @@ namespace xo { // invariant: slot_ix in [0 .. N) - auto & slot = slots_[slot_ix]; + auto & slot = store_.slots_[slot_ix]; - if (slot.first == kv_pair.first) { + if (equal_(slot.first, kv_pair.first)) { // we have match on existing key; // replace associated value slot.second = kv_pair.second; @@ -370,14 +414,14 @@ namespace xo { // invariant: slot_ix in [0 .. N) - auto & slot = slots_[slot_ix]; + auto & slot = store_.slots_[slot_ix]; // mark slot occupied in control space; // maintain copy-at-end for overflow this->_update_control(slot_ix, h2); new (&slot) value_type(kv_pair); - ++(this->size_); + ++(this->store_.size_); // true: increased table size return std::make_pair(&slot, true); @@ -395,6 +439,46 @@ namespace xo { } } + template + bool + DArenaHashMap::_try_grow() + { +#ifdef NOT_YET + size_type n_group_exponent_2x = n_group_exponent_ + 1; + size_type n_group_2x = n_group_ * 2; + size_type n_slot_2x_ = n_group_2x * c_group_size; + + auto control_2x = DArenaVector::map(ArenaConfig{.size_ = n_slot_2x_ + c_group_size}); + auto slot_2x = DArenaVector::map(ArenaConfig{.size_ = n_slot_2x_ * sizeof(value_type)); +#endif + + /* rehash contents -> [control_2x, slot_2x] */ + + return false; + } + + template + bool + DArenaHashMap::insert(const std::pair & kv_pair) + { + auto [slot_addr, ins_flag] = this->try_insert(kv_pair); + + if (slot_addr) + return ins_flag; + + assert((store_.size_ + 1) / static_cast(store_.n_slot_) >= c_max_load_factor); + + if (this->_try_grow()) { + /* retry insert, with bigger table */ + auto [slot_addr, ins_flag] = this->try_insert(kv_pair); + + return ins_flag; + } else { + // TODO: set last error. Presumeably reached max size + return false; + } + } + /** * Verify DArenaHashMap class invariants. * @@ -427,53 +511,54 @@ namespace xo { using xo::xtag; constexpr const char * c_self = "DArenaHashMap::verify_ok"; - scope log(XO_DEBUG(debug_flag_), xtag("size", size_)); + scope log(XO_DEBUG(debug_flag_), + xtag("size", store_.size_)); /* SM1.1: size_ <= n_slot_ */ - if (size_ > n_slot_) { + if (store_.size_ > store_.n_slot_) { return policy.report_error(log, c_self, ": expect .size <= .n_slot", - xtag("size", size_), - xtag("n_slot", n_slot_)); + xtag("size", store_.size_), + xtag("n_slot", store_.n_slot_)); } /* SM1.2: control_[] size consistent with slots_[] size */ - if (control_.size() != n_slot_ + c_group_size) { + if (store_.control_.size() != store_.n_slot_ + c_group_size) { return policy.report_error(log, c_self, ": expect .control_.size = .n_slot + c_group_size", - xtag("control_.size", control_.size()), - xtag("n_slot", n_slot_), + xtag("control_.size", store_.control_.size()), + xtag("n_slot", store_.n_slot_), xtag("c_group_size", c_group_size)); } - if (slots_.size() != n_slot_) { + if (store_.slots_.size() != store_.n_slot_) { return policy.report_error(log, c_self, ": expect .slots_.size = .n_slot", - xtag("slots_.size", slots_.size()), - xtag("n_slot", n_slot_)); + xtag("slots_.size", store_.slots_.size()), + xtag("n_slot", store_.n_slot_)); } /* SM1.3: n_group_ consistent with n_group_exponent_ */ - if (n_group_ != (size_type{1} << n_group_exponent_)) { + if (store_.n_group_ != (size_type{1} << store_.n_group_exponent_)) { return policy.report_error(log, c_self, ": expect .n_group = 2^.n_group_exponent", - xtag("n_group", n_group_), - xtag("n_group_exponent", n_group_exponent_)); + xtag("n_group", store_.n_group_), + xtag("n_group_exponent", store_.n_group_exponent_)); } /* SM1.4: n_slot_ consistent with n_group_ */ - if (n_slot_ != n_group_ * c_group_size) { + if (store_.n_slot_ != store_.n_group_ * c_group_size) { return policy.report_error(log, c_self, ": expect .n_slot = .n_group * c_group_size", - xtag("n_slot", n_slot_), - xtag("n_group", n_group_), + xtag("n_slot", store_.n_slot_), + xtag("n_group", store_.n_group_), xtag("c_group_size", c_group_size)); } /* SM1.5: n_slot_ a power of 2 */ - if ((n_slot_ & (n_slot_ - 1)) != 0) { + if ((store_.n_slot_ & (store_.n_slot_ - 1)) != 0) { return policy.report_error(log, c_self, ": expect .n_slot is power of 2", - xtag("n_slot", n_slot_)); + xtag("n_slot", store_.n_slot_)); } /* SM2.1: load_factor() <= c_max_load_factor */ @@ -486,37 +571,37 @@ namespace xo { /* SM3.1: control_[N+i] = control_[i] for i in [0, c_group_size) */ for (size_type i = 0; i < c_group_size; ++i) { - if (control_[n_slot_ + i] != control_[i]) { + if (store_.control_[store_.n_slot_ + i] != store_.control_[i]) { return policy.report_error(log, c_self, ": expect control_[N+i] = control_[i]", xtag("i", i), - xtag("control_[i]", control_[i]), - xtag("control_[N+i]", control_[n_slot_ + i])); + xtag("control_[i]", store_.control_[i]), + xtag("control_[N+i]", store_.control_[store_.n_slot_ + i])); } } /* SM3.2: {number of control_[i] spots with non-sentinel values} = size_ */ { size_type occupied_count = 0; - for (size_type i = 0; i < n_slot_; ++i) { - uint8_t c = control_[i]; + for (size_type i = 0; i < store_.n_slot_; ++i) { + uint8_t c = store_.control_[i]; if ((c != c_empty_slot) && (c != c_tombstone)) { ++occupied_count; } } - if (occupied_count != size_) { + if (occupied_count != store_.size_) { return policy.report_error(log, c_self, ": expect occupied control count = size", xtag("occupied_count", occupied_count), - xtag("size", size_)); + xtag("size", store_.size_)); } } /* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */ - for (size_type i = 0; i < n_slot_; ++i) { - uint8_t c = control_[i]; + for (size_type i = 0; i < store_.n_slot_; ++i) { + uint8_t c = store_.control_[i]; if ((c != c_empty_slot) && (c != c_tombstone)) { - uint8_t expected_h2 = hash_(slots_[i].first) & 0x7f; + uint8_t expected_h2 = hash_(store_.slots_[i].first) & 0x7f; if (c != expected_h2) { return policy.report_error(log, c_self, ": expect control[i] = hash(key) & 0x7f", @@ -530,13 +615,13 @@ namespace xo { /* SM4.1.2: if control_[i] is non-sentinel, all slots in range [h .. i] are non-empty, * where h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1) */ - for (size_type i = 0; i < n_slot_; ++i) { - uint8_t c = control_[i]; + for (size_type i = 0; i < store_.n_slot_; ++i) { + uint8_t c = store_.control_[i]; if ((c != c_empty_slot) && (c != c_tombstone)) { - size_type h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1); + size_type h = (hash_(store_.slots_[i].first) >> 7) & (store_.n_slot_ - 1); size_type j = h; while (j != i) { - uint8_t cj = control_[j]; + uint8_t cj = store_.control_[j]; if ((cj == c_empty_slot) || (cj == c_tombstone)) { return policy.report_error(log, c_self, ": expect non-empty slot in probe range [h..i]", @@ -545,16 +630,16 @@ namespace xo { xtag("j", j), xtag("control[j]", cj)); } - j = (j + 1) & (n_slot_ - 1); + j = (j + 1) & (store_.n_slot_ - 1); } } } /* SM4.2: if control_[i] is empty or tombstone, slots_[i].first = key_type() */ - for (size_type i = 0; i < n_slot_; ++i) { - uint8_t c = control_[i]; + for (size_type i = 0; i < store_.n_slot_; ++i) { + uint8_t c = store_.control_[i]; if ((c == c_empty_slot) || (c == c_tombstone)) { - if (!(slots_[i].first == key_type())) { + if (!(store_.slots_[i].first == key_type())) { return policy.report_error(log, c_self, ": expect empty/tombstone slot has default key", xtag("i", i), From f0bd68bc8ca360ee9c7abe0f0bb7426654b39311 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 00:36:51 -0500 Subject: [PATCH 046/111] xo-arena: DArenaHashMap: iterators + utest for them --- include/xo/arena/DArenaHashMap.hpp | 239 +++++++++++++++++++++-------- utest/DArenaHashMap.test.cpp | 42 +++++ 2 files changed, 217 insertions(+), 64 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 510f26a4..0c234631 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -162,6 +162,7 @@ namespace xo { struct HashMapStore : DArenaHashMapUtil { public: using value_type = std::pair; + using group_type = detail::Group; public: /** group_exp2: number of groups {x, 2^x} **/ @@ -175,23 +176,85 @@ namespace xo { slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} {} + size_type empty() const noexcept { return size_ == 0; } + size_type capacity() const noexcept { return n_group_ * c_group_size; } + float load_factor() const noexcept { return size_ / static_cast(n_slot_); } + + group_type _load_group(size_type ix) { + return group_type(&(control_[ix])); + } + + void _update_control(size_type ix, uint8_t h2) { + this->control_[ix] = h2; + + if (ix < c_group_size) { + size_type N = this->capacity(); + + // refresh end-of-array copy + std::memcpy(&(control_[N]), &(control_[0]), c_group_size); + } + } + public: /** number of pairs in this table **/ size_type size_ = 0; /** base-2 logarithm of n_group_ **/ size_type n_group_exponent_ = 0; - /** table has capacity for this number of groups. always an exact power of two. + /** table has capacity for this number of groups. + * always an exact power of two. * number of slots is n_group_ * c_group_size **/ size_type n_group_ = (1 << n_group_exponent_); /** table has capacity for this number of {key,value} pairs **/ size_type n_slot_ = n_group_ * c_group_size; - /** control_[] partitioned into groups of c_group_size (16) consecutive elements + /** control_[] partitioned into groups of + * c_group_size (16) consecutive elements **/ DArenaVector control_; /** slots_[] holds {key,value} pairs **/ DArenaVector slots_; }; + + template + struct DArenaHashMapIterator : public DArenaHashMapUtil { + using value_type = std::pair; + + public: + DArenaHashMapIterator(uint8_t * c, uint8_t * e, value_type * p) + : ctrl_{c}, ctrl_end_{e}, pos_{p} {} + + value_type & operator*() const { return *pos_; } + value_type * operator->() const { return pos_; } + + bool operator==(const DArenaHashMapIterator & x) const { + return this->pos_ == x.pos_; + } + + bool operator!=(const DArenaHashMapIterator & x) const { + return this->pos_ != x.pos_; + } + + DArenaHashMapIterator & operator++() { + do { + ++ctrl_; + ++pos_; + } while ((ctrl_ != ctrl_end_) && this->is_sentinel()); + + return *this; + } + + bool is_sentinel() const { + return ((*ctrl_ == c_tombstone) || (*ctrl_ == c_empty_slot)); + } + + private: + uint8_t * ctrl_ = nullptr; + uint8_t * ctrl_end_ = nullptr; + + value_type * pos_ = nullptr; + }; + } /** @brief flat hash map of key-value pairs using dedicated DArenas for storage @@ -215,6 +278,9 @@ namespace xo { using key_equal = Equal; using byte = std::byte; using group_type = detail::Group; + using store_type = detail::HashMapStore; + using insert_value_type = std::pair; + using iterator = detail::DArenaHashMapIterator; /** create hash map **/ DArenaHashMap(size_type hint_max_capacity, @@ -224,12 +290,32 @@ namespace xo { size_type hint_max_capacity = 0, bool debug_flag = false); - size_type empty() const noexcept { return store_.size_ == 0; } + size_type empty() const noexcept { return store_.empty(); } size_type groups() const noexcept { return store_.n_group_; } size_type size() const noexcept { return store_.size_; } - size_type capacity() const noexcept { return store_.n_group_ * c_group_size; } + size_type capacity() const noexcept { return store_.capacity(); } + float load_factor() const noexcept { return store_.load_factor(); } - float load_factor() const noexcept { return store_.size_ / static_cast(store_.n_slot_); } + iterator begin() { + iterator ix(&(store_.control_[0]), + &(store_.control_[store_.capacity()]), + &(store_.slots_[0])); + + if (ix.is_sentinel()) { + /* first occupied position in table */ + ++ix; + } + + return ix; + } + + iterator end() { + iterator ix(&(store_.control_[store_.capacity()]), + &(store_.control_[store_.capacity()]), + &(store_.slots_[store_.capacity()])); + + return ix; + } /** insert @p kv_pair into hash map. * Replaces any previous value stored under the same key. @@ -241,54 +327,43 @@ namespace xo { * When table is full retval.second will be nullptr, * with error captured in last_error_ **/ - std::pair try_insert(const std::pair & kv_pair); + insert_value_type try_insert(const value_type & kv_pair); /** insert @p kv_pair into hash map. * Increase table size if necessary **/ - bool insert(const std::pair & kv_pair); + bool insert(const value_type & kv_pair); bool verify_ok(verify_policy p = verify_policy::throw_only()) const; private: + /** insert @p kv_pair, + * where key hashes to @p hash_value, into @p *store + **/ + insert_value_type _try_insert_aux(size_type hash_value, + const value_type & kv_pair, + store_type * p_store); + /** increase hash table size (invoke when max load factor reached) **/ bool _try_grow(); /** load group abstraction from control bytes starting at @p ix **/ - group_type _load_group(size_type ix) { - return group_type(&(store_.control_[ix])); - } + group_type _load_group(size_type ix) { return store_._load_group(ix); } /** like ctrl_[ix] = h2, but maintain overflow copy * at end of ctrl_[] array **/ - void _update_control(size_type ix, uint8_t h2); + void _update_control(size_type ix, uint8_t h2) { + return store_._update_control(ix, h2); + } private: /** hash function **/ key_hash hash_; /** key equal **/ key_equal equal_; - /** hash table state contents + size-related attributes **/ - detail::HashMapStore store_; -#ifdef OBSOLETE - /** number of pairs in this table **/ - size_type size_ = 0; - /** base-2 logarithm of n_group_ **/ - size_type n_group_exponent_ = 0; - /** table has capacity for this number of groups. always an exact power of two. - * number of slots is n_group_ * c_group_size - **/ - size_type n_group_ = (1 << n_group_exponent_); - /** table has capacity for this number of {key,value} pairs **/ - size_type n_slot_ = n_group_ * c_group_size; - /** control_[] partitioned into groups of c_group_size (16) consecutive elements - **/ - DArenaVector control_; - /** slots_[] holds {key,value} pairs **/ - DArenaVector slots_; -#endif + store_type store_; /** true to enable debug logging **/ bool debug_flag_ = false; }; @@ -318,36 +393,46 @@ namespace xo { this->store_.control_.resize(store_.n_slot_ + c_group_size); /* all slots marked empty initially */ - std::fill(this->store_.control_.begin(), this->store_.control_.end(), c_empty_slot); + std::fill(this->store_.control_.begin(), + this->store_.control_.end(), + c_empty_slot); this->store_.slots_.resize(store_.n_slot_); } - template - void - DArenaHashMap::_update_control(size_type ix, uint8_t h2) - { - this->store_.control_[ix] = h2; - - if (ix < c_group_size) { - size_type N = this->capacity(); - - // refresh end-of-array copy - std::memcpy(&(store_.control_[N]), &(store_.control_[0]), c_group_size); - } - } - template auto - DArenaHashMap::try_insert(const std::pair & kv_pair) -> std::pair + DArenaHashMap::try_insert(const value_type & kv_pair) -> insert_value_type { size_type h = hash_(kv_pair.first); + + return _try_insert_aux(h, kv_pair, &store_); + } + + template + auto + DArenaHashMap::_try_insert_aux(size_type hash_value, + const std::pair & kv_pair, + store_type * p_store) + -> std::pair + + { + size_type h = hash_value; // h1: hi bits: probe sequence size_type h1 = h >> 7; // h2: lo bits: store in control byte uint8_t h2 = h & 0x7f; - size_type N = this->capacity(); + size_type N = p_store->capacity(); // same as: // ix = h1 % N @@ -356,7 +441,7 @@ namespace xo { // will make series of probes for (;;) { - auto grp = _load_group(ix); + auto grp = p_store->_load_group(ix); { // look for matching slot to update @@ -372,7 +457,7 @@ namespace xo { // invariant: slot_ix in [0 .. N) - auto & slot = store_.slots_[slot_ix]; + auto & slot = p_store->slots_[slot_ix]; if (equal_(slot.first, kv_pair.first)) { // we have match on existing key; @@ -404,7 +489,7 @@ namespace xo { // Check here so that table can stay at max load factor // indefinitely as long as updates only // - if (load_factor() >= c_max_load_factor) { + if (p_store->load_factor() >= c_max_load_factor) { return std::make_pair(nullptr, false); } @@ -414,14 +499,14 @@ namespace xo { // invariant: slot_ix in [0 .. N) - auto & slot = store_.slots_[slot_ix]; + auto & slot = p_store->slots_[slot_ix]; // mark slot occupied in control space; // maintain copy-at-end for overflow - this->_update_control(slot_ix, h2); + p_store->_update_control(slot_ix, h2); new (&slot) value_type(kv_pair); - ++(this->store_.size_); + ++(p_store->size_); // true: increased table size return std::make_pair(&slot, true); @@ -443,23 +528,49 @@ namespace xo { bool DArenaHashMap::_try_grow() { -#ifdef NOT_YET - size_type n_group_exponent_2x = n_group_exponent_ + 1; - size_type n_group_2x = n_group_ * 2; - size_type n_slot_2x_ = n_group_2x * c_group_size; + size_type n_group_exponent_2x = store_.n_group_exponent_ + 1; + size_type n_group_2x = store_.n_group_ * 2; - auto control_2x = DArenaVector::map(ArenaConfig{.size_ = n_slot_2x_ + c_group_size}); - auto slot_2x = DArenaVector::map(ArenaConfig{.size_ = n_slot_2x_ * sizeof(value_type)); -#endif + detail::HashMapStore store_2x(std::make_pair(n_group_exponent_2x, + n_group_2x)); + /* rehash everything in store_, + * into store_2x + */ - /* rehash contents -> [control_2x, slot_2x] */ + for (size_type i = 0, n = store_.capacity(); i < n; ++i) { + uint8_t ctrl = store_.control_[i]; + value_type & kv_pair = store_.slots_[i]; - return false; + if ((ctrl != c_empty_slot) + && (ctrl != c_tombstone)) + { + size_type h = hash_(kv_pair.first); + auto chk = this->_try_insert_aux(h, kv_pair, &store_2x); + + if (!chk.second) { + // shenanigans - something isn't right. + // - may have run out of memory + assert(false); + + return false; + } + } + } + + this->store_ = std::move(store_2x); + + return true; } - template + template bool - DArenaHashMap::insert(const std::pair & kv_pair) + DArenaHashMap::insert(const std::pair & kv_pair) { auto [slot_addr, ins_flag] = this->try_insert(kv_pair); diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 3f4a24fd..58d75ffb 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -65,6 +65,17 @@ namespace xo { REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 1/16.0); + + /* verify iteration */ + { + size_t n = 0; + for (auto & ix : map) { + REQUIRE(ix.first == 1); + REQUIRE(ix.second == 11); + ++n; + } + REQUIRE(n == map.size()); + } } { @@ -77,6 +88,15 @@ namespace xo { REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 2/16.0); + + /* verify iteration */ + { + size_t n = 0; + for (auto & ix : map) { + ++n; + } + REQUIRE(n == map.size()); + } } { @@ -89,6 +109,28 @@ namespace xo { REQUIRE(map.groups() == 1); REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); REQUIRE(map.load_factor() == 3/16.0); + + /* verify iteration */ + { + size_t n = 0; + for (auto & ix : map) { + switch (ix.first) { + case 1: + REQUIRE(ix.second == 11); + break; + case 2: + REQUIRE(ix.second == 9); + break; + case 259: + REQUIRE(ix.second == 12); + break; + default: + REQUIRE(false); + } + ++n; + } + REQUIRE(n == map.size()); + } } } From 25ab31449290231140ef0f8dd3cd647b4da5a213 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 12:27:48 -0500 Subject: [PATCH 047/111] xo-arena: DArenaHashMap try_expand bugfix + edge utest --- include/xo/arena/DArenaHashMap.hpp | 158 ++++++++++++++++++++++------- include/xo/arena/DArenaVector.hpp | 14 +++ utest/DArenaHashMap.test.cpp | 47 ++++++++- utest/random_hash_ops.hpp | 68 +++++++++++++ 4 files changed, 248 insertions(+), 39 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 0c234631..92d0ed87 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -174,12 +174,55 @@ namespace xo { n_slot_{group_exp2.second * c_group_size}, control_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ + c_group_size})}, slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} - {} + { + /* here: arenas have allocated address range, but no committed memory yet */ + + this->_init(); + } size_type empty() const noexcept { return size_ == 0; } size_type capacity() const noexcept { return n_group_ * c_group_size; } float load_factor() const noexcept { return size_ / static_cast(n_slot_); } + void resize_from_empty(const std::pair & group_exp2) + { + assert(size_ == 0); + + this->n_group_exponent_ = group_exp2.first; + this->n_group_ = group_exp2.second; + this->n_slot_ = group_exp2.second * c_group_size; + + this->_init(); + } + + void clear() { + /* remark: discontinuity in the sense that we lose n_group_ = 2 ^ n_group_epxonent_ + * + * juice may not be worth the squeeze here, + * since DArena doesn't yet (Jan 2026) unmap on clear + */ + + this->size_ = 0; + this->n_group_exponent_ = 0; + this->n_group_ = 0; + this->n_slot_ = 0; + this->control_.resize(0); + this->slots_.resize(0); + } + + public: + void _init() { + this->control_.resize(n_slot_ + c_group_size); + + /* all slots marked empty initially */ + std::fill(this->control_.begin(), + this->control_.end(), + c_empty_slot); + + this->slots_.resize(n_slot_); + } + group_type _load_group(size_type ix) { return group_type(&(control_[ix])); } @@ -296,6 +339,8 @@ namespace xo { size_type capacity() const noexcept { return store_.capacity(); } float load_factor() const noexcept { return store_.load_factor(); } + bool verify_ok(verify_policy p = verify_policy::throw_only()) const; + iterator begin() { iterator ix(&(store_.control_[0]), &(store_.control_[store_.capacity()]), @@ -334,7 +379,8 @@ namespace xo { **/ bool insert(const value_type & kv_pair); - bool verify_ok(verify_policy p = verify_policy::throw_only()) const; + /** reset to empty state **/ + void clear(); private: /** insert @p kv_pair, @@ -389,15 +435,6 @@ namespace xo { store_{lub_exp2(lub_group_mult(hint_max_capacity))}, debug_flag_{debug_flag} { - /* invariant: arenas have allocated address range, but no committed memory yet */ - this->store_.control_.resize(store_.n_slot_ + c_group_size); - - /* all slots marked empty initially */ - std::fill(this->store_.control_.begin(), - this->store_.control_.end(), - c_empty_slot); - - this->store_.slots_.resize(store_.n_slot_); } template @@ -426,6 +463,8 @@ namespace xo { -> std::pair { + scope log(XO_DEBUG(false)); + size_type h = hash_value; // h1: hi bits: probe sequence size_type h1 = h >> 7; @@ -434,6 +473,10 @@ namespace xo { size_type N = p_store->capacity(); + if (N == 0) [[unlikely]] { + return std::make_pair(nullptr, false); + } + // same as: // ix = h1 % N // since N is power of 2 @@ -528,36 +571,58 @@ namespace xo { bool DArenaHashMap::_try_grow() { - size_type n_group_exponent_2x = store_.n_group_exponent_ + 1; - size_type n_group_2x = store_.n_group_ * 2; + scope log(XO_DEBUG(false)); - detail::HashMapStore store_2x(std::make_pair(n_group_exponent_2x, - n_group_2x)); - /* rehash everything in store_, - * into store_2x - */ + size_type n_group_exponent_2x = 0; + size_type n_group_2x = 0; - for (size_type i = 0, n = store_.capacity(); i < n; ++i) { - uint8_t ctrl = store_.control_[i]; - value_type & kv_pair = store_.slots_[i]; - - if ((ctrl != c_empty_slot) - && (ctrl != c_tombstone)) - { - size_type h = hash_(kv_pair.first); - auto chk = this->_try_insert_aux(h, kv_pair, &store_2x); - - if (!chk.second) { - // shenanigans - something isn't right. - // - may have run out of memory - assert(false); - - return false; - } - } + if (store_.n_group_ == 0) [[unlikely]] { + // special case: grow from hard empty state + n_group_exponent_2x = 0; + n_group_2x = 1; + } else { + n_group_exponent_2x = store_.n_group_exponent_ + 1; + n_group_2x = 2 * n_group_exponent_2x; } - this->store_ = std::move(store_2x); + // optimization when table is empty. in that case can resize + // arenas in place + + if (this->empty()) { + log && log("resize-from-empty branch"); + + this->store_.resize_from_empty(std::make_pair(n_group_exponent_2x, n_group_2x)); + } else { + log && log("duplicate-and-replace branch"); + + detail::HashMapStore store_2x(std::make_pair(n_group_exponent_2x, + n_group_2x)); + /* rehash everything in store_, + * into store_2x + */ + + for (size_type i = 0, n = store_.capacity(); i < n; ++i) { + uint8_t ctrl = store_.control_[i]; + value_type & kv_pair = store_.slots_[i]; + + if ((ctrl != c_empty_slot) + && (ctrl != c_tombstone)) + { + size_type h = hash_(kv_pair.first); + auto chk = this->_try_insert_aux(h, kv_pair, &store_2x); + + if (!chk.second) { + // shenanigans - something isn't right. + // - may have run out of memory + assert(false); + + return false; + } + } + } + + this->store_ = std::move(store_2x); + } return true; } @@ -572,10 +637,15 @@ namespace xo { Hash, Equal>::insert(const std::pair & kv_pair) { + scope log(XO_DEBUG(false)); + auto [slot_addr, ins_flag] = this->try_insert(kv_pair); - if (slot_addr) + if (slot_addr) { + log && log("fast", xtag("slot_addr", (void*)slot_addr), xtag("ins_flag", ins_flag)); + return ins_flag; + } assert((store_.size_ + 1) / static_cast(store_.n_slot_) >= c_max_load_factor); @@ -585,11 +655,23 @@ namespace xo { return ins_flag; } else { + assert(false); + // TODO: set last error. Presumeably reached max size return false; } } + template + void + DArenaHashMap::clear() + { + this->store_.clear(); + } + /** * Verify DArenaHashMap class invariants. * diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index d1d66343..ea1557f1 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -84,6 +84,8 @@ namespace xo { void swap(DArenaVector & other) noexcept; + DArenaVector & operator=(DArenaVector && x) noexcept; + private: T * _address_of(size_type i) { return ((T *)store_.lo_) + i; } const T * _address_of(size_type i) const { return ((const T *)store_.lo_) + i; } @@ -126,6 +128,18 @@ namespace xo { } } + template + DArenaVector & + DArenaVector::operator=(DArenaVector && other) noexcept + { + this->size_ = other.size_; + this->store_ = std::move(other.store_); + + other.size_ = 0; + + return *this; + } + template DArenaVector DArenaVector::map(const ArenaConfig & cfg) diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 58d75ffb..0d51d197 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -132,6 +132,48 @@ namespace xo { REQUIRE(n == map.size()); } } + + { + map.clear(); + + REQUIRE(map.empty()); + REQUIRE(map.size() == 0); + REQUIRE(map.groups() == 0); + REQUIRE(map.capacity() == 0); + } + + /* slightly different starting point, 0 capacity! */ + { + auto x = map.try_insert(std::make_pair(1, 11)); + + /* try_insert should fail - no capacity */ + REQUIRE(!x.first); + REQUIRE(!x.second); + } + + { + /* insert will grow hash table */ + auto x = map.insert(std::make_pair(1, 11)); + + CHECK(x); + REQUIRE(!map.empty()); + REQUIRE(map.size() == 1); + REQUIRE(map.groups() == 1); + REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size); + REQUIRE(map.load_factor() == 1/16.0); + + /* verify iteration */ + { + size_t n = 0; + for (auto & ix : map) { + REQUIRE(ix.first == 1); + REQUIRE(ix.second == 11); + ++n; + } + REQUIRE(n == map.size()); + } + } + } TEST_CASE("DArenaHashMap-try-insert2", "[arena][DArenaHashMap]") @@ -153,7 +195,7 @@ namespace xo { * observes test failure */ - for (std::uint32_t n = 0; n <= 2; ) { + for (std::uint32_t n = 0; n <= 8; ) { HashMap hash_map; auto test_fn = [&rgen, &hash_map](bool dbg_flag, @@ -163,6 +205,9 @@ namespace xo { ok_flag &= HashMapUtil::random_inserts(n, dbg_flag, &rgen, &hash_map); + ok_flag &= HashMapUtil::check_forward_iterator(0.0 /*dvalue*/, + dbg_flag, hash_map); + return ok_flag; }; diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index 548f72c8..85ba8a96 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace utest { @@ -379,6 +380,73 @@ namespace utest { } /*random_lookups*/ #endif + /* Require: + * - hash has keys [0..n-1] where n=map size + * - tree value at key k is dvalue+10*k + */ + static bool + check_forward_iterator(uint32_t dvalue, + bool catch_flag, + HashMap & map) + { + using xo::scope; + using xo::xtag; + + /* -> flase if/when verification fails */ + bool ok_flag = true; + + std::size_t const n = map.size(); + + scope log(XO_DEBUG(catch_flag)); + + log && log("map with size n", xtag("n", n)); + + std::unordered_set keys; + + { + auto end_ix = map.end(); + + //log && log(xtag("end_ix", end_ix)); + + auto begin_ix = map.begin(); + auto ix = begin_ix; + + int last_key = -1; + + while (ix != end_ix) { + log && log("forward loop top" + //xtag("ix", ix) + ); + + /* verify: keys in map are in [0 .. n) */ + REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first); + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n); + + /* verify: keys in map are unique */ + REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first)); + keys.insert(ix->first); + + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first); + + last_key = ix->first; + ++ix; + + log && log("forward loop bottom", + xtag("last_key", last_key) + //xtag("next ix", ix) + ); + } + + /* should have visited exactly n locations */ + REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size()); + REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix); + + //log && log(xtag("ix", ix), xtag("begin_ix", begin_ix)); + } + + return ok_flag; + } + #ifdef NOT_YET /* Require: * - tree has keys [0..n-1], where n=treẹsize() From dcd8ced1cdc94fb95a7111136237c91fb32fbece Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 12:41:29 -0500 Subject: [PATCH 048/111] xo-arena: DArenaHashMap: improve sentinel abstraction for control --- include/xo/arena/DArenaHashMap.hpp | 44 +++++++++++++++---------- utest/random_hash_ops.hpp | 52 ------------------------------ 2 files changed, 27 insertions(+), 69 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 92d0ed87..a9a1c8b7 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -64,6 +64,8 @@ namespace xo { using size_type = std::size_t; using control_type = std::uint8_t; + /** control: mask for sentinel states **/ + static constexpr uint8_t c_sentinel_mask = 0xF0; /** control: sentinel for empty slot **/ static constexpr uint8_t c_empty_slot = 0xFF; /** control: tombstone for deleted slot **/ @@ -75,6 +77,16 @@ namespace xo { /** max load factor **/ static constexpr float c_max_load_factor = 0.875; + /** control: true for sentinel values **/ + static constexpr bool is_sentinel(control_type ctrl) { + return ctrl & c_sentinel_mask; + } + + /** control; true for non-sentinel values **/ + static constexpr bool is_data(control_type ctrl) { + return 0 == (ctrl & c_sentinel_mask); + } + /** find smallest multiple k : k * c_group_size >= n **/ static size_type lub_group_mult(size_t n) { return (n + c_group_size - 1) / c_group_size; @@ -288,7 +300,7 @@ namespace xo { } bool is_sentinel() const { - return ((*ctrl_ == c_tombstone) || (*ctrl_ == c_empty_slot)); + return DArenaHashMapUtil::is_sentinel(*ctrl_); } private: @@ -605,20 +617,18 @@ namespace xo { uint8_t ctrl = store_.control_[i]; value_type & kv_pair = store_.slots_[i]; - if ((ctrl != c_empty_slot) - && (ctrl != c_tombstone)) - { - size_type h = hash_(kv_pair.first); - auto chk = this->_try_insert_aux(h, kv_pair, &store_2x); + if (DArenaHashMapUtil::is_data(ctrl)) { + size_type h = hash_(kv_pair.first); + auto chk = this->_try_insert_aux(h, kv_pair, &store_2x); - if (!chk.second) { - // shenanigans - something isn't right. - // - may have run out of memory - assert(false); + if (!chk.second) { + // shenanigans - something isn't right. + // - may have run out of memory + assert(false); - return false; - } + return false; } + } } this->store_ = std::move(store_2x); @@ -778,7 +788,7 @@ namespace xo { size_type occupied_count = 0; for (size_type i = 0; i < store_.n_slot_; ++i) { uint8_t c = store_.control_[i]; - if ((c != c_empty_slot) && (c != c_tombstone)) { + if (DArenaHashMapUtil::is_data(c)) { ++occupied_count; } } @@ -793,7 +803,7 @@ namespace xo { /* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */ for (size_type i = 0; i < store_.n_slot_; ++i) { uint8_t c = store_.control_[i]; - if ((c != c_empty_slot) && (c != c_tombstone)) { + if (DArenaHashMapUtil::is_data(c)) { uint8_t expected_h2 = hash_(store_.slots_[i].first) & 0x7f; if (c != expected_h2) { return policy.report_error(log, @@ -810,12 +820,12 @@ namespace xo { */ for (size_type i = 0; i < store_.n_slot_; ++i) { uint8_t c = store_.control_[i]; - if ((c != c_empty_slot) && (c != c_tombstone)) { + if (DArenaHashMapUtil::is_data(c)) { size_type h = (hash_(store_.slots_[i].first) >> 7) & (store_.n_slot_ - 1); size_type j = h; while (j != i) { uint8_t cj = store_.control_[j]; - if ((cj == c_empty_slot) || (cj == c_tombstone)) { + if (DArenaHashMapUtil::is_sentinel(cj)) { return policy.report_error(log, c_self, ": expect non-empty slot in probe range [h..i]", xtag("i", i), @@ -831,7 +841,7 @@ namespace xo { /* SM4.2: if control_[i] is empty or tombstone, slots_[i].first = key_type() */ for (size_type i = 0; i < store_.n_slot_; ++i) { uint8_t c = store_.control_[i]; - if ((c == c_empty_slot) || (c == c_tombstone)) { + if (DArenaHashMapUtil::is_sentinel(c)) { if (!(store_.slots_[i].first == key_type())) { return policy.report_error(log, c_self, ": expect empty/tombstone slot has default key", diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index 85ba8a96..d7f21678 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -596,58 +596,6 @@ namespace utest { } /*check_bidirectional_iterator*/ #endif -#ifdef NOT_YET - /** Require: - * - tree has keys [0..n-1], where n=treesize() - * - tree valu at key k is dvalue+10*k - * - * @p catch_flag. control behavior at each test assertion. - * true -> log to console + interact with catch2 - * false -> verify iteration behavior for return code. - * - **/ - static bool - check_reduced_sum(uint32_t dvalue, - bool catch_flag, - Tree const & rbtree) - { - using xo::scope; - using xo::xtag; - - scope log(XO_DEBUG(catch_flag)); - - /* -> false if/when check fails */ - bool ok_flag = true; - - size_t const n = rbtree.size(); - - for(size_t i = 0; i < n; ++i) { - /* compute reduction up to key=i */ - double reduced_upto - = rbtree.reduce_lub(i /*key*/, - true /*is_closed*/); - - double reduced = (i+1) * (5*i + dvalue); - - INFO(tostr(xtag("i", i), xtag("n", n), - xtag("tree.reduced_upto", reduced_upto), - xtag("reduced", reduced), - xtag("dvalue", dvalue))); - - auto glb_ix = rbtree.cfind_sum_glb(reduced); - - REQUIRE_ORFAIL(ok_flag, catch_flag, reduced_upto == reduced); - REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix.is_dereferenceable()); - /* glb_ix is truth-y */ - REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix); - - REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix->first == i); - } - - return ok_flag; - } /*check_reduced_sum*/ -#endif - #ifdef NOT_YET /* Require: * - *p_rbtree has keys [0..n-1], where n=rbtree.size() From 2273096b48d20183c635215f813fe0b77a630ead Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 13:11:18 -0500 Subject: [PATCH 049/111] xo-arena: DArenaHashMap: provide for stub areas on control array --- include/xo/arena/DArenaHashMap.hpp | 62 +++++++++++++++++++----------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index a9a1c8b7..bfa4d327 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -77,6 +77,11 @@ namespace xo { /** max load factor **/ static constexpr float c_max_load_factor = 0.875; + /** Iterator sentinel at begin/end of control array. + * Load-bearing for bidirectional iterator implementation + **/ + static constexpr size_type c_control_stub = 0; //c_group_size; + /** control: true for sentinel values **/ static constexpr bool is_sentinel(control_type ctrl) { return ctrl & c_sentinel_mask; @@ -87,6 +92,11 @@ namespace xo { return 0 == (ctrl & c_sentinel_mask); } + /** control: compute size of control array for swiss hash map with @p n_slot cells **/ + static constexpr size_type control_size(size_type n_slot) { + return n_slot + c_group_size + 12 * c_control_stub; + } + /** find smallest multiple k : k * c_group_size >= n **/ static size_type lub_group_mult(size_t n) { return (n + c_group_size - 1) / c_group_size; @@ -235,18 +245,22 @@ namespace xo { this->slots_.resize(n_slot_); } + /** load control group for slot range [ix .. ix+c_group_size) **/ group_type _load_group(size_type ix) { - return group_type(&(control_[ix])); + return group_type(&(control_[ix + c_control_stub])); } + /** update control group for slot number @p ix, replace with @p h2 **/ void _update_control(size_type ix, uint8_t h2) { - this->control_[ix] = h2; + this->control_[ix + c_control_stub] = h2; if (ix < c_group_size) { size_type N = this->capacity(); // refresh end-of-array copy - std::memcpy(&(control_[N]), &(control_[0]), c_group_size); + std::memcpy(&(control_[N + c_control_stub]), + &(control_[c_control_stub]), + c_group_size); } } @@ -354,8 +368,8 @@ namespace xo { bool verify_ok(verify_policy p = verify_policy::throw_only()) const; iterator begin() { - iterator ix(&(store_.control_[0]), - &(store_.control_[store_.capacity()]), + iterator ix(&(store_.control_[c_control_stub]), + &(store_.control_[c_control_stub + store_.capacity()]), &(store_.slots_[0])); if (ix.is_sentinel()) { @@ -367,8 +381,8 @@ namespace xo { } iterator end() { - iterator ix(&(store_.control_[store_.capacity()]), - &(store_.control_[store_.capacity()]), + iterator ix(&(store_.control_[c_control_stub + store_.capacity()]), + &(store_.control_[c_control_stub + store_.capacity()]), &(store_.slots_[store_.capacity()])); return ix; @@ -614,7 +628,7 @@ namespace xo { */ for (size_type i = 0, n = store_.capacity(); i < n; ++i) { - uint8_t ctrl = store_.control_[i]; + uint8_t ctrl = store_.control_[c_control_stub + i]; value_type & kv_pair = store_.slots_[i]; if (DArenaHashMapUtil::is_data(ctrl)) { @@ -726,12 +740,14 @@ namespace xo { } /* SM1.2: control_[] size consistent with slots_[] size */ - if (store_.control_.size() != store_.n_slot_ + c_group_size) { - return policy.report_error(log, - c_self, ": expect .control_.size = .n_slot + c_group_size", - xtag("control_.size", store_.control_.size()), - xtag("n_slot", store_.n_slot_), - xtag("c_group_size", c_group_size)); + if (store_.control_.size() != control_size(store_.n_slot_)) { + return policy.report_error + (log, + c_self, ": expect .control_.size = .n_slot + c_group_size + 2 * c_control_stub", + xtag("control_.size", store_.control_.size()), + xtag("n_slot", store_.n_slot_), + xtag("c_group_size", c_group_size), + xtag("c_control_stub", c_control_stub)); } if (store_.slots_.size() != store_.n_slot_) { return policy.report_error(log, @@ -774,7 +790,7 @@ namespace xo { /* SM3.1: control_[N+i] = control_[i] for i in [0, c_group_size) */ for (size_type i = 0; i < c_group_size; ++i) { - if (store_.control_[store_.n_slot_ + i] != store_.control_[i]) { + if (store_.control_[store_.n_slot_ + i + c_control_stub] != store_.control_[i] + c_control_stub) { return policy.report_error(log, c_self, ": expect control_[N+i] = control_[i]", xtag("i", i), @@ -787,7 +803,7 @@ namespace xo { { size_type occupied_count = 0; for (size_type i = 0; i < store_.n_slot_; ++i) { - uint8_t c = store_.control_[i]; + uint8_t c = store_.control_[i + c_control_stub]; if (DArenaHashMapUtil::is_data(c)) { ++occupied_count; } @@ -802,14 +818,14 @@ namespace xo { /* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */ for (size_type i = 0; i < store_.n_slot_; ++i) { - uint8_t c = store_.control_[i]; + uint8_t c = store_.control_[i + c_control_stub]; if (DArenaHashMapUtil::is_data(c)) { uint8_t expected_h2 = hash_(store_.slots_[i].first) & 0x7f; if (c != expected_h2) { return policy.report_error(log, c_self, ": expect control[i] = hash(key) & 0x7f", xtag("i", i), - xtag("control[i]", c), + xtag("control[i+stub]", c), xtag("expected_h2", expected_h2)); } } @@ -819,19 +835,19 @@ namespace xo { * where h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1) */ for (size_type i = 0; i < store_.n_slot_; ++i) { - uint8_t c = store_.control_[i]; + uint8_t c = store_.control_[i + c_control_stub]; if (DArenaHashMapUtil::is_data(c)) { size_type h = (hash_(store_.slots_[i].first) >> 7) & (store_.n_slot_ - 1); size_type j = h; while (j != i) { - uint8_t cj = store_.control_[j]; + uint8_t cj = store_.control_[j + c_control_stub]; if (DArenaHashMapUtil::is_sentinel(cj)) { return policy.report_error(log, c_self, ": expect non-empty slot in probe range [h..i]", xtag("i", i), xtag("h", h), xtag("j", j), - xtag("control[j]", cj)); + xtag("control[j+stub]", cj)); } j = (j + 1) & (store_.n_slot_ - 1); } @@ -840,13 +856,13 @@ namespace xo { /* SM4.2: if control_[i] is empty or tombstone, slots_[i].first = key_type() */ for (size_type i = 0; i < store_.n_slot_; ++i) { - uint8_t c = store_.control_[i]; + uint8_t c = store_.control_[i + c_control_stub]; if (DArenaHashMapUtil::is_sentinel(c)) { if (!(store_.slots_[i].first == key_type())) { return policy.report_error(log, c_self, ": expect empty/tombstone slot has default key", xtag("i", i), - xtag("control[i]", c)); + xtag("control[i+stub]", c)); } } } From 8e4f4e8a878e39711842847be7dafc833f08bdca Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 13:55:19 -0500 Subject: [PATCH 050/111] xo-arena: bookends around control array (prep iterator support) --- include/xo/arena/DArenaHashMap.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index bfa4d327..d20b8c72 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -80,7 +80,7 @@ namespace xo { /** Iterator sentinel at begin/end of control array. * Load-bearing for bidirectional iterator implementation **/ - static constexpr size_type c_control_stub = 0; //c_group_size; + static constexpr size_type c_control_stub = c_group_size; //c_group_size; /** control: true for sentinel values **/ static constexpr bool is_sentinel(control_type ctrl) { @@ -194,7 +194,7 @@ namespace xo { n_group_exponent_{group_exp2.first}, n_group_{group_exp2.second}, n_slot_{group_exp2.second * c_group_size}, - control_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ + c_group_size})}, + control_{DArenaVector::map(ArenaConfig{.size_ = control_size(n_slot_)})}, slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} { /* here: arenas have allocated address range, but no committed memory yet */ @@ -235,7 +235,7 @@ namespace xo { public: void _init() { - this->control_.resize(n_slot_ + c_group_size); + this->control_.resize(control_size(n_slot_)); /* all slots marked empty initially */ std::fill(this->control_.begin(), @@ -790,12 +790,12 @@ namespace xo { /* SM3.1: control_[N+i] = control_[i] for i in [0, c_group_size) */ for (size_type i = 0; i < c_group_size; ++i) { - if (store_.control_[store_.n_slot_ + i + c_control_stub] != store_.control_[i] + c_control_stub) { + if (store_.control_[store_.n_slot_ + i + c_control_stub] != store_.control_[i + c_control_stub]) { return policy.report_error(log, c_self, ": expect control_[N+i] = control_[i]", xtag("i", i), - xtag("control_[i]", store_.control_[i]), - xtag("control_[N+i]", store_.control_[store_.n_slot_ + i])); + xtag("control_[i]", (int)(store_.control_[i + c_control_stub])), + xtag("control_[N+i]", (int)(store_.control_[store_.n_slot_ + i + c_control_stub]))); } } From ccfc5873069d04b910fc39735d20ba04cacee153 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 14:02:44 -0500 Subject: [PATCH 051/111] xo-arena: DArenaHashMap: populate front bookend in control array --- include/xo/arena/DArenaHashMap.hpp | 31 +++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index d20b8c72..624a6188 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -70,6 +70,10 @@ namespace xo { static constexpr uint8_t c_empty_slot = 0xFF; /** control: tombstone for deleted slot **/ static constexpr uint8_t c_tombstone = 0xFE; + /** control: bookends around control array, + * for iterator edge support + **/ + static constexpr uint8_t c_iterator_bookend = 0xF0; /** group size **/ static constexpr size_type c_group_size = 16; @@ -237,8 +241,13 @@ namespace xo { void _init() { this->control_.resize(control_size(n_slot_)); - /* all slots marked empty initially */ + /* front stub: iterator bookend */ std::fill(this->control_.begin(), + this->control_.begin() + c_control_stub, + c_iterator_bookend); + + /* all slots marked empty initially */ + std::fill(this->control_.begin() + c_control_stub, this->control_.end(), c_empty_slot); @@ -708,8 +717,9 @@ namespace xo { * SM2. load factor * - SM2.1 load_factor() <= c_max_load_factor * SM3. control_ - * - SM3.1 control_[N+i] = control_[i] for i in [0, c_group_size) - * - SM3.2 {number of control_[i] spots with non-sentinel values} = size_ + * - SM3.1 control_[i] = c_iterator_bookend + * - SM3.2 control_[N+i] = control_[i] for i in [0, c_group_size) + * - SM3.3 {number of control_[i] spots with non-sentinel values} = size_ * SM4. slots_ * - SM4.1 if control_[i] is non-sentinel: * - SM4.1.1 control_[i] = hash_(slots_[i].first) & 0x7f @@ -788,7 +798,18 @@ namespace xo { xtag("c_max_load_factor", c_max_load_factor)); } - /* SM3.1: control_[N+i] = control_[i] for i in [0, c_group_size) */ + /* SM3.1: control_[i] = c_iterator_bookend for i in [0, c_control_stub) */ + for (size_type i = 0; i < c_control_stub; ++i) { + if (store_.control_[i] != c_iterator_bookend) { + return policy.report_error(log, + c_self, ": expect control_[i] = c_iterator_bookend for front stub", + xtag("i", i), + xtag("control_[i]", (int)(store_.control_[i])), + xtag("c_iterator_bookend", (int)c_iterator_bookend)); + } + } + + /* SM3.2: control_[N+i] = control_[i] for i in [0, c_group_size) */ for (size_type i = 0; i < c_group_size; ++i) { if (store_.control_[store_.n_slot_ + i + c_control_stub] != store_.control_[i + c_control_stub]) { return policy.report_error(log, @@ -799,7 +820,7 @@ namespace xo { } } - /* SM3.2: {number of control_[i] spots with non-sentinel values} = size_ */ + /* SM3.3: {number of control_[i] spots with non-sentinel values} = size_ */ { size_type occupied_count = 0; for (size_type i = 0; i < store_.n_slot_; ++i) { From 5d22e1792d388a5f77d053b54bbd20de3089fd37 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 14:16:26 -0500 Subject: [PATCH 052/111] xo-arena: DArenaHashMap: populate control end stub --- include/xo/arena/DArenaHashMap.hpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 624a6188..ceb9961d 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -98,7 +98,7 @@ namespace xo { /** control: compute size of control array for swiss hash map with @p n_slot cells **/ static constexpr size_type control_size(size_type n_slot) { - return n_slot + c_group_size + 12 * c_control_stub; + return n_slot + c_group_size + 2 * c_control_stub; } /** find smallest multiple k : k * c_group_size >= n **/ @@ -248,6 +248,11 @@ namespace xo { /* all slots marked empty initially */ std::fill(this->control_.begin() + c_control_stub, + this->control_.end() - c_control_stub, + c_empty_slot); + + /* end stub: iterator bookend */ + std::fill(this->control_.end() - c_control_stub, this->control_.end(), c_empty_slot); @@ -717,9 +722,10 @@ namespace xo { * SM2. load factor * - SM2.1 load_factor() <= c_max_load_factor * SM3. control_ - * - SM3.1 control_[i] = c_iterator_bookend - * - SM3.2 control_[N+i] = control_[i] for i in [0, c_group_size) + * - SM3.1 control_[i] = c_iterator_bookend for i in [0, c_control_stub) + * - SM3.2 control_[stub+i] = control_[stub+N+i] for i in [0, c_group_size) * - SM3.3 {number of control_[i] spots with non-sentinel values} = size_ + * - SM3.4 control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub) * SM4. slots_ * - SM4.1 if control_[i] is non-sentinel: * - SM4.1.1 control_[i] = hash_(slots_[i].first) & 0x7f @@ -837,6 +843,19 @@ namespace xo { } } + /* SM3.4: control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub) */ + for (size_type i = 0; i < c_control_stub; ++i) { + size_type ix = c_control_stub + store_.n_slot_ + c_group_size + i; + if (store_.control_[ix] != c_iterator_bookend) { + return policy.report_error(log, + c_self, ": expect control_[stub+N+group+i] = c_iterator_bookend for end stub", + xtag("i", i), + xtag("ix", ix), + xtag("control_[ix]", (int)(store_.control_[ix])), + xtag("c_iterator_bookend", (int)c_iterator_bookend)); + } + } + /* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */ for (size_type i = 0; i < store_.n_slot_; ++i) { uint8_t c = store_.control_[i + c_control_stub]; From cf70e5cc2e7d2fb1733c2f2ccbf70da11d668e3a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 14:39:09 -0500 Subject: [PATCH 053/111] xo-arena: comment + bugfix --- include/xo/arena/DArenaHashMap.hpp | 39 ++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index ceb9961d..df6e15f8 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -60,6 +60,41 @@ namespace xo { }; #endif + /** @class DArenaHashMapUtil + * + * @pre + * + * control + * + * <----------------- control_size(n_slot) ----------------> + * <-stub-> <----------- n_slot ----------> <-stub-> + * +--------+-------------------------------+-------+--------+ + * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | + * +--------+-------------------------------+-------+--------+ + * ^ ^ + * | ... | control_[stub+i] <--> slots_[i] + * slots v v + * +-------------------------------+ + * | {k,v} pairs | + * +-------------------------------+ + * <--- n_slot key-value pairs --> + * + * sizes: + * - stub before+after bookends. c_control_stub bytes (16) + * - group c_group_size. power of 2 (16 bytes) + * - n_slot hash table slots. power of 2 multiple of c_group_size. + * + * control bytes: + * - 0b1xxxxxxx sentinel bitmask + * - 0xf0 sentinel for before/after stubs (iterator bookends) + * - 0xff sentinel for empty slot. + * - 0xfe sentinel for tombstone + * - 0b0xxxxxxx high bit clear; remainder hold low 7 bits of hash + * - wrap duplicate first c_group_size bytes (after front stub) + * for SIMD convenience + * + * @endpre + **/ struct DArenaHashMapUtil { using size_type = std::size_t; using control_type = std::uint8_t; @@ -88,7 +123,7 @@ namespace xo { /** control: true for sentinel values **/ static constexpr bool is_sentinel(control_type ctrl) { - return ctrl & c_sentinel_mask; + return c_sentinel_mask == (ctrl & c_sentinel_mask); } /** control; true for non-sentinel values **/ @@ -254,7 +289,7 @@ namespace xo { /* end stub: iterator bookend */ std::fill(this->control_.end() - c_control_stub, this->control_.end(), - c_empty_slot); + c_iterator_bookend); this->slots_.resize(n_slot_); } From 5be04f5ed206316dfcdfbf8feb5675d0ea316561 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 17:04:07 -0500 Subject: [PATCH 054/111] xo-arena: DArenaHashMap: find() + utest --- include/xo/arena/DArenaHashMap.hpp | 58 ++++++++++++++++++++++++++++++ utest/DArenaHashMap.test.cpp | 3 ++ utest/random_hash_ops.hpp | 32 ++++++++--------- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index df6e15f8..3621b3ca 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -457,6 +457,11 @@ namespace xo { /** reset to empty state **/ void clear(); + /** find element with key @p key. + * @return iterator to element if found, end() otherwise + **/ + iterator find(const key_type & key); + private: /** insert @p kv_pair, * where key hashes to @p hash_value, into @p *store @@ -745,6 +750,59 @@ namespace xo { this->store_.clear(); } + template + auto + DArenaHashMap::find(const key_type & key) -> iterator + { + size_type N = store_.capacity(); + + if (N == 0) [[unlikely]] { + return this->end(); + } + + size_type h = hash_(key); + size_type h1 = h >> 7; + uint8_t h2 = h & 0x7f; + + size_type ix = h1 & (N - 1); + + for (;;) { + auto grp = store_._load_group(ix); + + { + uint16_t m = grp.all_matches(h2); + + while (m) { + int skip = __builtin_ctz(m); + size_type slot_ix = (ix + skip) & (N - 1); + + auto & slot = store_.slots_[slot_ix]; + + if (equal_(slot.first, key)) { + return iterator(&(store_.control_[c_control_stub + slot_ix]), + &(store_.control_[c_control_stub + N]), + &slot); + } + + m &= (m - 1); + } + } + + { + uint16_t e = grp.empty_matches(); + + if (e) { + return this->end(); + } + } + + ix = (ix + c_group_size) & (N - 1); + } + } + /** * Verify DArenaHashMap class invariants. * diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 0d51d197..dc2562b5 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -208,6 +208,9 @@ namespace xo { ok_flag &= HashMapUtil::check_forward_iterator(0.0 /*dvalue*/, dbg_flag, hash_map); + ok_flag &= HashMapUtil::random_lookups(0.0 /*dvalue*/, + dbg_flag, &rgen, hash_map); + return ok_flag; }; diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index d7f21678..dd42c392 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -329,15 +329,15 @@ namespace utest { } /*random_removes*/ #endif -#ifdef NOT_YET /* Require: - * - tree has keys [0..n-1], where n=treẹsize() - * - for each key k, associated value is 10*k + * - map has keys [0..n-1], where n=map.size() + * - for each key k, associated value is dvalue+10*k */ static bool - random_lookups(bool catch_flag, - Tree const & tree, - xo::rng::xoshiro256ss * p_rgen) + random_lookups(uint32_t dvalue, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen, + HashMap & map) { using xo::scope; using xo::xtag; @@ -347,9 +347,9 @@ namespace utest { /* -> false if/when verification fails */ bool ok_flag = true; - REQUIRE_ORFAIL(ok_flag, catch_flag, tree.verify_ok(catch_flag)); + REQUIRE_ORFAIL(ok_flag, catch_flag, map.verify_ok()); - size_t n = tree.size(); + size_t n = map.size(); std::vector u = random_permutation(n, p_rgen); @@ -358,27 +358,23 @@ namespace utest { for (std::uint32_t x : u) { INFO(tostr(xtag("i", i), xtag("n", n), xtag("x", x))); - REQUIRE_ORFAIL(ok_flag, catch_flag, tree[x] == x*10); - REQUIRE_ORFAIL(ok_flag, catch_flag, tree.verify_ok(catch_flag)); - REQUIRE_ORFAIL(ok_flag, catch_flag, tree.size() == n); + auto find_ix = map.find(x); - /* also test treẹfind() */ - auto find_ix = tree.find(x); - - REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix != tree.end()); + REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix != map.end()); REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->first == x); - REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->second == x*10); + REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->second == dvalue + x*10); + REQUIRE_ORFAIL(ok_flag, catch_flag, map.verify_ok()); + REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == n); ++i; } - REQUIRE_ORFAIL(ok_flag, catch_flag, tree.size() == n); + REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == n); log.end_scope(); return ok_flag; } /*random_lookups*/ -#endif /* Require: * - hash has keys [0..n-1] where n=map size From 579a244c8c90b584e270109036c4b42f41f67747 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:01:24 -0500 Subject: [PATCH 055/111] xo-arena: DArenaHashmap: speedup iteration, relying on bookends --- include/xo/arena/DArenaHashMap.hpp | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 3621b3ca..43fe16fd 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -339,12 +339,15 @@ namespace xo { using value_type = std::pair; public: - DArenaHashMapIterator(uint8_t * c, uint8_t * e, value_type * p) - : ctrl_{c}, ctrl_end_{e}, pos_{p} {} + DArenaHashMapIterator(uint8_t * c, value_type * p) + : ctrl_{c}, pos_{p} {} value_type & operator*() const { return *pos_; } value_type * operator->() const { return pos_; } + /** true iff iterator at sentinel position (not dereferencable state !) **/ + bool _at_slot_sentinel() const { return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); } + bool operator==(const DArenaHashMapIterator & x) const { return this->pos_ == x.pos_; } @@ -357,19 +360,27 @@ namespace xo { do { ++ctrl_; ++pos_; - } while ((ctrl_ != ctrl_end_) && this->is_sentinel()); + + /** end condition: iterator ends at last non-wrapped position. + * relyin on bookend sentinel values at known offset from 'wrap' section + * + * ctrl_ ctrl_ + c_group_size + * | | + * v v + * <----------------- control_size(n_slot) ----------------> + * <-stub-> <----------- n_slot ----------> <-stub-> + * +--------+-------------------------------+-------+--------+ + * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | + * +--------+-------------------------------+-------+--------+ + **/ + } while (is_sentinel(*ctrl_) + && (*(ctrl_ + c_group_size) != c_iterator_bookend)); return *this; } - bool is_sentinel() const { - return DArenaHashMapUtil::is_sentinel(*ctrl_); - } - private: uint8_t * ctrl_ = nullptr; - uint8_t * ctrl_end_ = nullptr; - value_type * pos_ = nullptr; }; @@ -417,12 +428,15 @@ namespace xo { bool verify_ok(verify_policy p = verify_policy::throw_only()) const; iterator begin() { + if (this->empty()) [[unlikely]] { + return this->end(); + } + iterator ix(&(store_.control_[c_control_stub]), - &(store_.control_[c_control_stub + store_.capacity()]), &(store_.slots_[0])); - if (ix.is_sentinel()) { - /* first occupied position in table */ + if (ix._at_slot_sentinel()) { + /* advance to first occupied position in table */ ++ix; } @@ -431,7 +445,6 @@ namespace xo { iterator end() { iterator ix(&(store_.control_[c_control_stub + store_.capacity()]), - &(store_.control_[c_control_stub + store_.capacity()]), &(store_.slots_[store_.capacity()])); return ix; @@ -783,7 +796,6 @@ namespace xo { if (equal_(slot.first, key)) { return iterator(&(store_.control_[c_control_stub + slot_ix]), - &(store_.control_[c_control_stub + N]), &slot); } From 6f0b45c429a9ef7ecfddd7c9a33bf54ee657a89c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:20:27 -0500 Subject: [PATCH 056/111] xo-arena: bugfix: backwards iteration working now --- include/xo/arena/DArenaHashMap.hpp | 13 ++++++ utest/DArenaHashMap.test.cpp | 3 ++ utest/random_hash_ops.hpp | 69 +++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 43fe16fd..1c5e4bd6 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -379,6 +379,19 @@ namespace xo { return *this; } + DArenaHashMapIterator & operator--() { + /* simpler than forward iteration, since bookend immediately + * precedes control byte for first slot + */ + do { + --ctrl_; + --pos_; + } while (is_sentinel(*ctrl_) + && (*ctrl_ != c_iterator_bookend)); + + return *this; + } + private: uint8_t * ctrl_ = nullptr; value_type * pos_ = nullptr; diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index dc2562b5..757cd39a 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -207,6 +207,9 @@ namespace xo { ok_flag &= HashMapUtil::check_forward_iterator(0.0 /*dvalue*/, dbg_flag, hash_map); + /* regular forward iterator, but start at hash_map.end() and use operator-- */ + ok_flag &= HashMapUtil::check_backward_iterator(0.0 /*dvalue*/, + dbg_flag, hash_map); ok_flag &= HashMapUtil::random_lookups(0.0 /*dvalue*/, dbg_flag, &rgen, hash_map); diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index dd42c392..ef19c31e 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -378,7 +378,7 @@ namespace utest { /* Require: * - hash has keys [0..n-1] where n=map size - * - tree value at key k is dvalue+10*k + * - hash value at key k is dvalue+10*k */ static bool check_forward_iterator(uint32_t dvalue, @@ -443,6 +443,73 @@ namespace utest { return ok_flag; } + /* Require: + * - hash has keys [0..n-1] where n=map size + * - hash value at key k is dvalue+10*k + */ + static bool + check_backward_iterator(uint32_t dvalue, + bool catch_flag, + HashMap & map) + { + catch_flag=true; + + using xo::scope; + using xo::xtag; + + /* -> flase if/when verification fails */ + bool ok_flag = true; + + std::size_t const n = map.size(); + + scope log(XO_DEBUG(catch_flag)); + + log && log("map with size n", xtag("n", n)); + + std::unordered_set keys; + + { + auto end_ix = map.end(); + + //log && log(xtag("end_ix", end_ix)); + + auto begin_ix = map.begin(); + auto ix = end_ix; + + if (ix == begin_ix) [[unlikely]] { + return ok_flag; + } + + while (ix != begin_ix) { + log && log("backward loop top", + xtag("n", n) + ); + + --ix; + + /* verify: keys in map are in [0 .. n) */ + REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first); + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n); + + log && log(xtag("ix->first", ix->first)); + + /* verify: keys in map are unique */ + REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first)); + keys.insert(ix->first); + + REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first); + } + + /* should have visited exactly n locations */ + REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size()); + REQUIRE_ORFAIL(ok_flag, catch_flag, ix == begin_ix); + + //log && log(xtag("ix", ix), xtag("begin_ix", begin_ix)); + } + + return ok_flag; + } + #ifdef NOT_YET /* Require: * - tree has keys [0..n-1], where n=treẹsize() From 8fa0cc7050cfea32a95ea7957eb1b1181f344216 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:28:42 -0500 Subject: [PATCH 057/111] xo-arena: org: move DArenaHashMapUtil to dedicated file --- include/xo/arena/DArenaHashMap.hpp | 96 +--------------- .../xo/arena/hashmap/DArenaHashMapUtil.hpp | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 95 deletions(-) create mode 100644 include/xo/arena/hashmap/DArenaHashMapUtil.hpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 1c5e4bd6..1623d33f 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -6,6 +6,7 @@ #pragma once #include "DArenaVector.hpp" +#include "hashmap/DArenaHashMapUtil.hpp" #include #include #include @@ -60,101 +61,6 @@ namespace xo { }; #endif - /** @class DArenaHashMapUtil - * - * @pre - * - * control - * - * <----------------- control_size(n_slot) ----------------> - * <-stub-> <----------- n_slot ----------> <-stub-> - * +--------+-------------------------------+-------+--------+ - * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | - * +--------+-------------------------------+-------+--------+ - * ^ ^ - * | ... | control_[stub+i] <--> slots_[i] - * slots v v - * +-------------------------------+ - * | {k,v} pairs | - * +-------------------------------+ - * <--- n_slot key-value pairs --> - * - * sizes: - * - stub before+after bookends. c_control_stub bytes (16) - * - group c_group_size. power of 2 (16 bytes) - * - n_slot hash table slots. power of 2 multiple of c_group_size. - * - * control bytes: - * - 0b1xxxxxxx sentinel bitmask - * - 0xf0 sentinel for before/after stubs (iterator bookends) - * - 0xff sentinel for empty slot. - * - 0xfe sentinel for tombstone - * - 0b0xxxxxxx high bit clear; remainder hold low 7 bits of hash - * - wrap duplicate first c_group_size bytes (after front stub) - * for SIMD convenience - * - * @endpre - **/ - struct DArenaHashMapUtil { - using size_type = std::size_t; - using control_type = std::uint8_t; - - /** control: mask for sentinel states **/ - static constexpr uint8_t c_sentinel_mask = 0xF0; - /** control: sentinel for empty slot **/ - static constexpr uint8_t c_empty_slot = 0xFF; - /** control: tombstone for deleted slot **/ - static constexpr uint8_t c_tombstone = 0xFE; - /** control: bookends around control array, - * for iterator edge support - **/ - static constexpr uint8_t c_iterator_bookend = 0xF0; - - /** group size **/ - static constexpr size_type c_group_size = 16; - - /** max load factor **/ - static constexpr float c_max_load_factor = 0.875; - - /** Iterator sentinel at begin/end of control array. - * Load-bearing for bidirectional iterator implementation - **/ - static constexpr size_type c_control_stub = c_group_size; //c_group_size; - - /** control: true for sentinel values **/ - static constexpr bool is_sentinel(control_type ctrl) { - return c_sentinel_mask == (ctrl & c_sentinel_mask); - } - - /** control; true for non-sentinel values **/ - static constexpr bool is_data(control_type ctrl) { - return 0 == (ctrl & c_sentinel_mask); - } - - /** control: compute size of control array for swiss hash map with @p n_slot cells **/ - static constexpr size_type control_size(size_type n_slot) { - return n_slot + c_group_size + 2 * c_control_stub; - } - - /** find smallest multiple k : k * c_group_size >= n **/ - static size_type lub_group_mult(size_t n) { - return (n + c_group_size - 1) / c_group_size; - } - - /** find smallest x such that 2^x >= n. Return {x, 2^x} **/ - static std::pair lub_exp2(size_t n) { - size_type ngx = 0; - size_type ng = 1; - - while (ng < n) { - ++ngx; - ng *= 2; - } - - return std::make_pair(ngx, ng);; - } - }; - namespace detail { /** @brief 16x 8-bit control bytes. * diff --git a/include/xo/arena/hashmap/DArenaHashMapUtil.hpp b/include/xo/arena/hashmap/DArenaHashMapUtil.hpp new file mode 100644 index 00000000..689559c7 --- /dev/null +++ b/include/xo/arena/hashmap/DArenaHashMapUtil.hpp @@ -0,0 +1,105 @@ +/** @file DArenaHashMapUtil.hpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +namespace xo { + namespace mm { + /** @class DArenaHashMapUtil + * + * @pre + * + * control + * + * <----------------- control_size(n_slot) ----------------> + * <-stub-> <----------- n_slot ----------> <-stub-> + * +--------+-------------------------------+-------+--------+ + * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | + * +--------+-------------------------------+-------+--------+ + * ^ ^ + * | ... | control_[stub+i] <--> slots_[i] + * slots v v + * +-------------------------------+ + * | {k,v} pairs | + * +-------------------------------+ + * <--- n_slot key-value pairs --> + * + * sizes: + * - stub before+after bookends. c_control_stub bytes (16) + * - group c_group_size. power of 2 (16 bytes) + * - n_slot hash table slots. power of 2 multiple of c_group_size. + * + * control bytes: + * - 0b1xxxxxxx sentinel bitmask + * - 0xf0 sentinel for before/after stubs (iterator bookends) + * - 0xff sentinel for empty slot. + * - 0xfe sentinel for tombstone + * - 0b0xxxxxxx high bit clear; remainder hold low 7 bits of hash + * - wrap duplicate first c_group_size bytes (after front stub) + * for SIMD convenience + * + * @endpre + **/ + struct DArenaHashMapUtil { + using size_type = std::size_t; + using control_type = std::uint8_t; + + /** control: mask for sentinel states **/ + static constexpr uint8_t c_sentinel_mask = 0xF0; + /** control: sentinel for empty slot **/ + static constexpr uint8_t c_empty_slot = 0xFF; + /** control: tombstone for deleted slot **/ + static constexpr uint8_t c_tombstone = 0xFE; + /** control: bookends around control array, + * for iterator edge support + **/ + static constexpr uint8_t c_iterator_bookend = 0xF0; + + /** group size **/ + static constexpr size_type c_group_size = 16; + + /** max load factor **/ + static constexpr float c_max_load_factor = 0.875; + + /** Iterator sentinel at begin/end of control array. + * Load-bearing for bidirectional iterator implementation + **/ + static constexpr size_type c_control_stub = c_group_size; //c_group_size; + + /** control: true for sentinel values **/ + static constexpr bool is_sentinel(control_type ctrl) { + return c_sentinel_mask == (ctrl & c_sentinel_mask); + } + + /** control; true for non-sentinel values **/ + static constexpr bool is_data(control_type ctrl) { + return 0 == (ctrl & c_sentinel_mask); + } + + /** control: compute size of control array for swiss hash map with @p n_slot cells **/ + static constexpr size_type control_size(size_type n_slot) { + return n_slot + c_group_size + 2 * c_control_stub; + } + + /** find smallest multiple k : k * c_group_size >= n **/ + static size_type lub_group_mult(size_t n) { + return (n + c_group_size - 1) / c_group_size; + } + + /** find smallest x such that 2^x >= n. Return {x, 2^x} **/ + static std::pair lub_exp2(size_t n) { + size_type ngx = 0; + size_type ng = 1; + + while (ng < n) { + ++ngx; + ng *= 2; + } + + return std::make_pair(ngx, ng);; + } + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DArenaHashMapUtil.hpp */ From 18d53528b52f6ca97dd6426191028fed52b0745c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:29:54 -0500 Subject: [PATCH 058/111] xo-arena: header fix for prev commit --- include/xo/arena/hashmap/DArenaHashMapUtil.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/xo/arena/hashmap/DArenaHashMapUtil.hpp b/include/xo/arena/hashmap/DArenaHashMapUtil.hpp index 689559c7..403ff61d 100644 --- a/include/xo/arena/hashmap/DArenaHashMapUtil.hpp +++ b/include/xo/arena/hashmap/DArenaHashMapUtil.hpp @@ -3,6 +3,10 @@ * @author Roland Conybeare, Jan 2026 **/ +#pragma once + +#include + namespace xo { namespace mm { /** @class DArenaHashMapUtil From 82edc0b660ad0835db9bb962b5c387ddd79b9673 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:34:31 -0500 Subject: [PATCH 059/111] xo-arena: DArenaHashMap: move detail::Group to dedicated file --- include/xo/arena/DArenaHashMap.hpp | 63 +---------------- include/xo/arena/hashmap/ControlGroup.hpp | 82 +++++++++++++++++++++++ 2 files changed, 83 insertions(+), 62 deletions(-) create mode 100644 include/xo/arena/hashmap/ControlGroup.hpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 1623d33f..e560bdbc 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -7,6 +7,7 @@ #include "DArenaVector.hpp" #include "hashmap/DArenaHashMapUtil.hpp" +#include "hashmap/ControlGroup.hpp" #include #include #include @@ -62,68 +63,6 @@ namespace xo { #endif namespace detail { - /** @brief 16x 8-bit control bytes. - * - * Support optimization using SIMD operations - **/ - struct Group { - std::array ctrl_; - - explicit Group(uint8_t * lo) { - ::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); - } - - /** find all exact matches in ctrl_[0..15] for @p h2. - * for each match set corresponding bit in return value. - * Bits {0x1, 0x2, 0x4, ...} set iff exact match on - * {ctrl_[0], ctrl_[1], ctrl_2[], ...} respectively - **/ - uint16_t all_matches(uint8_t h2) const { - uint16_t retval = 0; - uint16_t bit = 1; - for (auto xi : ctrl_) { - if (xi == h2) - retval |= bit; - bit = bit << 1; - } - - return retval; - } - - /** find all empty sentinels in ctrl_[0..15]. - * for each empty, set corresponding bit in return value. - * Bits {0x1, 0x2, 0x4, ...} set iff empty spot - * {ctrl_[0], ctrl_[1], ctrl_[2], ...} respectively - **/ - uint16_t empty_matches() const { - uint16_t retval = 0; - uint16_t bit = 1; - for (auto xi : ctrl_) { - if (xi == DArenaHashMapUtil::c_empty_slot) - retval |= bit; - bit = bit << 1; - } - - return retval; - } - -#ifdef NOT_YET - __m128i ctrl; // 16 bytes loaded via SSE2 - - // Find all slots matching h2 - uint16_t Match(uint8_t h2) const { - __m128i pattern = _mm_set1_epi8(h2); - __m128i result = _mm_cmpeq_epi8(ctrl, pattern); - return _mm_movemask_epi8(result); // 16-bit mask - } - - // Find all empty slots (0xFF) - uint16_t MatchEmpty() const { - return _mm_movemask_epi8(_mm_cmpeq_epi8(ctrl, _mm_set1_epi8(0xFF))); - } -#endif - }; - template struct HashMapStore : DArenaHashMapUtil { diff --git a/include/xo/arena/hashmap/ControlGroup.hpp b/include/xo/arena/hashmap/ControlGroup.hpp new file mode 100644 index 00000000..2b60a79f --- /dev/null +++ b/include/xo/arena/hashmap/ControlGroup.hpp @@ -0,0 +1,82 @@ +/** @file ControlGroupo + * + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "DArenaHashMapUtil.hpp" +#include +#include +#include + +namespace xo { + namespace mm { + namespace detail { + /** @brief 16x 8-bit control bytes. + * + * Support optimization using SIMD operations + **/ + struct Group { + std::array ctrl_; + + explicit Group(uint8_t * lo) { + ::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); + } + + /** find all exact matches in ctrl_[0..15] for @p h2. + * for each match set corresponding bit in return value. + * Bits {0x1, 0x2, 0x4, ...} set iff exact match on + * {ctrl_[0], ctrl_[1], ctrl_2[], ...} respectively + **/ + uint16_t all_matches(uint8_t h2) const { + uint16_t retval = 0; + uint16_t bit = 1; + for (auto xi : ctrl_) { + if (xi == h2) + retval |= bit; + bit = bit << 1; + } + + return retval; + } + + /** find all empty sentinels in ctrl_[0..15]. + * for each empty, set corresponding bit in return value. + * Bits {0x1, 0x2, 0x4, ...} set iff empty spot + * {ctrl_[0], ctrl_[1], ctrl_[2], ...} respectively + **/ + uint16_t empty_matches() const { + uint16_t retval = 0; + uint16_t bit = 1; + + for (auto xi : ctrl_) { + if (xi == DArenaHashMapUtil::c_empty_slot) + retval |= bit; + bit = bit << 1; + } + + return retval; + } + +#ifdef NOT_YET + __m128i ctrl; // 16 bytes loaded via SSE2 + + // Find all slots matching h2 + uint16_t Match(uint8_t h2) const { + __m128i pattern = _mm_set1_epi8(h2); + __m128i result = _mm_cmpeq_epi8(ctrl, pattern); + return _mm_movemask_epi8(result); // 16-bit mask + } + + // Find all empty slots (0xFF) + uint16_t MatchEmpty() const { + return _mm_movemask_epi8(_mm_cmpeq_epi8(ctrl, _mm_set1_epi8(0xFF))); + } +#endif + }; + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ControlGroup.hpp */ From 63caccb8a7afbd889b691fbbfbed8eb2383242cb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:37:18 -0500 Subject: [PATCH 060/111] xo-arena: DArenaHashMap: compiler nits after refactor --- include/xo/arena/DArenaHashMap.hpp | 4 ++-- include/xo/arena/hashmap/ControlGroup.hpp | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index e560bdbc..c20695e4 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -68,7 +68,7 @@ namespace xo { struct HashMapStore : DArenaHashMapUtil { public: using value_type = std::pair; - using group_type = detail::Group; + using group_type = detail::ControlGroup; public: /** group_exp2: number of groups {x, 2^x} **/ @@ -264,7 +264,7 @@ namespace xo { using key_hash = Hash; using key_equal = Equal; using byte = std::byte; - using group_type = detail::Group; + using group_type = detail::ControlGroup; using store_type = detail::HashMapStore; using insert_value_type = std::pair; using iterator = detail::DArenaHashMapIterator; diff --git a/include/xo/arena/hashmap/ControlGroup.hpp b/include/xo/arena/hashmap/ControlGroup.hpp index 2b60a79f..d5db66af 100644 --- a/include/xo/arena/hashmap/ControlGroup.hpp +++ b/include/xo/arena/hashmap/ControlGroup.hpp @@ -17,10 +17,11 @@ namespace xo { * * Support optimization using SIMD operations **/ - struct Group { + struct ControlGroup { std::array ctrl_; - explicit Group(uint8_t * lo) { + /** Require: lo is aligned on c_group_size (probably 16 bytes) **/ + explicit ControlGroup(uint8_t * lo) { ::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); } From 5b7f9810fe8854bf9b30adfa3ad6c764e360f1b9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:38:23 -0500 Subject: [PATCH 061/111] xo-arena: DArenaHashMap: retire temporary debug --- utest/random_hash_ops.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index ef19c31e..52d443fa 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -452,8 +452,6 @@ namespace utest { bool catch_flag, HashMap & map) { - catch_flag=true; - using xo::scope; using xo::xtag; From 4ab0837c4d5ad07949388ed55b58e3a367c571e7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:42:51 -0500 Subject: [PATCH 062/111] xo-arena: reorg: move verify_policy to dedicated file --- include/xo/arena/DArenaHashMap.hpp | 38 +------------- include/xo/arena/hashmap/verify_policy.hpp | 59 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 37 deletions(-) create mode 100644 include/xo/arena/hashmap/verify_policy.hpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index c20695e4..77689f2f 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -6,6 +6,7 @@ #pragma once #include "DArenaVector.hpp" +#include "hashmap/verify_policy.hpp" #include "hashmap/DArenaHashMapUtil.hpp" #include "hashmap/ControlGroup.hpp" #include @@ -15,43 +16,6 @@ #include namespace xo { - struct verify_policy { - static verify_policy log_only() { - return verify_policy{.flags_ = 0x01}; - } - static verify_policy throw_only() { - return verify_policy{.flags_ = 0x02}; - } - static verify_policy chatty() { - return verify_policy{.flags_ = 0x03}; - } - - bool is_silent() const noexcept { return flags_ == 0; } - bool log_flag() const noexcept { return flags_ & 0x01; } - bool throw_flag() const noexcept { return flags_ & 0x02; } - - template - bool report_error(scope & log, Tn&&... args) - { - if (!this->is_silent()) { - // TODO: consider global arena here for string - std::string msg = tostr(std::forward(args)...); - - if (this->log_flag()) { - log.retroactively_enable(); - log(msg); - } - if (this->throw_flag()) { - throw std::runtime_error(msg); - } - } - return false; - } - - const char * c_self_ = "anonymous"; - uint8_t flags_; - }; - namespace mm { #ifdef NOT_YET enum class insert_error : int32_t { diff --git a/include/xo/arena/hashmap/verify_policy.hpp b/include/xo/arena/hashmap/verify_policy.hpp new file mode 100644 index 00000000..4d0d32aa --- /dev/null +++ b/include/xo/arena/hashmap/verify_policy.hpp @@ -0,0 +1,59 @@ +/** @file verify_policy.hpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include +#include + +namespace xo { + // TODO: move xo/indentlog + + /** @brief policy for verify_ok behavior. + * + * Remarke: wrote this for DArenaHashMap, + * want to incorporate into other subsystems + * that provide a verify_ok() method. + * e.g. RedBlackTree + **/ + struct verify_policy { + static verify_policy log_only() { + return verify_policy{.flags_ = 0x01}; + } + static verify_policy throw_only() { + return verify_policy{.flags_ = 0x02}; + } + static verify_policy chatty() { + return verify_policy{.flags_ = 0x03}; + } + + bool is_silent() const noexcept { return flags_ == 0; } + bool log_flag() const noexcept { return flags_ & 0x01; } + bool throw_flag() const noexcept { return flags_ & 0x02; } + + template + bool report_error(scope & log, Tn&&... args) + { + if (!this->is_silent()) { + // TODO: consider global arena here for string + std::string msg = tostr(std::forward(args)...); + + if (this->log_flag()) { + log.retroactively_enable(); + log(msg); + } + if (this->throw_flag()) { + throw std::runtime_error(msg); + } + } + return false; + } + + const char * c_self_ = "anonymous"; + uint8_t flags_; + }; +} /*namespace xo*/ + +/* end verify_policy.hpp */ From a929de7baab420122e251873a6b654e6ccf2b5f1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:46:45 -0500 Subject: [PATCH 063/111] xo-arena: move HashMapStore to dedicated file --- include/xo/arena/DArenaHashMap.hpp | 117 +------------------ include/xo/arena/hashmap/HashMapStore.hpp | 132 ++++++++++++++++++++++ 2 files changed, 133 insertions(+), 116 deletions(-) create mode 100644 include/xo/arena/hashmap/HashMapStore.hpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 77689f2f..75b2f958 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -7,8 +7,7 @@ #include "DArenaVector.hpp" #include "hashmap/verify_policy.hpp" -#include "hashmap/DArenaHashMapUtil.hpp" -#include "hashmap/ControlGroup.hpp" +#include "hashmap/HashMapStore.hpp" #include #include #include @@ -27,120 +26,6 @@ namespace xo { #endif namespace detail { - template - struct HashMapStore : DArenaHashMapUtil { - public: - using value_type = std::pair; - using group_type = detail::ControlGroup; - - public: - /** group_exp2: number of groups {x, 2^x} **/ - explicit HashMapStore(const std::pair & group_exp2) - : size_{0}, - n_group_exponent_{group_exp2.first}, - n_group_{group_exp2.second}, - n_slot_{group_exp2.second * c_group_size}, - control_{DArenaVector::map(ArenaConfig{.size_ = control_size(n_slot_)})}, - slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} - { - /* here: arenas have allocated address range, but no committed memory yet */ - - this->_init(); - } - - size_type empty() const noexcept { return size_ == 0; } - size_type capacity() const noexcept { return n_group_ * c_group_size; } - float load_factor() const noexcept { return size_ / static_cast(n_slot_); } - - void resize_from_empty(const std::pair & group_exp2) - { - assert(size_ == 0); - - this->n_group_exponent_ = group_exp2.first; - this->n_group_ = group_exp2.second; - this->n_slot_ = group_exp2.second * c_group_size; - - this->_init(); - } - - void clear() { - /* remark: discontinuity in the sense that we lose n_group_ = 2 ^ n_group_epxonent_ - * - * juice may not be worth the squeeze here, - * since DArena doesn't yet (Jan 2026) unmap on clear - */ - - this->size_ = 0; - this->n_group_exponent_ = 0; - this->n_group_ = 0; - this->n_slot_ = 0; - this->control_.resize(0); - this->slots_.resize(0); - } - - public: - void _init() { - this->control_.resize(control_size(n_slot_)); - - /* front stub: iterator bookend */ - std::fill(this->control_.begin(), - this->control_.begin() + c_control_stub, - c_iterator_bookend); - - /* all slots marked empty initially */ - std::fill(this->control_.begin() + c_control_stub, - this->control_.end() - c_control_stub, - c_empty_slot); - - /* end stub: iterator bookend */ - std::fill(this->control_.end() - c_control_stub, - this->control_.end(), - c_iterator_bookend); - - this->slots_.resize(n_slot_); - } - - /** load control group for slot range [ix .. ix+c_group_size) **/ - group_type _load_group(size_type ix) { - return group_type(&(control_[ix + c_control_stub])); - } - - /** update control group for slot number @p ix, replace with @p h2 **/ - void _update_control(size_type ix, uint8_t h2) { - this->control_[ix + c_control_stub] = h2; - - if (ix < c_group_size) { - size_type N = this->capacity(); - - // refresh end-of-array copy - std::memcpy(&(control_[N + c_control_stub]), - &(control_[c_control_stub]), - c_group_size); - } - } - - public: - /** number of pairs in this table **/ - size_type size_ = 0; - /** base-2 logarithm of n_group_ **/ - size_type n_group_exponent_ = 0; - /** table has capacity for this number of groups. - * always an exact power of two. - * number of slots is n_group_ * c_group_size - **/ - size_type n_group_ = (1 << n_group_exponent_); - /** table has capacity for this number of {key,value} pairs **/ - size_type n_slot_ = n_group_ * c_group_size; - /** control_[] partitioned into groups of - * c_group_size (16) consecutive elements - **/ - DArenaVector control_; - /** slots_[] holds {key,value} pairs **/ - DArenaVector slots_; - }; template diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp new file mode 100644 index 00000000..130f4f54 --- /dev/null +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -0,0 +1,132 @@ +/** @file HashMapStore.hpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "hashmap/DArenaHashMapUtil.hpp" +#include "hashmap/ControlGroup.hpp" + +namespace xo { + namespace mm { + namespace detail { + template + struct HashMapStore : DArenaHashMapUtil { + public: + using value_type = std::pair; + using group_type = detail::ControlGroup; + + public: + /** group_exp2: number of groups {x, 2^x} **/ + explicit HashMapStore(const std::pair & group_exp2) + : size_{0}, + n_group_exponent_{group_exp2.first}, + n_group_{group_exp2.second}, + n_slot_{group_exp2.second * c_group_size}, + control_{DArenaVector::map(ArenaConfig{.size_ = control_size(n_slot_)})}, + slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} + { + /* here: arenas have allocated address range, but no committed memory yet */ + + this->_init(); + } + + size_type empty() const noexcept { return size_ == 0; } + size_type capacity() const noexcept { return n_group_ * c_group_size; } + float load_factor() const noexcept { return size_ / static_cast(n_slot_); } + + void resize_from_empty(const std::pair & group_exp2) + { + assert(size_ == 0); + + this->n_group_exponent_ = group_exp2.first; + this->n_group_ = group_exp2.second; + this->n_slot_ = group_exp2.second * c_group_size; + + this->_init(); + } + + void clear() { + /* remark: discontinuity in the sense that we lose n_group_ = 2 ^ n_group_epxonent_ + * + * juice may not be worth the squeeze here, + * since DArena doesn't yet (Jan 2026) unmap on clear + */ + + this->size_ = 0; + this->n_group_exponent_ = 0; + this->n_group_ = 0; + this->n_slot_ = 0; + this->control_.resize(0); + this->slots_.resize(0); + } + + public: + void _init() { + this->control_.resize(control_size(n_slot_)); + + /* front stub: iterator bookend */ + std::fill(this->control_.begin(), + this->control_.begin() + c_control_stub, + c_iterator_bookend); + + /* all slots marked empty initially */ + std::fill(this->control_.begin() + c_control_stub, + this->control_.end() - c_control_stub, + c_empty_slot); + + /* end stub: iterator bookend */ + std::fill(this->control_.end() - c_control_stub, + this->control_.end(), + c_iterator_bookend); + + this->slots_.resize(n_slot_); + } + + /** load control group for slot range [ix .. ix+c_group_size) **/ + group_type _load_group(size_type ix) { + return group_type(&(control_[ix + c_control_stub])); + } + + /** update control group for slot number @p ix, replace with @p h2 **/ + void _update_control(size_type ix, uint8_t h2) { + this->control_[ix + c_control_stub] = h2; + + if (ix < c_group_size) { + size_type N = this->capacity(); + + // refresh end-of-array copy + std::memcpy(&(control_[N + c_control_stub]), + &(control_[c_control_stub]), + c_group_size); + } + } + + public: + /** number of pairs in this table **/ + size_type size_ = 0; + /** base-2 logarithm of n_group_ **/ + size_type n_group_exponent_ = 0; + /** table has capacity for this number of groups. + * always an exact power of two. + * number of slots is n_group_ * c_group_size + **/ + size_type n_group_ = (1 << n_group_exponent_); + /** table has capacity for this number of {key,value} pairs **/ + size_type n_slot_ = n_group_ * c_group_size; + /** control_[] partitioned into groups of + * c_group_size (16) consecutive elements + **/ + DArenaVector control_; + /** slots_[] holds {key,value} pairs **/ + DArenaVector slots_; + }; + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end HashMapStore.hpp */ From fcc29411c891f7a767eecfe0386a47b180ae0134 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:50:04 -0500 Subject: [PATCH 064/111] xo-arena: reorg: move DArenaHashMapIterator to dedicated file --- include/xo/arena/DArenaHashMap.hpp | 69 +--------------- .../arena/hashmap/DArenaHashMapIterator.hpp | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+), 68 deletions(-) create mode 100644 include/xo/arena/hashmap/DArenaHashMapIterator.hpp diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 75b2f958..63741e66 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -8,6 +8,7 @@ #include "DArenaVector.hpp" #include "hashmap/verify_policy.hpp" #include "hashmap/HashMapStore.hpp" +#include "hashmap/DArenaHashMapIterator.hpp" #include #include #include @@ -25,74 +26,6 @@ namespace xo { }; #endif - namespace detail { - - template - struct DArenaHashMapIterator : public DArenaHashMapUtil { - using value_type = std::pair; - - public: - DArenaHashMapIterator(uint8_t * c, value_type * p) - : ctrl_{c}, pos_{p} {} - - value_type & operator*() const { return *pos_; } - value_type * operator->() const { return pos_; } - - /** true iff iterator at sentinel position (not dereferencable state !) **/ - bool _at_slot_sentinel() const { return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); } - - bool operator==(const DArenaHashMapIterator & x) const { - return this->pos_ == x.pos_; - } - - bool operator!=(const DArenaHashMapIterator & x) const { - return this->pos_ != x.pos_; - } - - DArenaHashMapIterator & operator++() { - do { - ++ctrl_; - ++pos_; - - /** end condition: iterator ends at last non-wrapped position. - * relyin on bookend sentinel values at known offset from 'wrap' section - * - * ctrl_ ctrl_ + c_group_size - * | | - * v v - * <----------------- control_size(n_slot) ----------------> - * <-stub-> <----------- n_slot ----------> <-stub-> - * +--------+-------------------------------+-------+--------+ - * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | - * +--------+-------------------------------+-------+--------+ - **/ - } while (is_sentinel(*ctrl_) - && (*(ctrl_ + c_group_size) != c_iterator_bookend)); - - return *this; - } - - DArenaHashMapIterator & operator--() { - /* simpler than forward iteration, since bookend immediately - * precedes control byte for first slot - */ - do { - --ctrl_; - --pos_; - } while (is_sentinel(*ctrl_) - && (*ctrl_ != c_iterator_bookend)); - - return *this; - } - - private: - uint8_t * ctrl_ = nullptr; - value_type * pos_ = nullptr; - }; - - } - /** @brief flat hash map of key-value pairs using dedicated DArenas for storage * * Replicates (to the extent feasible) std::unordered_map diff --git a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp new file mode 100644 index 00000000..285e4492 --- /dev/null +++ b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp @@ -0,0 +1,82 @@ +/** @file DArenaHashMapIterator.hpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "hashmap/DArenaHashMapUtil.hpp" + +namespace xo { + namespace mm { + namespace detail { + + template + struct DArenaHashMapIterator : public DArenaHashMapUtil { + using value_type = std::pair; + + public: + DArenaHashMapIterator(uint8_t * c, value_type * p) + : ctrl_{c}, pos_{p} {} + + value_type & operator*() const { return *pos_; } + value_type * operator->() const { return pos_; } + + /** true iff iterator at sentinel position (not dereferencable state !) **/ + bool _at_slot_sentinel() const { return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); } + + bool operator==(const DArenaHashMapIterator & x) const { + return this->pos_ == x.pos_; + } + + bool operator!=(const DArenaHashMapIterator & x) const { + return this->pos_ != x.pos_; + } + + DArenaHashMapIterator & operator++() { + do { + ++ctrl_; + ++pos_; + + /** end condition: iterator ends at last non-wrapped position. + * relyin on bookend sentinel values at known offset from 'wrap' section + * + * ctrl_ ctrl_ + c_group_size + * | | + * v v + * <----------------- control_size(n_slot) ----------------> + * <-stub-> <----------- n_slot ----------> <-stub-> + * +--------+-------------------------------+-------+--------+ + * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | + * +--------+-------------------------------+-------+--------+ + **/ + } while (is_sentinel(*ctrl_) + && (*(ctrl_ + c_group_size) != c_iterator_bookend)); + + return *this; + } + + DArenaHashMapIterator & operator--() { + /* simpler than forward iteration, since bookend immediately + * precedes control byte for first slot + */ + do { + --ctrl_; + --pos_; + } while (is_sentinel(*ctrl_) + && (*ctrl_ != c_iterator_bookend)); + + return *this; + } + + private: + uint8_t * ctrl_ = nullptr; + value_type * pos_ = nullptr; + }; + + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DArenaHashMapIterator.hpp */ From 29cf7d4f79bff06be1a616ea565f40984a32e107 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 8 Jan 2026 18:54:39 -0500 Subject: [PATCH 065/111] xo-arena: DArenaHashMap move impl to xo::map namespace --- include/xo/arena/DArenaHashMap.hpp | 4 ++-- include/xo/arena/hashmap/ControlGroup.hpp | 4 ++-- include/xo/arena/hashmap/DArenaHashMapIterator.hpp | 4 ++-- include/xo/arena/hashmap/DArenaHashMapUtil.hpp | 4 ++-- include/xo/arena/hashmap/HashMapStore.hpp | 14 ++++++++------ utest/DArenaHashMap.test.cpp | 4 ++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 63741e66..2a51eaa8 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -16,7 +16,7 @@ #include namespace xo { - namespace mm { + namespace map { #ifdef NOT_YET enum class insert_error : int32_t { /** sentinel **/ @@ -654,7 +654,7 @@ namespace xo { return true; } - } + } /*namespace map*/ } /*namespace xo*/ /* end DArenaHashMap.hpp */ diff --git a/include/xo/arena/hashmap/ControlGroup.hpp b/include/xo/arena/hashmap/ControlGroup.hpp index d5db66af..7d3a129e 100644 --- a/include/xo/arena/hashmap/ControlGroup.hpp +++ b/include/xo/arena/hashmap/ControlGroup.hpp @@ -11,7 +11,7 @@ #include namespace xo { - namespace mm { + namespace map { namespace detail { /** @brief 16x 8-bit control bytes. * @@ -77,7 +77,7 @@ namespace xo { #endif }; } - } /*namespace mm*/ + } /*namespace map*/ } /*namespace xo*/ /* end ControlGroup.hpp */ diff --git a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp index 285e4492..57573b49 100644 --- a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp +++ b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp @@ -8,7 +8,7 @@ #include "hashmap/DArenaHashMapUtil.hpp" namespace xo { - namespace mm { + namespace map { namespace detail { template namespace xo { - namespace mm { + namespace map { /** @class DArenaHashMapUtil * * @pre @@ -103,7 +103,7 @@ namespace xo { return std::make_pair(ngx, ng);; } }; - } /*namespace mm*/ + } /*namespace map*/ } /*namespace xo*/ /* end DArenaHashMapUtil.hpp */ diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 130f4f54..545aa1c9 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -9,7 +9,7 @@ #include "hashmap/ControlGroup.hpp" namespace xo { - namespace mm { + namespace map { namespace detail { template @@ -17,6 +17,8 @@ namespace xo { public: using value_type = std::pair; using group_type = detail::ControlGroup; + using control_vector_type = xo::mm::DArenaVector; + using slot_vector_type = xo::mm::DArenaVector; public: /** group_exp2: number of groups {x, 2^x} **/ @@ -26,8 +28,8 @@ namespace xo { n_group_exponent_{group_exp2.first}, n_group_{group_exp2.second}, n_slot_{group_exp2.second * c_group_size}, - control_{DArenaVector::map(ArenaConfig{.size_ = control_size(n_slot_)})}, - slots_{DArenaVector::map(ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} + control_{control_vector_type::map(xo::mm::ArenaConfig{.size_ = control_size(n_slot_)})}, + slots_{slot_vector_type::map(xo::mm::ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} { /* here: arenas have allocated address range, but no committed memory yet */ @@ -121,12 +123,12 @@ namespace xo { /** control_[] partitioned into groups of * c_group_size (16) consecutive elements **/ - DArenaVector control_; + control_vector_type control_; /** slots_[] holds {key,value} pairs **/ - DArenaVector slots_; + slot_vector_type slots_; }; } - } /*namespace mm*/ + } /*namespace map*/ } /*namespace xo*/ /* end HashMapStore.hpp */ diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 757cd39a..088edf22 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -11,8 +11,8 @@ #include namespace xo { - using xo::mm::DArenaHashMapUtil; - using xo::mm::DArenaHashMap; + using xo::map::DArenaHashMapUtil; + using xo::map::DArenaHashMap; using xo::rng::random_seed; using xo::rng::xoshiro256ss; using utest::UtestTools; From 016ddf730f706992e55856554f3c142f1f764af0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 9 Jan 2026 17:48:54 -0500 Subject: [PATCH 066/111] xo-objectd2 xo-printable xo-facet: pp working for List(Integer) Also streamline facet switching --- utest/random_hash_ops.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utest/random_hash_ops.hpp b/utest/random_hash_ops.hpp index 52d443fa..affc56d9 100644 --- a/utest/random_hash_ops.hpp +++ b/utest/random_hash_ops.hpp @@ -76,6 +76,10 @@ namespace utest { * * test function should use REQUIRE_ORCAPTURE() / REQUIRE_ORFAIL(). * It should *not* use REQUIRE() or CHECK(). + * + * @p test_name banner for initial log message (only printed on 2nd pass) + * @p test_fn function to invoke test pass. + * @p n test size/id (cosmetic - printed in log messages) **/ static inline bool bimodal_test(std::string test_name, std::function test_fn, From 0366136e8239abee3d7a8d2017b6a6ea801f7189 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 12:29:12 -0500 Subject: [PATCH 067/111] xo-arena: + insert/erase in DArenaVector --- include/xo/arena/DArenaVector.hpp | 87 +++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index ea1557f1..a5834ea2 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -53,18 +53,23 @@ namespace xo { /** create empty vector using @p cfg to configure backing store **/ static DArenaVector map(const ArenaConfig & cfg); + /** true iff vector is emtpy **/ bool empty() const { return size_ == 0; } size_type size() const { return size_; } size_type max_size() const { return capacity(); } size_type capacity() const { return store_.reserved() / sizeof(T); } - T & operator[](size_t i) { return *(this->_address_of(i)); } - const T & operator[](size_t i) const { return *(this->_address_of(i)); } + /** get reference to element at zero-based index @p i. Do not check bounds **/ + T & operator[](size_t i) noexcept { return *(this->_address_of(i)); } + const T & operator[](size_t i) const noexcept { return *(this->_address_of(i)); } + /** get reference to element at zero-based index @p i. Do check bounds **/ T & at(size_type i) { _check_valid_index(i); return *(this->_address_of(i)); } const T & at(size_type i) const { _check_valid_index(i); return *(this->_address_of(i)); } + /** get to at first element of vector. Same as @p end if vector is empty **/ iterator begin() noexcept { return this->_address_of(0); } + /** get iterator to end of vector - "one past the last element" **/ iterator end() noexcept { return this->_address_of(size_); } const_iterator cbegin() const noexcept { return this->_address_of(0); } const_iterator begin() const noexcept { return this->cbegin(); } @@ -74,11 +79,20 @@ namespace xo { constexpr T * data() { return reinterpret_cast(store_.lo_); } constexpr const T * data() const { return reinterpret_cast(store_.lo_); } + /** reserve space, if possible, for at least @p z elements. + * Always limited by ArenaConfig.size_ + **/ void reserve(size_type z); void resize(size_type z); void shrink_to_fit(); + /** reset vector to empty state **/ void clear(); + T & insert(size_type pos, T && x); + T & insert(size_type pos, const T & x); + + void erase(size_type pos); + void push_back(T && x); void push_back(const T & x); @@ -211,19 +225,84 @@ namespace xo { throw std::out_of_range("DArenaVector index out of bounds"); } + template + T & + DArenaVector::insert(size_type pos, T && x) { + { + size_type new_z = size_ + 1; + size_type req_z = new_z * sizeof(T); + + store_.expand(req_z); + } + + // move elements [i .. z-1] right by one position. + // must proceed in reverse order! + for (size_type ip1 = size_; ip1 > pos; --ip1) { + (*this)[ip1] = std::move((*this)[ip1-1]); + } + + T * addr = this->_address_of(pos); + + new (addr) T{std::move(x)}; + + this->size_ = size_ + 1; + + return *addr; + } + + template + T & + DArenaVector::insert(size_type pos, const T & x) { + { + size_type new_z = size_ + 1; + size_type req_z = new_z * sizeof(T); + + store_.expand(req_z); + } + + // move elements [i .. z-1] right by one position. + // must proceed in reverse order! + for (size_type ip1 = size_; ip1 > pos; --ip1) { + (*this)[ip1] = std::move((*this)[ip1-1]); + } + + T * addr = this->_address_of(pos); + + new (addr) T{x}; + + this->size_ = size_ + 1; + + return *addr; + } + + template + void + DArenaVector::erase(size_type pos) { + // move elements [pos+1 .. z-1] left by one position. + + if (pos >= size_) [[unlikely]] + return; + + for (size_type i = pos; i+1 < size_; ++i) { + (*this)[i] = std::move((*this)[i+1]); + } + + --(this->size_); + } + template void DArenaVector::push_back(T && x) { size_type z = size_ + 1; size_type req_z = z * sizeof(T); - store_.expand(req_z); + this->store_.expand(req_z); T * addr = this->_address_of(size_); new (addr) T{std::move(x)}; - size_ = z; + this->size_ = z; } template From 15859d1430fb2f0eebfdb5b1edc813084498cb26 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 12:30:46 -0500 Subject: [PATCH 068/111] xo-arena: refactor: split DArenaVector to prep for circular buffer --- include/xo/arena/DArena.hpp | 2 + include/xo/arena/DArenaHashMap.hpp | 4 + include/xo/arena/mmap_util.hpp | 49 +++++ include/xo/arena/span.hpp | 304 +++++++++++++++++++++++++++++ src/arena/CMakeLists.txt | 2 + src/arena/DArena.cpp | 102 +--------- 6 files changed, 369 insertions(+), 94 deletions(-) create mode 100644 include/xo/arena/mmap_util.hpp create mode 100644 include/xo/arena/span.hpp diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index a3253220..8eb6125c 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -115,6 +115,7 @@ namespace xo { **/ bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); } +#ifdef OBSOLETE /** obtain uncommitted contiguous memory range comprising * a whole multiple of @p align_z bytes, of at least size @p req_z, * aligned on a @p align_z boundary. Uncommitted memory is not (yet) @@ -140,6 +141,7 @@ namespace xo { static range_type map_aligned_range(size_type req_z, size_type align_z, bool enable_hugepage_flag); +#endif /** true if arena is mapped i.e. has a reserved address range **/ bool is_mapped() const noexcept { return (lo_ != nullptr) && (hi_ != nullptr); } diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 2a51eaa8..aef9ed3a 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -65,6 +65,10 @@ namespace xo { size_type capacity() const noexcept { return store_.capacity(); } float load_factor() const noexcept { return store_.load_factor(); } + /** verify DArenaHashMap invariants + * Act on failure according to policy @p + * (combination of throw|log bits) + **/ bool verify_ok(verify_policy p = verify_policy::throw_only()) const; iterator begin() { diff --git a/include/xo/arena/mmap_util.hpp b/include/xo/arena/mmap_util.hpp new file mode 100644 index 00000000..64cd4d25 --- /dev/null +++ b/include/xo/arena/mmap_util.hpp @@ -0,0 +1,49 @@ +/** @file mmap_util.hpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "span.hpp" + +namespace xo { + namespace mm { + struct mmap_util { + using byte = std::byte; + using span_type = span; + using size_type = std::size_t; + + /** obtain uncommitted contiguous memory range comprising + * a whole multiple of @p align_z bytes, of at least size @p req_z, + * aligned on a @p align_z boundary. Uncommitted memory is not (yet) + * backed by physical memory. + * + * If @p enable_hugepage_flag is true and THP + * (transparent huge pages) are available, use THP for arena memory. + * This relieves TLB and page table memory when @p req_z is a lot larger than + * page size (likely 4KB). Cost is that arena will consum physical memory in unit + * of @p align_z. Arena may waste up to @p align_z bytes of memory as a result. + * + * If @p enable_hugepage_flag is true, @p align_z should be huge page size + * (probably 2MB) for optimal performance. + * + * At present the THP feature is not supported on OSX. + * May be supportable through mach_vm_allocate(). + * + * Note that we reject MAP_HUGETLB|MAP_HUGE_2MB flags to mmap here, + * since requires previously-reserved memory in /proc/sys/vm/nr_hugepages. + * + * Write log messages iff @p debug_flag is true. + * + * @return spqn giving reserved memory address range [lo,hi) + **/ + static span_type map_aligned_range(size_type req_z, + size_type align_z, + bool enable_hugepage_flag, + bool debug_flag); + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end mmap_util.hpp */ diff --git a/include/xo/arena/span.hpp b/include/xo/arena/span.hpp new file mode 100644 index 00000000..e902193f --- /dev/null +++ b/include/xo/arena/span.hpp @@ -0,0 +1,304 @@ +/** @file span.hpp + * + * @author Roland Conybeare, Jul 2024 + **/ + +#pragma once + +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/ppdetail_atomic.hpp" +#include +#include +#include + +namespace xo { + namespace mm { + /** @class span compression/span.hpp + * + * @brief A contiguous range of characters, without ownership. + * + * @tparam CharT type for elements referred to by this span. + **/ + template + class span { + public: + /** @defgroup span-type-traits span type traits **/ + ///@{ + + /** typealias for span size (in units of CharT) **/ + using size_type = std::uint64_t; + + ///@} + + public: + /** @defgroup span-ctors span constructors **/ + ///@{ + + /** null span **/ + span() : lo_{nullptr}, hi_{nullptr} {} + + /** Create span for the contiguous memory range [@p lo, @p hi) **/ + span(CharT * lo, CharT * hi) : lo_{lo}, hi_{hi} {} + + /** Create span for the contiguous memory range [@p lo, @p lo + z) **/ + span(CharT * lo, size_t z) : lo_{lo}, hi_{lo + z} {} + + /** explicit conversion from span **/ + template + span(const span & other, + std::enable_if_t + && !std::is_same_v> * = nullptr) + : lo_{other.lo()}, hi_{other.hi()} {} + + /** copy ctor (explicit to avoid ambiguity with template ctor) **/ + span(const span & other) = default; + span & operator=(const span & other) = default; + + /** Create a null span (i.e. with null @p lo, @p hi pointers) + * A null span can be concatenated with any other span + * without triggering matching-endpoint asserts. + **/ + static span make_null() { return span(static_cast(nullptr), static_cast(nullptr)); } + + /** @brief create span for C-style string @p cstr **/ + static span from_cstr(const CharT * cstr) { + CharT * lo = cstr; + CharT * hi = cstr ? cstr + strlen(cstr) : nullptr; + + return span(lo, hi); + } + + /** @brief create span from std::string @p str **/ + static span from_string(const std::string& str) { + CharT * lo = &(*str.begin()); + CharT * hi = &(*str.end()); + + return span(lo, hi); + } + + /** @brief concatenate two contiguous spans */ + static span concat(const span & span1, const span & span2) { + if (span1.is_null()) + return span2; + if (span2.is_null()) + return span1; + + if (span1.hi() != span2.lo()) { + scope log(XO_DEBUG(true)); + + log && log(xtag("span1.hi", (void*)span1.hi()), xtag("span2.lo", (void*)span2.lo())); + } + + assert(span1.hi() == span2.lo()); + + CharT * lo = span1.lo(); + CharT * hi = span2.hi(); + + return span(lo, hi); + } + + ///@} + + /** @defgroup span-access-methods **/ + ///@{ + + CharT * lo() const { return lo_; } /* get member span::lo_ */ + CharT * hi() const { return hi_; } /* get member span::hi_ */ + + /** true iff this span is null. distinct from empty. **/ + bool is_null() const { return lo_ == nullptr && hi_ == nullptr; } + /** true iff this span is empty (comprises 0 elements). **/ + bool empty() const { return lo_ == hi_; } + /** report the number of elements (of type CharT) in this span. **/ + size_type size() const { return hi_ - lo_; } + + /** true iff this span is a subspan of @p other. + * i.e. other.lo() <= this->lo() && this->hi() <= other.hi() + **/ + bool is_subspan_of(const span & other) const noexcept { + return (other.lo() <= lo_) && (hi_ <= other.hi()); + } + + ///@} + + /** @defgroup span-general-methods **/ + ///@{ + + /** @brief strip prefix until first occurence of '\n', including the newline **/ + void discard_until_newline() { + for (const CharT * p = lo_; p < hi_; ++p) { + if (*p == '\n') { + lo_ = p + 1; + return; + } + } + + lo_ = hi_; + } + + /** Create new span over supplied type, + * with identical (possibly misaligned) endpoints. + * + * @warning + * 1. New span uses exactly the same memory addresses. + * Endpoint pointers may not be aligned. + * 2. Implementation assumes code compiled with + * @code -fno-strict-aliasing @endcode enabled. + * + * @tparam OtherT element type for new span + **/ + template + span + cast() const { return span(reinterpret_cast(lo_), + reinterpret_cast(hi_)); } + + /** @brief create span including the first @p z members of this span. **/ + span prefix(size_type z) const { return span(lo_, lo_ + z); } + + /** @brief create span representing prefix up to (but not including) @p *p + **/ + span prefix_upto(CharT * p) const { + if (p <= hi_) + return span(lo_, p); + else + return span(lo_, hi_); + } + + /** @brief create span with first @p z members of this span removed **/ + span after_prefix(size_type z) const { + if (lo_ + z > hi_) + z = hi_ - lo_; + + return span(lo_ + z, hi_); + } + + /** @brief create span with @p prefix of this span removed **/ + span after_prefix(const span & prefix) const { + if (!prefix.is_null() && (prefix.lo() != lo_)) { + throw std::runtime_error + ("after_prefix: expected prefix of this span"); + } + + return after_prefix(prefix.size()); + } + + /** Create span starting with position @p p. + * Does boundary checking; will return empty span if @p p is outside @c [lo_,hi) + **/ + span suffix_from(CharT * p) const { + if ((lo_ <= p) && (p <= hi_)) + return span(p, hi_); + else + return span(hi_, hi_); + } + + /** increase extent of this spans to include @p x. + * Requires @c hi() == @c x.lo() + **/ + span & operator+=(const span & x) { + if (hi_ == x.lo_) { + hi_ = x.hi_; + } else if (!x.is_null()) { + assert(false); + } + + return *this; + } + + /** print representation for this span on stream @p os **/ + void print(std::ostream & os) const { + os << ""; + } + ///@} + + private: + /** @defgroup span-instance-vars **/ + ///@{ + + /** start of span. + Span comprises memory address between @p lo (inclusive) and @p hi (exclusive) + **/ + CharT * lo_ = nullptr; + + /** @brief end of span. + Span comprises memory address between @p lo (inclusive) and @p hi (exclusive) + **/ + CharT * hi_ = nullptr; + + ///@} + }; /*span*/ + + /** @defgroup span-operators **/ + ///@{ + + /** compare spans for equality. + * Two spans are equal iff both endpoints match exactly. + **/ + template + inline bool + operator==(const span & lhs, const span & rhs) { + return ((lhs.lo() == rhs.lo()) + && (lhs.hi() == rhs.hi())); + } + + /** compare spans for inequality. + * Two spans are unequal if either paired endpoint differs. + **/ + template + inline bool + operator!=(const span & lhs, const span & rhs) { + return ((lhs.lo() != rhs.lo()) + || (lhs.hi() != rhs.hi())); + } + + /** print a summary of @p x on stream @p os. Intended for diagnostics **/ + template + inline std::ostream & + operator<<(std::ostream & os, + const span & x) { + x.print(os); + return os; + } + + ///@} + } /*namespace scm*/ + + namespace print { + template + class printspan_impl { + public: + printspan_impl(xo::mm::span x) : span_{x} {} + + xo::mm::span span_; + }; + + template + printspan_impl printspan(const xo::mm::span& span) { + return printspan_impl(span); + } + + template + inline std::ostream & + operator<< (std::ostream & os, + const printspan_impl & x) + { + for (const CharT * p = x.span_.lo(); p < x.span_.hi(); ++p) + os << *p; + + return os; + } + +#ifndef ppdetail_atomic + template \ + PPDETAIL_ATOMIC_BODY(printspan_impl); + + template \ + PPDETAIL_ATOMIC_BODY(xo::scm::span); +#endif + + } /*namespace mm*/ +} /*namespace xo*/ diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 111bbe15..5288c393 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -3,10 +3,12 @@ set(SELF_LIB xo_arena) set(SELF_SRCS cmpresult.cpp + mmap_util.cpp AllocError.cpp AllocInfo.cpp DArena.cpp DArenaIterator.cpp + DCircularBuffer.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index ee03ddea..670666f5 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -6,6 +6,7 @@ //#include "alloc/AAllocator.hpp" #include "DArena.hpp" #include "DArenaIterator.hpp" +#include "mmap_util.hpp" #include #include #include @@ -22,94 +23,6 @@ namespace xo { using std::size_t; namespace mm { - auto - DArena::map_aligned_range(size_t req_z, - size_t align_z, - bool enable_hugepage_flag) -> range_type - { - scope log(XO_DEBUG(false), - xtag("req_z", req_z), xtag("align_z", align_z)); - - // 1. round up to multiple of align_z - size_t target_z = padding::with_padding(req_z, align_z); // 4. - - // 2. mmap() will give us page-aligned memory, - // but not hugepage-aligned. - // - // Over-request by align_z to ensure - // aligned subrange of size target_z - // - byte * base = (byte *)(::mmap(nullptr, - target_z + align_z, - PROT_NONE, - MAP_PRIVATE | MAP_ANONYMOUS, - -1, 0)); - - // on mmap success: upper limit of mapped address range - byte * hi = base + (target_z + align_z); - // lowest hugepage-aligned address in [base, hi) - byte * aligned_base = (byte *)(padding::with_padding((size_t)base, align_z)); - // end of hugeppage-aligned range starting at aligned_base - byte * aligned_hi = aligned_base + target_z; - - log && log("acquired memory [lo,hi) using mmap", - xtag("lo", base), - xtag("aligned_lo", aligned_base), - xtag("req_z", req_z), - xtag("target_z", target_z), - xtag("aligned_hi", aligned_hi), - xtag("hi", hi)); - - // 3. assess mmap success - { - if (base == MAP_FAILED) { - throw std::runtime_error(tostr("ArenaAlloc: uncommitted allocation failed", - xtag("size", req_z))); - } - - assert((size_t)aligned_base % align_z == 0); - assert(aligned_base >= base); - assert(aligned_base < base + align_z); - } - - // 4. release unaligned prefix - if (base < aligned_base) { - size_t ua_prefix = aligned_base - base; - - ::munmap(base, ua_prefix); - } - - // 5. release unaligned suffix - if (aligned_hi < hi) { - size_t suffix = hi - aligned_hi; - - ::munmap(aligned_hi, suffix); - } - - if (enable_hugepage_flag) { -#ifdef __linux__ - /** linux: - * opt-in to transparent huge pages (THP) - * provided OS configured to support them. - * otherwise fallback gracefully. - * - * Huge pages -> use fewer TLB entries + faster - * shorter path through page table. - * - * When we commit (i.e. obtain physical memory on page fault), - * typically expect to pay ~1us per superpage. - * Much better than ~500us to commit 512 4k VM pages. - * - * But wasted if we don't use the memory. - * - * Page table has a handful of levels - **/ - ::madvise(aligned_base, target_z, MADV_HUGEPAGE); // 8. -#endif - } - - return std::make_pair(aligned_base, aligned_hi); - } DArena DArena::map(const ArenaConfig & cfg) @@ -130,14 +43,15 @@ namespace xo { log && log(xtag("page_z", page_z), xtag("align_z", align_z)); - auto [lo, hi] = map_aligned_range(cfg.size_, - align_z, - enable_hugepage_flag); + auto span = mmap_util::map_aligned_range(cfg.size_, + align_z, + enable_hugepage_flag, + cfg.debug_flag_); - if (!lo) { + if (!span.lo()) { // control here implies mmap() failed silently - throw std::runtime_error(tostr("ArenaAlloc: allocation failed", + throw std::runtime_error(tostr("ArenaAlloc: reserve address range failed", xtag("size", cfg.size_))); } @@ -148,7 +62,7 @@ namespace xo { xtag("hugepage_z", hugepage_z_)); #endif - return DArena(cfg, page_z, align_z, lo, hi); + return DArena(cfg, page_z, align_z, span.lo(), span.hi()); } /*map*/ DArena::DArena(const ArenaConfig & cfg, From f0eeb28e75745839b1942f7530d9acf1c009f9ad Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 12:32:29 -0500 Subject: [PATCH 069/111] xo-arena: + DCircularBuffer + CircularBufferConfig --- include/xo/arena/CircularBufferConfig.hpp | 62 ++++ include/xo/arena/DCircularBuffer.hpp | 254 ++++++++++++++++ src/arena/DCircularBuffer.cpp | 352 ++++++++++++++++++++++ src/arena/mmap_util.cpp | 105 +++++++ 4 files changed, 773 insertions(+) create mode 100644 include/xo/arena/CircularBufferConfig.hpp create mode 100644 include/xo/arena/DCircularBuffer.hpp create mode 100644 src/arena/DCircularBuffer.cpp create mode 100644 src/arena/mmap_util.cpp diff --git a/include/xo/arena/CircularBufferConfig.hpp b/include/xo/arena/CircularBufferConfig.hpp new file mode 100644 index 00000000..710812a4 --- /dev/null +++ b/include/xo/arena/CircularBufferConfig.hpp @@ -0,0 +1,62 @@ +/** @file CircularBufferConfig.hpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + /** @class CircularBufferConfig + * + * @brief configuration for a @ref DCircularBuffer instance + **/ + struct CircularBufferConfig { + /** @defgroup mm-circularbufferconfig-instance-vars CircularBufferConfig members **/ + ///@{ + + /** optional name, for diagnostics **/ + std::string name_; + /** hard maximum buffer size = reserved virtual memory. + * Buffer will generally map much less than this amount of memory + **/ + std::size_t max_capacity_ = 0; + /** hugepage size -- using huge pages relieves some TLB pressure, + * at expense of inefficient memory consumption for (up to two) + * partially used superpages. + **/ + std::size_t hugepage_z_ = 2 * 1024 * 1024; + /** Threshold 'move efficeincy' = (move_distance / move_qty) + * applies to moving unread input to the beginning of mapped range, + * when not prevented by pinned ranges. + * + * Higher numbers reduce cpu consumption but increase memory consumption + * Reciprocal loose ceiling on relative effort that may be spent on + * moving fractional input + **/ + float threshold_move_efficiency_ = 50.0; + /** lower bound for hard maximum number of capture spans. + * + * Expected use case is to track spans that are currently referenced + * (rather than copied) from outside a DCircularBuffer instance. + * Circular buffer will not unmap or overwrite memory for such spans. + * + * Expect to generally release captured spans in the same order they + * were captured. Out of order release is supported, but cost + * of out-of-order release grows + * like O(r) for r remembered spans. + * + * A typical parser will need spans to remember one line of input + **/ + std::size_t max_captured_span_ = 0; + /** true to enable debug logging **/ + bool debug_flag_ = false; + + ///@} + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end CircularBufferConfig.hpp */ diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp new file mode 100644 index 00000000..17bd6b7a --- /dev/null +++ b/include/xo/arena/DCircularBuffer.hpp @@ -0,0 +1,254 @@ +/** @file DCircularBuffer.hpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#include "CircularBufferConfig.hpp" +#include "DArenaVector.hpp" +#include "hashmap/verify_policy.hpp" +#include "span.hpp" +#include + +namespace xo { + namespace mm { + /** @class DCircularBuffer + * + * @brief high performance vm-aware circular buffer + * + * Circular buffer implementation with parsing-friendly performance features. + * - generalization of DArena. + * Like DArena, maps superpages as needed. + * Unlike DArena memory at the beginning of reserved range can be unmapped. + * - allows address range >> physical range + * - admits "Cheney on the MTA" strategy. + * May be feasible to reserve a lifetime address range (say 1TB) + * as long as buffer only every maps a subrange that fits in physical memory. + * - zero copy support for parsing / protocol trnaslation: + * provides capture/release semantics for a fixed number + * of remembered spans. Will never unmap memory for a remembered span, + * until that span is released. + * - automatically resets to beginning of reserved range + * whenever occupied range is empty + **/ + struct DCircularBuffer { + public: + /** @defgroup mm-circularbuffer-types CircularBuffer type traits **/ + ///@{ + + /** an amount of memory **/ + using size_type = std::size_t; + using byte = std::byte; + /** a contiguous addres range **/ + using span_type = span; + using const_span_type = span; + + ///@} + + public: + /** @defgroup mm-cicrularbuffer-ctors CircularBuffer constructors **/ + ///@{ + + /** contruct instance + * @p config circular buffer configuration + * @p page_z o/s page size (via getpagesize()) + * @p buffer_align_z alignment for buffer memory + * @p reserved_range reserved virtual address range + **/ + DCircularBuffer(const CircularBufferConfig & config, + size_type page_z, + size_type buffer_align_z, + span_type reserved_range); + /** constructor */ + DCircularBuffer(const CircularBufferConfig & config); + /** non-copyable **/ + DCircularBuffer(const DCircularBuffer & other) = delete; + /** move ctor **/ + DCircularBuffer(DCircularBuffer && other); + + /** + * allocate virtual memory address (uncommitted!) for circular buffer + * with configuration @p config. + **/ + static DCircularBuffer map(const CircularBufferConfig & config); + + ///@} + + /** @defgroup mm-circularbuffer-const-methods CircularBuffer const methods **/ + ///@{ + + const_span_type reserved_range() const noexcept { return reserved_range_; } + const_span_type mapped_range() const noexcept { return mapped_range_; } + const_span_type occupied_range() const noexcept { return occupied_range_; } + + /** verify DCircularBuffer invariants. + * Act on failure according to policy @p p + * (combination of throw|log bits) + * + * verify invariants: + * CB1: mapped_range_ is subrange of reserved_range_ + * CB2: occupied_range_ is subrange of mapped_range_ + * CB3: each remembered_spans_[i] is subrange of occupied_range_ + * CB4: buffer_align_z_ > 0 when buffer is mapped + * CB5: reserved_range_.lo() aligned on buffer_align_z_ boundary + **/ + bool verify_ok(verify_policy p = verify_policy::throw_only()) const; + + ///@} + + /** @defgroup mm-circularbuffer-nonconst-methods CircularBuffer non-const methods **/ + ///@{ + + /** copy memory in span @p r into buffer starting at the end of + * @ref occupied_range_. Map new physical memory as needed. + * On success returns empty suffix of @p r. + * If buffer memory exhausted, may copy a prefix of @p r. + * In that case returns the remaining suffix of @p r. + **/ + span_type append(span_type r); + + /** DMA version of @ref append_span : get mapped span A at which + * buffer will receive new content. Upstream may write into + * A. It must then coordinate with buffer by calling + * @ref report_append(P) for some prefix P of A + * + * Example: + * @code + * CircularBuffer buf = ...; + * constexpr size_type z = 64*1024; + * auto span = buf.get_append_span(z); + * ssize_t nr = read(FD, span.lo(), span.size()); + * if (nr > 0) + * buf.report_append(span.prefix(nr)); + * @endcode + **/ + span_type get_append_span(size_type desired_z); + + /** update bookkeeping as if caller had invoked append(r); + * however caller has already written to mapped memory + * after using get_append_span(); so omit copy + **/ + void report_append(span_type r); + + /** expand hi end of mapped memory range to at least @p hi. + * + * Require: @p hi < @ref reserved_range_.hi + **/ + bool expand_to(byte * hi); + + /** consume span (or prefix thereof) previously obtained from @ref occupied_range() + * Caller represents that it won't need to read this memory again + * unless overlaps with a pinned span. + **/ + void consume(span_type r); + + /** pin memory range @p r. circular buffer will not touch + * addresses that appear in any pinned range. + * use to + **/ + void pin_range(span_type r); + + /** unwind a previous pin_range call on range @p r. + * both start and end or @p r should exactly match a pinned range. + **/ + void unpin_range(span_type r); + + ///@} + + private: + + /** @defgroup mm-circularbuffer-private-methods CircularBuffer non-const methods **/ + ///@{ + + /** shrink occupied rnage to the smallest contiguous range that contains both: + * all of .input_range_, and all pinned ranges in .pinned_spans_ + **/ + void _shrink_occupied_to_fit(); + + /** check for edge condition in which there are no pinned ranges. **/ + void _check_reset_map_start(); + + ///@} + + private: + /** @defgroup mm-circularbuffer-instance-vars CircularBuffer member variables **/ + ///@{ + + /* memory layout + * + * reserved_range_ : entire address range owned by buffer (may be huge, e.g., 1TB) + * mapped_range_ : subrange backed by physical memory (fits in RAM) + * occupied_range_ : subrange currently containing data + * input_range_ : subrange containing unread input + * pinned_spans_ : pinned subranges within occupied (prevents alteration or unmap) + * + * <------------------- .reserved_range ---------------------> + * . <------------- .mapped_range -------------> . + * . . <----- .occupied_range -----> . . + * . . . <- .input_range -----> . . + * . . . . . . . + * ........------XXXXXXXIIIIIIIIIIIIIIIIIIIIII--------........ + * pp ppp pp + * Legend: + * [.] reserved : uncommitted memory. may be huge (e.g. 1TB) + * [-] mapped : range backed by physical memory + * [X] consumed : preserved until last overlapping pin removed + * [I] input : unread content, waiting to be read + * [p] pinned : pinned memory will not be altered (let alone unmapped) + * + * Invariants: + * - .input_range <= .occupied_range <= .mapped_range <= .reserved_range + * - mapped_range_ cannot shrink to exclude any portion of a pinned span + */ + + /** buffer configuration **/ + CircularBufferConfig config_; + + /** size of a VM page (obtained automatically via getpagesize()). 4k on ubuntu. 16k on osx **/ + size_type page_z_; + + /** alignment for buffer address range. + * In practice will be either page_z_ or config_.hugepage_z_ + **/ + size_type buffer_align_z_; + + /** Circular buffer owns address range defined by this span. + * Aligned on @ref buffer_align_z_. + * Always a whole number of @ref page_z_ or @ref config_.hugepage_z_ + **/ + span_type reserved_range_; + + /** buffer owns memory defined by this span. + * Always a subrange of reserved_range + * These addresses backed by physical memory. + * Always a whole number of @ref page_z_ or @ref config_.hugepage_z_ + **/ + span_type mapped_range_; + + /** currently occupied buffer memory. + * Always a subrange of @ref mapped_range_ + **/ + span_type occupied_range_; + + /** portion of occupied buffer memory waiting to be read. + * Always represents a subspan of @ref occupied_range_, with the same + * hi endpoint. + * conversely @ref consume shrinks @ref input_range_ by increasing its lo endpoint. + **/ + span_type input_range_; + + /** remembered spans. For anticipated use cases expect one vm page sufficient. + * Spans in this vector always represent subranges of @ref occupied_range_ + * + * @ref pinned_spans_ is confined to @ref occupied_range_. + * (In particular it's *not* confined to @ref input_range_) + * + * sorted on increasing span.lo() + **/ + DArenaVector pinned_spans_; + + ///@} + }; + } +} /*namespace xo*/ + +/* end DCircularBuffer.hpp */ diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp new file mode 100644 index 00000000..e4d31f46 --- /dev/null +++ b/src/arena/DCircularBuffer.cpp @@ -0,0 +1,352 @@ +/** @file DCircularBuffer.cpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#include "DCircularBuffer.hpp" +#include "mmap_util.hpp" +#include +#include +#include + +namespace xo { + namespace mm { + + DCircularBuffer::DCircularBuffer(DCircularBuffer && other) + : config_{other.config_}, + page_z_{other.page_z_}, + buffer_align_z_{other.buffer_align_z_}, + reserved_range_{other.reserved_range_}, + mapped_range_{other.mapped_range_}, + occupied_range_{other.occupied_range_}, + pinned_spans_{std::move(other.pinned_spans_)} + { + other.reserved_range_ = span_type(); + other.mapped_range_ = span_type(); + other.occupied_range_ = span_type(); + } + + DCircularBuffer + DCircularBuffer::map(const CircularBufferConfig & config) + { + scope log(XO_DEBUG(config.debug_flag_)); + + /* vm page size. 4KB (probably if linux) or 16KB (probably if osx) */ + size_t page_z = getpagesize(); + + bool enable_hugepage_flag = (config.max_capacity_ >= config.hugepage_z_); + + /* Align start of arena memory on this boundary. + * Will use THP (transparent huge pages) if available + * and arena size is at least as large as hugepage size (2MB, probably) + */ + size_t align_z = (enable_hugepage_flag ? config.hugepage_z_ : page_z); + + log && log(xtag("page_z", page_z), + xtag("align_z", align_z)); + + auto span = mmap_util::map_aligned_range(config.max_capacity_, + align_z, + enable_hugepage_flag, + config.debug_flag_); + + if (!span.lo()) { + throw std::runtime_error(tostr("DCircularBuffer: reserve address range failed", + xtag("size", config.max_capacity_))); + } + + return DCircularBuffer(config, page_z, align_z, span); + } + + DCircularBuffer::DCircularBuffer(const CircularBufferConfig & config, + size_type page_z, + size_type buffer_align_z, + span_type reserved_range) + : config_{config}, + page_z_{page_z}, + buffer_align_z_{buffer_align_z}, + reserved_range_{reserved_range}, + mapped_range_{reserved_range_.prefix(0)}, + occupied_range_{mapped_range_.prefix(0)}, + input_range_{occupied_range_.prefix(0)}, + pinned_spans_{} + { + } + + bool + DCircularBuffer::verify_ok(verify_policy policy) const + { + using xo::xtag; + + constexpr const char * c_self = "DCircularBuffer::verify_ok"; + scope log(XO_DEBUG(false)); + + /* CB1: mapped_range_ is subrange of reserved_range_ */ + if ((mapped_range_.lo() < reserved_range_.lo()) + || (mapped_range_.hi() > reserved_range_.hi())) + { + return policy.report_error(log, + c_self, ": expect mapped_range subset of reserved_range", + xtag("mapped.lo", (void*)mapped_range_.lo()), + xtag("mapped.hi", (void*)mapped_range_.hi()), + xtag("reserved.lo", (void*)reserved_range_.lo()), + xtag("reserved.hi", (void*)reserved_range_.hi())); + } + + /* CB2: occupied_range_ is subrange of mapped_range_ */ + if ((occupied_range_.lo() < mapped_range_.lo()) + || (occupied_range_.hi() > mapped_range_.hi())) + { + return policy.report_error(log, + c_self, ": expect occupied_range subset of mapped_range", + xtag("occupied.lo", (void*)occupied_range_.lo()), + xtag("occupied.hi", (void*)occupied_range_.hi()), + xtag("mapped.lo", (void*)mapped_range_.lo()), + xtag("mapped.hi", (void*)mapped_range_.hi())); + } + + /* CB3: each remembered span is subrange of occupied_range_ */ + for (size_type i = 0, n = pinned_spans_.size(); i < n; ++i) { + const const_span_type & pin = pinned_spans_[i]; + + if ((pin.lo() < occupied_range_.lo()) + || (pin.hi() > occupied_range_.hi())) + { + return policy.report_error(log, + c_self, ": expect remembered_span subset of occupied_range", + xtag("i", i), + xtag("pin.lo", (void*)pin.lo()), + xtag("pin.hi", (void*)pin.hi()), + xtag("occupied.lo", (void*)occupied_range_.lo()), + xtag("occupied.hi", (void*)occupied_range_.hi())); + } + } + + /* CB4: buffer_align_z_ is non-zero (when buffer is mapped) */ + if (!reserved_range_.is_null() && (buffer_align_z_ == 0)) { + return policy.report_error(log, + c_self, ": expect buffer_align_z > 0 when buffer is mapped", + xtag("buffer_align_z", buffer_align_z_)); + } + + /* CB5: reserved_range_ aligned on buffer_align_z_ boundary */ + if (!reserved_range_.is_null() && (buffer_align_z_ > 0)) { + if (((size_type)(reserved_range_.lo()) % buffer_align_z_) != 0) { + return policy.report_error(log, + c_self, ": expect reserved_range.lo aligned on buffer_align_z", + xtag("reserved.lo", (void*)reserved_range_.lo()), + xtag("buffer_align_z", buffer_align_z_)); + } + } + + return true; + } + + auto + DCircularBuffer::append(span_type src) -> span_type + { + span_type dest = get_append_span(src.size()); + + size_t copy_z = std::min(src.size(), dest.size()); + + ::memcpy(occupied_range_.hi(), src.lo(), copy_z); + + this->occupied_range_ += span_type(dest.lo(), copy_z); + + return src.after_prefix(copy_z); + } + + auto + DCircularBuffer::get_append_span(size_type desired_z) -> span_type + { + span_type dest = span_type(occupied_range_.hi(), desired_z); + + if (dest.hi() > reserved_range_.hi()) { + /* under no circumstances go past the end of reserved range */ + dest = span_type(dest.lo(), reserved_range_.hi()); + } + + /* establish mapped range at least to dest.hi */ + this->expand_to(dest.hi()); + + /* report available memory */ + return span_type(occupied_range_.hi(), mapped_range_.hi()); + } + + void + DCircularBuffer::report_append(span_type r) + { + if (r.lo() != occupied_range_.hi()) { + // error! + + // this->capture_error(error::bad_append_report, r.size()) + assert(false); + + return; + } + + if (r.hi() > mapped_range_.hi()) { + // error! + + // this->capture_error(error::bad_append_report, r.size()) + assert(false); + + return; + } + + this->occupied_range_ += r; + } + + void + DCircularBuffer::consume(span_type r) + { + if (r.lo() != input_range_.lo()) { + assert(false); + + return; + } + + if (r.hi() > occupied_range_.hi()) { + assert(false); + + return; + } + + if (occupied_range_.lo() < input_range_.lo()) { + /* here: a pinned range prevents shrinking occupied_range */ + + this->input_range_ = input_range_.suffix_from(r.hi()); + } else { + /* here: input; recompute occupied boundary */ + + this->input_range_ = input_range_.suffix_from(r.hi()); + + this->_shrink_occupied_to_fit(); + } + + this->_check_reset_map_start(); + } + + bool + DCircularBuffer::expand_to(byte * hi) { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (hi < mapped_range_.hi()) { + /* nothing todo */ + return true; + } + + size_t add_z = hi - mapped_range_.hi(); + size_t add_commit_z = padding::with_padding(add_z, buffer_align_z_); + byte * commit_start = mapped_range_.hi(); + + if (::mprotect(commit_start, + add_commit_z, + PROT_READ | PROT_WRITE) != 0) + { + if (log) { + log("commit failed"); + log(xtag("commit_start", commit_start), + xtag("add_z", add_z), + xtag("add_commit_z", add_commit_z)); + } + + // this->capture_error(error::commit_failed, add_commit_z); + return false; + } + + this->mapped_range_ += span(commit_start, add_commit_z); + return true; + } + + void + DCircularBuffer::pin_range(span_type r) + { + // loop optimized for case where r falls + // _after_ any existing pinned ranges + + size_type z = pinned_spans_.size(); + size_type ip1 = z; // ip1 = i + 1 + + for (; ip1 > 0; --ip1) { + if (r.lo() > pinned_spans_[ip1 - 1].lo()) + break; + + // insert at i to maintain sorted order + pinned_spans_.insert(ip1 - 1, r); + return; + } + + pinned_spans_.push_back(r); + } + + void + DCircularBuffer::unpin_range(span_type r) + { + // loop optimized for case where r + // is the first pinned range + + assert(pinned_spans_.size() > 0); + + if (r == pinned_spans_[0]) { + this->pinned_spans_.erase(0); + + /* removing pinned span means can perhaps shrink + * occupied range + */ + this->_shrink_occupied_to_fit(); + this->_check_reset_map_start(); + } else { + for (size_type i = 1; i < pinned_spans_.size(); ++i) { + if (r == pinned_spans_[i]) { + this->pinned_spans_.erase(i); + + /* since this isn't the first pinned span, + * won't be able to shrink occupied range. + */ + return; + } + } + } + } + + void + DCircularBuffer::_shrink_occupied_to_fit() + { + if (pinned_spans_.empty()) { + this->occupied_range_ = input_range_; + } else if (occupied_range_.lo() < pinned_spans_[0].lo()) { + this->occupied_range_ = occupied_range_.suffix_from(pinned_spans_[0].lo()); + } + } + + void + DCircularBuffer::_check_reset_map_start() + { + if (pinned_spans_.empty() + && (input_range_ == occupied_range_)) { + + // here: permissible to move input range to the beginning of mapped range. + // decide (heuristically) whether we think this is optimal + + std::size_t input_z = input_range_.size(); + + // 1st clause checks efficiency. + // 2nd clause (probably redundant) check non-overlapping + if ((input_range_.lo() > (mapped_range_.lo() + + std::max(page_z_, + static_cast(config_.threshold_move_efficiency_ * input_z)))) + && (mapped_range_.lo() + input_z < input_range_.lo())) { + + ::memmove(mapped_range_.lo(), input_range_.lo(), input_z); + + this->occupied_range_ = mapped_range_.prefix(input_z); + this->input_range_ = mapped_range_.prefix(input_z); + } + } + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DCircularBuffer.cpp */ diff --git a/src/arena/mmap_util.cpp b/src/arena/mmap_util.cpp new file mode 100644 index 00000000..37db7149 --- /dev/null +++ b/src/arena/mmap_util.cpp @@ -0,0 +1,105 @@ +/** @file mmap_util.cpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#include "mmap_util.hpp" +#include "padding.hpp" +#include // for mmap + +namespace xo { + namespace mm { + auto + mmap_util::map_aligned_range(size_t req_z, + size_t align_z, + bool enable_hugepage_flag, + bool debug_flag) -> span_type + { + scope log(XO_DEBUG(debug_flag), + xtag("req_z", req_z), + xtag("align_z", align_z)); + + // 1. round up to multiple of align_z + size_t target_z = padding::with_padding(req_z, align_z); // 4. + + // 2. mmap() will give us page-aligned memory, + // but not hugepage-aligned. + // + // Over-request by align_z to ensure + // aligned subrange of size target_z + // + byte * base = (byte *)(::mmap(nullptr, + target_z + align_z, + PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0)); + + // on mmap success: upper limit of mapped address range + byte * hi = base + (target_z + align_z); + // lowest hugepage-aligned address in [base, hi) + byte * aligned_base = (byte *)(padding::with_padding((size_t)base, align_z)); + // end of hugeppage-aligned range starting at aligned_base + byte * aligned_hi = aligned_base + target_z; + + log && log("acquired memory [lo,hi) using mmap", + xtag("lo", base), + xtag("aligned_lo", aligned_base), + xtag("req_z", req_z), + xtag("target_z", target_z), + xtag("aligned_hi", aligned_hi), + xtag("hi", hi)); + + // 3. assess mmap success + { + if (base == MAP_FAILED) { + throw std::runtime_error(tostr("ArenaAlloc: uncommitted allocation failed", + xtag("size", req_z))); + } + + assert((size_t)aligned_base % align_z == 0); + assert(aligned_base >= base); + assert(aligned_base < base + align_z); + } + + // 4. release unaligned prefix + if (base < aligned_base) { + size_t ua_prefix = aligned_base - base; + + ::munmap(base, ua_prefix); + } + + // 5. release unaligned suffix + if (aligned_hi < hi) { + size_t suffix = hi - aligned_hi; + + ::munmap(aligned_hi, suffix); + } + + if (enable_hugepage_flag) { +#ifdef __linux__ + /** linux: + * opt-in to transparent huge pages (THP) + * provided OS configured to support them. + * otherwise fallback gracefully. + * + * Huge pages -> use fewer TLB entries + faster + * shorter path through page table. + * + * When we commit (i.e. obtain physical memory on page fault), + * typically expect to pay ~1us per superpage. + * Much better than ~500us to commit 512 4k VM pages. + * + * But wasted if we don't use the memory. + * + * Page table has a handful of levels + **/ + ::madvise(aligned_base, target_z, MADV_HUGEPAGE); // 8. +#endif + } + + return span_type(aligned_base, aligned_hi); + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end mmap_util.cpp */ From 516b57ae81d6e8efe3235c9aeeeb473be856e1a8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 16:54:30 -0500 Subject: [PATCH 070/111] xo-arena: CircularBuffer utest + bugfixes --- include/xo/arena/CircularBufferConfig.hpp | 1 + include/xo/arena/DCircularBuffer.hpp | 5 +- include/xo/arena/span.hpp | 21 +++++++- src/arena/DCircularBuffer.cpp | 33 ++++++++++-- utest/CMakeLists.txt | 3 +- utest/DCircularBuffer.test.cpp | 61 +++++++++++++++++++++++ 6 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 utest/DCircularBuffer.test.cpp diff --git a/include/xo/arena/CircularBufferConfig.hpp b/include/xo/arena/CircularBufferConfig.hpp index 710812a4..587b9d22 100644 --- a/include/xo/arena/CircularBufferConfig.hpp +++ b/include/xo/arena/CircularBufferConfig.hpp @@ -20,6 +20,7 @@ namespace xo { /** optional name, for diagnostics **/ std::string name_; /** hard maximum buffer size = reserved virtual memory. + * However actual max will be this value rounded up to at least page size. * Buffer will generally map much less than this amount of memory **/ std::size_t max_capacity_ = 0; diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp index 17bd6b7a..7e49def3 100644 --- a/include/xo/arena/DCircularBuffer.hpp +++ b/include/xo/arena/DCircularBuffer.hpp @@ -79,6 +79,7 @@ namespace xo { const_span_type reserved_range() const noexcept { return reserved_range_; } const_span_type mapped_range() const noexcept { return mapped_range_; } const_span_type occupied_range() const noexcept { return occupied_range_; } + const_span_type input_range() const noexcept { return input_range_; } /** verify DCircularBuffer invariants. * Act on failure according to policy @p p @@ -98,6 +99,8 @@ namespace xo { /** @defgroup mm-circularbuffer-nonconst-methods CircularBuffer non-const methods **/ ///@{ + span_type input_range() noexcept { return input_range_; } + /** copy memory in span @p r into buffer starting at the end of * @ref occupied_range_. Map new physical memory as needed. * On success returns empty suffix of @p r. @@ -139,7 +142,7 @@ namespace xo { * Caller represents that it won't need to read this memory again * unless overlaps with a pinned span. **/ - void consume(span_type r); + void consume(const_span_type input); /** pin memory range @p r. circular buffer will not touch * addresses that appear in any pinned range. diff --git a/include/xo/arena/span.hpp b/include/xo/arena/span.hpp index e902193f..c038fce5 100644 --- a/include/xo/arena/span.hpp +++ b/include/xo/arena/span.hpp @@ -28,6 +28,9 @@ namespace xo { /** typealias for span size (in units of CharT) **/ using size_type = std::uint64_t; + /** typealias for span elements **/ + using value_type = CharT; + ///@} public: @@ -58,7 +61,8 @@ namespace xo { * A null span can be concatenated with any other span * without triggering matching-endpoint asserts. **/ - static span make_null() { return span(static_cast(nullptr), static_cast(nullptr)); } + static span make_null() { return span(static_cast(nullptr), + static_cast(nullptr)); } /** @brief create span for C-style string @p cstr **/ static span from_cstr(const CharT * cstr) { @@ -69,13 +73,21 @@ namespace xo { } /** @brief create span from std::string @p str **/ - static span from_string(const std::string& str) { + static span from_string(const std::string & str) { CharT * lo = &(*str.begin()); CharT * hi = &(*str.end()); return span(lo, hi); } + /** @brief create span from std::string @p str **/ + static span from_string_view(const std::string_view & sv) { + CharT * lo = &(*sv.begin()); + CharT * hi = &(*sv.end()); + + return span(lo, hi); + } + /** @brief concatenate two contiguous spans */ static span concat(const span & span1, const span & span2) { if (span1.is_null()) @@ -119,6 +131,11 @@ namespace xo { return (other.lo() <= lo_) && (hi_ <= other.hi()); } + /** convert to string view **/ + std::string_view to_string_view() const { + return std::string_view((const char *)lo_, (const char *)hi_); + } + ///@} /** @defgroup span-general-methods **/ diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index e4d31f46..9e8b28b8 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -10,6 +10,9 @@ #include namespace xo { + using xo::print::operator<<; + using xo::print::printspan; + namespace mm { DCircularBuffer::DCircularBuffer(DCircularBuffer && other) @@ -152,6 +155,7 @@ namespace xo { ::memcpy(occupied_range_.hi(), src.lo(), copy_z); this->occupied_range_ += span_type(dest.lo(), copy_z); + this->input_range_ += span_type(dest.lo(), copy_z); return src.after_prefix(copy_z); } @@ -198,30 +202,49 @@ namespace xo { } void - DCircularBuffer::consume(span_type r) + DCircularBuffer::consume(const_span_type input) { - if (r.lo() != input_range_.lo()) { + scope log(XO_DEBUG(false), xtag("input", input.to_string_view())); + + if (input.lo() != input_range_.lo()) { assert(false); return; } - if (r.hi() > occupied_range_.hi()) { + if (input.hi() > occupied_range_.hi()) { assert(false); return; } if (occupied_range_.lo() < input_range_.lo()) { + log && log("pinned range prevents shrinking occupied range"); + /* here: a pinned range prevents shrinking occupied_range */ - this->input_range_ = input_range_.suffix_from(r.hi()); + this->input_range_ + = input_range_.suffix_from((span_type::value_type *)input.hi()); } else { + log && log(xtag("msg", "will shrink occupied range"), + xtag("input.lo", (void*)input.lo()), + xtag("input.hi", (void*)input.hi()), + xtag("stored.lo", (void*)input_range_.lo()), + xtag("stored.hi", (void*)input_range_.hi()) + ); + /* here: input; recompute occupied boundary */ - this->input_range_ = input_range_.suffix_from(r.hi()); + this->input_range_ + = input_range_.suffix_from((span_type::value_type *)input.hi()); + + log && log(xtag("occupied", occupied_range_.size()), + xtag("input", input_range_.size())); this->_shrink_occupied_to_fit(); + + log && log(xtag("occupied", occupied_range_.size()), + xtag("input", input_range_.size())); } this->_check_reset_map_start(); diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index c45a9710..886ecb7a 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -8,9 +8,8 @@ set(UTEST_SRCS DArena.test.cpp DArenaVector.test.cpp DArenaHashMap.test.cpp + DCircularBuffer.test.cpp # DArenaIterator.test.cpp -# Collector.test.cpp -# DX1CollectorIterator.test.cpp # random_allocs.cpp ) diff --git a/utest/DCircularBuffer.test.cpp b/utest/DCircularBuffer.test.cpp new file mode 100644 index 00000000..debfc41f --- /dev/null +++ b/utest/DCircularBuffer.test.cpp @@ -0,0 +1,61 @@ +/** @file DCircularBuffer.test.cpp +* + * @author Roland Conybeare, Jan 2026 + **/ + +#include "DCircularBuffer.hpp" +#include "print.hpp" +#include +#include + +namespace xo { + using xo::mm::DCircularBuffer; + using xo::mm::CircularBufferConfig; + using xo::mm::span; + using std::byte; + + namespace ut { + TEST_CASE("DCircularBuffer-tiny", "[arena][DCircularBuffer]") + { + // buffer works with bytes, not chars + + CircularBufferConfig cfg { .name_ = "testcbuf", + .max_capacity_ = 1 }; + DCircularBuffer buf = DCircularBuffer::map(cfg); + + REQUIRE(buf.reserved_range().size() == getpagesize()); + REQUIRE(buf.mapped_range().size() == 0); + REQUIRE(buf.occupied_range().size() == 0); + REQUIRE(buf.input_range().size() == 0); + + REQUIRE(buf.verify_ok(verify_policy::log_only())); + REQUIRE(buf.get_append_span(1).size() == getpagesize()); + REQUIRE(buf.mapped_range().size() == getpagesize()); + REQUIRE(buf.occupied_range().size() == 0); + REQUIRE(buf.input_range().size() == 0); + + std::string_view s0 = "abcdefghijk"; + /* return value is unaccepted suffix of input */ + REQUIRE(buf.append(DCircularBuffer::span_type((byte *)s0.begin(), + (byte *)s0.end())).empty()); + REQUIRE(buf.verify_ok(verify_policy::log_only())); + REQUIRE(buf.mapped_range().size() == getpagesize()); + REQUIRE(buf.occupied_range().size() == s0.size()); + REQUIRE(buf.input_range().size() == s0.size()); + + std::string_view s1 = "lmnopq"; + REQUIRE(buf.append(DCircularBuffer::span_type((byte *)s1.begin(), + (byte *)s1.end())).empty()); + REQUIRE(buf.mapped_range().size() == getpagesize()); + REQUIRE(buf.occupied_range().size() == s0.size() + s1.size()); + + REQUIRE(buf.occupied_range().to_string_view() == std::string_view("abcdefghijklmnopq")); + + buf.consume(buf.occupied_range().prefix(3)); + + REQUIRE(buf.occupied_range().to_string_view() == std::string_view("defghijklmnopq")); + } + } /*namespace ut*/ +} /*namespace xo*/ + +/* end DCircularBuffer.test.cpp */ From 770879aa68129ef2e07455e5c189c183c2da979c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 16:58:57 -0500 Subject: [PATCH 071/111] xo-arena: tidy: make CircularBuffer::_expand_to() private --- include/xo/arena/DCircularBuffer.hpp | 12 ++--- src/arena/DCircularBuffer.cpp | 66 ++++++++++++++-------------- utest/DCircularBuffer.test.cpp | 2 + 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp index 7e49def3..c7c8abf9 100644 --- a/include/xo/arena/DCircularBuffer.hpp +++ b/include/xo/arena/DCircularBuffer.hpp @@ -132,12 +132,6 @@ namespace xo { **/ void report_append(span_type r); - /** expand hi end of mapped memory range to at least @p hi. - * - * Require: @p hi < @ref reserved_range_.hi - **/ - bool expand_to(byte * hi); - /** consume span (or prefix thereof) previously obtained from @ref occupied_range() * Caller represents that it won't need to read this memory again * unless overlaps with a pinned span. @@ -162,6 +156,12 @@ namespace xo { /** @defgroup mm-circularbuffer-private-methods CircularBuffer non-const methods **/ ///@{ + /** expand hi end of mapped memory range to at least @p hi. + * + * Require: @p hi < @ref reserved_range_.hi + **/ + bool _expand_to(byte * hi); + /** shrink occupied rnage to the smallest contiguous range that contains both: * all of .input_range_, and all pinned ranges in .pinned_spans_ **/ diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 9e8b28b8..70efcd2d 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -171,7 +171,7 @@ namespace xo { } /* establish mapped range at least to dest.hi */ - this->expand_to(dest.hi()); + this->_expand_to(dest.hi()); /* report available memory */ return span_type(occupied_range_.hi(), mapped_range_.hi()); @@ -250,38 +250,6 @@ namespace xo { this->_check_reset_map_start(); } - bool - DCircularBuffer::expand_to(byte * hi) { - scope log(XO_DEBUG(config_.debug_flag_)); - - if (hi < mapped_range_.hi()) { - /* nothing todo */ - return true; - } - - size_t add_z = hi - mapped_range_.hi(); - size_t add_commit_z = padding::with_padding(add_z, buffer_align_z_); - byte * commit_start = mapped_range_.hi(); - - if (::mprotect(commit_start, - add_commit_z, - PROT_READ | PROT_WRITE) != 0) - { - if (log) { - log("commit failed"); - log(xtag("commit_start", commit_start), - xtag("add_z", add_z), - xtag("add_commit_z", add_commit_z)); - } - - // this->capture_error(error::commit_failed, add_commit_z); - return false; - } - - this->mapped_range_ += span(commit_start, add_commit_z); - return true; - } - void DCircularBuffer::pin_range(span_type r) { @@ -333,6 +301,38 @@ namespace xo { } } + bool + DCircularBuffer::_expand_to(byte * hi) { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (hi < mapped_range_.hi()) { + /* nothing todo */ + return true; + } + + size_t add_z = hi - mapped_range_.hi(); + size_t add_commit_z = padding::with_padding(add_z, buffer_align_z_); + byte * commit_start = mapped_range_.hi(); + + if (::mprotect(commit_start, + add_commit_z, + PROT_READ | PROT_WRITE) != 0) + { + if (log) { + log("commit failed"); + log(xtag("commit_start", commit_start), + xtag("add_z", add_z), + xtag("add_commit_z", add_commit_z)); + } + + // this->capture_error(error::commit_failed, add_commit_z); + return false; + } + + this->mapped_range_ += span(commit_start, add_commit_z); + return true; + } + void DCircularBuffer::_shrink_occupied_to_fit() { diff --git a/utest/DCircularBuffer.test.cpp b/utest/DCircularBuffer.test.cpp index debfc41f..be869478 100644 --- a/utest/DCircularBuffer.test.cpp +++ b/utest/DCircularBuffer.test.cpp @@ -55,6 +55,8 @@ namespace xo { REQUIRE(buf.occupied_range().to_string_view() == std::string_view("defghijklmnopq")); } + + // TODO: test pin_range() / unpin_range() } /*namespace ut*/ } /*namespace xo*/ From 5e771a99ec04a0a28484931d5ccb334d295295cd Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 18:42:08 -0500 Subject: [PATCH 072/111] xo-tokenizer2: use xo-arena DCircularBuffer to buffer input line --- include/xo/arena/DCircularBuffer.hpp | 4 +++- src/arena/DCircularBuffer.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp index c7c8abf9..0cb007ad 100644 --- a/include/xo/arena/DCircularBuffer.hpp +++ b/include/xo/arena/DCircularBuffer.hpp @@ -58,8 +58,10 @@ namespace xo { size_type page_z, size_type buffer_align_z, span_type reserved_range); +#ifdef NOT_YET /** constructor */ DCircularBuffer(const CircularBufferConfig & config); +#endif /** non-copyable **/ DCircularBuffer(const DCircularBuffer & other) = delete; /** move ctor **/ @@ -107,7 +109,7 @@ namespace xo { * If buffer memory exhausted, may copy a prefix of @p r. * In that case returns the remaining suffix of @p r. **/ - span_type append(span_type r); + const_span_type append(const_span_type r); /** DMA version of @ref append_span : get mapped span A at which * buffer will receive new content. Upstream may write into diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 70efcd2d..5ad7e021 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -146,7 +146,7 @@ namespace xo { } auto - DCircularBuffer::append(span_type src) -> span_type + DCircularBuffer::append(const_span_type src) -> const_span_type { span_type dest = get_append_span(src.size()); From 94e02f0eebbc7c436d033c6eec5625555fd2cd08 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 11 Jan 2026 19:10:42 -0500 Subject: [PATCH 073/111] xo-tokenizer: example tokenrepl restored to wokring order Now with CBufferedInput in Tokenizer --- include/xo/arena/DCircularBuffer.hpp | 6 +++--- include/xo/arena/span.hpp | 8 ++++++++ src/arena/DCircularBuffer.cpp | 19 +++++++++++-------- utest/DCircularBuffer.test.cpp | 10 ++++------ 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp index 0cb007ad..30a38960 100644 --- a/include/xo/arena/DCircularBuffer.hpp +++ b/include/xo/arena/DCircularBuffer.hpp @@ -39,8 +39,8 @@ namespace xo { using size_type = std::size_t; using byte = std::byte; /** a contiguous addres range **/ - using span_type = span; - using const_span_type = span; + using span_type = span; + using const_span_type = span; ///@} @@ -162,7 +162,7 @@ namespace xo { * * Require: @p hi < @ref reserved_range_.hi **/ - bool _expand_to(byte * hi); + bool _expand_to(char * hi); /** shrink occupied rnage to the smallest contiguous range that contains both: * all of .input_range_, and all pinned ranges in .pinned_spans_ diff --git a/include/xo/arena/span.hpp b/include/xo/arena/span.hpp index c038fce5..b9192662 100644 --- a/include/xo/arena/span.hpp +++ b/include/xo/arena/span.hpp @@ -88,6 +88,14 @@ namespace xo { return span(lo, hi); } + /** @brief create span from raw memory **/ + static span from_memory(span span_memory) { + CharT * lo = (CharT *)span_memory.lo(); + CharT * hi = (CharT *)span_memory.hi(); + + return span(lo, hi); + } + /** @brief concatenate two contiguous spans */ static span concat(const span & span1, const span & span2) { if (span1.is_null()) diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 5ad7e021..e00aaaf7 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -48,17 +48,19 @@ namespace xo { log && log(xtag("page_z", page_z), xtag("align_z", align_z)); - auto span = mmap_util::map_aligned_range(config.max_capacity_, - align_z, - enable_hugepage_flag, - config.debug_flag_); + auto mapped_span + = span::from_memory(mmap_util::map_aligned_range + (config.max_capacity_, + align_z, + enable_hugepage_flag, + config.debug_flag_)); - if (!span.lo()) { + if (!mapped_span.lo()) { throw std::runtime_error(tostr("DCircularBuffer: reserve address range failed", xtag("size", config.max_capacity_))); } - return DCircularBuffer(config, page_z, align_z, span); + return DCircularBuffer(config, page_z, align_z, mapped_span); } DCircularBuffer::DCircularBuffer(const CircularBufferConfig & config, @@ -302,7 +304,8 @@ namespace xo { } bool - DCircularBuffer::_expand_to(byte * hi) { + DCircularBuffer::_expand_to(char * hi) + { scope log(XO_DEBUG(config_.debug_flag_)); if (hi < mapped_range_.hi()) { @@ -312,7 +315,7 @@ namespace xo { size_t add_z = hi - mapped_range_.hi(); size_t add_commit_z = padding::with_padding(add_z, buffer_align_z_); - byte * commit_start = mapped_range_.hi(); + char * commit_start = mapped_range_.hi(); if (::mprotect(commit_start, add_commit_z, diff --git a/utest/DCircularBuffer.test.cpp b/utest/DCircularBuffer.test.cpp index be869478..2d8df93d 100644 --- a/utest/DCircularBuffer.test.cpp +++ b/utest/DCircularBuffer.test.cpp @@ -34,18 +34,16 @@ namespace xo { REQUIRE(buf.occupied_range().size() == 0); REQUIRE(buf.input_range().size() == 0); - std::string_view s0 = "abcdefghijk"; + auto s0 = DCircularBuffer::const_span_type::from_cstr("abcdefghijk"); /* return value is unaccepted suffix of input */ - REQUIRE(buf.append(DCircularBuffer::span_type((byte *)s0.begin(), - (byte *)s0.end())).empty()); + REQUIRE(buf.append(s0).empty()); REQUIRE(buf.verify_ok(verify_policy::log_only())); REQUIRE(buf.mapped_range().size() == getpagesize()); REQUIRE(buf.occupied_range().size() == s0.size()); REQUIRE(buf.input_range().size() == s0.size()); - std::string_view s1 = "lmnopq"; - REQUIRE(buf.append(DCircularBuffer::span_type((byte *)s1.begin(), - (byte *)s1.end())).empty()); + auto s1 = DCircularBuffer::const_span_type::from_cstr("lmnopq"); + REQUIRE(buf.append(s1).empty()); REQUIRE(buf.mapped_range().size() == getpagesize()); REQUIRE(buf.occupied_range().size() == s0.size() + s1.size()); From 6660db9d5373d84bb3f5b1489a2732e26bb8d67b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 12 Jan 2026 01:03:49 -0500 Subject: [PATCH 074/111] xo-alloc2 xo-object2 : refactor to build on osx --- src/arena/DCircularBuffer.cpp | 1 + utest/DCircularBuffer.test.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index e00aaaf7..2a415c49 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -8,6 +8,7 @@ #include #include #include +#include // for ::getpagesize() on osx namespace xo { using xo::print::operator<<; diff --git a/utest/DCircularBuffer.test.cpp b/utest/DCircularBuffer.test.cpp index 2d8df93d..ea5a01be 100644 --- a/utest/DCircularBuffer.test.cpp +++ b/utest/DCircularBuffer.test.cpp @@ -7,6 +7,7 @@ #include "print.hpp" #include #include +#include // for getpagesize() on osx namespace xo { using xo::mm::DCircularBuffer; From c65819d3eb5ac202b768ae6dcbd36539646afb46 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 15 Jan 2026 13:42:20 -0500 Subject: [PATCH 075/111] xo-arena: memory checks in DArenaVector::push_back() --- include/xo/arena/DArenaVector.hpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index a5834ea2..f8bd9820 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -296,25 +296,27 @@ namespace xo { size_type z = size_ + 1; size_type req_z = z * sizeof(T); - this->store_.expand(req_z); + if (this->store_.expand(req_z)) { + T * addr = this->_address_of(size_); - T * addr = this->_address_of(size_); + new (addr) T{std::move(x)}; - new (addr) T{std::move(x)}; - - this->size_ = z; + this->size_ = z; + } } template void DArenaVector::push_back(const T & x) { size_type z = size_ + 1; - store_.expand(z * sizeof(T)); - T * addr = this->_address_of(size_); - new (addr) T{x}; + if (this->store_.expand(z * sizeof(T))) { + T * addr = this->_address_of(size_); - size_ = z; + new (addr) T{x}; + + this->size_ = z; + } } template From 3711ab21a414a99ef9a3e0261aec101be185c92d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 15 Jan 2026 19:36:31 -0500 Subject: [PATCH 076/111] xo-arena: + DArenaHashMap::operator[] + utest --- include/xo/arena/DArenaHashMap.hpp | 48 +++++++++++++++++++++-- include/xo/arena/hashmap/HashMapStore.hpp | 7 ++-- utest/DArenaHashMap.test.cpp | 48 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index aef9ed3a..cf508958 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -98,8 +98,8 @@ namespace xo { * Replaces any previous value stored under the same key. * * Return pair retval with: - * reval.first: true if size incremented; - * retval.second: address of slots_[p] at which pair inserted/updated + * retval.first: address of slots_[p] at which pair inserted/updated + * retval.second: true if size incremented; * * When table is full retval.second will be nullptr, * with error captured in last_error_ @@ -119,6 +119,9 @@ namespace xo { **/ iterator find(const key_type & key); + /** establish kv pair for @p key in this table; return address of value part **/ + mapped_type & operator[](const key_type & key); + private: /** insert @p kv_pair, * where key hashes to @p hash_value, into @p *store @@ -169,7 +172,7 @@ namespace xo { bool debug_flag) : hash_{std::move(hash)}, equal_{std::move(eq)}, - store_{lub_exp2(lub_group_mult(hint_max_capacity))}, + store_{"arenahashmap", lub_exp2(lub_group_mult(hint_max_capacity))}, debug_flag_{debug_flag} { } @@ -332,7 +335,8 @@ namespace xo { } else { log && log("duplicate-and-replace branch"); - detail::HashMapStore store_2x(std::make_pair(n_group_exponent_2x, + detail::HashMapStore store_2x("arenahashmap", + std::make_pair(n_group_exponent_2x, n_group_2x)); /* rehash everything in store_, * into store_2x @@ -459,6 +463,42 @@ namespace xo { } } + template + auto + DArenaHashMap::operator[](const key_type & key) -> mapped_type & + { + { + auto ix = this->find(key); + + if (ix != this->end()) + return ix->second; + } + + // key-value pair + value_type kv_pair = std::make_pair(key, mapped_type{}); + + auto [slot_addr, ins_flag] = this->try_insert(kv_pair); + + if (slot_addr) + return slot_addr->second; + + if (!this->_try_grow()) { + // we are out of room + + throw std::runtime_error("DArenaHashMap::operator[]: table capacity exhausted"); + } + + /* retry insert, now with bigger capacity */ + std::tie(slot_addr, ins_flag) = this->try_insert(kv_pair); + + assert(slot_addr); + + return slot_addr->second; + } + /** * Verify DArenaHashMap class invariants. * diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 545aa1c9..dd8ace12 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -22,14 +22,15 @@ namespace xo { public: /** group_exp2: number of groups {x, 2^x} **/ - explicit HashMapStore(const std::pair & group_exp2) : size_{0}, n_group_exponent_{group_exp2.first}, n_group_{group_exp2.second}, n_slot_{group_exp2.second * c_group_size}, - control_{control_vector_type::map(xo::mm::ArenaConfig{.size_ = control_size(n_slot_)})}, - slots_{slot_vector_type::map(xo::mm::ArenaConfig{.size_ = n_slot_ * sizeof(value_type)})} + control_{control_vector_type::map(xo::mm::ArenaConfig{.name_ = name, .size_ = control_size(n_slot_)})}, + slots_{slot_vector_type::map(xo::mm::ArenaConfig{.name_ = name, .size_ = n_slot_ * sizeof(value_type)})} { /* here: arenas have allocated address range, but no committed memory yet */ diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 088edf22..28da62cf 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -226,6 +226,54 @@ namespace xo { } } + TEST_CASE("DArenaHashMap-operator-bracket", "[arena][DArenaHashMap]") + { + using HashMap = DArenaHashMap; + + HashMap map; + + // insert via operator[] + map[1] = 100; + map[2] = 200; + map[3] = 300; + + REQUIRE(map.size() == 3); + + // read back via operator[] + REQUIRE(map[1] == 100); + REQUIRE(map[2] == 200); + REQUIRE(map[3] == 300); + + // update via operator[] + map[2] = 250; + REQUIRE(map[2] == 250); + REQUIRE(map.size() == 3); // size unchanged + + // verify via find + { + auto it = map.find(1); + REQUIRE(it != map.end()); + REQUIRE(it->second == 100); + } + { + auto it = map.find(2); + REQUIRE(it != map.end()); + REQUIRE(it->second == 250); + } + { + auto it = map.find(3); + REQUIRE(it != map.end()); + REQUIRE(it->second == 300); + } + + // operator[] on non-existent key creates default entry + int & val = map[999]; + REQUIRE(map.size() == 4); + REQUIRE(val == 0); // default-initialized + val = 999; + REQUIRE(map[999] == 999); + } + // TODO: // - let's try getting lcov to work in xo-umbrella2 } From 7aa232f1ea227bce3bf1f297b135e35ec2475e20 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 15 Jan 2026 20:32:10 -0500 Subject: [PATCH 077/111] xo-arena: + const_iterator support for DValueHashMap --- include/xo/arena/DArenaHashMap.hpp | 72 +++++++++++------ include/xo/arena/hashmap/ControlGroup.hpp | 2 +- .../arena/hashmap/DArenaHashMapIterator.hpp | 79 ++++++++++++++++++- include/xo/arena/hashmap/HashMapStore.hpp | 2 +- 4 files changed, 124 insertions(+), 31 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index cf508958..ee5fb794 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -50,7 +50,9 @@ namespace xo { using store_type = detail::HashMapStore; using insert_value_type = std::pair; using iterator = detail::DArenaHashMapIterator; + using const_iterator = detail::DArenaHashMapConstIterator; + public: /** create hash map **/ DArenaHashMap(size_type hint_max_capacity, bool debug_flag = false); @@ -71,28 +73,14 @@ namespace xo { **/ bool verify_ok(verify_policy p = verify_policy::throw_only()) const; - iterator begin() { - if (this->empty()) [[unlikely]] { - return this->end(); - } + const_iterator cbegin() const { return this->_begin_aux(); } + const_iterator cend() const { return this->_end_aux(); } - iterator ix(&(store_.control_[c_control_stub]), - &(store_.slots_[0])); + const_iterator begin() const { return this->_begin_aux(); } + const_iterator end() const { return this->_end_aux(); } - if (ix._at_slot_sentinel()) { - /* advance to first occupied position in table */ - ++ix; - } - - return ix; - } - - iterator end() { - iterator ix(&(store_.control_[c_control_stub + store_.capacity()]), - &(store_.slots_[store_.capacity()])); - - return ix; - } + iterator begin() { return _promote_iterator(_begin_aux()); } + iterator end() { return _promote_iterator(_end_aux()); } /** insert @p kv_pair into hash map. * Replaces any previous value stored under the same key. @@ -117,12 +105,46 @@ namespace xo { /** find element with key @p key. * @return iterator to element if found, end() otherwise **/ - iterator find(const key_type & key); + const_iterator find(const key_type & key) const { return _find(key); } + iterator find(const key_type & key) { return _promote_iterator(_find(key)); } /** establish kv pair for @p key in this table; return address of value part **/ mapped_type & operator[](const key_type & key); private: + iterator _promote_iterator(const_iterator ix) { + return iterator(const_cast(ix._ctrl()), + const_cast(ix._pos())); + } + + const_iterator _begin_aux() const { + if (this->empty()) [[unlikely]] { + return this->end(); + } + + const_iterator ix(&(store_.control_[c_control_stub]), + &(store_.slots_[0])); + + if (ix._at_slot_sentinel()) { + /* advance to first occupied position in table */ + ++ix; + } + + return ix; + } + + const_iterator _end_aux() const { + const_iterator ix(&(store_.control_[c_control_stub + store_.capacity()]), + &(store_.slots_[store_.capacity()])); + + return ix; + } + + /** search hash map on key @p key, return iterator to table member. + * return end-iterator if @p key not found + **/ + const_iterator _find(const key_type & key) const; + /** insert @p kv_pair, * where key hashes to @p hash_value, into @p *store **/ @@ -416,12 +438,12 @@ namespace xo { typename Hash, typename Equal> auto - DArenaHashMap::find(const key_type & key) -> iterator + DArenaHashMap::_find(const key_type & key) const -> const_iterator { size_type N = store_.capacity(); if (N == 0) [[unlikely]] { - return this->end(); + return this->cend(); } size_type h = hash_(key); @@ -443,8 +465,8 @@ namespace xo { auto & slot = store_.slots_[slot_ix]; if (equal_(slot.first, key)) { - return iterator(&(store_.control_[c_control_stub + slot_ix]), - &slot); + return const_iterator(&(store_.control_[c_control_stub + slot_ix]), + &slot); } m &= (m - 1); diff --git a/include/xo/arena/hashmap/ControlGroup.hpp b/include/xo/arena/hashmap/ControlGroup.hpp index 7d3a129e..744ddb5b 100644 --- a/include/xo/arena/hashmap/ControlGroup.hpp +++ b/include/xo/arena/hashmap/ControlGroup.hpp @@ -21,7 +21,7 @@ namespace xo { std::array ctrl_; /** Require: lo is aligned on c_group_size (probably 16 bytes) **/ - explicit ControlGroup(uint8_t * lo) { + explicit ControlGroup(const uint8_t * lo) { ::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size); } diff --git a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp index 57573b49..ea87c0ce 100644 --- a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp +++ b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp @@ -23,6 +23,9 @@ namespace xo { value_type & operator*() const { return *pos_; } value_type * operator->() const { return pos_; } + uint8_t * _ctrl() const { return ctrl_; } + value_type * _pos() const { return pos_; } + /** true iff iterator at sentinel position (not dereferencable state !) **/ bool _at_slot_sentinel() const { return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); } @@ -36,8 +39,8 @@ namespace xo { DArenaHashMapIterator & operator++() { do { - ++ctrl_; - ++pos_; + ++(this->ctrl_); + ++(this->pos_); /** end condition: iterator ends at last non-wrapped position. * relyin on bookend sentinel values at known offset from 'wrap' section @@ -62,8 +65,8 @@ namespace xo { * precedes control byte for first slot */ do { - --ctrl_; - --pos_; + --(this->ctrl_); + --(this->pos_); } while (is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend)); @@ -75,6 +78,74 @@ namespace xo { value_type * pos_ = nullptr; }; + template + struct DArenaHashMapConstIterator : public DArenaHashMapUtil { + using value_type = std::pair; + + public: + DArenaHashMapConstIterator(const uint8_t * c, const value_type * p) + : ctrl_{c}, pos_{p} {} + + const value_type & operator*() const { return *pos_; } + const value_type * operator->() const { return pos_; } + + const uint8_t * _ctrl() const { return ctrl_; } + const value_type * _pos() const { return pos_; } + + /** true iff iterator at sentinel position (not dereferencable state !) **/ + bool _at_slot_sentinel() const { + return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); + } + + bool operator==(const DArenaHashMapConstIterator & x) const { + return this->pos_ == x.pos_; + } + + bool operator!=(const DArenaHashMapConstIterator & x) const { + return this->pos_ != x.pos_; + } + + DArenaHashMapConstIterator & operator++() { + do { + ++(this->ctrl_); + ++(this->pos_); + + /** end condition: iterator ends at last non-wrapped position. + * relyin on bookend sentinel values at known offset from 'wrap' section + * + * ctrl_ ctrl_ + c_group_size + * | | + * v v + * <----------------- control_size(n_slot) ----------------> + * <-stub-> <----------- n_slot ----------> <-stub-> + * +--------+-------------------------------+-------+--------+ + * | 0xF0 | empty / data / tombstone | wrap | 0xF0 | + * +--------+-------------------------------+-------+--------+ + **/ + } while (is_sentinel(*ctrl_) + && (*(ctrl_ + c_group_size) != c_iterator_bookend)); + + return *this; + } + + DArenaHashMapConstIterator & operator--() { + /* simpler than forward iteration, since bookend immediately + * precedes control byte for first slot + */ + do { + --(this->ctrl_); + --(this->pos_); + } while (is_sentinel(*ctrl_) + && (*ctrl_ != c_iterator_bookend)); + + return *this; + } + + private: + const uint8_t * ctrl_ = nullptr; + const value_type * pos_ = nullptr; + }; } } /*namespace map*/ } /*namespace xo*/ diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index dd8ace12..661a6916 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -91,7 +91,7 @@ namespace xo { } /** load control group for slot range [ix .. ix+c_group_size) **/ - group_type _load_group(size_type ix) { + group_type _load_group(size_type ix) const { return group_type(&(control_[ix + c_control_stub])); } From 0816c0a3191376b3adcf7904e40f5b72b89460ca Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 15 Jan 2026 20:52:09 -0500 Subject: [PATCH 078/111] xo-expression2: + StringTable + utest [WIP] [FAILING] --- utest/DArenaHashMap.test.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 28da62cf..0962def5 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -274,6 +274,24 @@ namespace xo { REQUIRE(map[999] == 999); } + TEST_CASE("DArenaHashMap-string_view-key", "[arena][DArenaHashMap]") + { + using HashMap = DArenaHashMap; + + HashMap map(1024); + + map["hello"] = 42; + REQUIRE(map.size() == 1); + REQUIRE(map.verify_ok()); + + map["world"] = 100; + REQUIRE(map.size() == 2); + REQUIRE(map.verify_ok()); + + REQUIRE(map["hello"] == 42); + REQUIRE(map["world"] == 100); + } + // TODO: // - let's try getting lcov to work in xo-umbrella2 } From 3dbee22a5369b94df939e884a1c9f575977dca9d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 15 Jan 2026 23:51:00 -0500 Subject: [PATCH 079/111] xo-arena: bugfix DArenaHashMap + expand utest verification --- include/xo/arena/DArenaHashMap.hpp | 52 +++++++++++----- .../xo/arena/hashmap/DArenaHashMapUtil.hpp | 12 +++- include/xo/arena/hashmap/HashMapStore.hpp | 11 +++- utest/DArenaHashMap.test.cpp | 60 +++++++++++++++++++ 4 files changed, 115 insertions(+), 20 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index ee5fb794..50b7b4aa 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -67,12 +67,6 @@ namespace xo { size_type capacity() const noexcept { return store_.capacity(); } float load_factor() const noexcept { return store_.load_factor(); } - /** verify DArenaHashMap invariants - * Act on failure according to policy @p - * (combination of throw|log bits) - **/ - bool verify_ok(verify_policy p = verify_policy::throw_only()) const; - const_iterator cbegin() const { return this->_begin_aux(); } const_iterator cend() const { return this->_end_aux(); } @@ -111,6 +105,26 @@ namespace xo { /** establish kv pair for @p key in this table; return address of value part **/ mapped_type & operator[](const key_type & key); + /** verify DArenaHashMap invariants + * Act on failure according to policy @p + * (combination of throw|log bits) + **/ + bool verify_ok(verify_policy p = verify_policy::throw_only()) const; + + store_type * _store() noexcept { return &store_; } + + auto _hash(const key_type & key) const { + size_type h = hash_(key); + size_type h1 = h >> 7; // slot# + size_type h2 = h % 0x7f; // fingerprint + + size_type N = store_.capacity(); + + size_type slot_ix = h1 % (N - 1); + + return std::make_pair(slot_ix, h2); + } + private: iterator _promote_iterator(const_iterator ix) { return iterator(const_cast(ix._ctrl()), @@ -584,7 +598,9 @@ namespace xo { } /* SM1.3: n_group_ consistent with n_group_exponent_ */ - if (store_.n_group_ != (size_type{1} << store_.n_group_exponent_)) { + if ((store_.n_group_ > 0) + && (store_.n_group_ != (size_type{1} << store_.n_group_exponent_))) + { return policy.report_error(log, c_self, ": expect .n_group = 2^.n_group_exponent", xtag("n_group", store_.n_group_), @@ -655,15 +671,19 @@ namespace xo { } /* SM3.4: control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub) */ - for (size_type i = 0; i < c_control_stub; ++i) { - size_type ix = c_control_stub + store_.n_slot_ + c_group_size + i; - if (store_.control_[ix] != c_iterator_bookend) { - return policy.report_error(log, - c_self, ": expect control_[stub+N+group+i] = c_iterator_bookend for end stub", - xtag("i", i), - xtag("ix", ix), - xtag("control_[ix]", (int)(store_.control_[ix])), - xtag("c_iterator_bookend", (int)c_iterator_bookend)); + if (store_.n_slot_ > 0) { + for (size_type i = 0; i < c_control_stub; ++i) { + size_type ix = c_control_stub + store_.n_slot_ + c_group_size + i; + if (store_.control_[ix] != c_iterator_bookend) { + return policy.report_error + (log, + c_self, ": expect control_[stub+N+group+i] = c_iterator_bookend for end stub", + xtag("i", i), + xtag("N", store_.n_slot_), + xtag("ix", ix), + xtag("control_[ix]", (int)(store_.control_[ix])), + xtag("c_iterator_bookend", (int)c_iterator_bookend)); + } } } diff --git a/include/xo/arena/hashmap/DArenaHashMapUtil.hpp b/include/xo/arena/hashmap/DArenaHashMapUtil.hpp index f2f72d17..372c99e0 100644 --- a/include/xo/arena/hashmap/DArenaHashMapUtil.hpp +++ b/include/xo/arena/hashmap/DArenaHashMapUtil.hpp @@ -49,7 +49,7 @@ namespace xo { using control_type = std::uint8_t; /** control: mask for sentinel states **/ - static constexpr uint8_t c_sentinel_mask = 0xF0; + static constexpr uint8_t c_sentinel_mask = 0x80; /** control: sentinel for empty slot **/ static constexpr uint8_t c_empty_slot = 0xFF; /** control: tombstone for deleted slot **/ @@ -82,7 +82,15 @@ namespace xo { /** control: compute size of control array for swiss hash map with @p n_slot cells **/ static constexpr size_type control_size(size_type n_slot) { - return n_slot + c_group_size + 2 * c_control_stub; + if (n_slot == 0) + return 0; + + /* control: + * - c_group_size overflow slots + * - 2x c_control_stub begin/end sentinels + */ + + return n_slot + c_group_size + (2 * c_control_stub); } /** find smallest multiple k : k * c_group_size >= n **/ diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 661a6916..fd7c87f7 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -29,8 +29,14 @@ namespace xo { n_group_exponent_{group_exp2.first}, n_group_{group_exp2.second}, n_slot_{group_exp2.second * c_group_size}, - control_{control_vector_type::map(xo::mm::ArenaConfig{.name_ = name, .size_ = control_size(n_slot_)})}, - slots_{slot_vector_type::map(xo::mm::ArenaConfig{.name_ = name, .size_ = n_slot_ * sizeof(value_type)})} + control_{control_vector_type::map + (xo::mm::ArenaConfig{ + .name_ = name, + .size_ = control_size(n_slot_)})}, + slots_{slot_vector_type::map + (xo::mm::ArenaConfig{ + .name_ = name, + .size_ = n_slot_ * sizeof(value_type)})} { /* here: arenas have allocated address range, but no committed memory yet */ @@ -64,6 +70,7 @@ namespace xo { this->n_group_exponent_ = 0; this->n_group_ = 0; this->n_slot_ = 0; + this->control_.resize(0); this->slots_.resize(0); } diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index 0962def5..bc672c8b 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace xo { @@ -76,6 +77,8 @@ namespace xo { } REQUIRE(n == map.size()); } + + REQUIRE(map.verify_ok(verify_policy::chatty())); } { @@ -97,6 +100,8 @@ namespace xo { } REQUIRE(n == map.size()); } + + REQUIRE(map.verify_ok(verify_policy::chatty())); } { @@ -131,6 +136,8 @@ namespace xo { } REQUIRE(n == map.size()); } + + REQUIRE(map.verify_ok(verify_policy::chatty())); } { @@ -140,6 +147,8 @@ namespace xo { REQUIRE(map.size() == 0); REQUIRE(map.groups() == 0); REQUIRE(map.capacity() == 0); + + REQUIRE(map.verify_ok(verify_policy::chatty())); } /* slightly different starting point, 0 capacity! */ @@ -149,6 +158,8 @@ namespace xo { /* try_insert should fail - no capacity */ REQUIRE(!x.first); REQUIRE(!x.second); + + REQUIRE(map.verify_ok(verify_policy::chatty())); } { @@ -172,6 +183,8 @@ namespace xo { } REQUIRE(n == map.size()); } + + REQUIRE(map.verify_ok(verify_policy::chatty())); } } @@ -228,14 +241,27 @@ namespace xo { TEST_CASE("DArenaHashMap-operator-bracket", "[arena][DArenaHashMap]") { + scope log(XO_DEBUG(false)); + using HashMap = DArenaHashMap; HashMap map; + // copy keys here so we can print stuff + std::vector key_v; + // insert via operator[] map[1] = 100; + key_v.push_back(1); + REQUIRE(map.verify_ok(verify_policy::chatty())); + map[2] = 200; + key_v.push_back(2); + REQUIRE(map.verify_ok(verify_policy::chatty())); + map[3] = 300; + key_v.push_back(3); + REQUIRE(map.verify_ok(verify_policy::chatty())); REQUIRE(map.size() == 3); @@ -248,6 +274,7 @@ namespace xo { map[2] = 250; REQUIRE(map[2] == 250); REQUIRE(map.size() == 3); // size unchanged + REQUIRE(map.verify_ok(verify_policy::chatty())); // verify via find { @@ -265,9 +292,40 @@ namespace xo { REQUIRE(it != map.end()); REQUIRE(it->second == 300); } + { + auto it = map.find(4); + REQUIRE(it == map.end()); + } + + REQUIRE(map.verify_ok(verify_policy::chatty())); // operator[] on non-existent key creates default entry int & val = map[999]; + key_v.push_back(999); + + for (uint64_t i_slot = 0, N = map._store()->n_slot_; i_slot < N; ++i_slot) { + auto key = map._store()->slots_[i_slot].first; + auto ctrl = map._store()->control_ + [i_slot + DArenaHashMapUtil::c_control_stub]; + auto isdata = DArenaHashMapUtil::is_data(ctrl); + auto [h1,h2] = map._hash(key); + + if ((key != 0) + || (h1 != 0) + || (h2 != 0) + || (ctrl != DArenaHashMapUtil::c_empty_slot) + || isdata + ) { + log && log(xtag("i", i_slot), + xtag("key[i]", key), + xtag("h1", h1), xtag("h2", h2), + xtag("ctrl[i]", (int)ctrl), + xtag("isdata", isdata)); + } + } + + REQUIRE(map.verify_ok(verify_policy::chatty())); + REQUIRE(map.size() == 4); REQUIRE(val == 0); // default-initialized val = 999; @@ -280,6 +338,8 @@ namespace xo { HashMap map(1024); + REQUIRE(map.verify_ok()); + map["hello"] = 42; REQUIRE(map.size() == 1); REQUIRE(map.verify_ok()); From 69d873c64049a7d58eda5dc7dd1c92d81d88efa8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 18 Jan 2026 19:17:54 -0500 Subject: [PATCH 080/111] xo-arena: + DArena methods {checkpoint, restore} --- include/xo/arena/DArena.hpp | 45 +++++++++++++------------------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 8eb6125c..8d9372a0 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -62,6 +62,12 @@ namespace xo { sub_complete, }; + /** @brief Checkpoint for unwinding arena state **/ + struct Checkpoint { + explicit Checkpoint(std::byte * x) : free_{x} {} + std::byte * free_; + }; + ///@} /** @defgroup mm-arena-ctors arena constructors and destructors **/ @@ -115,34 +121,6 @@ namespace xo { **/ bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); } -#ifdef OBSOLETE - /** obtain uncommitted contiguous memory range comprising - * a whole multiple of @p align_z bytes, of at least size @p req_z, - * aligned on a @p align_z boundary. Uncommitted memory is not (yet) - * backed by physical memory. - * - * If @p enable_hugepage_flag is true and THP - * (transparent huge pages) are available, use THP for arena memory. - * This relieves TLB and page table memory when @p req_z is a lot larger than - * page size (likely 4KB). Cost is that arena will consum physical memory in unit - * of @p align_z. Arena may waste up to @p align_z bytes of memory as a result. - * - * If @p enable_hugepage_flag is true, @p align_z should be huge page size - * (probably 2MB) for optimal performance. - * - * At present the THP feature is not supported on OSX. - * May be supportable through mach_vm_allocate(). - * - * Note that we reject MAP_HUGETLB|MAP_HUGE_2MB flags to mmap here, - * since requires previously-reserved memory in /proc/sys/vm/nr_hugepages. - * - * @return pair giving reserved memory address range [lo,hi) - **/ - static range_type map_aligned_range(size_type req_z, - size_type align_z, - bool enable_hugepage_flag); -#endif - /** true if arena is mapped i.e. has a reserved address range **/ bool is_mapped() const noexcept { return (lo_ != nullptr) && (hi_ != nullptr); } @@ -165,7 +143,8 @@ namespace xo { * * Require: * 1. @p mem is address returned by allocation on this arena - * i.e. by @ref IAllocator_DArena::alloc() or @ref IAllocator_DArena::alloc_super() + * i.e. by @ref IAllocator_DArena::alloc() or + * @ref IAllocator_DArena::alloc_super() * 2. @p mem has not been invalidated since it was allocated * i.e. by call to @ref DArena::clear * @@ -218,6 +197,14 @@ namespace xo { /** create initial guard **/ void establish_initial_guard() noexcept; + /** checkpoint arena state. Revert to the same state with + * @ref rstore + **/ + Checkpoint checkpoint() noexcept { return Checkpoint(free_); } + + /** restore arena state to previously-established checkpoint **/ + void restore(Checkpoint ckp) noexcept { free_ = ckp.free_; } + /** discard all allocated memory, return to empty state * Promise: * - committed memory unchanged From 2c1aac277a983aa8a06fdd1cec2f03f2e5233b8d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 18 Jan 2026 20:23:00 -0500 Subject: [PATCH 081/111] xo-reader2: corrections to toplevel SchematikaParser setup --- include/xo/arena/DArena.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 8d9372a0..ffd710f0 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -64,8 +64,9 @@ namespace xo { /** @brief Checkpoint for unwinding arena state **/ struct Checkpoint { + Checkpoint() = default; explicit Checkpoint(std::byte * x) : free_{x} {} - std::byte * free_; + std::byte * free_ = nullptr; }; ///@} From 2ae1ad892d0793171720eb08840b877c4f18fb6f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 23 Jan 2026 11:54:32 -0500 Subject: [PATCH 082/111] xo-reader2: + example app 'readerreplxx' --- include/xo/arena/ArenaConfig.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp index 4d79637b..db5a4400 100644 --- a/include/xo/arena/ArenaConfig.hpp +++ b/include/xo/arena/ArenaConfig.hpp @@ -17,6 +17,14 @@ namespace xo { * @brief configuration for a @ref DArena instance **/ struct ArenaConfig { + /** @defgroup mm-arenaconfig-ctors **/ + + ArenaConfig with_size(std::size_t z) { + ArenaConfig copy(*this); + copy.size_ = z; + return copy; + } + /** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/ ///@{ From c89e28367c51bbb8c768816b46b3cb819cb60409 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 2 Feb 2026 21:55:34 -0500 Subject: [PATCH 083/111] xo-interpreter2: scaffold repl + alloc measurement frameowkr --- include/xo/arena/ArenaConfig.hpp | 6 + include/xo/arena/DArena.hpp | 5 + include/xo/arena/DArenaHashMap.hpp | 6 + include/xo/arena/DArenaVector.hpp | 5 + include/xo/arena/DCircularBuffer.hpp | 3 + include/xo/arena/ErrorArena.hpp | 54 +++++ include/xo/arena/MemorySizeInfo.hpp | 37 ++++ include/xo/arena/arena_streambuf.hpp | 236 ++++++++++++++++++++++ include/xo/arena/hashmap/HashMapStore.hpp | 13 ++ src/arena/CMakeLists.txt | 2 + src/arena/DArena.cpp | 9 + src/arena/DCircularBuffer.cpp | 25 +++ src/arena/ErrorArena.cpp | 43 ++++ src/arena/arena_streambuf.cpp | 214 ++++++++++++++++++++ 14 files changed, 658 insertions(+) create mode 100644 include/xo/arena/ErrorArena.hpp create mode 100644 include/xo/arena/MemorySizeInfo.hpp create mode 100644 include/xo/arena/arena_streambuf.hpp create mode 100644 src/arena/ErrorArena.cpp create mode 100644 src/arena/arena_streambuf.cpp diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp index db5a4400..4d72b912 100644 --- a/include/xo/arena/ArenaConfig.hpp +++ b/include/xo/arena/ArenaConfig.hpp @@ -19,6 +19,12 @@ namespace xo { struct ArenaConfig { /** @defgroup mm-arenaconfig-ctors **/ + ArenaConfig with_name(std::string name) { + ArenaConfig copy(*this); + copy.name_ = name; + return copy; + } + ArenaConfig with_size(std::size_t z) { ArenaConfig copy(*this); copy.size_ = z; diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index ffd710f0..8c7203d6 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -7,6 +7,7 @@ #include "ArenaConfig.hpp" #include "AllocError.hpp" +#include "MemorySizeInfo.hpp" #include "AllocInfo.hpp" #include @@ -140,6 +141,9 @@ namespace xo { /** get header from allocated object address **/ header_type * obj2hdr(void * obj) noexcept; + /** resource ocnsumption in normal form **/ + MemorySizeInfo _store_info() const noexcept; + /** report alloc book-keeping info for allocation at @p mem * * Require: @@ -206,6 +210,7 @@ namespace xo { /** restore arena state to previously-established checkpoint **/ void restore(Checkpoint ckp) noexcept { free_ = ckp.free_; } + /** discard all allocated memory, return to empty state * Promise: * - committed memory unchanged diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 50b7b4aa..042dc900 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -45,6 +45,7 @@ namespace xo { using value_type = std::pair; using key_hash = Hash; using key_equal = Equal; + using MemorySizeInfo = xo::mm::MemorySizeInfo; using byte = std::byte; using group_type = detail::ControlGroup; using store_type = detail::HashMapStore; @@ -76,6 +77,11 @@ namespace xo { iterator begin() { return _promote_iterator(_begin_aux()); } iterator end() { return _promote_iterator(_end_aux()); } + std::size_t _n_store() const noexcept { return store_._n_store(); } + MemorySizeInfo _store_info(std::size_t i) const noexcept { + return store_._store_info(i); + } + /** insert @p kv_pair into hash map. * Replaces any previous value stored under the same key. * diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index f8bd9820..b7a0b2aa 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -79,6 +79,11 @@ namespace xo { constexpr T * data() { return reinterpret_cast(store_.lo_); } constexpr const T * data() const { return reinterpret_cast(store_.lo_); } + /** arena used for element storage + * (Might prefer obj here; refrain to avoid leveling violation) + **/ + MemorySizeInfo _store_info() const { return store_._store_info(); } + /** reserve space, if possible, for at least @p z elements. * Always limited by ArenaConfig.size_ **/ diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp index 30a38960..538d272d 100644 --- a/include/xo/arena/DCircularBuffer.hpp +++ b/include/xo/arena/DCircularBuffer.hpp @@ -83,6 +83,9 @@ namespace xo { const_span_type occupied_range() const noexcept { return occupied_range_; } const_span_type input_range() const noexcept { return input_range_; } + std::size_t _n_store() const noexcept; + MemorySizeInfo _store_info(std::size_t i) const noexcept; + /** verify DCircularBuffer invariants. * Act on failure according to policy @p p * (combination of throw|log bits) diff --git a/include/xo/arena/ErrorArena.hpp b/include/xo/arena/ErrorArena.hpp new file mode 100644 index 00000000..0236c325 --- /dev/null +++ b/include/xo/arena/ErrorArena.hpp @@ -0,0 +1,54 @@ +/** @file ErrorArena.hpp +* + * @author Roland Conybeare, Feb 2026 + **/ + +#pragma once + +#include "DArena.hpp" + +namespace xo { + namespace mm { + + /** @brief Dedicated arena for error reporting + * + * Reserving memory for error messaages. + * Motivation + * 1. so we have room to report an out-of-memory condition + * 2. so we have place to allocate for an error that + * doesn't interfere with other allocator state + * + * Expect to reset arena between errors, so only need + * enough room to report one error. + * + * To initialize explicitly: + * @code + * // before any other ErrorArena method calls: + * ErrorArena::init_once(cfg...); + * + * // do stuff with ErrorArena.. + * ErrorArena::instance() + * @endcode + * + * Reminder: can't use obj here, + * would be leveling violation. + **/ + class ErrorArena { + public: + /** default configuration for error arena **/ + static ArenaConfig default_config(); + + /** idempotent initialization **/ + static void init_once(const ArenaConfig & cfg = default_config()); + + /** get initialized instnace **/ + static DArena * instance(); + + private: + static DArena s_instance; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ErrorArena.hpp */ diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp new file mode 100644 index 00000000..7cba082c --- /dev/null +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -0,0 +1,37 @@ +/** @file MemorySizeInfo.hpp +* + * @author Roland Conybeare, Feb 2026 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + + struct MemorySizeInfo { + using size_type = std::size_t; + + MemorySizeInfo(std::string_view name, std::size_t a, std::size_t c, std::size_t r) + : resource_name_{name}, allocated_{a}, committed_{c}, reserved_{r} + {} + + static MemorySizeInfo sentinel() { return MemorySizeInfo("", 0, 0, 0); } + + /** resource name **/ + std::string_view resource_name_; + /** memory in-use **/ + std::size_t allocated_ = 0; + /** memory committed (backed by physical memory) **/ + std::size_t committed_ = 0; + /** memory reserved: + * virtual memory addresses range obtained, whether or not committed + **/ + std::size_t reserved_ = 0; + }; + + } +} + +/* end MemorySizeInfo.hpp */ diff --git a/include/xo/arena/arena_streambuf.hpp b/include/xo/arena/arena_streambuf.hpp new file mode 100644 index 00000000..2b364622 --- /dev/null +++ b/include/xo/arena/arena_streambuf.hpp @@ -0,0 +1,236 @@ +/** @file arena_streambuf.hpp +* + * @author Roland Conybeare, Feb 2026 + **/ + +#pragma once + +#include "DArena.hpp" +//#include "print/quoted_char.hpp" +#include +#include +#include +#include // e.g. for std::memcpy() +#include +#include + +namespace xo { + namespace mm { + /** @brief Arena-based buffer for logging and pretty-printing + * + * Arena-based using mmap + * Write to self-extending storage array + * Track position relative to start of line + **/ + class arena_streambuf : public std::streambuf { + public: + struct rewind_state { + explicit rewind_state(std::size_t solpos, std::size_t color_esc, std::uint32_t p) + : solpos{solpos}, color_escape_chars{color_esc}, pos{p} {} + + std::size_t solpos = 0; + std::size_t color_escape_chars = 0; + std::uint32_t pos = 0; + }; + + public: + /** arena should be ready-to-allocate i.e. have committed > 0 **/ + arena_streambuf(DArena * arena, bool debug_flag = false) : arena_{arena}, debug_flag_{debug_flag} { + this->reset_stream(); + } /*ctor*/ + + std::streamsize capacity() const { return arena_->committed(); } + const char * lo() const { return this->pbase(); } + const char * hi() const { return this->lo() + this->capacity(); } + std::uint32_t pos() const { return this->pptr() - this->pbase(); } + + /** output position (relative to pbase) when local state last computed. Exposed here for unit tests **/ + std::size_t _local_ppos() const { return local_ppos_; } + /** position (relative to pbase) one character after last \n or \r. For unit tests **/ + std::uint32_t _solpos() const { return solpos_; } + /** start of incomplete color-escape sequence **/ + const char * _color_escape_start() const { return color_escape_start_; } + /** number of non-printing chars after @ref solpos_ from completed color-escape sequences **/ + std::uint32_t _color_escape_chars() const { return color_escape_chars_; } + + /** number of visible characters since start of line (last \n or \r) **/ + std::uint32_t lpos() const; + + rewind_state checkpoint() const; + + bool debug_flag() const { return debug_flag_; } + + operator std::string_view () const { return std::string_view(this->pbase(), this->pptr()); } + + void reset_stream(); + + void rewind_to(rewind_state s); + + protected: + /** expand buffer storage (by 2x), preserve current contents **/ + void expand_to(std::size_t new_z); + + virtual std::streamsize xsputn(const char * s, std::streamsize n) override; + + virtual int_type overflow(int_type new_ch) override; + + /* off. offset, relative to starting point dir. + * dir. + * which. in|out|both + * + * Note that off=0,dir=cur,which=out reads offset + */ + virtual pos_type seekoff(off_type off, + std::ios_base::seekdir dir, + std::ios_base::openmode which) override; + + private: + void _update_local_state_char(const char * p_lo, const char * p) + { + if ((*p == '\n') || (*p == '\r')) { + this->solpos_ = (p+1 - this->pbase()); + /* reset, since these chars relevant as correction to solpos */ + this->color_escape_chars_ = 0; + /* -> incomplete color escape, broken by newline */ + this->color_escape_start_ = nullptr; + } else if (*p == '\033') { + if (debug_flag_) [[unlikely]] { + std::cout << "xsputn: \\033 at p-p_lo=" << (p - p_lo) << std::endl; + } + this->color_escape_start_ = p; + } else if (this->color_escape_start_ != nullptr) { + if (*p == 'm') { + /* escape seq non-printing including both endpoints */ + std::int64_t esc_chars = (p+1 - color_escape_start_); + + this->color_escape_chars_ += esc_chars; + + if (debug_flag_) [[unlikely]] { + std::cout << "xsputn: m at p-p_lo" << (p - p_lo) << " +" << esc_chars + << " -> color_escape_chars=" << color_escape_chars_ << std::endl; + } + this->color_escape_start_ = nullptr; + } else if (!isdigit(*p) && (*p != '[') && (*p != ';')) { + /* not color escape after all */ + this->color_escape_start_ = nullptr; + } + } + } + + /** recognize stale local state vars: + * @ref solpos_, @ref color_escape_chars_, @ref color_escape_start_. + * + * Require: + * - {pbase, pptr} in consistent state + * Promise: + * - @c local_ppos_ + @c pbase = @c pptr + * - @c solpos_, @c color_escape_chars_, @c color_escape_start_ all up-to-date + **/ + void _check_update_local_state() { + const char * p0 = this->pbase(); + const char * pn = this->pptr(); + + if (debug_flag_) { + std::cerr << "_check_update_local_state:" << std::endl; + std::cerr << " buf: (p0=" << (void*)p0 << ", pn=" << (void*)pn << ")" << std::endl; + std::cerr << " solpos_=" << solpos_ << ", color_escape_chars_=" << color_escape_chars_ << std::endl; + } + + if (p0 + local_ppos_ == pn) [[likely]] { + // solpos_, color_escape_chars_, color_escape_start_ all up-to-date + } else { + // [pnew, pn): input that hasn't been incorporated into + // {solpos_, color_escape_chars_, color_escape_start_) + + const char * pnew = this->pbase() + this->local_ppos_; + + if (debug_flag_) { + std::cerr << "_check_update_local_state: range: (pnew=" << (void*)pnew << ", pn=" << (void*)pn << ")" << std::endl; + } + + for(const char * p = pnew; p < pn; ++p) { + this->_update_local_state_char(p0, p); + } + } + + // solpos_, color_escape_chars_, color_escape_start_ all up-to-date + // for current buffered contents + + this->local_ppos_ = pn - p0; + + if (debug_flag_) { + std::cerr << "_check_update_local_state: pos=" << pos(); + std::cerr << ", solpos=" << solpos_; + std::cerr << ", color_escape_chars=" << color_escape_chars_ << std::endl; + } + + assert(pos() >= solpos_ + color_escape_chars_); + } + + private: + /* + * pbase: start of buffered text. Thils will be arena_->lo_ + * + * + * pbase pptr epptr + * v >e1< >e2< v v + * |xx\xxEEExxx\xxxxxxxEExxxxEExxxxxxxEExxx\xEExxxxxx..................| + * ^ ^<------new-------> + * solpos local_ppos + * + * solpos : first character after newline (stale) + * color_escape_pos : e1+e2+.. (stale) + * new : new characters not reflected + * in local_ppos_, color_escape_chars_ etc. + * + * Legend: + * [\] newline + * [x] visible character + * [E] color escape chars + * + * + * after _check_update_local_state(): + * + * + * pbase pptr epptr + * v >e1< v v + * |xx\xxEEExxx\xxxxxxxEExxxxEExxxxxxxEExxx\xEExxxxxx..................| + * ^ ^ + * solpos local_ppos + * + */ + + /** @defgroup logstreambuf-instance-vars **/ + ///@{ + + /** value of pptr (relative to pbase) when _check_update_local_state() last ran **/ + std::size_t local_ppos_ = 0; + /** position (relative to pbase) one character after last \n or \r. + * Use to drive @ref lpos. This _has_ to be lazy, since + * xsputn() isn'g guaranteed to be called when there's room in + * in buffer. + **/ + std::size_t solpos_ = 0; + /** number of non-printing chars after @ref solpos_, from + * completed color escape sequences. + * (ansi color escapes = text between '\033' and 'm') + **/ + std::size_t color_escape_chars_ = 0; + /** non-null: start of incomplete color escape sequence **/ + const char * color_escape_start_ = nullptr; + + /** buffered output stored here. + * We don't use arena's allocation api, just treat as a block of available memory + **/ + DArena * arena_ = nullptr;; + /** true to debug log_streambuf itself **/ + bool debug_flag_ = false; + + ///@} + }; /*log_streambuf*/ + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end arena_streambuf.hpp */ + diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index fd7c87f7..74f2f4e7 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -19,6 +19,7 @@ namespace xo { using group_type = detail::ControlGroup; using control_vector_type = xo::mm::DArenaVector; using slot_vector_type = xo::mm::DArenaVector; + using MemorySizeInfo = xo::mm::MemorySizeInfo; public: /** group_exp2: number of groups {x, 2^x} **/ @@ -47,6 +48,18 @@ namespace xo { size_type capacity() const noexcept { return n_group_ * c_group_size; } float load_factor() const noexcept { return size_ / static_cast(n_slot_); } + std::size_t _n_store() const noexcept { return 2; } + MemorySizeInfo _store_info(std::size_t i) const noexcept { + switch (i) { + case 0: + return control_._store_info(); + case 1: + return slots_._store_info(); + } + + return MemorySizeInfo::sentinel(); + } + void resize_from_empty(const std::pair & group_exp2) { diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 5288c393..b3bb35a6 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -2,6 +2,8 @@ set(SELF_LIB xo_arena) set(SELF_SRCS + arena_streambuf.cpp + ErrorArena.cpp cmpresult.cpp mmap_util.cpp AllocError.cpp diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index 670666f5..3156fc1c 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -165,6 +165,15 @@ namespace xo { return (header_type *)((byte *)obj - sizeof(header_type)); } + MemorySizeInfo + DArena::_store_info() const noexcept + { + return MemorySizeInfo(config_.name_, + this->allocated(), + this->committed(), + this->reserved()); + } + AllocInfo DArena::alloc_info(value_type mem) const noexcept { diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 2a415c49..2a5fb486 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -79,6 +79,31 @@ namespace xo { { } + std::size_t + DCircularBuffer::_n_store() const noexcept + { + return 2; + } + + MemorySizeInfo + DCircularBuffer::_store_info(std::size_t i) const noexcept + { + switch (i) { + case 0: + return MemorySizeInfo(config_.name_, + occupied_range_.size(), + mapped_range_.size(), + reserved_range_.size()); + case 1: + return pinned_spans_._store_info(); + default: + break; + } + + return MemorySizeInfo::sentinel(); + } + + bool DCircularBuffer::verify_ok(verify_policy policy) const { diff --git a/src/arena/ErrorArena.cpp b/src/arena/ErrorArena.cpp new file mode 100644 index 00000000..d1d3d62b --- /dev/null +++ b/src/arena/ErrorArena.cpp @@ -0,0 +1,43 @@ +/** @file ErrorArena.cpp +* + * @author Roland Conybeare, Feb 2026 + **/ + +#include "ErrorArena.hpp" + +namespace xo { + namespace mm { + DArena + ErrorArena::s_instance; + + ArenaConfig + ErrorArena::default_config() + { + return ArenaConfig().with_name("error-arena").with_size(16 * 1024); + } + + namespace { + bool s_init_done = false; + } + + void + ErrorArena::init_once(const ArenaConfig & cfg) + { + if (!s_init_done) { + s_init_done = true; + s_instance = DArena::map(cfg); + } + } + + DArena * + ErrorArena::instance() + { + init_once(default_config()); + + return &s_instance; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ErrorArena.cpp */ diff --git a/src/arena/arena_streambuf.cpp b/src/arena/arena_streambuf.cpp new file mode 100644 index 00000000..18ea132e --- /dev/null +++ b/src/arena/arena_streambuf.cpp @@ -0,0 +1,214 @@ +/** @file arena_streambuf.cpp + * + * @author Roland Conybeare, Feb 2026 + **/ + +#include "arena_streambuf.hpp" + +namespace xo { + namespace mm { + + std::uint32_t + arena_streambuf::lpos() const + { + if (debug_flag_) { + std::cerr << "log_streambuf::lpos: enter" << std::endl; + } + + // logically-const. lazy implementation + arena_streambuf * self = const_cast(this); + + self->_check_update_local_state(); + + return pos() - solpos_ - color_escape_chars_; + } + + auto + arena_streambuf::checkpoint() const -> rewind_state + { + // logically-const. lazy implementation + arena_streambuf * self = const_cast(this); + + self->_check_update_local_state(); + + return rewind_state(solpos_, color_escape_chars_, pos()); + } + + void + arena_streambuf::reset_stream() + { + assert(arena_); + assert(arena_->committed() > 0); + + char * p_lo = (char *)(arena_->lo_); + char * p_hi = (char *)(arena_->limit_); + + /* tells parent our buffer extent */ + this->setp(p_lo, p_hi); + + this->local_ppos_ = 0; + this->solpos_ = 0; + this->color_escape_chars_ = 0; + this->color_escape_start_ = nullptr; + } + + void + arena_streambuf::rewind_to(rewind_state s) + { + if (debug_flag_) { + std::cout << "rewind_to: pos " << pos() << "->" << s.pos + << " solpos " << solpos_ << "->" << s.solpos + << " color_esc " << color_escape_chars_ << "->" << s.color_escape_chars + << std::endl; + } + + /* .setp(): using just for side effect: sets .pptr to .pbase */ + this->setp(this->pbase(), this->epptr()); + /* advance pptr to saved position */ + this->pbump(s.pos); + + this->local_ppos_ = this->pptr() - this->pbase(); + this->solpos_ = s.solpos; + this->color_escape_chars_ = s.color_escape_chars; + /* assuming we never try to capture rewind state with incomplete color escape */ + this->color_escape_start_ = nullptr; + } + + void + arena_streambuf::expand_to(std::size_t new_z) + { + char * old_pptr = pptr(); + std::streamsize old_n = old_pptr - pbase(); + + assert(old_n <= static_cast(arena_->allocated())); + assert(new_z > arena_->committed()); + + /* note: local_ppos_ invariant across expand_to() */ + + arena_->expand(new_z); + + char * p_base = (char *)(arena_->lo_); + char * p_hi = (char *)(arena_->limit_); + + this->setp(p_base, p_hi); + this->pbump(old_n); + } + + std::streamsize + arena_streambuf::xsputn(const char * s, std::streamsize n) + { + /* s must be an address in [this->lo() .. this->lo() + capacity()] */ + + assert(hi() >= pptr()); + + if (pptr() + n > hi()) { + std::size_t new_z = std::max(2 * arena_->committed(), std::size_t(this->pos() + n + 1)); + + if (new_z > arena_->reserved()) + new_z = arena_->reserved(); + + this->expand_to(new_z); + } + + if (debug_flag_) { + std::cout << "xsputn: pbase=" << (void *)(this->pbase()) + << ", pptr=" << (void*)(this->pptr()) + << "(+" << (this->pptr() - this->lo()) << ")" + << ", n=" << n << " -> (+" << (this->pptr() + n - this->lo()) << ")" + << ", arena.size=" << this->arena_->committed() + << std::endl; + } + + std::streamsize ncopied = 0; + + if (this->pptr() + n > this->hi()) { + ncopied = this->hi() - this->pptr(); + } else { + ncopied = n; + } + + if (false /*debug_flag_*/) { + std::cout << "xsputn: copying ncopied=" << ncopied << " (/n=" << n << ") bytes into range [lo,hi)" + << ", lo=" << (void*)this->pptr() + << ", hi=" << (void*)(this->pptr() + n) + << std::endl; + } + + std::memcpy(this->pptr(), s, ncopied); + + this->pbump(ncopied); + + /* now {pbase, pptr} consistent with new input */ + + this->_check_update_local_state(); + + return ncopied; + } + + auto + arena_streambuf::overflow(int_type new_ch) -> int_type + { + char * old_base = this->pbase(); + char * old_pptr = this->pptr(); + /* #of chars buffered */ + std::streamsize old_n = old_pptr - old_base; + + assert(old_n <= static_cast(arena_->committed())); + + // if (debug_flag_) { + // std::cout << "overflow: new_ch=" << quoted_char(new_ch) << std::endl; + // } + + /* increase buffer size */ + this->expand_to(2 * arena_->committed()); + + arena_->lo_[old_n] = static_cast(new_ch); + this->pbump(1); + + if ((new_ch == static_cast('\n')) || (new_ch == static_cast('\r'))) { + this->solpos_ = this->pos(); + + // what if new_ch starts color escape ? + } + + if (new_ch == std::char_traits::eof()) { + /* reminder: returning eof sets badbit on ostream */ + return std::char_traits::not_eof(new_ch); + } else { + return new_ch; + } + } + + auto + arena_streambuf::seekoff(off_type off, + std::ios_base::seekdir dir, + std::ios_base::openmode which) -> pos_type + { + //std::cout << "seekoff: off=" << off << ", dir=" << dir << ", which=" << which << std::endl; + if (debug_flag_) { + std::cout << "seekoff(off,dir,which)" << std::endl; + } + + // Only output stream is supported + if (which != std::ios_base::out) + throw std::runtime_error("log_streambuf: only output mode supported"); + + if (dir == std::ios_base::cur) { + this->pbump(off); + } else if (dir == std::ios_base::end) { + /* .setp(): using for side effect: sets .pptr to .pbase */ + this->setp(this->pbase(), this->epptr()); + this->pbump(off); + } else if (dir == std::ios_base::beg) { + /* .setp(): using for side effect: sets .pptr to .pbase */ + this->setp(this->pbase(), this->epptr()); + this->pbump(this->capacity() + off); + } + + return this->pptr() - this->pbase(); + } /*seekoff*/ + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end arena_streambuf.cpp */ From 11293d254e9374ab1ccc1ac5700910838cb85fed Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 3 Feb 2026 01:05:36 -0500 Subject: [PATCH 084/111] xo-interpreter2 .. xo-arena. memory pool introspection --- include/xo/arena/ArenaConfig.hpp | 4 ++-- include/xo/arena/DArena.hpp | 8 ++++--- include/xo/arena/DArenaHashMap.hpp | 7 +++--- include/xo/arena/DArenaVector.hpp | 2 +- include/xo/arena/DCircularBuffer.hpp | 4 ++-- include/xo/arena/MemorySizeInfo.hpp | 10 ++++++-- include/xo/arena/hashmap/HashMapStore.hpp | 15 ++++-------- src/arena/DArena.cpp | 13 +++++------ src/arena/DCircularBuffer.cpp | 28 ++++++----------------- 9 files changed, 38 insertions(+), 53 deletions(-) diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp index 4d72b912..9ca2a387 100644 --- a/include/xo/arena/ArenaConfig.hpp +++ b/include/xo/arena/ArenaConfig.hpp @@ -19,13 +19,13 @@ namespace xo { struct ArenaConfig { /** @defgroup mm-arenaconfig-ctors **/ - ArenaConfig with_name(std::string name) { + ArenaConfig with_name(std::string name) const { ArenaConfig copy(*this); copy.name_ = name; return copy; } - ArenaConfig with_size(std::size_t z) { + ArenaConfig with_size(std::size_t z) const { ArenaConfig copy(*this); copy.size_ = z; return copy; diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 8c7203d6..4c12c8cb 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -138,12 +138,14 @@ namespace xo { **/ AllocHeader * end_header() const noexcept; + /** report memory use for this arena to @p fn. + * For DArena reporting just one pool = arena's memory range + **/ + void visit_pools(const MemorySizeVisitor & fn) const; + /** get header from allocated object address **/ header_type * obj2hdr(void * obj) noexcept; - /** resource ocnsumption in normal form **/ - MemorySizeInfo _store_info() const noexcept; - /** report alloc book-keeping info for allocation at @p mem * * Require: diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 042dc900..da0e742a 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -45,7 +45,7 @@ namespace xo { using value_type = std::pair; using key_hash = Hash; using key_equal = Equal; - using MemorySizeInfo = xo::mm::MemorySizeInfo; + using MemorySizeVisitor = xo::mm::MemorySizeVisitor; using byte = std::byte; using group_type = detail::ControlGroup; using store_type = detail::HashMapStore; @@ -77,9 +77,8 @@ namespace xo { iterator begin() { return _promote_iterator(_begin_aux()); } iterator end() { return _promote_iterator(_end_aux()); } - std::size_t _n_store() const noexcept { return store_._n_store(); } - MemorySizeInfo _store_info(std::size_t i) const noexcept { - return store_._store_info(i); + void visit_pools(const MemorySizeVisitor & visitor) const { + return store_.visit_pools(visitor); } /** insert @p kv_pair into hash map. diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index b7a0b2aa..33230b0c 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -82,7 +82,7 @@ namespace xo { /** arena used for element storage * (Might prefer obj here; refrain to avoid leveling violation) **/ - MemorySizeInfo _store_info() const { return store_._store_info(); } + void visit_pools(const MemorySizeVisitor & fn) const { store_.visit_pools(fn); } /** reserve space, if possible, for at least @p z elements. * Always limited by ArenaConfig.size_ diff --git a/include/xo/arena/DCircularBuffer.hpp b/include/xo/arena/DCircularBuffer.hpp index 538d272d..ba744531 100644 --- a/include/xo/arena/DCircularBuffer.hpp +++ b/include/xo/arena/DCircularBuffer.hpp @@ -83,8 +83,8 @@ namespace xo { const_span_type occupied_range() const noexcept { return occupied_range_; } const_span_type input_range() const noexcept { return input_range_; } - std::size_t _n_store() const noexcept; - MemorySizeInfo _store_info(std::size_t i) const noexcept; + /** report memory-size info for this buffer to @p fn **/ + void visit_pools(const MemorySizeVisitor & fn) const; /** verify DCircularBuffer invariants. * Act on failure according to policy @p p diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp index 7cba082c..f12dcce5 100644 --- a/include/xo/arena/MemorySizeInfo.hpp +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -5,7 +5,8 @@ #pragma once -#include +#include +#include namespace xo { namespace mm { @@ -31,7 +32,12 @@ namespace xo { std::size_t reserved_ = 0; }; - } + /** function that visits MemorySizeInfo for a collection of @p n memory pools. + * Each pool reported with index @p i in [0, n), with associated + * size record @p info. + **/ + using MemorySizeVisitor = std::function; + } } /* end MemorySizeInfo.hpp */ diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 74f2f4e7..22bfb120 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -19,7 +19,7 @@ namespace xo { using group_type = detail::ControlGroup; using control_vector_type = xo::mm::DArenaVector; using slot_vector_type = xo::mm::DArenaVector; - using MemorySizeInfo = xo::mm::MemorySizeInfo; + using MemorySizeVisitor = xo::mm::MemorySizeVisitor; public: /** group_exp2: number of groups {x, 2^x} **/ @@ -48,16 +48,9 @@ namespace xo { size_type capacity() const noexcept { return n_group_ * c_group_size; } float load_factor() const noexcept { return size_ / static_cast(n_slot_); } - std::size_t _n_store() const noexcept { return 2; } - MemorySizeInfo _store_info(std::size_t i) const noexcept { - switch (i) { - case 0: - return control_._store_info(); - case 1: - return slots_._store_info(); - } - - return MemorySizeInfo::sentinel(); + void visit_pools(const MemorySizeVisitor & visitor) const { + control_.visit_pools(visitor); + slots_.visit_pools(visitor); } void resize_from_empty(const std::pairallocated(), - this->committed(), - this->reserved()); + void + DArena::visit_pools(const MemorySizeVisitor & fn) const { + fn(MemorySizeInfo(config_.name_, + this->allocated(), + this->committed(), + this->reserved())); } AllocInfo diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 2a5fb486..667126af 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -79,30 +79,16 @@ namespace xo { { } - std::size_t - DCircularBuffer::_n_store() const noexcept + void + DCircularBuffer::visit_pools(const MemorySizeVisitor & visitor) const { - return 2; - } + visitor(MemorySizeInfo(config_.name_, + occupied_range_.size(), + mapped_range_.size(), + reserved_range_.size())); - MemorySizeInfo - DCircularBuffer::_store_info(std::size_t i) const noexcept - { - switch (i) { - case 0: - return MemorySizeInfo(config_.name_, - occupied_range_.size(), - mapped_range_.size(), - reserved_range_.size()); - case 1: - return pinned_spans_._store_info(); - default: - break; - } - - return MemorySizeInfo::sentinel(); + pinned_spans_.visit_pools(visitor); } - bool DCircularBuffer::verify_ok(verify_policy policy) const From 64a780ce19f3c73fddccd109dfae24824ddd2d50 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 3 Feb 2026 10:32:43 -0500 Subject: [PATCH 085/111] xo-reader2 stack: curate memory pool naming --- include/xo/arena/DArenaHashMap.hpp | 16 ++++++++++------ include/xo/arena/MemorySizeInfo.hpp | 1 + include/xo/arena/hashmap/HashMapStore.hpp | 4 ++-- src/arena/DCircularBuffer.cpp | 2 +- utest/DArenaHashMap.test.cpp | 12 ++++++------ 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index da0e742a..c40f6926 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -55,9 +55,11 @@ namespace xo { public: /** create hash map **/ - DArenaHashMap(size_type hint_max_capacity, + DArenaHashMap(const std::string & name, + size_type hint_max_capacity, bool debug_flag = false); - DArenaHashMap(Hash && hash = Hash(), + DArenaHashMap(const std::string & name, + Hash && hash = Hash(), Equal && eq = Equal(), size_type hint_max_capacity = 0, bool debug_flag = false); @@ -196,9 +198,10 @@ namespace xo { }; template - DArenaHashMap::DArenaHashMap(size_type hint_max_capacity, + DArenaHashMap::DArenaHashMap(const std::string & name, + size_type hint_max_capacity, bool debug_flag) - : DArenaHashMap(Hash(), Equal(), hint_max_capacity, debug_flag) + : DArenaHashMap(name, Hash(), Equal(), hint_max_capacity, debug_flag) { } @@ -207,13 +210,14 @@ namespace xo { * last 16 bytes will be copy of first 16 bytes */ template - DArenaHashMap::DArenaHashMap(Hash && hash, + DArenaHashMap::DArenaHashMap(const std::string & name, + Hash && hash, Equal && eq, size_type hint_max_capacity, bool debug_flag) : hash_{std::move(hash)}, equal_{std::move(eq)}, - store_{"arenahashmap", lub_exp2(lub_group_mult(hint_max_capacity))}, + store_{name, lub_exp2(lub_group_mult(hint_max_capacity))}, debug_flag_{debug_flag} { } diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp index f12dcce5..540453fd 100644 --- a/include/xo/arena/MemorySizeInfo.hpp +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -5,6 +5,7 @@ #pragma once +#include #include #include diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 22bfb120..34850222 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -32,11 +32,11 @@ namespace xo { n_slot_{group_exp2.second * c_group_size}, control_{control_vector_type::map (xo::mm::ArenaConfig{ - .name_ = name, + .name_ = name + "-ctl", .size_ = control_size(n_slot_)})}, slots_{slot_vector_type::map (xo::mm::ArenaConfig{ - .name_ = name, + .name_ = name + "-slots", .size_ = n_slot_ * sizeof(value_type)})} { /* here: arenas have allocated address range, but no committed memory yet */ diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 667126af..372462f4 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -75,7 +75,7 @@ namespace xo { mapped_range_{reserved_range_.prefix(0)}, occupied_range_{mapped_range_.prefix(0)}, input_range_{occupied_range_.prefix(0)}, - pinned_spans_{} + pinned_spans_{DArenaVector::map(ArenaConfig().with_name(config.name_ + "-pins"))} { } diff --git a/utest/DArenaHashMap.test.cpp b/utest/DArenaHashMap.test.cpp index bc672c8b..02ac3fc3 100644 --- a/utest/DArenaHashMap.test.cpp +++ b/utest/DArenaHashMap.test.cpp @@ -24,7 +24,7 @@ namespace xo { { using HashMap = DArenaHashMap; - HashMap map; + HashMap map("utest"); REQUIRE(map.empty()); REQUIRE(map.size() == 0); @@ -36,7 +36,7 @@ namespace xo { { using HashMap = DArenaHashMap; - HashMap map(257); + HashMap map("utest", 257); REQUIRE(map.empty()); REQUIRE(map.size() == 0); @@ -49,7 +49,7 @@ namespace xo { { using HashMap = DArenaHashMap; - HashMap map; + HashMap map("utest"); REQUIRE(map.empty()); REQUIRE(map.size() == 0); @@ -209,7 +209,7 @@ namespace xo { */ for (std::uint32_t n = 0; n <= 8; ) { - HashMap hash_map; + HashMap hash_map("utest"); auto test_fn = [&rgen, &hash_map](bool dbg_flag, std::uint32_t n) @@ -245,7 +245,7 @@ namespace xo { using HashMap = DArenaHashMap; - HashMap map; + HashMap map("utest"); // copy keys here so we can print stuff std::vector key_v; @@ -336,7 +336,7 @@ namespace xo { { using HashMap = DArenaHashMap; - HashMap map(1024); + HashMap map("utest", 1024); REQUIRE(map.verify_ok()); From f9e266b0fc5b8fd5154010571b95844969b2cddd Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 3 Feb 2026 11:55:50 -0500 Subject: [PATCH 086/111] xo-interpreter2 stack: cleanup memory reporting --- include/xo/arena/ArenaConfig.hpp | 8 +++++++- include/xo/arena/DArena.hpp | 2 +- include/xo/arena/DArenaHashMap.hpp | 6 ++++-- include/xo/arena/DArenaVector.hpp | 22 +++++++++++++++++++--- include/xo/arena/hashmap/HashMapStore.hpp | 6 ++++-- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp index 9ca2a387..d1ecbdca 100644 --- a/include/xo/arena/ArenaConfig.hpp +++ b/include/xo/arena/ArenaConfig.hpp @@ -31,6 +31,12 @@ namespace xo { return copy; } + ArenaConfig with_store_header_flag(bool x) const { + ArenaConfig copy(*this); + copy.store_header_flag_ = x; + return copy; + } + /** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/ ///@{ @@ -44,7 +50,7 @@ namespace xo { std::size_t hugepage_z_ = 2 * 1024 * 1024; /** true to store header (8 bytes) at the beginning of each allocation. * necessary and sufficient to allows iterating over allocs - * present in arena + * present in arena. **/ bool store_header_flag_ = false; /** configuration for per-alloc header **/ diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 4c12c8cb..658d2b1b 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -205,7 +205,7 @@ namespace xo { void establish_initial_guard() noexcept; /** checkpoint arena state. Revert to the same state with - * @ref rstore + * @ref restore **/ Checkpoint checkpoint() noexcept { return Checkpoint(free_); } diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index c40f6926..c52b9894 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -30,8 +30,10 @@ namespace xo { * * Replicates (to the extent feasible) std::unordered_map * - * @tparam K key type. - * @tparam V value type. + * @tparam Key key type. + * @tparam Value value type. + * @tparam Hash hash function for keys + * @tparam Equal equality function for keys **/ template @@ -122,14 +123,16 @@ namespace xo { size_type arena_align_z, DArena::value_type lo, DArena::value_type hi) - : store_{cfg, page_z, arena_align_z, lo, hi} + : store_{cfg, page_z, arena_align_z, lo, hi}, + zero_ckp_{store_.checkpoint()} {} template DArenaVector::DArenaVector(DArenaVector && other) - : size_{other.size_}, store_{std::move(other.store_)} + : size_{other.size_}, store_{std::move(other.store_)}, zero_ckp_{std::move(other.zero_ckp_)} { other.size_ = 0; + other.zero_ckp_ = DArena::Checkpoint(); } template @@ -153,8 +156,10 @@ namespace xo { { this->size_ = other.size_; this->store_ = std::move(other.store_); + this->zero_ckp_ = std::move(other.zero_ckp_); other.size_ = 0; + other.zero_ckp_ = DArena::Checkpoint(); return *this; } @@ -166,6 +171,7 @@ namespace xo { DArenaVector retval; retval.store_ = std::move(DArena::map(cfg)); + retval.zero_ckp_ = retval.store_.checkpoint(); return retval; } @@ -179,9 +185,11 @@ namespace xo { template void DArenaVector::resize(size_type z) { + // new arena size in bytes + size_t req_z = z * sizeof(T); + if (z > size_) { // expand arena to accomodate - size_t req_z = z * sizeof(T); store_.expand(req_z); @@ -208,6 +216,14 @@ namespace xo { } } + // rewind to checkpoint, then reallocate. + // This is for form's sake, so that DArena considers memory + // to be 'allocated'. DArenaVector doesn't care for itself, + // but this preserves expected behavior of visit_pools(). + // + store_.restore(zero_ckp_); + store_.alloc(xo::reflect::typeseq::id(), req_z); + this->size_ = z; } diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 34850222..436e88ed 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -33,11 +33,13 @@ namespace xo { control_{control_vector_type::map (xo::mm::ArenaConfig{ .name_ = name + "-ctl", - .size_ = control_size(n_slot_)})}, + .size_ = control_size(n_slot_), + .store_header_flag_ = false})}, slots_{slot_vector_type::map (xo::mm::ArenaConfig{ .name_ = name + "-slots", - .size_ = n_slot_ * sizeof(value_type)})} + .size_ = n_slot_ * sizeof(value_type), + .store_header_flag_ = false})} { /* here: arenas have allocated address range, but no committed memory yet */ From 1c29400495c7d2161383b4f0cbb330041b25fd28 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 3 Feb 2026 12:23:56 -0500 Subject: [PATCH 087/111] xo-interpreter2 stack: + MemorySizeInfo.used + pop for DArenaHashMap --- include/xo/arena/MemorySizeInfo.hpp | 11 +++++++---- include/xo/arena/hashmap/HashMapStore.hpp | 17 +++++++++++++++-- src/arena/DArena.cpp | 5 +++++ src/arena/DCircularBuffer.cpp | 1 + 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp index 540453fd..764eae6d 100644 --- a/include/xo/arena/MemorySizeInfo.hpp +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -15,15 +15,18 @@ namespace xo { struct MemorySizeInfo { using size_type = std::size_t; - MemorySizeInfo(std::string_view name, std::size_t a, std::size_t c, std::size_t r) - : resource_name_{name}, allocated_{a}, committed_{c}, reserved_{r} + MemorySizeInfo() = default; + MemorySizeInfo(std::string_view name, std::size_t u, std::size_t a, std::size_t c, std::size_t r) + : resource_name_{name}, used_{u}, allocated_{a}, committed_{c}, reserved_{r} {} - static MemorySizeInfo sentinel() { return MemorySizeInfo("", 0, 0, 0); } + static MemorySizeInfo sentinel() { return MemorySizeInfo(); } /** resource name **/ std::string_view resource_name_; - /** memory in-use **/ + /** memory used (excluding wasted space) **/ + std::size_t used_ = 0; + /** memory allocated (including wasted space e.g. empty slots in hash tables **/ std::size_t allocated_ = 0; /** memory committed (backed by physical memory) **/ std::size_t committed_ = 0; diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 436e88ed..6393bb02 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -20,6 +20,7 @@ namespace xo { using control_vector_type = xo::mm::DArenaVector; using slot_vector_type = xo::mm::DArenaVector; using MemorySizeVisitor = xo::mm::MemorySizeVisitor; + using MemorySizeInfo = xo::mm::MemorySizeInfo; public: /** group_exp2: number of groups {x, 2^x} **/ @@ -51,8 +52,20 @@ namespace xo { float load_factor() const noexcept { return size_ / static_cast(n_slot_); } void visit_pools(const MemorySizeVisitor & visitor) const { - control_.visit_pools(visitor); - slots_.visit_pools(visitor); + // complexity here in service of HashMapStore-specific value for MemorySizeInfo.used + + MemorySizeInfo ctl_info; + MemorySizeInfo slot_info; + + control_.visit_pools([&ctl_info](const auto & x) { ctl_info = x; }); + slots_.visit_pools([&slot_info](const auto & x) { slot_info = x; }); + + // control: 1 byte per (key,value) pair + ctl_info.used_ = size_; + slot_info.used_ = size_ * sizeof(value_type); + + visitor(ctl_info); + visitor(slot_info); } void resize_from_empty(const std::pairallocated() /*used*/, this->allocated(), this->committed(), this->reserved())); diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index 372462f4..e5b1c725 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -83,6 +83,7 @@ namespace xo { DCircularBuffer::visit_pools(const MemorySizeVisitor & visitor) const { visitor(MemorySizeInfo(config_.name_, + occupied_range_.size() /*used*/, occupied_range_.size(), mapped_range_.size(), reserved_range_.size())); From 0fcf2f6fe211c704db1995ce90500ea9c98dd380 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:03:48 -0500 Subject: [PATCH 088/111] xo-arena: + ArenaHashMapConfig with functional mutators --- include/xo/arena/ArenaHashMapConfig.hpp | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 include/xo/arena/ArenaHashMapConfig.hpp diff --git a/include/xo/arena/ArenaHashMapConfig.hpp b/include/xo/arena/ArenaHashMapConfig.hpp new file mode 100644 index 00000000..69174141 --- /dev/null +++ b/include/xo/arena/ArenaHashMapConfig.hpp @@ -0,0 +1,58 @@ +/** @file ArenaHashMapConfig.hpp + * + * @author Roland Conybeare, Feb 2026 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace map { + /** @class ArenaHashMapConfig + * + * @brief configuration for a @ref DArenaHashMap instance + **/ + struct ArenaHashMapConfig { + /** @defgroup map-arenahashmapconfig-ctors **/ + ///@{ + + ArenaHashMapConfig with_name(std::string name) const { + ArenaHashMapConfig copy(*this); + copy.name_ = name; + return copy; + } + + ArenaHashMapConfig with_hint_max_capacity(std::size_t z) const { + ArenaHashMapConfig copy(*this); + copy.hint_max_capacity_ = z; + return copy; + } + + ArenaHashMapConfig with_debug_flag(bool x) const { + ArenaHashMapConfig copy(*this); + copy.debug_flag_ = x; + return copy; + } + + ///@} + /** @defgroup mm-arenahashmapconfig-instance-vars ArenaHashMapConfig members **/ + ///@{ + + /** optional name, for diagnostics **/ + std::string name_; + /** desired hard max hashmap size -> reserved virtual memory + * hint: actual max may be larger, because of power-of-2 considerations. + **/ + std::size_t hint_max_capacity_ = 0; + /** true to enable debug logging **/ + bool debug_flag_ = false; + + ///@} + }; + + } /*namespace map*/ +} /*namespace xo*/ + +/* end ArenaHashMapConfig.hpp */ From 055681047178b6f0ab3877ecf0b6b0b46f62fa0b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:08:02 -0500 Subject: [PATCH 089/111] xo-arena: doxygen nit --- include/xo/arena/ArenaConfig.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/xo/arena/ArenaConfig.hpp b/include/xo/arena/ArenaConfig.hpp index d1ecbdca..05904f90 100644 --- a/include/xo/arena/ArenaConfig.hpp +++ b/include/xo/arena/ArenaConfig.hpp @@ -18,6 +18,9 @@ namespace xo { **/ struct ArenaConfig { /** @defgroup mm-arenaconfig-ctors **/ + ///@{ + + /** NOTE: not providing explicit ctors so we can use designated initializers **/ ArenaConfig with_name(std::string name) const { ArenaConfig copy(*this); @@ -37,6 +40,7 @@ namespace xo { return copy; } + ///@} /** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/ ///@{ From 5b937ebb9d639637dfabf03bd3c8c89ed38ae62a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:09:09 -0500 Subject: [PATCH 090/111] xo-arena: + mapping DArena ctor from ArenaConfig --- include/xo/arena/DArena.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 658d2b1b..24429188 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -80,7 +80,9 @@ namespace xo { /** null ctor **/ DArena() = default; - /** ctor from already-mapped (but not committed) address range **/ + /** create arena from @p cfg. Will reserve memory for allocation **/ + DArena(const ArenaConfig & cfg); + /** ctor from already-mapped (but not committed) address range [lo,hi] **/ DArena(const ArenaConfig & cfg, size_type page_z, size_type arena_align_z, From dbeb0bbb96bf06d092f774f07753814e59b33138 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:10:40 -0500 Subject: [PATCH 091/111] xo-arena: + DArenaHashMap ctor from ArenaHashMapConfig --- include/xo/arena/DArenaHashMap.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index c52b9894..50e66698 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -5,6 +5,7 @@ #pragma once +#include "ArenaHashMapConfig.hpp" #include "DArenaVector.hpp" #include "hashmap/verify_policy.hpp" #include "hashmap/HashMapStore.hpp" @@ -57,6 +58,7 @@ namespace xo { public: /** create hash map **/ + DArenaHashMap(const ArenaHashMapConfig & cfg); DArenaHashMap(const std::string & name, size_type hint_max_capacity, bool debug_flag = false); @@ -199,6 +201,12 @@ namespace xo { bool debug_flag_ = false; }; + template + DArenaHashMap::DArenaHashMap(const ArenaHashMapConfig & cfg) + : DArenaHashMap(cfg.name_, Hash(), Equal(), cfg.hint_max_capacity_, cfg.debug_flag_) + { + } + template DArenaHashMap::DArenaHashMap(const std::string & name, size_type hint_max_capacity, From b8d1492265ef95e668d44ae1553ec6dc7f8336ca Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:13:41 -0500 Subject: [PATCH 092/111] xo-arena: impl for DArena ctor w/ ArenaConfig arg --- src/arena/DArena.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index 32cb7c8c..cb305f9a 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -65,6 +65,11 @@ namespace xo { return DArena(cfg, page_z, align_z, span.lo(), span.hi()); } /*map*/ + DArena::DArena(const ArenaConfig & cfg) + { + *this = std::move(map(cfg)); + } + DArena::DArena(const ArenaConfig & cfg, size_type page_z, size_type arena_align_z, From 94514bf04fc64816ee2b9b957c8069a6fa1865c1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 22:57:15 -0500 Subject: [PATCH 093/111] xo-reader2 stack: + TypeRegistry --- include/xo/arena/MemorySizeInfo.hpp | 6 ++++-- src/arena/DArena.cpp | 4 ++-- utest/DArena.test.cpp | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp index 764eae6d..37b5ce08 100644 --- a/include/xo/arena/MemorySizeInfo.hpp +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -16,8 +16,10 @@ namespace xo { using size_type = std::size_t; MemorySizeInfo() = default; - MemorySizeInfo(std::string_view name, std::size_t u, std::size_t a, std::size_t c, std::size_t r) - : resource_name_{name}, used_{u}, allocated_{a}, committed_{c}, reserved_{r} + MemorySizeInfo(std::string_view name, + std::size_t u, std::size_t a, std::size_t c, std::size_t r) + : resource_name_{name}, + used_{u}, allocated_{a}, committed_{c}, reserved_{r} {} static MemorySizeInfo sentinel() { return MemorySizeInfo(); } diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index cb305f9a..72d8cbf7 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -67,7 +67,7 @@ namespace xo { DArena::DArena(const ArenaConfig & cfg) { - *this = std::move(map(cfg)); + *this = map(cfg); } DArena::DArena(const ArenaConfig & cfg, @@ -290,7 +290,7 @@ namespace xo { (complete_flag ? alloc_mode::sub_complete : alloc_mode::sub_incomplete), - typeseq::anon() /*typeseq: ignored*/, + typeseq::sentinel() /*typeseq: ignored*/, 0 /*age - ignored */); } diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp index e999c35f..03e1633f 100644 --- a/utest/DArena.test.cpp +++ b/utest/DArena.test.cpp @@ -147,7 +147,7 @@ namespace xo { REQUIRE(arena.allocated() == 0); size_t z0 = 1; - byte * m0 = arena.alloc(typeseq::anon(), 1); + byte * m0 = arena.alloc(typeseq::sentinel(), 1); REQUIRE(m0); REQUIRE(arena.last_error().error_ == error::ok); @@ -159,7 +159,7 @@ namespace xo { REQUIRE(arena.committed() <= arena.reserved()); size_t z1 = 16; - byte * m1 = arena.alloc(typeseq::anon(), z1); + byte * m1 = arena.alloc(typeseq::sentinel(), z1); REQUIRE(m1); REQUIRE(arena.last_error().error_ == error::ok); @@ -195,7 +195,7 @@ namespace xo { REQUIRE(arena.allocated() == 0); size_t z0 = 1; - byte * m0 = arena.alloc(typeseq::anon(), 1); + byte * m0 = arena.alloc(typeseq::sentinel(), 1); REQUIRE(m0); From aa946bdc30a5dd5f2d723fa098ac5d4b60b8c8d6 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 16 Feb 2026 00:48:00 -0500 Subject: [PATCH 094/111] xo-expression2 stack: expand MemorySizeInfo w/ per-type detail --- include/xo/arena/MemorySizeInfo.hpp | 21 ++++++++++++++++-- src/arena/DArena.cpp | 34 ++++++++++++++++++++++++++++- src/arena/DCircularBuffer.cpp | 3 ++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp index 37b5ce08..a9649b3e 100644 --- a/include/xo/arena/MemorySizeInfo.hpp +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -5,6 +5,7 @@ #pragma once +#include #include #include #include @@ -12,14 +13,27 @@ namespace xo { namespace mm { + struct MemorySizeDetail { + using typeseq = xo::reflect::typeseq; + + /** identifies a c++ type T. See xo/facet/TypeRegistry **/ + typeseq tseq_; + /** number of T-instances **/ + uint32_t n_alloc_ = 0; + /** bytes used by T-instances **/ + uint32_t z_alloc_ = 0; + }; + struct MemorySizeInfo { using size_type = std::size_t; + using DetailArrayType = std::array; MemorySizeInfo() = default; MemorySizeInfo(std::string_view name, - std::size_t u, std::size_t a, std::size_t c, std::size_t r) + std::size_t u, std::size_t a, std::size_t c, std::size_t r, + DetailArrayType * detail) : resource_name_{name}, - used_{u}, allocated_{a}, committed_{c}, reserved_{r} + used_{u}, allocated_{a}, committed_{c}, reserved_{r}, detail_{detail} {} static MemorySizeInfo sentinel() { return MemorySizeInfo(); } @@ -36,6 +50,9 @@ namespace xo { * virtual memory addresses range obtained, whether or not committed **/ std::size_t reserved_ = 0; + + /** optional histogram with per-data-type counts **/ + DetailArrayType * detail_ = nullptr; }; /** function that visits MemorySizeInfo for a collection of @p n memory pools. diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index 72d8cbf7..2cd36dc1 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -176,11 +176,43 @@ namespace xo { * must assume it's all used **/ + // assemble histogram + MemorySizeInfo::DetailArrayType detail_v; + MemorySizeInfo::DetailArrayType * p_detail = nullptr; + + if (config_.store_header_flag_) { + p_detail = &detail_v; + + for (const auto & ix : *this) { + typeseq ix_tseq(ix.tseq()); + + // totals in detail_v[0] + MemorySizeDetail & d = detail_v[0]; + ++d.n_alloc_; + d.z_alloc_ += ix.size(); + + // O(n) insertion here + for (size_t i = 1; i < detail_v.size(); ++i) { + if (detail_v[i].tseq_.is_sentinel() + || (detail_v[i].tseq_ == ix_tseq)) + { + MemorySizeDetail & d = detail_v[i]; + + d.tseq_ = ix_tseq; + ++d.n_alloc_; + d.z_alloc_ += ix.size(); + break; + } + } + } + } + fn(MemorySizeInfo(config_.name_, this->allocated() /*used*/, this->allocated(), this->committed(), - this->reserved())); + this->reserved(), + p_detail)); } AllocInfo diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index e5b1c725..bd4dc0cd 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -86,7 +86,8 @@ namespace xo { occupied_range_.size() /*used*/, occupied_range_.size(), mapped_range_.size(), - reserved_range_.size())); + reserved_range_.size(), + nullptr /*detail*/)); pinned_spans_.visit_pools(visitor); } From ec004393b9c0c4d364d3787f07c5e5e96e31935d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 16 Feb 2026 11:03:36 -0500 Subject: [PATCH 095/111] xo-reader2: enable alloc headers for parser stack --- include/xo/arena/AllocHeader.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/xo/arena/AllocHeader.hpp b/include/xo/arena/AllocHeader.hpp index ec99f599..db8b6785 100644 --- a/include/xo/arena/AllocHeader.hpp +++ b/include/xo/arena/AllocHeader.hpp @@ -11,6 +11,13 @@ namespace xo { namespace mm { + /** @brief per-alloc header + * + * Appears immediately before each allocation when + * ArenaConfig.store_header_flag_ is set. + * + * See AllocInfo.hpp for encoding of @ref repr_ + **/ struct AllocHeader { using repr_type = std::uintptr_t; using size_type = std::size_t; From d99f5d5c76b667ed487eb1e60684d109a5eebaf2 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Feb 2026 00:40:47 +1100 Subject: [PATCH 096/111] xo-arena: fix paths for when xo-arena is a nix-build dep --- include/xo/arena/hashmap/DArenaHashMapIterator.hpp | 2 +- include/xo/arena/hashmap/HashMapStore.hpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp index ea87c0ce..77c450d7 100644 --- a/include/xo/arena/hashmap/DArenaHashMapIterator.hpp +++ b/include/xo/arena/hashmap/DArenaHashMapIterator.hpp @@ -5,7 +5,7 @@ #pragma once -#include "hashmap/DArenaHashMapUtil.hpp" +#include namespace xo { namespace map { diff --git a/include/xo/arena/hashmap/HashMapStore.hpp b/include/xo/arena/hashmap/HashMapStore.hpp index 6393bb02..23b9d31f 100644 --- a/include/xo/arena/hashmap/HashMapStore.hpp +++ b/include/xo/arena/hashmap/HashMapStore.hpp @@ -5,8 +5,8 @@ #pragma once -#include "hashmap/DArenaHashMapUtil.hpp" -#include "hashmap/ControlGroup.hpp" +#include +#include namespace xo { namespace map { @@ -52,7 +52,7 @@ namespace xo { float load_factor() const noexcept { return size_ / static_cast(n_slot_); } void visit_pools(const MemorySizeVisitor & visitor) const { - // complexity here in service of HashMapStore-specific value for MemorySizeInfo.used + // complexity here in service of HashMapStore-specific value for MemorySizeInfo.used MemorySizeInfo ctl_info; MemorySizeInfo slot_info; From f7a9eea8409fba44337be0a809c3ffbc39f46c8e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Feb 2026 11:47:14 +1100 Subject: [PATCH 097/111] nix-build: xo-arena: build docs + minor doc facepalms --- CMakeLists.txt | 5 +++++ docs/AllocInfo-reference.rst | 26 ++++++++++++++++++++++++++ docs/cmpresult-reference.rst | 2 +- include/xo/arena/cmpresult.hpp | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/AllocInfo-reference.rst diff --git a/CMakeLists.txt b/CMakeLists.txt index 36fbfcb0..0b6c7020 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,4 +28,9 @@ add_subdirectory(utest) xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) +# ---------------------------------------------------------------- +# docs targets depend on all the other library/utest targets +# +add_subdirectory(docs) + # end CMakeLists.txt diff --git a/docs/AllocInfo-reference.rst b/docs/AllocInfo-reference.rst new file mode 100644 index 00000000..05da0eac --- /dev/null +++ b/docs/AllocInfo-reference.rst @@ -0,0 +1,26 @@ +.. _AllocInfo-reference: + +AllocInfo Reference +=================== + +Describes a single allocation. +Requires allocator configured to store per-allocation headers + +Context +------- + +.. ditaa:: + :--scale: 0.99 + + +-----------------------------------------------------+ + | DArena | + | DArenaIterator | + +-----------------------------------------------------+ + | ArenaConfig | + +--------------+------------------------+-------------+ + | | AllocInfo cBLU| | + | +------------------------+ | + | AllocError | AllocHeaderConfig | cmpresult | + | +------------------------+ | + | | AllocHeader | | + +--------------+------------------------+-------------+ diff --git a/docs/cmpresult-reference.rst b/docs/cmpresult-reference.rst index 46998543..ecaa4ba2 100644 --- a/docs/cmpresult-reference.rst +++ b/docs/cmpresult-reference.rst @@ -37,7 +37,7 @@ Class Constructors ------------ -.. doxgyengroup:: mm-cmpresult-ctors +.. doxygengroup:: mm-cmpresult-ctors Methods ------- diff --git a/include/xo/arena/cmpresult.hpp b/include/xo/arena/cmpresult.hpp index 0a9a9110..bf09af57 100644 --- a/include/xo/arena/cmpresult.hpp +++ b/include/xo/arena/cmpresult.hpp @@ -24,7 +24,7 @@ namespace xo { return os; } - /** Result of a generic comparison operation + /** @brief result of a generic comparison operation **/ struct cmpresult { /** @defgroup mm-cmpresult-ctors cmpresult ctors **/ From 09e59840c035e60ed197b99ce6c53de16a14a3a7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Feb 2026 19:38:53 +1100 Subject: [PATCH 098/111] xo-cmake: setup to make share target available via cmake install --- cmake/xo_arenaConfig.cmake.in | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/xo_arenaConfig.cmake.in b/cmake/xo_arenaConfig.cmake.in index 1700fb8a..20b8f877 100644 --- a/cmake/xo_arenaConfig.cmake.in +++ b/cmake/xo_arenaConfig.cmake.in @@ -10,4 +10,5 @@ find_dependency(xo_reflectutil) find_dependency(indentlog) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake") check_required_components("@PROJECT_NAME@") From bca03275bcfabd3212954306f0640094346ced79 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 1 Mar 2026 13:06:57 +1100 Subject: [PATCH 099/111] xo-reader2 stack: + #q token + QuoteSsm [WIP - not functional] --- include/xo/arena/DArena.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 24429188..83ebd9c3 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -161,6 +161,12 @@ namespace xo { **/ AllocInfo alloc_info(value_type mem) const noexcept; + /** convenience template for allocating for a T-instance **/ + template + void * alloc_for(size_type n = sizeof(T)) { + return this->alloc(typeseq::id(), n); + } + /** allocate at least @p z bytes of memory. * Return nullptr and capture error if unable to satisfy request. * May expand committed memory, as long as resulting committed size From 4c80ff93968be5a346be1b1c665d4647b1cbe3ac Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 24 Mar 2026 22:11:25 -0400 Subject: [PATCH 100/111] xo-gc: + ACollector.assign_member() --- include/xo/arena/AllocHeaderConfig.hpp | 6 ++++-- include/xo/arena/DArena.hpp | 8 ++++++++ include/xo/arena/DArenaHashMap.hpp | 3 +++ include/xo/arena/DArenaVector.hpp | 3 ++- src/arena/DArena.cpp | 11 ++++++++++- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/include/xo/arena/AllocHeaderConfig.hpp b/include/xo/arena/AllocHeaderConfig.hpp index ab560886..d2b6db99 100644 --- a/include/xo/arena/AllocHeaderConfig.hpp +++ b/include/xo/arena/AllocHeaderConfig.hpp @@ -11,7 +11,9 @@ namespace xo { namespace mm { - /* + /** + * @brief specifies alloc header layout + * * Each allocation is preceded by a 64-bit header. * Header is split into 3 configurable-width bit fields, * labelled (from hi to lo bit order) {tseq, age, size}. @@ -36,7 +38,7 @@ namespace xo { * 0..............01111111 gen_mask_unshifted * 0..011111110..........0 gen_mask_shifted * > < gen_shift - */ + **/ struct AllocHeaderConfig { using repr_type = AllocHeader; using span_type = std::pair; diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 83ebd9c3..73ca67db 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -103,6 +103,9 @@ namespace xo { /** @defgroup mm-arena-methods **/ ///@{ + /** false -> not eligible for GC (allocates own memory + not moveable) **/ + static constexpr bool is_gc_eligible() { return false; } + /** Reserved memory, in bytes. This is the maximum size of this arena. **/ size_type reserved() const noexcept { return hi_ - lo_; } /** Allocated memory in bytes: memory consumed by allocs from this arena, @@ -125,6 +128,9 @@ namespace xo { **/ bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); } + /** Truee iff address @p addr is owned by this arena and in allocated regions **/ + bool contains_allocated(const void * addr) const noexcept { return (lo_ <= addr) && (addr < free_); } + /** true if arena is mapped i.e. has a reserved address range **/ bool is_mapped() const noexcept { return (lo_ != nullptr) && (hi_ != nullptr); } @@ -147,6 +153,8 @@ namespace xo { /** get header from allocated object address **/ header_type * obj2hdr(void * obj) noexcept; + /** get header from allocated object address (const version) **/ + const header_type * obj2hdr(void * obj) const noexcept; /** report alloc book-keeping info for allocation at @p mem * diff --git a/include/xo/arena/DArenaHashMap.hpp b/include/xo/arena/DArenaHashMap.hpp index 50e66698..c97f7bad 100644 --- a/include/xo/arena/DArenaHashMap.hpp +++ b/include/xo/arena/DArenaHashMap.hpp @@ -68,6 +68,9 @@ namespace xo { size_type hint_max_capacity = 0, bool debug_flag = false); + /** true for types that support the AGCObject facet; DArenaHashMap gets its own memory! **/ + static constexpr bool is_gc_eligible() { return false; } + size_type empty() const noexcept { return store_.empty(); } size_type groups() const noexcept { return store_.n_group_; } size_type size() const noexcept { return store_.size_; } diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index 79653fe0..0ac5d689 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -76,6 +76,7 @@ namespace xo { const_iterator cend() const noexcept { return this->_address_of(size_); } const_iterator end() const noexcept { return this->cend(); } + constexpr const DArena * store() const { return &store_; } constexpr T * data() { return reinterpret_cast(store_.lo_); } constexpr const T * data() const { return reinterpret_cast(store_.lo_); } @@ -223,7 +224,7 @@ namespace xo { // store_.restore(zero_ckp_); store_.alloc(xo::reflect::typeseq::id(), req_z); - + this->size_ = z; } diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index 2cd36dc1..e5bbb112 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -170,8 +170,17 @@ namespace xo { return (header_type *)((byte *)obj - sizeof(header_type)); } + auto + DArena::obj2hdr(void * obj) const noexcept -> const header_type * + { + assert(config_.store_header_flag_); + + return (const header_type *)((byte *)obj - sizeof(header_type)); + } + void - DArena::visit_pools(const MemorySizeVisitor & fn) const { + DArena::visit_pools(const MemorySizeVisitor & fn) const + { /** arena can't tell purpose of allocated memory; * must assume it's all used **/ From de52b3b5ddf22cfdf7bd0e25163f73eaf0302239 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 24 Mar 2026 22:15:53 -0400 Subject: [PATCH 101/111] xo-alloc2: + DArena.scrub() for diagnostics --- include/xo/arena/DArena.hpp | 3 +++ src/arena/DArena.cpp | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 73ca67db..74f48322 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -229,6 +229,9 @@ namespace xo { void restore(Checkpoint ckp) noexcept { free_ = ckp.free_; } + /** zero out all allocated memory. Likely use case is diagnostics **/ + void scrub() noexcept; + /** discard all allocated memory, return to empty state * Promise: * - committed memory unchanged diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index e5bbb112..ebee471b 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -583,6 +583,12 @@ namespace xo { return true; } /*expand*/ + void + DArena::scrub() noexcept + { + ::memset(this->lo_, 0, this->free_ - this->lo_); + } + void DArena::clear() noexcept { From 5548b5e6ce1ee0f08b103f261391b74624a9a7cd Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 28 Mar 2026 13:16:29 -0400 Subject: [PATCH 102/111] xo-gc: in verify report memory ranges for gc space when debug on --- include/xo/arena/MemorySizeInfo.hpp | 7 ++++++- src/arena/DArena.cpp | 2 ++ src/arena/DCircularBuffer.cpp | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/MemorySizeInfo.hpp b/include/xo/arena/MemorySizeInfo.hpp index a9649b3e..c626201c 100644 --- a/include/xo/arena/MemorySizeInfo.hpp +++ b/include/xo/arena/MemorySizeInfo.hpp @@ -31,9 +31,10 @@ namespace xo { MemorySizeInfo() = default; MemorySizeInfo(std::string_view name, std::size_t u, std::size_t a, std::size_t c, std::size_t r, + const void * lo, const void * hi, DetailArrayType * detail) : resource_name_{name}, - used_{u}, allocated_{a}, committed_{c}, reserved_{r}, detail_{detail} + used_{u}, allocated_{a}, committed_{c}, reserved_{r}, lo_{lo}, hi_{hi}, detail_{detail} {} static MemorySizeInfo sentinel() { return MemorySizeInfo(); } @@ -50,6 +51,10 @@ namespace xo { * virtual memory addresses range obtained, whether or not committed **/ std::size_t reserved_ = 0; + /** start address (optional) **/ + const void * lo_ = 0; + /** end address (optional) **/ + const void * hi_ = 0; /** optional histogram with per-data-type counts **/ DetailArrayType * detail_ = nullptr; diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index ebee471b..d3ca1f23 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -221,6 +221,8 @@ namespace xo { this->allocated(), this->committed(), this->reserved(), + lo_, + hi_, p_detail)); } diff --git a/src/arena/DCircularBuffer.cpp b/src/arena/DCircularBuffer.cpp index bd4dc0cd..23bc0684 100644 --- a/src/arena/DCircularBuffer.cpp +++ b/src/arena/DCircularBuffer.cpp @@ -87,6 +87,8 @@ namespace xo { occupied_range_.size(), mapped_range_.size(), reserved_range_.size(), + reserved_range_.lo(), + reserved_range_.hi(), nullptr /*detail*/)); pinned_spans_.visit_pools(visitor); From 8de4bbd83f0ff5f1f1fa51b60d24518edc8a9a7e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 29 Mar 2026 15:17:31 -0400 Subject: [PATCH 103/111] xo-gc: use DArenaVector for DX1Collector.object_types_ Original implementation predated DArenaVector, using it is more natural --- include/xo/arena/DArenaVector.hpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index 0ac5d689..21e059d1 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -89,7 +89,8 @@ namespace xo { * Always limited by ArenaConfig.size_ **/ void reserve(size_type z); - void resize(size_type z); + /** resize to size @p z. Return true on success. May fail iff oom. **/ + bool resize(size_type z); void shrink_to_fit(); /** reset vector to empty state **/ void clear(); @@ -184,7 +185,7 @@ namespace xo { } template - void + bool DArenaVector::resize(size_type z) { // new arena size in bytes size_t req_z = z * sizeof(T); @@ -192,7 +193,8 @@ namespace xo { if (z > size_) { // expand arena to accomodate - store_.expand(req_z); + if (!store_.expand(req_z)) + return false; // run ctors if constexpr (std::is_trivially_constructible_v) { @@ -226,6 +228,8 @@ namespace xo { store_.alloc(xo::reflect::typeseq::id(), req_z); this->size_ = z; + + return true; } template From 0954945e1613b419dfe2d0cdd058b4103a75d09a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 30 Mar 2026 14:51:51 -0400 Subject: [PATCH 104/111] xo-procedure2 stack: + report-gc-object-ages() primitive --- include/xo/arena/AllocHeaderConfig.hpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/AllocHeaderConfig.hpp b/include/xo/arena/AllocHeaderConfig.hpp index d2b6db99..d574b292 100644 --- a/include/xo/arena/AllocHeaderConfig.hpp +++ b/include/xo/arena/AllocHeaderConfig.hpp @@ -60,6 +60,12 @@ namespace xo { std::uint64_t mkheader(std::uint64_t t, std::uint64_t a, std::uint64_t z) const noexcept { + + // don't let age wrap around. + // Expect std::min() to compile to cmov (no branch) + // + a = std::min(a, this->max_age()); + uint64_t tseq_bits = (t << (age_bits_ + size_bits_)) & tseq_mask(); uint64_t age_bits = (a << size_bits_) & age_mask(); uint64_t size_bits = z & size_mask();; @@ -75,12 +81,16 @@ namespace xo { return ((1ul << tseq_bits_) - 1) << (age_bits_ + size_bits_); } + std::uint64_t max_age() const noexcept { + return ((1ul << age_bits_) - 1); + } + std::uint64_t age_mask() const noexcept { // e.g. // 00 00 00 FF 00 00 00 00 // with age_bits=8, size_bits=32 // - return ((1ul << age_bits_) - 1) << size_bits_; + return this->max_age() << size_bits_; } std::uint64_t size_mask() const noexcept { From 9915534f070308c394ad3f5899ad1491b3b751c0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 23:18:45 -0400 Subject: [PATCH 105/111] refactor: xo-gc: + GCObjectVisitor.generation_of() Concession to narrow MutationLogStore to only use GCObjectVisitor instead of assuming X1Collector. --- include/xo/arena/AllocInfo.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/xo/arena/AllocInfo.hpp b/include/xo/arena/AllocInfo.hpp index 30113e46..599e0b78 100644 --- a/include/xo/arena/AllocInfo.hpp +++ b/include/xo/arena/AllocInfo.hpp @@ -17,7 +17,8 @@ namespace xo { * {@ref AAllocator::alloc, @ref AAllocator::alloc_super} * **/ - struct AllocInfo { + class AllocInfo { + public: /** @defgroup mm-allocinfo-traits **/ ///@{ From c8ba4d42b871248b432d9765fbd58dfa21b84b35 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 10 Apr 2026 20:28:59 -0400 Subject: [PATCH 106/111] xo-arena: + print_backtrace() + print_backtrace_dwarf() using libunwind and elfutils --- include/xo/arena/backtrace.hpp | 13 +++ src/arena/backtrace.cpp | 158 +++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 include/xo/arena/backtrace.hpp create mode 100644 src/arena/backtrace.cpp diff --git a/include/xo/arena/backtrace.hpp b/include/xo/arena/backtrace.hpp new file mode 100644 index 00000000..d29bcb60 --- /dev/null +++ b/include/xo/arena/backtrace.hpp @@ -0,0 +1,13 @@ +/** @file backtrace.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +namespace xo { + void print_backtrace(bool demangle_flag); + void print_backtrace_dwarf(bool demangle_flag); +} + +/* end backtrace.hpp */ diff --git a/src/arena/backtrace.cpp b/src/arena/backtrace.cpp new file mode 100644 index 00000000..a1f5e009 --- /dev/null +++ b/src/arena/backtrace.cpp @@ -0,0 +1,158 @@ +/** @file backtrace.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "backtrace.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace xo { + void + print_backtrace(bool demangle_flag) { + unw_cursor_t cursor; + unw_context_t cx; + + // capture cpu register state at this call site + unw_getcontext(&cx); + + // stack frame iterator for current thread. + // local -> this process + // + unw_init_local(&cursor, &cx); + + // depth relative to top of call stack + int depth = 0; + while (unw_step(&cursor) > 0) { + unw_word_t pc = 0; + + // read return address of current frame into pc. + // This determines the function that is executing + // when print_backtrace() invoked + // + unw_get_reg(&cursor, UNW_REG_IP, &pc); + + std::array name; + unw_word_t offset = 0; + + // mangled function name for current frame's pc. + // + if (unw_get_proc_name(&cursor, name.data(), sizeof(name), &offset) == 0) { + int status = 0; + + // we are resaponsible for calling ::free() on non-null demangled value + char * demangled = nullptr; + + if (demangle_flag) + demangled = abi::__cxa_demangle(name.data(), nullptr, nullptr, &status); + + if ((status == 0) && demangled) { + fprintf(stderr, "#%d 0x%lx %s+0x%lx\n", + depth, (long)pc, demangled, (long)offset); + free(demangled); + } else { + // demangle failed (or disabled) + fprintf(stderr, "#%d 0x%lx %s+0x%lx\n", + depth, (long)pc, name.data(), (long)offset); + } + } else { + // unable to get function name + fprintf(stderr, "#%d 0x%lx ???\n", depth, (long)pc); + } + } + } + namespace { + // libdwfl requires callbacks for find_elf and find_debuginfo. + // The offline defaults work for the current process. + // + static const Dwfl_Callbacks dwfl_callbacks = { + .find_elf = dwfl_linux_proc_find_elf, + .find_debuginfo = dwfl_standard_find_debuginfo, + .section_address = nullptr, + .debuginfo_path = nullptr, + }; + } + + void + print_backtrace_dwarf(bool demangle_flag) { + unw_cursor_t cursor; + unw_context_t cx; + + unw_getcontext(&cx); + unw_init_local(&cursor, &cx); + + // set up dwfl for resolving addresses to source locations. + // + Dwfl * dwfl = dwfl_begin(&dwfl_callbacks); + + if (dwfl) { + // populate module list from /proc/self/maps + dwfl_linux_proc_report(dwfl, getpid()); + dwfl_report_end(dwfl, nullptr, nullptr); + } + + int depth = 0; + while (unw_step(&cursor) > 0) { + unw_word_t pc = 0; + unw_get_reg(&cursor, UNW_REG_IP, &pc); + + std::array name; + unw_word_t offset = 0; + + // resolve function name via libunwind + // + const char * func_name = "???"; + char * demangled = nullptr; + int status = -1; + + if (unw_get_proc_name(&cursor, name.data(), name.size(), &offset) == 0) { + if (demangle_flag) + demangled = abi::__cxa_demangle(name.data(), nullptr, nullptr, &status); + + func_name = ((status == 0) && demangled) ? demangled : name.data(); + } + + // resolve source file + line via DWARF debug info + // + const char * source_file = nullptr; + int line = 0; + + if (dwfl) { + Dwfl_Module * module = dwfl_addrmodule(dwfl, pc); + + if (module) { + Dwfl_Line * dwfl_line = dwfl_module_getsrc(module, pc); + + if (dwfl_line) { + source_file = dwfl_lineinfo(dwfl_line, nullptr, &line, + nullptr, nullptr, nullptr); + } + } + } + + if (source_file) { + fprintf(stderr, "#%d 0x%lx %s+0x%lx at %s:%d\n", + depth, (long)pc, func_name, (long)offset, + source_file, line); + } else { + fprintf(stderr, "#%d 0x%lx %s+0x%lx\n", + depth, (long)pc, func_name, (long)offset); + } + + if (demangled) + free(demangled); + + ++depth; + } + + if (dwfl) + dwfl_end(dwfl); + } +} /*namespace xo*/ + +/* end backtrace.cpp */ From a3e72f33a54c528d5485d1680a21b9299d3ac8a6 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 10 Apr 2026 20:32:23 -0400 Subject: [PATCH 107/111] xo-arena: + src_fn argument in alloc_error + contributaries --- include/xo/arena/AllocError.hpp | 17 +++++++---- include/xo/arena/DArena.hpp | 8 +++-- include/xo/arena/DArenaVector.hpp | 12 ++++---- include/xo/arena/print.hpp | 9 ++++-- src/arena/CMakeLists.txt | 3 ++ src/arena/DArena.cpp | 49 ++++++++++++++++++++++--------- src/arena/DArenaIterator.cpp | 4 +-- src/arena/arena_streambuf.cpp | 6 ++-- utest/DArena.test.cpp | 2 +- 9 files changed, 73 insertions(+), 37 deletions(-) diff --git a/include/xo/arena/AllocError.hpp b/include/xo/arena/AllocError.hpp index ca98b367..19ae9f08 100644 --- a/include/xo/arena/AllocError.hpp +++ b/include/xo/arena/AllocError.hpp @@ -48,20 +48,25 @@ namespace xo { uint32_t seq) : error_{err}, error_seq_{seq} {} AllocError(error err, + const char * src_fn, uint32_t seq, size_type req_z, size_type com_z, - size_type rsv_z) : error_{err}, - error_seq_{seq}, - request_z_{req_z}, - committed_z_{com_z}, - reserved_z_{rsv_z} {} + size_type rsv_z) : error_{err}, + src_fn_{src_fn}, + error_seq_{seq}, + request_z_{req_z}, + committed_z_{com_z}, + reserved_z_{rsv_z} {} static const char * error_description(error x); /** error code **/ error error_ = error::ok; - + /** source function. Typically injected with __PRETTY_FUNCTION__ + * somewhere suitable on stack + **/ + const char * src_fn_ = nullptr; /** sequence# of this error. * Each error event within an allocator gets next sequence number **/ diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 74f48322..13850d75 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -203,19 +203,21 @@ namespace xo { /** capture error information: advance error count + set last_error **/ void capture_error(error err, + const char * src_fn, size_type target_z = 0) const; /** alloc driver. shared by alloc(), super_alloc(), sub_alloc() **/ value_type _alloc(std::size_t req_z, alloc_mode mode, typeseq tseq, - uint32_t age); + uint32_t age, + const char * src_fn); /** expand committed space in arena @p d - * to size at least @p z + * to size at least @p z, on behalf of @p src_fn * In practice will round up to a multiple of @ref page_z_. **/ - bool expand(size_type z) noexcept; + bool expand(size_type z, const char * src_fn) noexcept; /** create initial guard **/ void establish_initial_guard() noexcept; diff --git a/include/xo/arena/DArenaVector.hpp b/include/xo/arena/DArenaVector.hpp index 21e059d1..7b017268 100644 --- a/include/xo/arena/DArenaVector.hpp +++ b/include/xo/arena/DArenaVector.hpp @@ -181,7 +181,7 @@ namespace xo { template void DArenaVector::reserve(size_type z) { - store_.expand(z * sizeof(T)); + store_.expand(z * sizeof(T), __PRETTY_FUNCTION__); } template @@ -193,7 +193,7 @@ namespace xo { if (z > size_) { // expand arena to accomodate - if (!store_.expand(req_z)) + if (!store_.expand(req_z, __PRETTY_FUNCTION__)) return false; // run ctors @@ -258,7 +258,7 @@ namespace xo { size_type new_z = size_ + 1; size_type req_z = new_z * sizeof(T); - store_.expand(req_z); + store_.expand(req_z, __PRETTY_FUNCTION__); } // move elements [i .. z-1] right by one position. @@ -283,7 +283,7 @@ namespace xo { size_type new_z = size_ + 1; size_type req_z = new_z * sizeof(T); - store_.expand(req_z); + store_.expand(req_z, __PRETTY_FUNCTION__); } // move elements [i .. z-1] right by one position. @@ -322,7 +322,7 @@ namespace xo { size_type z = size_ + 1; size_type req_z = z * sizeof(T); - if (this->store_.expand(req_z)) { + if (this->store_.expand(req_z, __PRETTY_FUNCTION__)) { T * addr = this->_address_of(size_); new (addr) T{std::move(x)}; @@ -336,7 +336,7 @@ namespace xo { DArenaVector::push_back(const T & x) { size_type z = size_ + 1; - if (this->store_.expand(z * sizeof(T))) { + if (this->store_.expand(z * sizeof(T), __PRETTY_FUNCTION__)) { T * addr = this->_address_of(size_); new (addr) T{x}; diff --git a/include/xo/arena/print.hpp b/include/xo/arena/print.hpp index 5c474762..4618d5d4 100644 --- a/include/xo/arena/print.hpp +++ b/include/xo/arena/print.hpp @@ -20,12 +20,17 @@ namespace xo { inline std::ostream & operator<<(std::ostream & os, const AllocError & x) { os << ""; + return os; } } diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index b3bb35a6..6b87729e 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -11,6 +11,7 @@ set(SELF_SRCS DArena.cpp DArenaIterator.cpp DCircularBuffer.cpp + backtrace.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) @@ -24,5 +25,7 @@ xo_install_include_tree3(include/xo/arena) xo_dependency(${SELF_LIB} xo_reflectutil) xo_dependency(${SELF_LIB} indentlog) +xo_external_pkgconfig_dependency(${SELF_LIB} LIBUNWIND libunwind-generic) +xo_external_pkgconfig_dependency(${SELF_LIB} LIBDW libdw) # end src/CMakeLists.txt diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index d3ca1f23..cf5bc090 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -7,6 +7,7 @@ #include "DArena.hpp" #include "DArenaIterator.hpp" #include "mmap_util.hpp" +#include "backtrace.hpp" #include #include #include @@ -230,7 +231,7 @@ namespace xo { DArena::alloc_info(value_type mem) const noexcept { if (!config_.store_header_flag_) [[unlikely]] { - this->capture_error(error::alloc_info_disabled); + this->capture_error(error::alloc_info_disabled, __PRETTY_FUNCTION__); return AllocInfo::error_not_configured(&config_.header_); } @@ -272,7 +273,7 @@ namespace xo { DArena::begin_header() const noexcept { if (config_.store_header_flag_ == false) { - this->capture_error(error::alloc_iterator_not_supported); + this->capture_error(error::alloc_iterator_not_supported, __PRETTY_FUNCTION__); return nullptr; } @@ -284,7 +285,7 @@ namespace xo { DArena::end_header() const noexcept { if (config_.store_header_flag_ == false) { - this->capture_error(error::alloc_iterator_not_supported); + this->capture_error(error::alloc_iterator_not_supported, __PRETTY_FUNCTION__); return nullptr; } @@ -299,7 +300,11 @@ namespace xo { * exactly 1 header per alloc() call. * - store_header_flag follows configuration */ - return _alloc(req_z, alloc_mode::standard, t, 0 /*age*/); + return _alloc(req_z, + alloc_mode::standard, + t, + 0 /*age*/, + __PRETTY_FUNCTION__); } std::byte * @@ -316,7 +321,8 @@ namespace xo { return _alloc(req_z, alloc_mode::super, t, - 0 /*age*/); + 0 /*age*/, + __PRETTY_FUNCTION__); } std::byte * @@ -334,7 +340,8 @@ namespace xo { ? alloc_mode::sub_complete : alloc_mode::sub_incomplete), typeseq::sentinel() /*typeseq: ignored*/, - 0 /*age - ignored */); + 0 /*age - ignored */, + __PRETTY_FUNCTION__); } std::byte * @@ -354,17 +361,20 @@ namespace xo { typeseq tseq = typeseq(src_info.tseq()); uint32_t age = src_info.age(); - return _alloc(req_z, alloc_mode::standard, tseq, age + 1); + return _alloc(req_z, alloc_mode::standard, tseq, age + 1, + __PRETTY_FUNCTION__); } void DArena::capture_error(error err, + const char * src_fn, size_type target_z) const { DArena * self = const_cast(this); ++(self->error_count_); self->last_error_ = AllocError(err, + src_fn, error_count_, target_z, committed_z_, @@ -375,7 +385,8 @@ namespace xo { DArena::_alloc(std::size_t req_z, alloc_mode mode, typeseq tseq, - uint32_t age) + uint32_t age, + const char * src_fn) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -444,7 +455,7 @@ namespace xo { hz = sizeof(header); } else { /* req_z doesn't fit in configured header_size_mask bits */ - capture_error(error::header_size_mask); + capture_error(error::header_size_mask, src_fn); return nullptr; } } @@ -453,7 +464,7 @@ namespace xo { assert(padding::is_aligned(z1)); - if (!this->expand(this->allocated() + z1)) [[unlikely]] { + if (!this->expand(this->allocated() + z1, src_fn)) [[unlikely]] { /* (error state already captured) */ return nullptr; } @@ -509,11 +520,12 @@ namespace xo { } bool - DArena::expand(size_t target_z) noexcept + DArena::expand(size_t target_z, const char * src_fn) noexcept { scope log(XO_DEBUG(config_.debug_flag_), xtag("target_z", target_z), - xtag("committed_z", committed_z_)); + xtag("committed_z", committed_z_), + xtag("src_fn", src_fn)); if (target_z <= committed_z_) [[likely]] { log && log("trivial success, offset within committed range", @@ -523,7 +535,12 @@ namespace xo { } if (lo_ + target_z > hi_) [[unlikely]] { - this->capture_error(error::reserve_exhausted, target_z); + this->capture_error(error::reserve_exhausted, src_fn, target_z); + + fprintf(stderr, "DArena::expand: reserve exhausted"); + print_backtrace_dwarf(true /*demangle_flag*/); + std::terminate(); + return false; } @@ -566,7 +583,11 @@ namespace xo { ); } - capture_error(error::commit_failed, add_commit_z); + this->capture_error(error::commit_failed, src_fn, add_commit_z); + + fprintf(stderr, "DArena::expand: mprotect failed (system oom?)"); + print_backtrace_dwarf(false /*!demangle_flag*/); + return false; } diff --git a/src/arena/DArenaIterator.cpp b/src/arena/DArenaIterator.cpp index 931108bb..78769b7a 100644 --- a/src/arena/DArenaIterator.cpp +++ b/src/arena/DArenaIterator.cpp @@ -74,7 +74,7 @@ namespace xo { xtag("bounds_flag", bounds_flag)); if (!contains_flag || !bounds_flag) { - arena_->capture_error(error::alloc_iterator_deref); + arena_->capture_error(error::alloc_iterator_deref, __PRETTY_FUNCTION__); return AllocInfo::error_invalid_iterator(&(arena_->config_.header_)); } @@ -122,7 +122,7 @@ namespace xo { xtag("bounds_flag", bounds_flag)); if (!contains_flag || !bounds_flag) { - arena_->capture_error(error::alloc_iterator_next); + arena_->capture_error(error::alloc_iterator_next, __PRETTY_FUNCTION__); return; } diff --git a/src/arena/arena_streambuf.cpp b/src/arena/arena_streambuf.cpp index 18ea132e..7947da0d 100644 --- a/src/arena/arena_streambuf.cpp +++ b/src/arena/arena_streambuf.cpp @@ -51,7 +51,7 @@ namespace xo { this->color_escape_chars_ = 0; this->color_escape_start_ = nullptr; } - + void arena_streambuf::rewind_to(rewind_state s) { @@ -85,7 +85,7 @@ namespace xo { /* note: local_ppos_ invariant across expand_to() */ - arena_->expand(new_z); + arena_->expand(new_z, __PRETTY_FUNCTION__); char * p_base = (char *)(arena_->lo_); char * p_hi = (char *)(arena_->limit_); @@ -207,7 +207,7 @@ namespace xo { return this->pptr() - this->pbase(); } /*seekoff*/ - + } /*namespace mm*/ } /*namespace xo*/ diff --git a/utest/DArena.test.cpp b/utest/DArena.test.cpp index 03e1633f..9ac40384 100644 --- a/utest/DArena.test.cpp +++ b/utest/DArena.test.cpp @@ -118,7 +118,7 @@ namespace xo { REQUIRE(arena.allocated() == 0); size_t z2 = 512; - bool ok = arena.expand(z2); + bool ok = arena.expand(z2, __PRETTY_FUNCTION__); INFO(xtag("last_error", arena.last_error())); From 790e81f36c94a92a61c4d5b551cb29b2cf7c85ef Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 19:54:49 -0400 Subject: [PATCH 108/111] xo-gc: utest: verify no alloc errors --- include/xo/arena/AllocError.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/xo/arena/AllocError.hpp b/include/xo/arena/AllocError.hpp index 19ae9f08..0b302768 100644 --- a/include/xo/arena/AllocError.hpp +++ b/include/xo/arena/AllocError.hpp @@ -45,13 +45,13 @@ namespace xo { AllocError() = default; explicit AllocError(error err, - uint32_t seq) : error_{err}, - error_seq_{seq} {} + uint32_t seq) : error_{err}, + error_seq_{seq} {} AllocError(error err, const char * src_fn, - uint32_t seq, - size_type req_z, - size_type com_z, + uint32_t seq, + size_type req_z, + size_type com_z, size_type rsv_z) : error_{err}, src_fn_{src_fn}, error_seq_{seq}, From c206dd2e166ed40d03d292a2028fb89995f2d974 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 19 Apr 2026 20:47:40 -0400 Subject: [PATCH 109/111] xo-arena: build: can't assume elfutils on osx --- src/arena/CMakeLists.txt | 4 ++++ src/arena/DArena.cpp | 3 ++- src/arena/backtrace.cpp | 18 ++++++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/arena/CMakeLists.txt b/src/arena/CMakeLists.txt index 6b87729e..34832560 100644 --- a/src/arena/CMakeLists.txt +++ b/src/arena/CMakeLists.txt @@ -25,7 +25,11 @@ xo_install_include_tree3(include/xo/arena) xo_dependency(${SELF_LIB} xo_reflectutil) xo_dependency(${SELF_LIB} indentlog) + +if (NOT APPLE) xo_external_pkgconfig_dependency(${SELF_LIB} LIBUNWIND libunwind-generic) xo_external_pkgconfig_dependency(${SELF_LIB} LIBDW libdw) +else() +endif() # end src/CMakeLists.txt diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index cf5bc090..fb60031a 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include // for ::munmap() #include // for ::getpagesize() #include // for ::memset() @@ -587,7 +588,7 @@ namespace xo { fprintf(stderr, "DArena::expand: mprotect failed (system oom?)"); print_backtrace_dwarf(false /*!demangle_flag*/); - + return false; } diff --git a/src/arena/backtrace.cpp b/src/arena/backtrace.cpp index a1f5e009..708ba5e6 100644 --- a/src/arena/backtrace.cpp +++ b/src/arena/backtrace.cpp @@ -4,13 +4,16 @@ **/ #include "backtrace.hpp" +#include #include #include #include #include #include #include -#include +#ifndef __APPLE__ +# include +#endif namespace xo { void @@ -67,6 +70,7 @@ namespace xo { } } namespace { +#ifndef __APPLE__ // libdwfl requires callbacks for find_elf and find_debuginfo. // The offline defaults work for the current process. // @@ -76,10 +80,18 @@ namespace xo { .section_address = nullptr, .debuginfo_path = nullptr, }; +#endif } void - print_backtrace_dwarf(bool demangle_flag) { + print_backtrace_dwarf(bool demangle_flag) + { + +#ifdef __APPLE__ + (void)demangle_flag; + + std::cerr << "backtrace with dwarf symbols not supported on osx" << std::endl; +#else unw_cursor_t cursor; unw_context_t cx; @@ -152,6 +164,8 @@ namespace xo { if (dwfl) dwfl_end(dwfl); +#endif + } } /*namespace xo*/ From eee261d5733ca0688c9a89443f78401c37cdcee0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 10 May 2026 18:19:41 -0400 Subject: [PATCH 110/111] xo-alloc2 xo-gc: assorted utest-guided cleanup ++ coverage --- include/xo/arena/AllocInfo.hpp | 2 +- include/xo/arena/DArena.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xo/arena/AllocInfo.hpp b/include/xo/arena/AllocInfo.hpp index 599e0b78..16878b9b 100644 --- a/include/xo/arena/AllocInfo.hpp +++ b/include/xo/arena/AllocInfo.hpp @@ -78,7 +78,7 @@ namespace xo { /** Number of guard bytes **/ size_type guard_z() const noexcept { return p_config_->guard_z_; } /** Value (fixed test pattern) of guard byte **/ - char guard_byte() const noexcept { return p_config_->guard_byte_; } + uint8_t guard_byte() const noexcept { return p_config_->guard_byte_; } ///@} diff --git a/include/xo/arena/DArena.hpp b/include/xo/arena/DArena.hpp index 13850d75..f485b3de 100644 --- a/include/xo/arena/DArena.hpp +++ b/include/xo/arena/DArena.hpp @@ -128,7 +128,7 @@ namespace xo { **/ bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); } - /** Truee iff address @p addr is owned by this arena and in allocated regions **/ + /** True iff address @p addr is owned by this arena and in allocated regions **/ bool contains_allocated(const void * addr) const noexcept { return (lo_ <= addr) && (addr < free_); } /** true if arena is mapped i.e. has a reserved address range **/ From 5567b43c7d13905930391e46f78a3113b446e4ce Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 19 May 2026 08:27:10 -0400 Subject: [PATCH 111/111] xo-alloc2: + utest harness for catch2 accept additional commandline arguments --- src/arena/DArena.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/arena/DArena.cpp b/src/arena/DArena.cpp index fb60031a..0c429558 100644 --- a/src/arena/DArena.cpp +++ b/src/arena/DArena.cpp @@ -538,9 +538,9 @@ namespace xo { if (lo_ + target_z > hi_) [[unlikely]] { this->capture_error(error::reserve_exhausted, src_fn, target_z); - fprintf(stderr, "DArena::expand: reserve exhausted"); - print_backtrace_dwarf(true /*demangle_flag*/); - std::terminate(); + //fprintf(stderr, "DArena::expand: reserve exhausted"); + //print_backtrace_dwarf(true /*demangle_flag*/); + //std::terminate(); return false; }