From 84adc35aec57175ef9450911cf56c4ae46b4d365 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 4 Jan 2026 00:34:19 -0500 Subject: [PATCH] xo-gc: copy/move step for collection phase --- .../include/xo/alloc2/AllocHeaderConfig.hpp | 11 + xo-alloc2/include/xo/alloc2/AllocInfo.hpp | 9 +- xo-alloc2/include/xo/alloc2/padding.hpp | 2 - xo-gc/include/xo/gc/DX1Collector.hpp | 36 +- xo-gc/src/gc/DX1Collector.cpp | 310 +++++++++++++++++- xo-gc/src/gc/ICollector_DX1Collector.cpp | 104 +----- xo-object2/src/object2/IGCObject_DFloat.cpp | 3 +- xo-object2/src/object2/IGCObject_DInteger.cpp | 5 +- xo-object2/src/object2/IGCObject_DList.cpp | 3 +- xo-object2/utest/X1Collector.test.cpp | 4 +- 10 files changed, 369 insertions(+), 118 deletions(-) diff --git a/xo-alloc2/include/xo/alloc2/AllocHeaderConfig.hpp b/xo-alloc2/include/xo/alloc2/AllocHeaderConfig.hpp index 754a5c98..ab560886 100644 --- a/xo-alloc2/include/xo/alloc2/AllocHeaderConfig.hpp +++ b/xo-alloc2/include/xo/alloc2/AllocHeaderConfig.hpp @@ -6,6 +6,7 @@ #pragma once #include "AllocHeader.hpp" +#include "padding.hpp" #include namespace xo { @@ -121,6 +122,11 @@ namespace xo { return (hdr.repr_ & size_mask()); } + /** extract padded size from alloc header @p hdr **/ + std::size_t size_with_padding(repr_type hdr) const noexcept { + return padding::with_padding(this->size(hdr)); + } + /** true iff sentinel tseq, flagging a forwarding pointer **/ bool is_forwarding_tseq(repr_type hdr) const noexcept { // e.g. @@ -134,6 +140,11 @@ namespace xo { bool is_size_enabled() const noexcept { return size_bits_ > 0; } + /** construct alloc header for a forwarding object **/ + AllocHeader mark_forwarding_tseq(AllocHeader hdr) const noexcept { + return AllocHeader((hdr.repr_ & ~tseq_mask()) | tseq_mask()); + } + /** if non-zero, allocate extra space between allocs, and fill * with fixed test-pattern contents. Allows for simple * runtime arena sanitizing checks. diff --git a/xo-alloc2/include/xo/alloc2/AllocInfo.hpp b/xo-alloc2/include/xo/alloc2/AllocInfo.hpp index 0f8d0e1b..30113e46 100644 --- a/xo-alloc2/include/xo/alloc2/AllocInfo.hpp +++ b/xo-alloc2/include/xo/alloc2/AllocInfo.hpp @@ -52,8 +52,15 @@ namespace xo { /** @defgroup mm-allocinfo-methods **/ ///@{ + AllocHeader header() const noexcept { return *p_header_; } + /** true for non-sentinel AllocInfo instance **/ - bool is_valid() const { return (p_config_ != nullptr) && (p_header_ != nullptr); } + bool is_valid() const noexcept { return ((p_config_ != nullptr) + && (p_header_ != nullptr)); } + /** true iff sentinel tseq, flagging a forwarding pointer **/ + bool is_forwarding_tseq() const noexcept { + return p_config_->is_forwarding_tseq(*p_header_); + } /** Guard bytes preceding allocation-header **/ span_type guard_lo() const noexcept; diff --git a/xo-alloc2/include/xo/alloc2/padding.hpp b/xo-alloc2/include/xo/alloc2/padding.hpp index 151d5588..10d03b82 100644 --- a/xo-alloc2/include/xo/alloc2/padding.hpp +++ b/xo-alloc2/include/xo/alloc2/padding.hpp @@ -40,8 +40,6 @@ namespace xo { */ std::size_t dz = (align - (z % align)) % align; - z += dz; - return dz; } diff --git a/xo-gc/include/xo/gc/DX1Collector.hpp b/xo-gc/include/xo/gc/DX1Collector.hpp index f6ec9b2d..bde9f04d 100644 --- a/xo-gc/include/xo/gc/DX1Collector.hpp +++ b/xo-gc/include/xo/gc/DX1Collector.hpp @@ -144,10 +144,10 @@ namespace xo { 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_; } + bool is_running() const { return gc_upto_ > 0; } + private: /** running gc collecting all generations gi < gc_upto **/ generation gc_upto_; @@ -217,7 +217,12 @@ namespace xo { /** Retreive bookkeeping info for allocation at @p mem. **/ AllocInfo alloc_info(value_type mem) const noexcept; - // ----- type registration ----- + // ----- app memory model ----- + + /** lookup interface from type sequence + * (can use tseq = typeseq::id() for type T) + **/ + const AGCObject * lookup_type(typeseq tseq) const noexcept; /** Register object type with this collector. * Provides shallow copy and pointer forwarding for instances of this @@ -228,6 +233,8 @@ namespace xo { /** add GC root at @p root_addr, with type @p typeseq **/ void add_gc_root_poly(obj * p_root) noexcept; + // ----- collection ----- + /** Request immediate collection. * 1. if collection is enabled, immediately collect all generations * up to (but not including) g @@ -241,6 +248,23 @@ namespace xo { /** Execute gc immediately, for all generations < @p upto **/ void execute_gc(generation upto) noexcept; + /** Evacuate object at @p *lhs_data to to-space. + * Replace original with forwarding pointer to new location + **/ + void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); + + /** evacuate object with type @p iface at address @p from_src + * to to-space. Return new to-space location. + **/ + void * shallow_move(const AGCObject * iface, void * from_src); + + /** true iff {alloc_hdr, object_data} should move for + * currently-running collection. + * + * Require: runstate_.is_running() + **/ + bool check_move_policy(header_type alloc_hdr, void * object_data) const noexcept; + // ----- allocation ----- /** simple allocation. allocate @p z bytes of memory @@ -295,6 +319,12 @@ namespace xo { /** copy roots + everything reachable from them, to to-space **/ void copy_roots(generation upto) noexcept; + /** move subgraph at @p from_src to to-space. + * + * Require: runstate_.is_running() + **/ + void * deep_move(void * from_src, generation upto); + public: /** garbage collector configuration **/ CollectorConfig config_; diff --git a/xo-gc/src/gc/DX1Collector.cpp b/xo-gc/src/gc/DX1Collector.cpp index 2c86d5dd..cc7eb051 100644 --- a/xo-gc/src/gc/DX1Collector.cpp +++ b/xo-gc/src/gc/DX1Collector.cpp @@ -4,6 +4,8 @@ **/ #include "Allocator.hpp" +#include "detail/IAllocator_DX1Collector.hpp" +#include "detail/ICollector_DX1Collector.hpp" #include "arena/IAllocator_DArena.hpp" #include "xo/gc/DX1Collector.hpp" #include "xo/gc/DX1CollectorIterator.hpp" @@ -221,6 +223,15 @@ namespace xo { return (vtable != nullptr); } + const AGCObject * + DX1Collector::lookup_type(typeseq tseq) const noexcept + { + AGCObject * v = reinterpret_cast(object_types_.lo_); + + return &(v[tseq.seqno()]); + } + + /* editor bait: register_type */ bool DX1Collector::install_type(const AGCObject & meta) noexcept { @@ -268,8 +279,13 @@ namespace xo { { scope log(XO_DEBUG(true), xtag("upto", upto)); + assert(!runstate_.is_running()); + //auto t0 = std::chrono::steady_clock::now(); + log && log("step 0a : update run state"); + this->runstate_ = GCRunState::gc_upto(upto); + log && log("step 0a : [STUB] snapshot alloc state"); log && log("step 0b : [STUB] scan for object statistics"); @@ -294,10 +310,302 @@ namespace xo { } } + /* + * rules: + * - from_src must be in from-space + * - object type stored in alloc header + * - return value is new location in to-space + * + * - preserving i/face pointer + * - replace destination with forwarding pointer + * + * EDITOR: gc -> self + */ + void * + DX1Collector::deep_move(void * from_src, generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (!from_src) + return nullptr; + + if (!this->contains(role::from_space(), from_src)) { + /* presumeably memory not owned by collector + * (e.g. Boolean {true, false}) + */ + return from_src; + } + + AllocInfo info = this->alloc_info((std::byte *)from_src); + AllocHeader hdr = info.header(); + typeseq tseq(info.tseq()); + + if (is_forwarding_header(hdr)) { + /* already forwarded - pickup destination + * + * Coordinates with forward_inplace() + */ + + return *(void **)from_src; + } + + /* here: object at from_src not already forwarded */ + + if (!this->check_move_policy(hdr, from_src)) { + /* object at from_src in generation that is not being collected */ + return from_src; + } + + /** + * To-space: + * + * to_lo = start of to-space + * w,W = white objects. An object x is white if x + * + all immediate children of x are in to-space + * (also implies this GC cycle put it there) + * g,G = grey objects. An object x is gray if it's in to-space, + * but possibly has >0 black children + * _ = free to-space memory + * N = nursery space (generation{0}) + * T = tenured space (generation{1}) + * + * wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * After moving children of one object, + * advancing {nursery_grey_lo, nursery_free_ptr} + * + * wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * Invariant: + * + * objects in [to_lo, gray_lo) are white. + * all gray objects are in [gray_lo, free_ptr) + * memory starting at free_ptr is free. + * + * deep_move terminates when gray_lo catches up to free_ptr + * + * Above is simplified. Complication is that GC (including incremental) may + * promote objects from nursery (N) to tenured (T) + * + * So more accurate before/after picture + * + * N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwgggggggggggg_______________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(N) + * + * After moving children of one object, + * advancing {nursery_grey_lo, nursery_free_ptr} + * + * N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(T) + * + * deep_move terminates when both: + * - gray_lo(N) catches up with free_ptr(N) + * - gray_lo(T) catches up with free_ptr(T) + * + **/ + + /* TODO: AllocIterator pointing to free pointer */ + std::array gray_lo_v; + { + for (uint32_t g = 0; g < upto; ++g) { + gray_lo_v[g] = this->to_space(generation{g})->free_; + } + } + + obj alloc(this); + const AGCObject * iface = lookup_type(tseq); + void * to_dest = this->shallow_move(iface, from_src); + + std::size_t fixup_work = 0; + + /* TODO: + * - loop here is bad for memory locality + * - replace with depth-first traversal + */ + do { + fixup_work = 0; + + for (generation g = generation{0}; g < upto; ++g) { + /* TODO: use AllocIterator here */ + while(gray_lo_v[g] < to_space(g)->free_) { + AllocHeader * hdr = (AllocHeader *)gray_lo_v[g]; + void * src = (hdr + 1); + + log && log("fwd children", xtag("src", src)); + + const auto & hdr_cfg = config_.arena_config_.header_; + typeseq tseq = typeseq(hdr_cfg.tseq(*hdr)); + size_t z = hdr_cfg.size_with_padding(*hdr); + + const AGCObject * iface = this->lookup_type(tseq); + obj gc(this); + + iface->forward_children(src, gc); + + gray_lo_v[g] = ((std::byte *)src) + z; + ++fixup_work; + } + } + } while (fixup_work > 0); + + return to_dest; + } + void DX1Collector::copy_roots(generation upto) noexcept { - scope log(XO_DEBUG(true), "STUB", xtag("upto", upto)); + for (obj ** p_root = (obj **)roots_.lo_; + p_root < (obj **)roots_.free_; ++p_root) + { + (*p_root)->reset_opaque(this->deep_move((*p_root)->data_, upto)); + } + } + + void + DX1Collector::forward_inplace(AGCObject * lhs_iface, + void ** lhs_data) + { + /* coordinates with DX1Collector::_deep_move() */ + + (void)lhs_iface; + assert(runstate_.is_running()); + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | | T |G|size| header + * +---------+ object_data +---+-+----+ + * | .data x----------------->| alloc | + * +---------+ | data | + * | for | + * | instance | + * | ... | + * +----------+ + */ + + void * object_data = (std::byte *)lhs_data; + + if (!this->contains(role::from_space(), object_data)) { + /* *lhs isn't in GC-allocated space. + * + * This happens for a modest number of global + * constants, 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. + * Only using this to get alloc header + * + **/ + DArena * some_arena = this->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_with_padding(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 (this->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_data = dest; + } else if (this->check_move_policy(alloc_hdr, object_data)) { + /* copy object *lhs + replace with forwarding pointer */ + + /* which arena are we writing to? need allocator interface */ + + *lhs_data = this->shallow_move(lhs_iface, *lhs_data); + + } else { + /* object doesn't need to move. + * e.g. incremental collection + object is tenured + */ + } + } /*forward_inplace*/ + + void * + DX1Collector::shallow_move(const AGCObject * iface, void * from_src) + { + AllocInfo info = this->alloc_info((std::byte *)from_src); + obj alloc(this); + + void * to_dest = iface->shallow_copy(from_src, alloc); + + if(to_dest == from_src) { + assert(false); + } else { + *(const_cast(info.p_header_)) + = AllocHeader(config_ + .arena_config_ + .header_ + .mark_forwarding_tseq(*info.p_header_)); + + *(void **)from_src = to_dest; + } + + return to_dest; + } + + bool + DX1Collector::check_move_policy(header_type alloc_hdr, + void * object_data) const noexcept + { + (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 = this->header2age(alloc_hdr); + + generation g = config_.age2gen(age); + + assert(runstate_.is_running()); + + return (g < runstate_.gc_upto()); } auto diff --git a/xo-gc/src/gc/ICollector_DX1Collector.cpp b/xo-gc/src/gc/ICollector_DX1Collector.cpp index a138c9b6..e30182de 100644 --- a/xo-gc/src/gc/ICollector_DX1Collector.cpp +++ b/xo-gc/src/gc/ICollector_DX1Collector.cpp @@ -79,95 +79,7 @@ namespace xo { AGCObject * lhs_iface, void ** lhs_data) { - (void)lhs_iface; - 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_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_data = 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 - */ - } + d.forward_inplace(lhs_iface, lhs_data); } bool @@ -175,19 +87,7 @@ namespace xo { 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()); + return d.check_move_policy(alloc_hdr, object_data); } } /*namespace mm*/ diff --git a/xo-object2/src/object2/IGCObject_DFloat.cpp b/xo-object2/src/object2/IGCObject_DFloat.cpp index 9d9a65de..be4849ac 100644 --- a/xo-object2/src/object2/IGCObject_DFloat.cpp +++ b/xo-object2/src/object2/IGCObject_DFloat.cpp @@ -25,8 +25,7 @@ namespace xo { IGCObject_DFloat::shallow_copy(const DFloat & src, obj mm) noexcept { - DFloat * copy = (DFloat *)mm.alloc(typeseq::id(), - sizeof(DFloat)); + DFloat * copy = (DFloat *)mm.alloc_copy((std::byte *)&src); if (copy) *copy = src; diff --git a/xo-object2/src/object2/IGCObject_DInteger.cpp b/xo-object2/src/object2/IGCObject_DInteger.cpp index d6c80bb6..cbc27f63 100644 --- a/xo-object2/src/object2/IGCObject_DInteger.cpp +++ b/xo-object2/src/object2/IGCObject_DInteger.cpp @@ -23,10 +23,9 @@ namespace xo { DInteger * IGCObject_DInteger::shallow_copy(const DInteger & src, - obj mm) noexcept + obj mm) noexcept { - DInteger * copy = (DInteger *)mm.alloc(typeseq::id(), - sizeof(DInteger)); + DInteger * copy = (DInteger *)mm.alloc_copy((std::byte *)&src); if (copy) *copy = src; diff --git a/xo-object2/src/object2/IGCObject_DList.cpp b/xo-object2/src/object2/IGCObject_DList.cpp index a30f2c6a..2139d682 100644 --- a/xo-object2/src/object2/IGCObject_DList.cpp +++ b/xo-object2/src/object2/IGCObject_DList.cpp @@ -24,8 +24,7 @@ namespace xo { IGCObject_DList::shallow_copy(const DList & src, obj mm) noexcept { - /* FIXME: need to supply object age here */ - DList * copy = (DList *)mm.alloc(typeseq::id(), sizeof(DList)); + DList * copy = (DList *)mm.alloc_copy((std::byte *)&src); if (copy) *copy = src; diff --git a/xo-object2/utest/X1Collector.test.cpp b/xo-object2/utest/X1Collector.test.cpp index bc579e93..b15a0c56 100644 --- a/xo-object2/utest/X1Collector.test.cpp +++ b/xo-object2/utest/X1Collector.test.cpp @@ -194,14 +194,14 @@ namespace ut { REQUIRE(info.tseq() == typeseq::id().seqno()); REQUIRE(info.size() >= sizeof(DList)); REQUIRE(info.size() < sizeof(DList) + padding::c_alloc_alignment); - } } + + /* no GC roots, so GC is trivial */ c_o.request_gc(generation{1}); - } catch (std::exception & ex) { std::cerr << "caught exception: " << ex.what() << std::endl; REQUIRE(false);