diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4480a1a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +# xo-gc/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_gc VERSION 0.1) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +set(PROJECT_CXX_FLAGS "") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") # gcc-only! +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- + +# must complete definition of expression lib before configuring examples +add_subdirectory(src/gc) +add_subdirectory(utest) +#xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# docs targets depend on other library/utest/exec targets above, +# --> must come after them. +# +#add_subdirectory(docs) + +# end CMakeLists.txt diff --git a/cmake/xo-bootstrap-macros.cmake b/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 0000000..aba3116 --- /dev/null +++ b/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------- +# 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 ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND") + message(FATAL "could not find xo-cmake-config executable") +endif() + +message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}") + +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-gcConfig.cmake.in b/cmake/xo-gcConfig.cmake.in new file mode 100644 index 0000000..c32a836 --- /dev/null +++ b/cmake/xo-gcConfig.cmake.in @@ -0,0 +1,7 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +#find_dependency(indentlog) +find_dependency(xo_alloc2) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/xo/gc/Collector.hpp b/include/xo/gc/Collector.hpp new file mode 100644 index 0000000..b421403 --- /dev/null +++ b/include/xo/gc/Collector.hpp @@ -0,0 +1,13 @@ +/** @file Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "detail/ACollector.hpp" +#include "detail/ICollector_Any.hpp" +#include "detail/ICollector_Xfer.hpp" +#include "detail/RCollector.hpp" + +/* end Collector.hpp */ diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp new file mode 100644 index 0000000..778f051 --- /dev/null +++ b/include/xo/gc/DX1Collector.hpp @@ -0,0 +1,261 @@ +/** @file DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "arena/ArenaConfig.hpp" +#include "arena/DArena.hpp" +#include "generation.hpp" +#include "object_age.hpp" +#include "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; + +#ifdef OBSOLETE // get from arena_config_.header_ + /* + * alloc header + * TTTTTTTTTTTTGGGGGZZZZZZZZZZZZ + * < tseq >< size > + * + * masking + * + * ..432107654321076543210 bit + * + * > < .gen_bits + * 0..............01111111 gen_mask_unshifted + * 0..011111110..........0 gen_mask_shifted + * > < gen_shift + */ + //constexpr std::uint64_t gen_mult() const; + constexpr std::uint64_t gen_shift() const; + constexpr std::uint64_t gen_mask_unshifted() const; + constexpr std::uint64_t gen_mask_shifted() const; + + //constexpr std::uint64_t tseq_mult() const; + constexpr std::uint64_t tseq_shift() const; + constexpr std::uint64_t tseq_mask_unshifted() const; + constexpr std::uint64_t tseq_mask_shifted() const; +#endif + + generation age2gen(object_age age) const noexcept { + return generation(age % n_survive_threshold_); + } + + public: + // ----- Instance Variables ----- + + /** optional name, for diagnostics **/ + std::string name_; + + /** 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 + **/ + ArenaConfig arena_config_; + + /** number of bits to represent generation **/ + std::uint64_t gen_bits_ = 8; + + /** number of bits to represent tseq **/ + std::uint64_t tseq_bits_ = 24; + + /** 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; + }; + + // ----- GCRunState ----- + + /** @class GCRunState + * @brief encapsulate state needed while GC is running + **/ + struct GCRunState { + GCRunState() : gc_upto_{0} {} + explicit GCRunState(generation gc_upto); + + static GCRunState gc_not_running(); + static GCRunState gc_upto(generation g); + + bool is_running() const { return gc_upto_ > 0; } + + generation gc_upto() const { return gc_upto_; } + + private: + /** running gc collecting all generations gi < gc_upto **/ + generation gc_upto_; + }; + + struct DX1CollectorIterator; + + // ----- DX1Collector ----- + + struct DX1Collector { + using size_type = DArena::size_type; + using value_type = DArena::value_type; + using header_type = DArena::header_type; + + explicit DX1Collector(const CollectorConfig & cfg); + + const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } + DArena * get_space(role r, generation g) noexcept { return space_[r][g]; } + DArena * from_space(generation g) noexcept { return get_space(role::from_space(), g); } + DArena * to_space(generation g) noexcept { return get_space(role::to_space(), g); } + DArena * new_space() noexcept { return to_space(generation{0}); } + + /** total reserved memory in bytes, across all {role, generation} **/ + size_type reserved_total() const noexcept; + /** total size in bytes (same as committed_total()) **/ + size_type size_total() const noexcept; + /** total committed memory in bytes, across all {role, generation} **/ + size_type committed_total() const noexcept; + /** total available memory in bytes, across all {role, generation} **/ + size_type available_total() const noexcept; + /** total allocated memory in bytes, across all {role, generation} **/ + size_type allocated_total() const noexcept; + + /** true iff address @p addr allocated from this collector + * in role @p r (according to current GC state) + **/ + bool contains(role r, const void * addr) const noexcept; + + /** return details from last error (will be in gen0 to-space) **/ + AllocError last_error() const noexcept; + + /** get allocation size from header **/ + std::size_t header2size(header_type hdr) const noexcept; + /** get generation counter from alloc header **/ + object_age header2age(header_type hdr) const noexcept; + /** get tseq from alloc header **/ + uint32_t header2tseq(header_type hdr) const noexcept; + + /** true iff original alloc has been replaced by a forwarding pointer **/ + bool is_forwarding_header(header_type hdr) const noexcept; + + /** Retreive bookkeeping info for allocation at @p mem. **/ + AllocInfo alloc_info(value_type mem) const noexcept; + + // ----- allocation ----- + + /** simple allocation. new allocs always in gen0 to-space **/ + value_type alloc(size_type z) noexcept; + /** compound allocation. To be followed immediately by: + * 1. zero or more calls to sub_alloc(zi, complete=false), then + * 2. exactly one call to sub_alloc(zi, complete=true) + * all the allocs in a compound allocation share the same + * allocation header. End state is equivalent to a single + * allocation with size z + sum(zi). + * New allocs always in gen0 to-space + **/ + value_type super_alloc(size_type z) noexcept; + /** sub-allocation with preceding compound allocation. + * New allocs always in gen0 to-space + **/ + value_type sub_alloc(size_type z, bool complete) noexcept; + /** expand gen0 committed size to at least @p z. + **/ + bool expand(size_type z) noexcept; + + // ----- iteration ----- + + /** alloc iterator at begin position **/ + DX1CollectorIterator begin() const noexcept; + /** alloc iterator at end position + * (valid, but cannot be dereferenced) + **/ + DX1CollectorIterator end() const noexcept; + + // ----- book-keeping ----- + + /** reverse to-space and from-space roles for generation g **/ + void reverse_roles(generation g) noexcept; + + /** discard all allocated memory **/ + void clear() noexcept; + + public: + /** garbage collector configuration **/ + CollectorConfig config_; + + /** current gc state **/ + GCRunState runstate_; + + /** 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/gc/DX1CollectorIterator.hpp b/include/xo/gc/DX1CollectorIterator.hpp new file mode 100644 index 0000000..af1e867 --- /dev/null +++ b/include/xo/gc/DX1CollectorIterator.hpp @@ -0,0 +1,95 @@ +/** @file DX1CollectorIterator.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocInfo.hpp" +#include "generation.hpp" +#include "arena/DArenaIterator.hpp" +#include "cmpresult.hpp" + +namespace xo { + namespace mm { + struct DX1Collector; + + /** @class DX1CollectorIterator + * @brief Representation for alloc iterator over X1 collector + * + * Will iterate across all allocs in all generations + **/ + struct DX1CollectorIterator { + DX1CollectorIterator() = default; + DX1CollectorIterator(const DX1Collector * gc, + generation gen_ix, + generation gen_hi, + DArenaIterator arena_ix, + DArenaIterator arena_hi); + + /** Invalid iterator. Does not compare equal to anything, including itself **/ + static DX1CollectorIterator invalid() { return DX1CollectorIterator(); } + /** Create iterator pointing to the beginning of @p gc. + * Iterator cannot modify payload memory + **/ + static DX1CollectorIterator begin(DX1Collector * gc); + /** Create iterator pointing to the end of @p gc. + * Iterator cannot modify payload memory. + **/ + static DX1CollectorIterator end(DX1Collector * gc); + + /** true if iterator is invalid. invalid iterators are not comparable **/ + bool is_valid() const noexcept { return (gc_ != nullptr); } + bool is_invalid() const noexcept { return !is_valid(); } + + generation gen_ix() const { return gen_ix_; } + generation gen_hi() const { return gen_hi_; } + DArenaIterator arena_ix() const { return arena_ix_; } + DArenaIterator arena_hi() const { return arena_hi_; } + + /** fetch contents at current iterator position **/ + AllocInfo deref() const noexcept; + /** compare two iterators. To be comparable, + * iterators must refer to the same collector + **/ + cmpresult compare(const DX1CollectorIterator & other) const noexcept; + /** advance iterator to next allocation **/ + void next() noexcept; + + /** for *ix synonym for ix.deref() **/ + AllocInfo operator*() const noexcept { return this->deref(); } + + private: + /** if non-empty, normalize to state with arena_ix_ != arena_hi_ **/ + void normalize() noexcept; + + private: + /** Iterator visits allocations from this collector **/ + const DX1Collector * gc_ = nullptr; + /** Iterating over generations in [@p gen_ix_, @p gen_hi_). + * Current position is within arena for @p gen_ix_ to-space, + * Provided @p gen_ix_ < @p gen_hi_ + **/ + generation gen_ix_; + generation gen_hi_; + /** Iterating over allocs in [@p arena_ix_, @p arena_hi_). + * Current position is at @p arena_ix_ + **/ + DArenaIterator arena_ix_; + DArenaIterator arena_hi_; + }; + + inline bool + operator==(const DX1CollectorIterator & x, const DX1CollectorIterator & y) { + return x.compare(y).is_equal(); + } + + inline bool + operator!=(const DX1CollectorIterator & x, const DX1CollectorIterator & y) { + return !x.compare(y).is_equal(); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1CollectorIterator.hpp */ diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp new file mode 100644 index 0000000..edc89c3 --- /dev/null +++ b/include/xo/gc/GCObject.hpp @@ -0,0 +1,13 @@ +/** @file GCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "gcobject/AGCObject.hpp" +#include "gcobject/IGCObject_Any.hpp" +#include "gcobject/IGCObject_Xfer.hpp" +#include "gcobject/RGCObject.hpp" + +/* end GCObject.hpp */ diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp new file mode 100644 index 0000000..9c32125 --- /dev/null +++ b/include/xo/gc/detail/ACollector.hpp @@ -0,0 +1,60 @@ +/** @file ACollector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "gcobject/IGCObject_Any.hpp" + +#include +#include +#include + +#include "generation.hpp" +#include "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/gc/detail/IAllocIterator_DX1CollectorIterator.hpp b/include/xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp new file mode 100644 index 0000000..c221b58 --- /dev/null +++ b/include/xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp @@ -0,0 +1,36 @@ +/** @file IAllocIterator_DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include "xo/gc/DX1CollectorIterator.hpp" + +namespace xo { + namespace mm { struct IAllocIterator_DX1CollectorIterator; } + + namespace facet { + template <> + struct FacetImplementation { + using ImplType = xo::mm::IAllocIterator_Xfer; + }; + } + + namespace mm { + /** @class IAllocIterator_DX1Collector + * @brief alloc iteration for the DX1Collector allocator + **/ + struct IAllocIterator_DX1CollectorIterator { + static AllocInfo deref(const DX1CollectorIterator &) noexcept; + static cmpresult compare(const DX1CollectorIterator &, + const obj & other) noexcept; + static void next(DX1CollectorIterator &) noexcept; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IAllocIterator_DX1Collector.hpp */ diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp new file mode 100644 index 0000000..c525dad --- /dev/null +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -0,0 +1,77 @@ +/** @file IAllocator_DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.hpp" +#include "DX1Collector.hpp" + +namespace xo { + namespace mm { struct IAllocator_DX1Collector; } + + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::IAllocator_Xfer; + }; + } + + namespace mm { + /* changes here coordinate with + * AAllocator AAllocator.hpp + * IAllocator_Any IAllocator_Any.hpp + * IAllocator_Xfer IAllocator_Xfer.hpp + * RAllocator RCollector.hpp + */ + struct IAllocator_DX1Collector { + using size_type = std::size_t; + using value_type = std::byte *; + using range_type = AAllocator::range_type; + + // todo: available() + + static std::string_view name(const DX1Collector &) noexcept; + /** reserved memory across all {roles, generations} **/ + static size_type reserved(const DX1Collector &) noexcept; + /** 'size'. synonym for committed size **/ + static size_type size(const DX1Collector &) noexcept; + /** committed size accross all {roles, generations} **/ + static size_type committed(const DX1Collector &) noexcept; + /** available (committed but unused) space across all {roles, generations} **/ + static size_type available(const DX1Collector &) noexcept; + /** space used by @p d across all {roles, generations}. **/ + static size_type allocated(const DX1Collector &) noexcept; + /** true iff address @p p comes from collector @p d **/ + static bool contains(const DX1Collector & d, const void * p) noexcept; + /** report last error, if any, for collector @p d **/ + static AllocError last_error(const DX1Collector &) noexcept; + /** fetch allocation bookkeeping info **/ + static AllocInfo alloc_info(const DX1Collector & d, value_type mem) noexcept; + /** create alloc-iterator range over allocs in @d, + * with iterator working storage obtained from @p ialloc + **/ + static range_type alloc_range(const DX1Collector & d, DArena & ialloc) noexcept; + + /** always alloc in gen0 to-space **/ + static value_type alloc(DX1Collector & d, size_type z) noexcept; + static value_type super_alloc(DX1Collector & d, size_type z) noexcept; + static value_type sub_alloc(DX1Collector & d, size_type z, bool complete) noexcept; + /** expand gen0 spaces (both from-space and to-space) **/ + static bool expand(DX1Collector & d, size_type z) noexcept; + + /** reset to empty state; clears all generations **/ + static void clear(DX1Collector & d); + /** invoke destructor **/ + static void destruct_data(DX1Collector & d); + }; + + } /*namespace mm*/ +} /*namespace xo*/ + + +/* end IAllocator_DX1Collector.hpp */ diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp new file mode 100644 index 0000000..c977968 --- /dev/null +++ b/include/xo/gc/detail/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 override { _fatal(); } + [[noreturn]] size_type reserved(Copaque, generation, role) const noexcept override { _fatal(); } + [[noreturn]] size_type committed(Copaque, generation, role) const noexcept override { _fatal(); } + + // non-const methods + [[noreturn]] void install_type(Opaque, int32_t, IGCObject_Any &) noexcept override { _fatal(); } + [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) override { _fatal(); } + [[noreturn]] void forward_inplace(Opaque, obj *) override { _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/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp new file mode 100644 index 0000000..9bc6e12 --- /dev/null +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -0,0 +1,55 @@ +/** @file ICollector_DX1Collector.hpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#include "ACollector.hpp" +#include "ICollector_Xfer.hpp" +#include "DX1Collector.hpp" + +namespace xo { + namespace mm { struct ICollector_DX1Collector; } + + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::ICollector_Xfer; + }; + } + + namespace mm { + /* changes here coordinate with + * ACollector ACollector.hpp + * ICollector_Any ICollector_Any.hpp + * ICollector_Xfer ICollector_Xfer.hpp + * RCollector RCollector.hpp + */ + struct ICollector_DX1Collector { + using size_type = std::size_t; + using header_type = DArena::header_type; + + static bool check_move_policy(const DX1Collector & d, + header_type alloc_hdr, + void * object_data); + + // todo: available() + + static size_type allocated(const DX1Collector & d, generation g, role r); + static size_type reserved(const DX1Collector & d, generation g, role r); + static size_type committed(const DX1Collector & d, generation g, role r); + + static void install_type(DX1Collector & d, + int32_t seq, IGCObject_Any & iface); + static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); + static void forward_inplace(DX1Collector & d, obj * lhs); + + static int32_t s_typeseq; + static bool _valid; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_DX1_Collector.hpp */ diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp new file mode 100644 index 0000000..2177b63 --- /dev/null +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -0,0 +1,75 @@ +/** @file ICollector_Xfer.hpp + * + * @author Roland Conybeare, 2025 + **/ + +#pragma once + +#include "ACollector.hpp" + +namespace xo { + namespace mm { + /** @class ICollector_Xfer + * + * Adapts typed ACollector implementation @tparam ICollector_DRepr + * to type-erased @ref ACollector interface + * + * See for example + * @ref ICollector_DX1Collector + **/ + template + struct ICollector_Xfer : public ACollector { + public: + using Impl = ICollector_DRepr; + using size_type = ACollector::size_type; + + static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } + static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } + + // from ACollector + + // const methods + + int32_t _typeseq() const noexcept override { return s_typeseq; } + size_type allocated(Copaque d, generation g, role r) const noexcept override { + return I::allocated(_dcast(d), g, r); + } + size_type reserved(Copaque d, generation g, role r) const noexcept override { + return I::reserved(_dcast(d), g, r); + } + size_type committed(Copaque d, generation g, role r) const noexcept override { + return I::committed(_dcast(d), g, r); + } + + // non-const methods + + void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) override { + I::install_type(_dcast(d), tseq, iface); + } + void add_gc_root(Opaque d, int32_t tseq, Opaque * root) override { + I::add_gc_root(_dcast(d), tseq, root); + } + void forward_inplace(Opaque d, obj * lhs) override { + I::forward_inplace(_dcast(d), lhs); + } + + private: + using I = Impl; + + public: + static int32_t s_typeseq; + static bool _valid; + }; + + template + int32_t + ICollector_Xfer::s_typeseq = facet::typeseq::id(); + + template + bool + ICollector_Xfer::_valid = facet::valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_Xfer.hpp */ diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp new file mode 100644 index 0000000..753af3c --- /dev/null +++ b/include/xo/gc/detail/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(), g, r); } + size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data(), g, r); } + size_type committed(generation g, role r) const noexcept { return O::iface()->committed(O::data(), g, r); } + + void install_type(int32_t tseq, IGCObject_Any & iface) { return O::iface()->install_type(O::data(), tseq, iface); } + void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } + + 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/gc/gcobject/AGCObject.hpp b/include/xo/gc/gcobject/AGCObject.hpp new file mode 100644 index 0000000..1e0a6d4 --- /dev/null +++ b/include/xo/gc/gcobject/AGCObject.hpp @@ -0,0 +1,53 @@ +/** @file AGCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.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/gc/gcobject/IGCObject_Any.hpp b/include/xo/gc/gcobject/IGCObject_Any.hpp new file mode 100644 index 0000000..039a125 --- /dev/null +++ b/include/xo/gc/gcobject/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/gc/gcobject/IGCObject_Xfer.hpp b/include/xo/gc/gcobject/IGCObject_Xfer.hpp new file mode 100644 index 0000000..75f78fd --- /dev/null +++ b/include/xo/gc/gcobject/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/gc/gcobject/RGCObject.hpp b/include/xo/gc/gcobject/RGCObject.hpp new file mode 100644 index 0000000..054b995 --- /dev/null +++ b/include/xo/gc/gcobject/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/include/xo/gc/generation.hpp b/include/xo/gc/generation.hpp new file mode 100644 index 0000000..12b36d0 --- /dev/null +++ b/include/xo/gc/generation.hpp @@ -0,0 +1,36 @@ +/** @file generation.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + /** hard maximum number of generations **/ + static constexpr uint32_t c_max_generation = 16; + + /** @class generation + * @brief type-safe generation number + **/ + struct generation { + using value_type = std::uint32_t; + + constexpr generation() = default; + explicit constexpr generation(value_type x) : value_{x} {} + + static generation nursery() { return generation{0}; } + + constexpr operator value_type() const { return value_; } + + generation & operator++() { ++value_; return *this; } + + std::uint32_t value_ = 0; + }; + } +} + +/* end generation.hpp */ diff --git a/include/xo/gc/object_age.hpp b/include/xo/gc/object_age.hpp new file mode 100644 index 0000000..ae876bd --- /dev/null +++ b/include/xo/gc/object_age.hpp @@ -0,0 +1,32 @@ +/** @file object_age.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + /** hard maximum remembered object age **/ + static constexpr uint32_t c_max_object_age = 127; + + /** @class object_age + * @brief type-safe object age + * + * Object age measured in number of garbage collections survived. + **/ + struct object_age { + using value_type = std::uint32_t; + + explicit object_age(value_type x) : value_{x} {} + + operator value_type() const { return value_; } + + std::uint32_t value_; + }; + } +} + +/* end object_age.hpp */ diff --git a/include/xo/gc/role.hpp b/include/xo/gc/role.hpp new file mode 100644 index 0000000..58fc72a --- /dev/null +++ b/include/xo/gc/role.hpp @@ -0,0 +1,35 @@ +/** @file role.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + static constexpr uint32_t c_n_role = 2; + + struct role { + using value_type = std::uint32_t; + + explicit constexpr role(value_type x) : role_{x} {} + + static constexpr role to_space() { return role{0}; } + static constexpr role from_space() { return role{1}; } + + static constexpr std::array all() { return {{to_space(), from_space()}}; } + + static constexpr role begin() { return role{0}; } + static constexpr role end() { return role{2}; } + + operator value_type() const { return role_; } + + value_type role_ = 0; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end role.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt new file mode 100644 index 0000000..2d42c4c --- /dev/null +++ b/src/gc/CMakeLists.txt @@ -0,0 +1,21 @@ +# gc/CMakeLists.txt + +set(SELF_LIB xo_gc) +set(SELF_SRCS + + ICollector_Any.cpp + IGCObject_Any.cpp + IAllocator_DX1Collector.cpp + ICollector_DX1Collector.cpp + IAllocIterator_DX1CollectorIterator.cpp + + DX1Collector.cpp + DX1CollectorIterator.cpp + + ) + +xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) +# note: deps here must also appear in cmake/xo_alloc2Config.cmake.in +xo_dependency(${SELF_LIB} xo_alloc2) +xo_dependency(${SELF_LIB} xo_facet) +xo_dependency(${SELF_LIB} indentlog) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp new file mode 100644 index 0000000..7e91ddb --- /dev/null +++ b/src/gc/DX1Collector.cpp @@ -0,0 +1,289 @@ +/** @file DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "Allocator.hpp" +#include "arena/IAllocator_DArena.hpp" +#include "xo/gc/DX1Collector.hpp" +#include "xo/gc/DX1CollectorIterator.hpp" +#include "generation.hpp" +#include "object_age.hpp" +#include +#include +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::facet::with_facet; + + namespace mm { +#ifdef NOT_USING + constexpr std::uint64_t + CollectorConfig::gen_mult() const { + return 1ul << arena_config_.header_size_bits_; + } +#endif + +#ifdef NOT_USING + constexpr std::uint64_t + CollectorConfig::tseq_mult() const { + return 1ul << (gen_bits_ + arena_config_.header_size_bits_); + } +#endif + + // ----- GCRunState ----- + + GCRunState::GCRunState(generation gc_upto) + : gc_upto_{gc_upto} + {} + + GCRunState + GCRunState::gc_not_running() + { + return GCRunState(generation(0)); + } + + GCRunState + GCRunState::gc_upto(generation g) + { + return GCRunState(generation(g + 1)); + } + + // ----- DX1Collector ----- + + using size_type = xo::mm::DX1Collector::size_type; + + DX1Collector::DX1Collector(const CollectorConfig & cfg) : config_{cfg} + { + assert(config_.arena_config_.header_.size_bits_ + + config_.arena_config_.header_.age_bits_ + + config_.arena_config_.header_.tseq_bits_ <= 64); + + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { + space_storage_[0][igen] = DArena::map(cfg.arena_config_); + space_storage_[1][igen] = 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; + } + } + + bool + DX1Collector::contains(role r, const void * addr) const noexcept + { + for (generation gi{0}; gi < config_.n_generation_; ++gi) { + const DArena * arena = get_space(r, gi); + + if (arena->contains(addr)) + return true; + } + + return false; + } + + AllocError + DX1Collector::last_error() const noexcept + { + // TODO: + // need to adjust here if runtime errors + // encountered during gc. + + return get_space(role::to_space(), generation::nursery())->last_error_; + } + + namespace { + size_type + accumulate_total_aux(const DX1Collector & d, + size_t (DArena::* get_stat_fn)() const) noexcept + { + size_t z = 0; + + for (role ri : role::all()) { + for (generation gj{0}; gj < d.config_.n_generation_; ++gj) { + const DArena * arena = d.get_space(ri, gj); + + assert(arena); + + z += (arena->*get_stat_fn)(); + } + } + + return z; + } + } + + size_type + DX1Collector::reserved_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::reserved); + } + + size_type + DX1Collector::size_total() const noexcept + { + return committed_total(); + } + + size_type + DX1Collector::committed_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::committed); + } + + size_type + DX1Collector::available_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::available); + } + + size_type + DX1Collector::allocated_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::allocated); + } + + size_type + DX1Collector::header2size(header_type hdr) const noexcept + { + uint32_t z = config_.arena_config_.header_.size(hdr); + + return z; + } + + object_age + DX1Collector::header2age(header_type hdr) const noexcept + { + uint32_t age = config_.arena_config_.header_.age(hdr); + + assert(age < c_max_object_age); + + return object_age(age); + } + + uint32_t + DX1Collector::header2tseq(header_type hdr) const noexcept + { + uint32_t tseq = config_.arena_config_.header_.tseq(hdr); + + return tseq; + } + + bool + DX1Collector::is_forwarding_header(header_type hdr) const noexcept + { + /** forwarding pointer encoded as sentinel tseq **/ + return config_.arena_config_.header_.is_forwarding_tseq(hdr); + } + + auto + DX1Collector::alloc(size_type z) noexcept -> value_type + { + return with_facet::mkobj(new_space()).alloc(z); + } + + auto + DX1Collector::super_alloc(size_type z) noexcept -> value_type { + return with_facet::mkobj(new_space()).super_alloc(z); + } + + auto + DX1Collector::sub_alloc(size_type z, bool complete) noexcept -> value_type { + return with_facet::mkobj(new_space()).sub_alloc(z, complete); + } + + bool + DX1Collector::expand(size_type z) noexcept + { + if (with_facet::mkobj(to_space(generation{0})).expand(z)) + return with_facet::mkobj(from_space(generation{0})).expand(z); + + return false; + } + + AllocInfo + DX1Collector::alloc_info(value_type mem) const noexcept { + for (role ri : role::all()) { + for (generation gj{0}; gj < config_.n_generation_; ++gj) { + const DArena * arena = this->get_space(ri, gj); + + assert(arena); + + if (arena->contains(mem)) { + return arena->alloc_info(mem); + } + } + } + + // deliberately attempt on nursery to-space, to capture error info + return sentinel + return this->get_space(role::to_space(), generation{0})->alloc_info(mem); + } + + DX1CollectorIterator + DX1Collector::begin() const noexcept + { + scope log(XO_DEBUG(false)); + + const DArena * arena + = get_space(role::to_space(), + generation{0}); + + return DX1CollectorIterator(this, + generation{0}, + generation{config_.n_generation_}, + arena->begin(), + arena->end()); + } + + DX1CollectorIterator + DX1Collector::end() const noexcept { + scope log(XO_DEBUG(false)); + + generation gen_hi = generation{config_.n_generation_}; + + /** valid iterator for end points to end of last DArena. + * otherwise will interfere with working compare + * (since invalid iterators are incomparable) + **/ + + const DArena * arena + = get_space(role::to_space(), + generation(config_.n_generation_ - 1)); + DArenaIterator arena_end = arena->end(); + + return DX1CollectorIterator(this, + gen_hi, + gen_hi, + arena_end, + arena_end); + } + + void + DX1Collector::reverse_roles(generation g) noexcept { + assert(g < config_.n_generation_); + + std::swap(space_[0][g], space_[1][g]); + } + + void + DX1Collector::clear() noexcept { + for (role ri : role::all()) { + for (generation gj{0}; gj < config_.n_generation_; ++gj) { + DArena * arena = this->get_space(ri, gj); + + assert(arena); + + arena->clear(); + } + } + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1Collector.cpp */ diff --git a/src/gc/DX1CollectorIterator.cpp b/src/gc/DX1CollectorIterator.cpp new file mode 100644 index 0000000..3942468 --- /dev/null +++ b/src/gc/DX1CollectorIterator.cpp @@ -0,0 +1,126 @@ +/** @file DX1CollectorIterator.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "xo/gc/DX1CollectorIterator.hpp" +#include "xo/gc/DX1Collector.hpp" +#include +#include + +namespace xo { + namespace mm { + DX1CollectorIterator::DX1CollectorIterator(const DX1Collector * gc, + generation gen_ix, + generation gen_hi, + DArenaIterator arena_ix, + DArenaIterator arena_hi) : gc_{gc}, + gen_ix_{gen_ix}, + gen_hi_{gen_hi}, + arena_ix_{arena_ix}, + arena_hi_{arena_hi} + { + this->normalize(); + } + + void + DX1CollectorIterator::normalize() noexcept + { + scope log(XO_DEBUG(false), + xtag("gen_ix", gen_ix_), + xtag("gen_hi", gen_hi_), + xtag("arena_ix.pos", arena_ix_.pos_), + xtag("arena_hi.pos", arena_hi_.pos_)); + + /* normalize: find lowest generation with non-empty to-space */ + if (arena_ix_.pos_ == arena_hi_.pos_) { + log && log(xtag("action", "look-lub-nonempty-gen")); + + if (gen_ix_ < gen_hi_) + ++gen_ix_; + + for (; gen_ix_ < gen_hi_; ++gen_ix_) { + const DArena * arena + = gc_->get_space(role::to_space(), gen_ix_); + + assert(arena); + + arena_ix_ = arena->begin(); + arena_hi_ = arena->end(); + + if (arena_ix_ != arena_hi_) { + // normalization achieved! + break; + } + } + + log && log(xtag("gen_ix", gen_ix_), + xtag("arena_ix.pos", arena_ix_.pos_), + xtag("arena_hi.pos", arena_hi_.pos_)); + } else { + log && log(xtag("action", "noop")); + } + } + + AllocInfo + DX1CollectorIterator::deref() const noexcept + { + return arena_ix_.deref(); + } + + cmpresult + DX1CollectorIterator::compare(const DX1CollectorIterator & other_ix) const noexcept + { + scope log(XO_DEBUG(false), + xtag("is_valid", is_valid()), + xtag("other_ix.is_valid", other_ix.is_valid()) ); + + if (is_invalid() || (gc_ != other_ix.gc_)) { + log && log("incomparable!"); + return cmpresult::incomparable(); + } + + if (gen_ix_ != other_ix.gen_ix_) { + log && log(xtag("gen", gen_ix_), xtag("other.gen", other_ix.gen_ix_)); + + /* same collector, different arenas -> compare based on gen# */ + + return cmpresult::from_cmp(gen_ix_, other_ix.gen_ix_); + } + + /* both iterators refer to the same arena, + * so can compare their arena iterators directly + */ + cmpresult retval = arena_ix_.compare(other_ix.arena_ix_); + + log && log(xtag("retval", retval)); + + return retval; + } + + void + DX1CollectorIterator::next() noexcept + { + scope log(XO_DEBUG(false), + xtag("arena_ix.arena", arena_ix_.arena_), + xtag("arena_ix.pos", arena_ix_.pos_), + xtag("arena_hi.arena", arena_hi_.arena_), + xtag("arena_hi.pos", arena_hi_.pos_)); + + if (arena_ix_ != arena_hi_) { + ++arena_ix_; + + log && log(xtag("++arena_ix.pos", arena_ix_.pos_)); + + this->normalize(); + + log && log(xtag("arena_ix.arena", arena_ix_.arena_), + xtag("arena_ix.pos", arena_ix_.pos_)); + } else { + log && log(xtag("action", "arena-at-end")); + } + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1CollectorIterator.cpp */ diff --git a/src/gc/IAllocIterator_DX1CollectorIterator.cpp b/src/gc/IAllocIterator_DX1CollectorIterator.cpp new file mode 100644 index 0000000..9746870 --- /dev/null +++ b/src/gc/IAllocIterator_DX1CollectorIterator.cpp @@ -0,0 +1,41 @@ +/** @file IAllocIterator_DX1CollectorIterator.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp" +#include "AllocIterator.hpp" +//#include + +namespace xo { + namespace mm { + AllocInfo + IAllocIterator_DX1CollectorIterator::deref(const DX1CollectorIterator & ix) noexcept + { + return ix.deref(); + } + + cmpresult + IAllocIterator_DX1CollectorIterator::compare(const DX1CollectorIterator & ix, + const obj & other_arg) noexcept + { + /* downcast from variant */ + auto other = obj::from(other_arg); + + if (!other) + return cmpresult::incomparable(); + + DX1CollectorIterator & other_ix = *other.data(); + + return ix.compare(other_ix); + } + + void + IAllocIterator_DX1CollectorIterator::next(DX1CollectorIterator & ix) noexcept + { + ix.next(); + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IAllocIterator_DX1CollectorIterator.cpp */ diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp new file mode 100644 index 0000000..d3e172f --- /dev/null +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -0,0 +1,127 @@ +/** @file IAllocator_DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * See also ICollector_DX1Collector.cpp for collector facet + **/ + +#include "detail/IAllocator_DX1Collector.hpp" +#include "detail/IAllocIterator_DX1CollectorIterator.hpp" +#include "DX1CollectorIterator.hpp" +#include + +namespace xo { + using xo::facet::with_facet; + using std::size_t; + using std::byte; + + namespace mm { + using value_type = IAllocator_DX1Collector::value_type; + + std::string_view + IAllocator_DX1Collector::name(const DX1Collector & d) noexcept + { + return d.config_.name_; + } + + auto + IAllocator_DX1Collector::reserved(const DX1Collector & d) noexcept -> size_type + { + return d.reserved_total(); + } + + auto + IAllocator_DX1Collector::size(const DX1Collector & d) noexcept -> size_type + { + return d.size_total(); + } + + auto + IAllocator_DX1Collector::committed(const DX1Collector & d) noexcept -> size_type + { + return d.committed_total(); + } + + auto + IAllocator_DX1Collector::available(const DX1Collector & d) noexcept -> size_type + { + return d.available_total(); + } + + auto + IAllocator_DX1Collector::allocated(const DX1Collector & d) noexcept -> size_type + { + return d.allocated_total(); + } + + bool + IAllocator_DX1Collector::contains(const DX1Collector & d, const void * addr) noexcept + { + return d.contains(role::to_space(), addr); + } + + AllocError + IAllocator_DX1Collector::last_error(const DX1Collector & d) noexcept + { + return d.last_error(); + } + + auto + IAllocator_DX1Collector::alloc_range(const DX1Collector & d, + DArena & ialloc) noexcept -> range_type + { + DX1CollectorIterator * begin_ix = construct_with(ialloc, d.begin()); + DX1CollectorIterator * end_ix = construct_with(ialloc, d.end()); + + obj begin_obj = with_facet::mkobj(begin_ix); + obj end_obj = with_facet::mkobj( end_ix); + + return AllocRange(std::make_pair(begin_obj, end_obj)); + } + + auto + IAllocator_DX1Collector::alloc(DX1Collector & d, size_type z) noexcept -> value_type + { + return d.alloc(z); + } + + auto + IAllocator_DX1Collector::super_alloc(DX1Collector & d, size_type z) noexcept -> value_type + { + return d.super_alloc(z); + } + + auto + IAllocator_DX1Collector::sub_alloc(DX1Collector & d, size_type z, bool complete) noexcept -> value_type + { + return d.sub_alloc(z, complete); + } + + bool + IAllocator_DX1Collector::expand(DX1Collector & d, size_type z) noexcept + { + return d.expand(z); + } + + AllocInfo + IAllocator_DX1Collector::alloc_info(const DX1Collector & d, value_type mem) noexcept + { + return d.alloc_info(mem); + } + + void + IAllocator_DX1Collector::clear(DX1Collector & d) + { + d.clear(); + } + + void + IAllocator_DX1Collector::destruct_data(DX1Collector & d) + { + d.~DX1Collector(); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IAllocator_DX1Collector.cpp */ diff --git a/src/gc/ICollector_Any.cpp b/src/gc/ICollector_Any.cpp new file mode 100644 index 0000000..d3ea1fb --- /dev/null +++ b/src/gc/ICollector_Any.cpp @@ -0,0 +1,39 @@ +/** @file ICollector_Any.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "detail/ICollector_Any.hpp" +#include + +namespace xo { + using xo::facet::DVariantPlaceholder; + using xo::facet::typeseq; + using xo::facet::valid_facet_implementation; + + namespace mm { + + void + ICollector_Any::_fatal() { + /* control here on uninitialized ICollector_Any. + * Initialized instance will have specific implementation type + * e.g. ICollector_Xfer + */ + + std::cerr << "fatal" + << ": attempt to call uninitialized" + << " ICollector_Any method" + << std::endl; + std::terminate(); + } + + int32_t + ICollector_Any::s_typeseq = typeseq::id(); + + bool + ICollector_Any::_valid = valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/** end ICollector_Any.cpp */ diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp new file mode 100644 index 0000000..41e05f9 --- /dev/null +++ b/src/gc/ICollector_DX1Collector.cpp @@ -0,0 +1,191 @@ +/** @file ICollector_DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * See also IAllocator_DX1Collector.cpp for allocator facet + **/ + +#include "xo/gc/detail/ICollector_DX1Collector.hpp" +#include "GCObject.hpp" + +namespace xo { + namespace mm { + using size_type = ICollector_DX1Collector::size_type; + using std::byte; + + namespace { + size_type + stat_helper(const DX1Collector & d, + size_type (DArena::* getter)() const, + generation g, + role r) + { + const DArena * arena = d.get_space(r, g); + + if (arena) [[likely]] + return (arena->*getter)(); + + return 0; + } + } + + size_type + ICollector_DX1Collector::reserved(const DX1Collector & d, generation g, role r) + { + return stat_helper(d, &DArena::reserved, g, r); + } + + size_type + ICollector_DX1Collector::allocated(const DX1Collector & d, generation g, role r) + { + return stat_helper(d, &DArena::allocated, g, r); + } + + size_type + ICollector_DX1Collector::committed(const DX1Collector & d, generation g, role r) + { + return stat_helper(d, &DArena::committed, g, r); + } + + void + ICollector_DX1Collector::install_type(DX1Collector & d, + std::int32_t tseq, + IGCObject_Any & iface) + { + (void)d; + (void)tseq; + (void)iface; + + assert(false); + } + + void + ICollector_DX1Collector::add_gc_root(DX1Collector & d, + int32_t tseq, + Opaque * root) + { + (void)d; + (void)tseq; + (void)root; + + assert(false); + } + + void + ICollector_DX1Collector::forward_inplace(DX1Collector & d, + obj * lhs) + { + assert(d.runstate_.is_running()); + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | | T |G|size| header + * +---------+ object_data +---+-+----+ + * | .data x----------------->| alloc | + * +---------+ | data | + * | for | + * | instance | + * | ... | + * +----------+ + */ + + void * object_data = (byte *)(*lhs).opaque_data(); + + if (!d.contains(role::from_space(), object_data)) { + /* *lhs isn't in GC-allocated space. + * + * This happens for a modest number of global + * constant, for example DBoolean {true, false}. + * + * It's important we recognize these up front. + * Since not allocated from GC, they don't have + * an alloc-header. + */ + return; + } + + /** NOTE: for form's sake: + * better to lookup actual arena that + * allocated object data. + * + **/ + DArena * some_arena = d.to_space(generation(0)); + + DArena::header_type * p_header + = some_arena->obj2hdr(object_data); + + DArena::header_type alloc_hdr = *p_header; + + /* recover allocation size */ + std::size_t alloc_z = some_arena->config_.header_.size(alloc_hdr); + + /* need to be able to fit forwarding pointer + * in place of forwarded object. + * + * This is guaranteed anyway, by alignment rules + */ + assert(alloc_z > sizeof(uintptr_t)); + + if (d.is_forwarding_header(alloc_hdr)) { + /* *lhs already refers to a forwarding pointer */ + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|size| alloc_hdr + * +---------+ object_data +---+-+----+ + * | .data x----------------->| x--------> + * +---------+ | | dest + * | | + * +----------+ + */ + void * dest = *(void**)object_data; + + /* update *lhs in-place */ + (*lhs).reset_opaque(dest); + } else if (check_move_policy(d, alloc_hdr, object_data)) { + /* copy object *lhs + replace with forwarding pointer */ + + /* which arena are we writing to? need allocator interface */ + + assert(false); + +#ifdef NOT_YET + // to do this need IAllocator_DX1Collector fully implemented + + void * copy = (*lhs).shallow_copy(xxx); + + xxx mm xxx; +#endif + } else { + /* object doesn't need to move. + * e.g. incremental collection + object is tenured + */ + } + } + + bool + ICollector_DX1Collector::check_move_policy(const DX1Collector & d, + header_type alloc_hdr, + void * object_data) + { + (void)object_data; + + // when gc is moving objects, to- and from- spaces have been + // reversed: forwarding pointers are located in from-space and + // refer to to-space. + + object_age age = d.header2age(alloc_hdr); + + generation g = d.config_.age2gen(age); + + assert(d.runstate_.is_running()); + + return (g < d.runstate_.gc_upto()); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_DX1Collector.cpp */ diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp new file mode 100644 index 0000000..22e1cf4 --- /dev/null +++ b/src/gc/IGCObject_Any.cpp @@ -0,0 +1,34 @@ +/** @file IGCObject_Any.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "gcobject/IGCObject_Any.hpp" +#include + +namespace xo { + using xo::facet::DVariantPlaceholder; + using xo::facet::typeseq; + using xo::facet::valid_facet_implementation; + + namespace mm { + + void + IGCObject_Any::_fatal() { + std::cerr << "fatal" + << ": attempt to call uninitialized" + << " IGCObject_Any method" + << std::endl; + std::terminate(); + } + + int32_t + IGCObject_Any::s_typeseq = typeseq::id(); + + bool + IGCObject_Any::_valid = valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObject_Any.cpp */ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt new file mode 100644 index 0000000..c48dbdb --- /dev/null +++ b/utest/CMakeLists.txt @@ -0,0 +1,21 @@ +# xo-gc/utest/CMakeLists.txt +# + +set(UTEST_EXE utest.gc) +set(UTEST_SRCS + gc_utest_main.cpp + Collector.test.cpp + DX1CollectorIterator.test.cpp + random_allocs.cpp +) + +if (ENABLE_TESTING) + xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) + xo_headeronly_dependency(${UTEST_EXE} randomgen) + xo_self_dependency(${UTEST_EXE} xo_gc) +# xo_headeronly_dependency(${UTEST_EXE} indentlog) + xo_headeronly_dependency(${UTEST_EXE} xo_facet) + xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) +endif() + +# end CMakeLists.txt diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp new file mode 100644 index 0000000..f322c28 --- /dev/null +++ b/utest/Collector.test.cpp @@ -0,0 +1,243 @@ +/** @file Collector.test.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * NOTE: properly unit testing gc behavior requires + * xo-object2 dependency; + * see xo-object2/utest + **/ + +#include +#include "Collector.hpp" +#include "random_allocs.hpp" +#include "detail/ICollector_DX1Collector.hpp" +#include "detail/IAllocator_DX1Collector.hpp" +//#include "gc/DX1Collector.hpp" +#include +#include +#include +#include +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::mm::ACollector; + using xo::mm::CollectorConfig; + using xo::mm::DX1Collector; + using xo::mm::ArenaConfig; + using xo::mm::AllocHeaderConfig; + using xo::mm::generation; + using xo::mm::c_max_generation; + using xo::facet::with_facet; + using xo::scope; + + namespace ut { + // checklist + // - obj constructible [ ] + // - obj truthy [ ] + // - obj constructible [ ] + // + // - obj constructible [ ] + // - obj allocation + + TEST_CASE("collector-any-null", "[alloc2][gc][ACollector]") + { + /* empty variant collector */ + obj gc1; + + REQUIRE(!gc1); + REQUIRE(gc1.iface() != nullptr); + REQUIRE(gc1.data() == nullptr); + } + + TEST_CASE("DX1Collector-1", "[alloc2][gc][DX1Collector]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + 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()); + } + } + + TEST_CASE("collector-x1-obj", "[alloc2][gc]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + 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}; + + /* typed collector -- repr known at compile time */ + obj x1(&gc); + + REQUIRE(x1.iface()); + REQUIRE(x1.data()); + } + + TEST_CASE("collector-x1-facet-mkobj", "[alloc2][gc]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), }; + 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}; + + /* typed collector -- repr inferred at compile time */ + auto x1 = with_facet::mkobj(&gc); + + REQUIRE(x1.iface()); + REQUIRE(x1.data()); + } + + TEST_CASE("collector-x1-alloc", "[alloc2][gc]") + { + scope log(XO_DEBUG(false), "DX1Collector alloc test"); + + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard-byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), }; + + /* collector with one generation collapses to a non-generational copying collector */ + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 1, + .gc_trigger_v_ = {{64*1024, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector x1state = DX1Collector{cfg}; + + /* typed collector */ + auto x1gc = with_facet::mkobj(&x1state); + auto x1alloc = with_facet::mkobj(&x1state); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); + + REQUIRE(x1alloc.iface()); + REQUIRE(x1alloc.data()); + + rng::Seed seed; + log && log(xtag("seed", seed)); + + auto rng = rng::xoshiro256ss(seed); + + bool catch_flag = false; + REQUIRE(utest::AllocUtil::random_allocs(25, catch_flag, &rng, x1alloc)); + } + + TEST_CASE("collector-x1-alloc2", "[alloc2][gc]") + { + scope log(XO_DEBUG(false), + "DX1Collector alloc test2"); + + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(8 /*guard_z*/, + 0xfd /*guard-byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), + }; + + /* collector with one generation collapses to a non-generational copying collector */ + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 1, + .gc_trigger_v_ = {{64*1024, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + /* X1 allocator+collector */ + DX1Collector x1state = DX1Collector{cfg}; + + /* typed collector i/face */ + auto x1gc = with_facet::mkobj(&x1state); + /* typed allocator i/face */ + auto x1alloc = with_facet::mkobj(&x1state); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); + + REQUIRE(x1alloc.iface()); + REQUIRE(x1alloc.data()); + + rng::Seed seed; + log && log("ratio: seed=", seed); + + auto rng = rng::xoshiro256ss(seed); + + REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); + } + } +} + +/* end Collector.test.cpp */ diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp new file mode 100644 index 0000000..ba3ba09 --- /dev/null +++ b/utest/DX1CollectorIterator.test.cpp @@ -0,0 +1,221 @@ +/** @file DX1CollectorIterator.test.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include +#include "AllocIterator.hpp" +#include "DX1CollectorIterator.hpp" +#include "detail/IAllocator_DX1Collector.hpp" +#include "detail/IAllocIterator_DX1CollectorIterator.hpp" +#include "arena/ArenaConfig.hpp" +#include "padding.hpp" +#include +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::mm::AAllocIterator; +// using xo::mm::IAllocIterator_Any; + using xo::mm::IAllocIterator_Xfer; + using xo::mm::IAllocIterator_DX1CollectorIterator; + using xo::mm::DX1Collector; + using xo::mm::DX1CollectorIterator; + using xo::mm::DArena; + using xo::mm::DArenaIterator; + using xo::mm::CollectorConfig; + using xo::mm::ArenaConfig; + using xo::mm::AllocHeaderConfig; + using xo::mm::cmpresult; + using xo::mm::padding; + using xo::facet::with_facet; + using std::byte; + + namespace ut { + TEST_CASE("IAllocIterator_Xfer_DX1CollectorIterator", "[alloc2]") + { + /* verify IAllocIterator_Xfer is constructible + satisfies concept checks */ + IAllocIterator_Xfer xfer; + REQUIRE(IAllocIterator_Xfer::_valid); + } + + TEST_CASE("DX1CollectorIterator-1", "[alloc2][gc][DX1Collector]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + 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}; + + auto ix = gc.begin(); + auto end_ix = gc.end(); + + REQUIRE(ix.is_valid()); + REQUIRE(end_ix.is_valid()); + REQUIRE(ix == end_ix); + + /* verify obj 'fat pointer' packaging */ + obj ix_vt{&ix}; + obj end_ix_vt{&end_ix}; + + REQUIRE(ix_vt.iface()); + REQUIRE(ix_vt.data()); + REQUIRE(end_ix_vt.iface()); + REQUIRE(end_ix_vt.data()); + + cmpresult cmp = ix_vt.compare(end_ix_vt); + + REQUIRE(cmp.is_equal()); + REQUIRE(ix_vt == end_ix_vt); + } + + TEST_CASE("DX1CollectorIterator-2", "[alloc2][gc][DX1Collector]") + { + scope log(XO_DEBUG(false)); + + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + 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}; + obj a1o{&gc}; + + REQUIRE(a1o.reserved() >= arena_cfg.size_); + REQUIRE(a1o.committed() == 0); + REQUIRE(a1o.available() == 0); + REQUIRE(a1o.allocated() == 0); + + size_t req_z = 13; + byte * mem = gc.alloc(req_z); + + REQUIRE(mem != nullptr); + + log && log("should have iterators separated by one alloc"); + + { + auto ix = gc.begin(); + auto end_ix = gc.end(); + + REQUIRE(ix.is_valid()); + REQUIRE(end_ix.is_valid()); + REQUIRE(ix != end_ix); + + /* verify obj 'fat pointer' packaging */ + auto ix_vt = with_facet::mkobj(&ix); + auto end_ix_vt = with_facet::mkobj(&end_ix); + + REQUIRE(ix_vt.iface()); + REQUIRE(ix_vt.data()); + REQUIRE(end_ix_vt.iface()); + REQUIRE(end_ix_vt.data()); + + cmpresult cmp = ix_vt.compare(end_ix_vt); + + REQUIRE(cmp.is_lesser()); + REQUIRE(ix_vt != end_ix_vt); + + /* we only did one alloc, should be able + * to visit it + */ + auto info = ix_vt.deref(); + + REQUIRE(info.is_valid()); + REQUIRE(info.payload().first == mem); + REQUIRE(info.size() == padding::with_padding(req_z)); + + ix_vt.next(); + + log && log(xtag("ix.gen", ix.gen_ix()), + xtag("ix.arena_ix.arena", ix.arena_ix().arena_)); + log && log(xtag("end_ix.gen", end_ix.gen_ix()), + xtag("end_ix.arena_ix.arena", end_ix.arena_ix().arena_)); + + REQUIRE(ix_vt == end_ix_vt); + } + + { + //auto range = gc.alloc_range + + DArena scratch_mm + = DArena::map( + ArenaConfig{ + .size_ = 4*1024, + .hugepage_z_ = 4*1024}); + + auto range = a1o.alloc_range(scratch_mm); + + obj ix = range.begin(); + obj end_ix = range.end(); + + REQUIRE(ix.iface()); + REQUIRE(ix.data()); + REQUIRE(end_ix.iface()); + REQUIRE(end_ix.data()); + + REQUIRE(scratch_mm.allocated() >= 2*sizeof(DArenaIterator)); + REQUIRE(scratch_mm.available() > 0); + + REQUIRE(ix.compare(ix).is_equal()); + + REQUIRE(ix != end_ix); + + { + REQUIRE(ix.deref().is_valid()); + REQUIRE(ix.deref().size() == padding::with_padding(req_z)); + + auto [payload_lo, payload_hi] = ix.deref().payload(); + + REQUIRE(payload_lo == mem); + REQUIRE(payload_hi == mem + ix.deref().size()); + } + + { + ++ix; + + REQUIRE(ix == end_ix); + } + } + + // repeat, this time using range iteration + { + DArena scratch_mm + = DArena::map( + ArenaConfig{ + .size_ = 4*1024, + .hugepage_z_ = 4*1024}); + + for (const auto & info : a1o.alloc_range(scratch_mm)) { + REQUIRE(info.is_valid()); + REQUIRE(info.size() == padding::with_padding(req_z)); + REQUIRE(info.payload().first == mem); + REQUIRE(info.payload().second == mem + info.size()); + } + } + } + } /*namespace ut*/ +} /*namespace xo*/ + +/* end DX1CollectorIterator.test.cpp */ diff --git a/utest/gc_utest_main.cpp b/utest/gc_utest_main.cpp new file mode 100644 index 0000000..8c42600 --- /dev/null +++ b/utest/gc_utest_main.cpp @@ -0,0 +1,6 @@ +/* file gc_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end gc_utest_main.cpp */ diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp new file mode 100644 index 0000000..56d0cb8 --- /dev/null +++ b/utest/random_allocs.cpp @@ -0,0 +1,217 @@ +/** @file random_allocs.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "random_allocs.hpp" +#include "arena/DArena.hpp" +#include "padding.hpp" +#include +#include +#include +#include + +namespace utest { + using xo::mm::AllocInfo; + using xo::mm::DArena; + using xo::mm::ArenaConfig; + using xo::mm::padding; + using xo::rng::xoshiro256ss; + using xo::facet::obj; + using xo::scope; + using xo::xtag; + using std::uint32_t; + using std::byte; + + /* remember an allocation result. + * application owns memory in [lo, lo+z) + */ + struct Alloc { + Alloc() = default; + Alloc(byte * lo, size_t z) : lo_{lo}, z_{z} {} + + byte * lo() const { return lo_; } + byte * hi() const { return lo_ + z_; } + + byte * lo_ = nullptr; + size_t z_ = 0; + }; + + bool + AllocUtil::random_allocs(uint32_t n_alloc, + bool catch_flag, + xoshiro256ss * p_rgen, + obj mm) + { + scope log(XO_DEBUG(catch_flag), xtag("n-alloc", n_alloc)); + + /* track allocs. verify: + * - allocs are non-overlapping + * - allocs have valid alloc header + * - allocs surrounded by guard bytes + * + * allocs sorted on Alloc::lo + */ + std::map allocs_by_lo_map; + /* allocs sorted on Alloc::hi */ + std::map allocs_by_hi_map; + + for (uint32_t i_alloc = 0; i_alloc < n_alloc; ++i_alloc) { + std::normal_distribution ngen{5.0, 1.5}; + + double si = ngen(*p_rgen); + double zi = ::pow(2.0, si); + std::size_t z = ::ceil(zi); + + bool ok_flag = true; + + std::byte * mem = mm.alloc(z); + + log && log(xtag("i_alloc", i_alloc), + xtag("si", si), + xtag("zi", zi), + xtag("mem", mem)); + log && log(xtag("used", mm.allocated()), + xtag("avail", mm.available()), + xtag("commit", mm.committed()), + xtag("resv", mm.reserved())); + + + REQUIRE_ORFAIL(ok_flag, catch_flag, mem != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.contains(mem)); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_seq_ == 0); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_ == xo::mm::error::none); + + { + auto ix = allocs_by_lo_map.lower_bound(mem); + if (ix != allocs_by_lo_map.end()) { + REQUIRE_ORFAIL(ok_flag, catch_flag, (ix->first > mem + z)); + } + } + + { + auto ix = allocs_by_hi_map.upper_bound(mem); + if (ix != allocs_by_hi_map.end()) { + --ix; + REQUIRE_ORFAIL(ok_flag, catch_flag, (ix->first < mem)); + } + } + + allocs_by_lo_map[mem] = Alloc(mem, z); + allocs_by_hi_map[mem + z] = &(allocs_by_lo_map[mem]); + + /* verify we can recover alloc info */ + AllocInfo info = mm.alloc_info(mem); + + REQUIRE_ORFAIL(ok_flag, catch_flag, info.is_valid()); + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.size() == padding::with_padding(z)); + + /* age isn't configured -> 0 = sentinel */ + REQUIRE_ORFAIL(ok_flag, catch_flag, info.age() == 0); + /* tseq isn't configured -> 0 = sentinel */ + REQUIRE_ORFAIL(ok_flag, catch_flag, info.tseq() == 0); + + if ((info.p_config_->guard_z_ > 0) + || info.guard_lo().first + || info.guard_lo().second + || info.guard_hi().first + || info.guard_hi().second) + { + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().first != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().second != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().first + info.guard_z() + == info.guard_lo().second); + + for (const byte * p = info.guard_lo().first; + p != info.guard_lo().second; ++p) + { + REQUIRE_ORFAIL(ok_flag, catch_flag, (char)*p == info.guard_byte()); + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().first != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().second != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().first + info.guard_z() + == info.guard_hi().second); + + for (const byte * p = info.guard_hi().first; + p != info.guard_hi().second; ++p) + { + REQUIRE_ORFAIL(ok_flag, catch_flag, (char)*p == info.guard_byte()); + } + + + } else { + /* control here only if all of: + * - guard_z is zero + * - guard_lo empty + * - guard_hi empty + */ + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().first == nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().second == nullptr); + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().first == nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().second == nullptr); + + } + + /** scratch arena for iterators **/ + DArena scratch_mm = DArena::map(ArenaConfig{.name_ = "scratch", + .size_ = 4*1024, + .hugepage_z_ = 4*1024 }); + auto range = mm.alloc_range(scratch_mm); + + /* limit iteration test to a few cases: + * - 1st loop + * - median loop + * - last loop + */ + if (i_alloc == 0 || i_alloc == n_alloc || 2*i_alloc == n_alloc) + { + /* verify iteration visits all the allocs, exactly once */ + + /* temp copy; remove allocs from this map as we encounter + * them via range iteration below + */ + auto alloc_map = allocs_by_lo_map; + + if (log) { + log(xtag("allocs_by_lo_map.size", allocs_by_lo_map.size())); + + for (auto & kv : allocs_by_lo_map) { + log(xtag("key", kv.first), xtag("value", kv.second.lo()), xtag("hi", kv.second.hi())); + } + } + + for (AllocInfo info : range) { + INFO(tostr(xtag("alloc_map.size", alloc_map.size()), + xtag("i_alloc", i_alloc))); + INFO(tostr(xtag("payload.first", info.payload().first))); + + const std::byte * alloc_lo = info.payload().first; + + REQUIRE_ORFAIL(ok_flag, catch_flag, + alloc_map.find(alloc_lo) != alloc_map.end()); + + alloc_map.erase(alloc_lo); + } + } + } + + return true; + } +} + +/* end random_allocs.cpp */ diff --git a/utest/random_allocs.hpp b/utest/random_allocs.hpp new file mode 100644 index 0000000..ab0afc4 --- /dev/null +++ b/utest/random_allocs.hpp @@ -0,0 +1,45 @@ +/** @file random_allocs.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.hpp" +#include +#include + +namespace utest { + +/* 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 + + + + struct AllocUtil { + using AAllocator = xo::mm::AAllocator; + + /** generate a random sequence of allocations. + * verify allocator behavior + **/ + static bool random_allocs(std::uint32_t n_alloc, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen, + xo::facet::obj alloc); + }; +} + +/* end random_allocs.hpp */