From df8892442cfb402dd130a388809c75c0188d3d39 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 14 Dec 2025 16:58:25 -0500 Subject: [PATCH] xo-alloc2: header reorg + DX1Collector utest --- include/xo/alloc2/AllocatorError.hpp | 63 +++++++++++++ include/xo/alloc2/gc/ACollector.hpp | 60 ++++++++++++ include/xo/alloc2/gc/AGCObject.hpp | 54 +++++++++++ include/xo/alloc2/gc/Collector.hpp | 12 +++ include/xo/alloc2/gc/DX1Collector.hpp | 117 ++++++++++++++++++++++++ include/xo/alloc2/gc/ICollector_Any.hpp | 51 +++++++++++ include/xo/alloc2/gc/IGCObject_Any.hpp | 48 ++++++++++ include/xo/alloc2/gc/IGCObject_Xfer.hpp | 62 +++++++++++++ include/xo/alloc2/gc/RCollector.hpp | 51 +++++++++++ include/xo/alloc2/gc/RGCObject.hpp | 47 ++++++++++ src/alloc2/DX1Collector.cpp | 36 ++++++++ utest/Collector.test.cpp | 84 +++++++++++++++++ 12 files changed, 685 insertions(+) create mode 100644 include/xo/alloc2/AllocatorError.hpp create mode 100644 include/xo/alloc2/gc/ACollector.hpp create mode 100644 include/xo/alloc2/gc/AGCObject.hpp create mode 100644 include/xo/alloc2/gc/Collector.hpp create mode 100644 include/xo/alloc2/gc/DX1Collector.hpp create mode 100644 include/xo/alloc2/gc/ICollector_Any.hpp create mode 100644 include/xo/alloc2/gc/IGCObject_Any.hpp create mode 100644 include/xo/alloc2/gc/IGCObject_Xfer.hpp create mode 100644 include/xo/alloc2/gc/RCollector.hpp create mode 100644 include/xo/alloc2/gc/RGCObject.hpp create mode 100644 src/alloc2/DX1Collector.cpp create mode 100644 utest/Collector.test.cpp diff --git a/include/xo/alloc2/AllocatorError.hpp b/include/xo/alloc2/AllocatorError.hpp new file mode 100644 index 0000000..43299ac --- /dev/null +++ b/include/xo/alloc2/AllocatorError.hpp @@ -0,0 +1,63 @@ +/** @file AllocatorError.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 **/ + none, + /** 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, + }; + + struct AllocatorError { + using size_type = std::size_t; + using value_type = std::byte*; + + AllocatorError() = default; + explicit AllocatorError(error err, + uint32_t seq) : error_{err}, + error_seq_{seq} {} + AllocatorError(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} {} + + /** error code **/ + error error_ = error::none; + + /** 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 AllocatorError.hpp */ diff --git a/include/xo/alloc2/gc/ACollector.hpp b/include/xo/alloc2/gc/ACollector.hpp new file mode 100644 index 0000000..1c6d1cb --- /dev/null +++ b/include/xo/alloc2/gc/ACollector.hpp @@ -0,0 +1,60 @@ +/** @file ACollector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "IGCObject_Any.hpp" + +#include +#include +#include + +#include "gc/generation.hpp" +#include "gc/role.hpp" + +#include +#include + +namespace xo { + namespace mm { + using Copaque = const void *; + using Opaque = void *; + + struct IGCObject_Any; // see IGCObject_Any.hpp + + /** @class ACollector + * @brief Abstract facet for the XO garbage collector + * + * Collector also supports the @ref AAllocator facet, see also + **/ + struct ACollector { + using size_type = std::size_t; + + virtual int32_t _typeseq() const noexcept = 0; + + virtual size_type allocated(Copaque d, generation g, role r) const noexcept = 0; + virtual size_type reserved(Copaque d, generation g, role r) const noexcept = 0; + virtual size_type committed(Copaque d, generation g, role r) const noexcept = 0; + + /** install interface @p iface for representation with typeseq @p tseq + * in collector @p d. + * + * The type AGCObject_Any here is misleading. + * Will have been replaced by an instance of + * @c AGCObject_Xfer for some @c DFoo + * in which case calls through @c std::launder(&iface) + * will properly act on @c DFoo. + **/ + virtual void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) = 0; + virtual void add_gc_root(Opaque d, int32_t tseq, Opaque * root) = 0; + + /** evacuate @p *lhs to to-space and replace with forwarding pointer + * Require: gc in progress + **/ + virtual void forward_inplace(Opaque d, obj * lhs) = 0; + }; + } + +} /*namespace xo*/ diff --git a/include/xo/alloc2/gc/AGCObject.hpp b/include/xo/alloc2/gc/AGCObject.hpp new file mode 100644 index 0000000..c558624 --- /dev/null +++ b/include/xo/alloc2/gc/AGCObject.hpp @@ -0,0 +1,54 @@ +/** @file AGCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "IAllocator_Any.hpp" +#include "RAllocator.hpp" +#include "xo/facet/facet_implementation.hpp" +#include "xo/facet/typeseq.hpp" +#include "xo/facet/obj.hpp" // for obj in shallow_copy +#include +#include + +namespace xo { + namespace mm { + using Copaque = const void *; + using Opaque = void *; + + /** @class AObject + * @brief Abstract facet for collector-eligible data + * + * Data that supports AGCObject can have memory managed + * by ACollector + **/ + struct AGCObject { + using size_type = std::size_t; + + /** RTTI: unique id# for actual runtime data representation **/ + virtual int32_t _typeseq() const noexcept = 0; + + virtual size_type shallow_size(Copaque d) const noexcept = 0; + virtual Opaque * shallow_copy(Copaque d, + obj mm) const noexcept = 0; + virtual size_type forward_children(Opaque d) const noexcept = 0; + }; + + // implementation IGCObject_DRepr of AGCObject for state DRepr + // should provide a specialization: + // + // template <> + // struct xo::facet::FacetImplementation { + // using ImplType = IGCObject_DRepr; + // }; + // + // then IGCObject_ImplType --> IGCObject_DRepr + // + template + using IGCObject_ImplType = xo::facet::FacetImplType; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AGCObject.hpp */ diff --git a/include/xo/alloc2/gc/Collector.hpp b/include/xo/alloc2/gc/Collector.hpp new file mode 100644 index 0000000..58d6bde --- /dev/null +++ b/include/xo/alloc2/gc/Collector.hpp @@ -0,0 +1,12 @@ +/** @file Collector.hpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "ACollector.hpp" +#include "ICollector_Any.hpp" +#include "RCollector.hpp" + +/* end Collector.hpp */ diff --git a/include/xo/alloc2/gc/DX1Collector.hpp b/include/xo/alloc2/gc/DX1Collector.hpp new file mode 100644 index 0000000..6cec8cd --- /dev/null +++ b/include/xo/alloc2/gc/DX1Collector.hpp @@ -0,0 +1,117 @@ +/** @file DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "ArenaConfig.hpp" +#include "DArena.hpp" +#include "gc/generation.hpp" +#include "gc/role.hpp" +#include +#include + +namespace xo { + namespace mm { + template + using up = std::unique_ptr; + +#ifdef NOT_YET + /** State associated with a single DX1Collector generation + **/ + struct Generation { + Generation(uint8_t gen_id, up from_space, up to_space); + ~Generation() = default; + + /** identity of this generation. Generations are numbered from + * 0 (youngest) to N (oldest), with N <= c_max_generation + **/ + uint8_t gen_id_; + /** from-space. empty between collection episodes. + * During collection holds former to-space + **/ + up from_space_; + /** to-space. New allocations occur here **/ + up to_space_; + }; +#endif + + struct CollectorConfig { + using size_type = std::size_t; + + /** Configuration for collector spaces. + * Will have at least {nursery,tenured} x {from,to} spaces. + * Not using name_ member. + * + * REQUIRE: + * - arena_config_.store_header_flag_ must be true + * - arena_config_.header_size_mask_ must be 0x0000ffff + **/ + ArenaConfig arena_config_; + + /** Number of generations. + * Must be at least 2. + **/ + uint32_t n_generation_ = 2; + + /** Number of promotion steps. + * An object that survives this number of collections + * advances to the next generation. + **/ + uint32_t n_survive_threshold_ = 2; + + /** Trigger garbage collection when to-space allocation for + * generation g reaches gc_trigger_v_[g] + **/ + std::array gc_trigger_v_; + + /** true -> enable incremental collection. + * false -> only do full collection. + * + * Incremental collection requires mutation logs. + **/ + bool allow_incremental_gc_ = true; + + /** If non-zero remember statistics for + * the last @p stats_history_z_ collections. + **/ + uint32_t stats_history_z_ = false; + + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + struct DX1Collector { + explicit DX1Collector(const CollectorConfig & cfg); + + DArena * get_space(role r, generation g) { return space_[r][g]; } + DArena * from_space(generation g) { return get_space(role::from_space(), g); } + DArena * to_space(generation g) { return get_space(role::to_space(), g); } + + /** reverse to-space and from-space roles for generation g **/ + void reverse_roles(generation g); + + /** garbage collector configuration **/ + CollectorConfig config_; + + /** collector-managed memory here. + * - space_[1] is from-space + * - space_[0] is to-space + * coordinates with role ingc/role.hpp, see also. + **/ + + /** arena objects for collector managed memory + * 1:1 with roles, but polarity reverses for each collection + **/ + std::array space_storage_[c_n_role]; + + /** arena pointers. The roles of space_storage_[0][g] and space_storage_[1][g] + * are reversed each time generation g gets collected. + **/ + std::array space_[c_n_role]; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1Collector.hpp */ diff --git a/include/xo/alloc2/gc/ICollector_Any.hpp b/include/xo/alloc2/gc/ICollector_Any.hpp new file mode 100644 index 0000000..9e5d998 --- /dev/null +++ b/include/xo/alloc2/gc/ICollector_Any.hpp @@ -0,0 +1,51 @@ +/** @file ICollector_Any.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "ACollector.hpp" +//#include + +namespace xo { + namespace mm { struct ICollector_Any; } + + namespace facet { + template <> + struct FacetImplementation { + using ImplType = xo::mm::ICollector_Any; + }; + } + + namespace mm { + /** @class ICollector_Any + * @brief Stub Collector Implementation for empty variant instance + **/ + struct ICollector_Any : public ACollector { + using size_type = std::size_t; + + // from ACollector + int32_t _typeseq() const noexcept override { return s_typeseq; } + + // const methods + [[noreturn]] size_type allocated(Copaque, generation, role) const noexcept { _fatal(); } + [[noreturn]] size_type reserved(Copaque, generation, role) const noexcept { _fatal(); } + [[noreturn]] size_type committed(Copaque, generation, role) const noexcept { _fatal(); } + + // non-const methods + [[noreturn]] void install_type(Opaque, int32_t, IGCObject_Any &) noexcept { _fatal(); } + [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) { _fatal(); } + [[noreturn]] void forward_inplace(Opaque, obj *) { _fatal(); } + + private: + [[noreturn]] static void _fatal(); + + public: + static int32_t s_typeseq; + static bool _valid; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_Any.hpp */ diff --git a/include/xo/alloc2/gc/IGCObject_Any.hpp b/include/xo/alloc2/gc/IGCObject_Any.hpp new file mode 100644 index 0000000..039a125 --- /dev/null +++ b/include/xo/alloc2/gc/IGCObject_Any.hpp @@ -0,0 +1,48 @@ +/** @file IGCObject_Any.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AGCObject.hpp" +#include + +namespace xo { + namespace mm { struct IGCObject_Any; } + + namespace facet { + template <> + struct FacetImplementation { + using ImplType = xo::mm::IGCObject_Any; + }; + } + + namespace mm { + /** @class IGCObject_Any + * @brief AGCObject implementation for empty variant instance + **/ + struct IGCObject_Any : public AGCObject { + using size_type = std::size_t; + + const AGCObject * iface() const { return std::launder(this); } + + // from AGCObject + int32_t _typeseq() const noexcept override { return s_typeseq; } + + [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } + [[noreturn]] Opaque * shallow_copy(Copaque, + obj) const noexcept override { _fatal(); } + [[noreturn]] size_type forward_children(Opaque) const noexcept override { _fatal(); } + + private: + [[noreturn]] static void _fatal(); + + public: + static int32_t s_typeseq; + static bool _valid; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObject_Any.hpp */ diff --git a/include/xo/alloc2/gc/IGCObject_Xfer.hpp b/include/xo/alloc2/gc/IGCObject_Xfer.hpp new file mode 100644 index 0000000..75f78fd --- /dev/null +++ b/include/xo/alloc2/gc/IGCObject_Xfer.hpp @@ -0,0 +1,62 @@ +/** @file IGCObject_Xfer.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AGCObject.hpp" + +namespace xo { + namespace mm { + /** @class IGCObject_Xfer + * + * Adapts typed GC object implementation @tparam IGCObject_DRepr + * to type-erased @ref AGCObject interface + **/ + template + struct IGCObject_Xfer : public AGCObject { + using Impl = IGCObject_DRepr; + using size_type = AGCObject::size_type; + + static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } + static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } + + // from AGCObject + + // const methods + + int32_t _typeseq() const noexcept override { return s_typeseq; } + size_type shallow_size(Copaque d) const noexcept override { + return I::shallow_copy(_dcast(d)); + } + Opaque * shallow_copy(Copaque d, obj mm) const noexcept override { + return I::shallow_size(_dcast(d), mm); + } + + // non-const methods + + size_type forward_children(Opaque d) const noexcept override { + return I::forward_children(d); + } + + private: + using I = Impl; + + public: + static int32_t s_typeseq; + static bool _valid; + }; + + template + int32_t + IGCObject_Xfer::s_typeseq = facet::typeseq::id(); + + template + bool + IGCObject_Xfer::_valid = facet::valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObject_Xfer.hpp */ diff --git a/include/xo/alloc2/gc/RCollector.hpp b/include/xo/alloc2/gc/RCollector.hpp new file mode 100644 index 0000000..5da5b30 --- /dev/null +++ b/include/xo/alloc2/gc/RCollector.hpp @@ -0,0 +1,51 @@ +/** @file RCollector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "ACollector.hpp" +#include + +namespace xo { + namespace mm { + /** @class RCollector **/ + template + struct RCollector : public Object { + private: + using O = Object; + public: + using ObjectType = Object; + using DataPtr = Object::DataPtr; + using size_type = std::size_t; + //using value_type = std::byte *; + + RCollector() = default; + RCollector(DataPtr data) : Object{std::move(data)} {} + + int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } + size_type allocated(generation g, role r) const noexcept { return O::iface()->allocated(O::data()); } + size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data()); } + size_type committed(generation g, role r) const noexcept { return O::iface()->committed(O::data()); } + + void install_type(int32_t tseq, IGCObject_Any & iface) { return O::iface()->install_type(O::data()); } + void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data()); } + + void forward_inplace(obj * lhs) { O::iface()->forward_inplace(O::data(), lhs); } + + static bool _valid; + }; + + template + bool + RCollector::_valid = facet::valid_object_router(); + } /*namespace mm*/ + + namespace facet { + template + struct RoutingFor { + using RoutingType = xo::mm::RCollector; + }; + } +} /*namespace xo*/ + +/* end RCollector.hpp */ diff --git a/include/xo/alloc2/gc/RGCObject.hpp b/include/xo/alloc2/gc/RGCObject.hpp new file mode 100644 index 0000000..054b995 --- /dev/null +++ b/include/xo/alloc2/gc/RGCObject.hpp @@ -0,0 +1,47 @@ +/** @file RGCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AGCObject.hpp" +#include + +namespace xo { + namespace mm { + /** @class RGCObject **/ + template + struct RGCObject : public Object { + private: + using O = Object; + public: + using ObjectType = Object; + using DataPtr = Object::DataPtr; + using size_type = std::size_t; + + RGCObject() = default; + RGCObject(Object::DataPtr data) : Object{std::move(data)} {} + + int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } + size_type shallow_size() const noexcept { O::iface()->shallow_size(O::data()); } + Opaque * shallow_copy(obj mm) const noexcept { O::iface()->shallow_copy(O::data(), mm); } + size_type forward_children() noexcept { O::iface()->forward_children(O::data()); } + + static bool _valid; + }; + + template + bool + RGCObject::_valid = facet::valid_object_router(); + } /*namespace mm*/ + + namespace facet { + template + struct RoutingFor { + using RoutingType = xo::mm::RGCObject; + }; + } +} /*namespace xo*/ + +/* end RGCObject.hpp */ diff --git a/src/alloc2/DX1Collector.cpp b/src/alloc2/DX1Collector.cpp new file mode 100644 index 0000000..025d5f8 --- /dev/null +++ b/src/alloc2/DX1Collector.cpp @@ -0,0 +1,36 @@ +/** @file DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "gc/DX1Collector.hpp" +#include + +namespace xo { + namespace mm { + DX1Collector::DX1Collector(const CollectorConfig & cfg) : config_{cfg} + { + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { + space_storage_[0][igen] = std::move(DArena::map(cfg.arena_config_)); + space_storage_[1][igen] = std::move(DArena::map(cfg.arena_config_)); + + space_[role::to_space()][igen] = &space_storage_[0][igen]; + space_[role::from_space()][igen] = &space_storage_[1][igen]; + } + + for (uint32_t igen = cfg.n_generation_; igen < c_max_generation; ++igen) { + space_[role::to_space()][igen] = nullptr; + space_[role::from_space()][igen] = nullptr; + } + } + + void + DX1Collector::reverse_roles(generation g) { + assert(g < config_.n_generation_); + + std::swap(space_[0][g], space_[1][g]); + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1Collector.cpp */ diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp new file mode 100644 index 0000000..d23db5e --- /dev/null +++ b/utest/Collector.test.cpp @@ -0,0 +1,84 @@ +/** @file Collector.test.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * NOTE: properly unit testing gc behavior requires + * xo-object2 dependency; + * see xo-object2/utest + **/ + +#include "gc/Collector.hpp" +#include "gc/DX1Collector.hpp" +#include +#include + +namespace xo { + using xo::mm::ACollector; + using xo::mm::CollectorConfig; + using xo::mm::DX1Collector; + using xo::mm::ArenaConfig; + using xo::mm::generation; + using xo::mm::c_max_generation; + + namespace ut { + // checklist + // - obj constructible [ ] + // - obj truthy [ ] + + TEST_CASE("collector-any-null", "[alloc2][ACollector]") + { + /* empty variant collector */ + obj gc1; + + REQUIRE(!gc1); + REQUIRE(gc1.iface() != nullptr); + REQUIRE(gc1.data() == nullptr); + } + + TEST_CASE("DX1Collector-1", "[alloc2][DX1Collector]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_size_mask_ = 0x0000ffff, }; + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector gc = DX1Collector{cfg}; + + generation g0 = generation{0}; + REQUIRE(gc.to_space(g0)); + REQUIRE(gc.from_space(g0)); + REQUIRE(gc.to_space(g0)->is_mapped()); + REQUIRE(gc.from_space(g0)->is_mapped()); + + generation g1 = generation{1}; + REQUIRE(gc.to_space(g1)); + REQUIRE(gc.from_space(g1)); + REQUIRE(gc.to_space(g1)->is_mapped()); + REQUIRE(gc.from_space(g1)->is_mapped()); + + /* verify from/to x N/T are unique */ + REQUIRE(gc.to_space(g0) != gc.from_space(g0)); + REQUIRE(gc.to_space(g1) != gc.to_space(g0)); + REQUIRE(gc.from_space(g1) != gc.from_space(g0)); + REQUIRE(gc.to_space(g0) != gc.from_space(g1)); + + for (generation gi = generation(2); gi < c_max_generation; ++gi) { + INFO(xtag("gi", gi)); + + REQUIRE(!gc.to_space(gi)); + REQUIRE(!gc.from_space(gi)); + + REQUIRE(!gc.space_storage_[0][gi].is_mapped()); + REQUIRE(!gc.space_storage_[1][gi].is_mapped()); + } + } + } +} + +/* end Collector.test.cpp */