From 5d2fcf649872a922d8cce7be7aae8191be0baac2 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 5 Aug 2025 11:08:36 -0500 Subject: [PATCH] xo-alloc: mutation log tracking in working state + unit test --- docs/glossary.rst | 9 + xo-alloc/docs/index.rst | 3 + xo-alloc/include/xo/alloc/ArenaAlloc.hpp | 24 +- xo-alloc/include/xo/alloc/Forwarding1.hpp | 14 +- xo-alloc/include/xo/alloc/GC.hpp | 136 ++++- xo-alloc/include/xo/alloc/IAlloc.hpp | 16 +- xo-alloc/include/xo/alloc/ListAlloc.hpp | 23 +- xo-alloc/include/xo/alloc/Object.hpp | 25 +- xo-alloc/src/alloc/Forwarding1.cpp | 5 + xo-alloc/src/alloc/GC.cpp | 488 ++++++++++++++++-- xo-alloc/src/alloc/IAlloc.cpp | 6 + xo-alloc/src/alloc/ListAlloc.cpp | 10 + xo-alloc/src/alloc/Object.cpp | 4 +- xo-alloc/utest/CMakeLists.txt | 12 +- xo-alloc/utest/GC.test.cpp | 3 + .../include/xo/expression/Primitive.hpp | 33 -- xo-object/include/xo/object/BooleanObj.hpp | 37 -- xo-object/include/xo/object/Integer.hpp | 5 + xo-object/include/xo/object/List.hpp | 28 +- xo-object/include/xo/object/String.hpp | 2 +- xo-object/src/object/Integer.cpp | 6 +- xo-object/src/object/List.cpp | 25 +- xo-object/src/object/String.cpp | 6 +- xo-object/utest/CMakeLists.txt | 3 +- xo-object/utest/List.test.cpp | 13 +- 25 files changed, 743 insertions(+), 193 deletions(-) delete mode 100644 xo-object/include/xo/object/BooleanObj.hpp diff --git a/docs/glossary.rst b/docs/glossary.rst index b6b43838..682583cc 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -4,9 +4,18 @@ Glossary -------- .. glossary:: + b/c + | abbreviation for ``because`` + + iff + | abbreviation for ``if, and only if`` + schematika scm | Experimental programming language. | Designed for convenient integration with C++ and python. + w/ + | abbreviation for ``with`` + .. toctree:: diff --git a/xo-alloc/docs/index.rst b/xo-alloc/docs/index.rst index 198cf01c..de643009 100644 --- a/xo-alloc/docs/index.rst +++ b/xo-alloc/docs/index.rst @@ -12,3 +12,6 @@ xo-alloc provides arena allocators and a generation garbage collector install introduction implementation + glossary + genindex + search diff --git a/xo-alloc/include/xo/alloc/ArenaAlloc.hpp b/xo-alloc/include/xo/alloc/ArenaAlloc.hpp index e2e74e8f..12033646 100644 --- a/xo-alloc/include/xo/alloc/ArenaAlloc.hpp +++ b/xo-alloc/include/xo/alloc/ArenaAlloc.hpp @@ -45,24 +45,24 @@ namespace xo { std::size_t z, bool debug_flag); - const std::string & name() const { return name_; } std::byte * free_ptr() const { return free_ptr_; } void set_free_ptr(std::byte * x); // inherited from IAlloc... - virtual std::size_t size() const override; - virtual std::size_t available() const override; - virtual std::size_t allocated() const override; - virtual bool contains(const void * x) const override; - virtual bool is_before_checkpoint(const void * x) const override; - virtual std::size_t before_checkpoint() const override; - virtual std::size_t after_checkpoint() const override; + virtual const std::string & name() const final override { return name_; } + virtual std::size_t size() const final override; + virtual std::size_t available() const final override; + virtual std::size_t allocated() const final override; + virtual bool contains(const void * x) const final override; + virtual bool is_before_checkpoint(const void * x) const final override; + virtual std::size_t before_checkpoint() const final override; + virtual std::size_t after_checkpoint() const final override; - virtual void clear() override; - virtual void checkpoint() override; - virtual std::byte * alloc(std::size_t z) override; - virtual void release_redline_memory() override; + virtual void clear() final override; + virtual void checkpoint() final override; + virtual std::byte * alloc(std::size_t z) final override; + virtual void release_redline_memory() final override; private: ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag); diff --git a/xo-alloc/include/xo/alloc/Forwarding1.hpp b/xo-alloc/include/xo/alloc/Forwarding1.hpp index 62536651..4fb61d08 100644 --- a/xo-alloc/include/xo/alloc/Forwarding1.hpp +++ b/xo-alloc/include/xo/alloc/Forwarding1.hpp @@ -12,11 +12,15 @@ namespace xo { explicit Forwarding1(gp dest); // inherited from Object.. - virtual bool _is_forwarded() const override { return true; } - virtual Object * _offset_destination(Object * src) const; - virtual std::size_t _shallow_size() const override; - virtual Object * _shallow_copy() const override; - virtual std::size_t _forward_children() override; + virtual bool _is_forwarded() const final override { return true; } + virtual Object * _offset_destination(Object * src) const final override; + virtual Object * _destination() final override; + /** never called on Forwarding1 **/ + virtual std::size_t _shallow_size() const final override; + /** never called on Forwarding1 **/ + virtual Object * _shallow_copy() const final override; + /** never called on Forwarding1 **/ + virtual std::size_t _forward_children() final override; private: /** the object that used to be located at this address (i.e. @c this) diff --git a/xo-alloc/include/xo/alloc/GC.hpp b/xo-alloc/include/xo/alloc/GC.hpp index f42864b7..0c50a722 100644 --- a/xo-alloc/include/xo/alloc/GC.hpp +++ b/xo-alloc/include/xo/alloc/GC.hpp @@ -23,6 +23,12 @@ namespace xo { constexpr std::size_t gen2int(generation x) { return static_cast(x); } + enum class generation_result { + nursery, + tenured, + not_found + }; + enum class role { /** nursery: generation for new objects **/ from_space, @@ -131,6 +137,17 @@ namespace xo { /** total bytes promoted from nursery->tenured since inception **/ std::size_t total_promoted_ = 0; + /** total number of mutations to already-allocated objects, + * whether or not GC needs to log them. + **/ + std::size_t n_mutation_ = 0; + /** total number of mutation eligible for logging **/ + std::size_t n_logged_mutation_ = 0; + /** total number of cross-generation mutations (tenured->nursery when reported) **/ + std::size_t n_xgen_mutation_ = 0; + /** total number of cross-checkpoint mutations (N0 -> N1 when reported) **/ + std::size_t n_xckp_mutation_ = 0; + /** per-type statistics (placeholder) **/ ObjectStatistics per_type_stats_; }; @@ -163,6 +180,35 @@ namespace xo { bool full_move_ = false; }; + class MutationLogEntry { + public: + MutationLogEntry(Object * parent, Object ** lhs) : parent_{parent}, lhs_{lhs} {} + + Object * parent() const { return parent_; } + Object ** lhs() const { return lhs_; } + + Object * child() const { return *lhs_; } + + bool is_child_forwarded() const; + bool is_parent_forwarded() const; + + Object * parent_destination() const; + + /** Flag obsolete mutation. + * Future proofing, never happens for regular objects + **/ + bool is_dead() const { return false; } + + MutationLogEntry update_parent_moved(Object * parent_to) const; + void fixup_parent_child_moved(Object * child_to) { *lhs_ = child_to; } + + private: + Object * parent_; + Object ** lhs_; + }; + + using MutationLog = std::vector; + /** @class GC * @brief generational garbage collector * @@ -185,16 +231,18 @@ namespace xo { /** true iff GC permitted in current state **/ bool is_gc_enabled() const { return gc_enabled_ == 0; } - /** @return generation to which object at @p x belongs **/ - generation generation_of(const void * x) const; - /** @return generation that contains @p x, given it's in from-space **/ - generation fromspace_generation_of(const void * x) const; - /** true iff from-space contains @p x **/ - bool fromspace_contains(const void * x) const; /** true during (and only during) a GC cycle **/ bool gc_in_progress() const { return runstate_.in_progress(); } - /** return free pointer for generation @p gen, i.e. nursery or tenured space **/ + /** @return generation to which object at @p x belongs **/ + generation_result tospace_generation_of(const void * x) const; + /** @return generation that contains @p x, given it's in from-space **/ + generation_result fromspace_generation_of(const void * x) const; + /** true iff from-space contains @p x **/ + bool fromspace_contains(const void * x) const; + /** @return free pointer for generation @p gen, i.e. nursery or tenured space **/ std::byte * free_ptr(generation gen); + /** @return current size of (number of entries in) mutation log **/ + std::size_t mlog_size() const; /** add gc root at address @p addr . Gc will keep alive anything reachable * from @c *addr @@ -217,27 +265,43 @@ namespace xo { // inherited from IAlloc.. + virtual const std::string & name() const final override; /** capacity in bytes (counting both free+allocated) for object storage. * only counts one of {to-space, from-space}, * since one role is always held empty between collections. **/ - virtual std::size_t size() const override; + virtual std::size_t size() const final override; - virtual std::size_t allocated() const override; - virtual std::size_t available() const override; + virtual std::size_t allocated() const final override; + virtual std::size_t available() const final override; /** only tests to-space **/ - virtual bool contains(const void * x) const override; - virtual bool is_before_checkpoint(const void * x) const override; - virtual std::size_t before_checkpoint() const override; - virtual std::size_t after_checkpoint() const override; + virtual bool contains(const void * x) const final override; + virtual bool is_before_checkpoint(const void * x) const final override; + virtual std::size_t before_checkpoint() const final override; + virtual std::size_t after_checkpoint() const final override; + virtual bool debug_flag() const final override; - virtual void clear() override; - virtual void checkpoint() override; + virtual void clear() final override; + virtual void checkpoint() final override; - virtual std::byte * alloc(std::size_t z) override; - virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) override; + /** GC bookkeeping for an assignment that modifes an Object reference. + * Whenever an @ref Object instance P contains a member variable that can refer + * to another @ref Object, then we need to involve GC to perform the assignment. + * In particular a side-effect that changes the target of such reference to Q after P + * has been promoted, may lead to a tenured->nursery cross-generational pointer. + * GC needs to know about such pointers to it can update them as part of subsequent + * incremental collections. + * + * @param parent. object with member variable being modified + * @param lhs. address of a member variable within the allocation of @p parent. + * @param rhs. new target for @p *lhs + **/ + virtual void assign_member(Object * parent, Object ** lhs, Object* rhs) final override; - virtual void release_redline_memory() override; + virtual std::byte * alloc(std::size_t z) final override; + virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) final override; + + virtual void release_redline_memory() final override; private: /** begin GC now **/ @@ -248,12 +312,35 @@ namespace xo { void swap_nursery(); /** swap roles of From/To spaces for tenured generation **/ void swap_tenured(); + /** swap roles of From/To spaces for mutation log **/ + void swap_mutation_log(); /** swap roles of FromSpace/ToSpace **/ void swap_spaces(generation g); /** copy object **/ void copy_object(Object ** addr, generation upto, ObjectStatistics * object_stats); /** copy everything reachable from global gc roots **/ void copy_globals(generation g); + /** review mutation log; may discover+rescue reachable objects. + **/ + void forward_mutation_log(generation upto); + /** Aux function for @ref execute_gc. Updates bookkeeping for cross-generational + * (T->N, aka xgen) and (N1->N0, aka xckp) pointers + **/ + void incremental_gc_forward_mlog(ObjectStatistics * per_type_stats); + + /** + * Aux function for @ref incremental_gc_forward_mlog. Calls this function until + * fixpoint. + * + * @param from_mlog incoming mutation log. Contains {xgen,xckp} pointers before GC. + * Contents of this log is consumed (+discarded) before method returns. + * @param to_mlog outgoing mutation log. Will contain {xgen,xckp} pointers after GC. + * @param defer_mlog contains log entries associated with possible garbage. + **/ + void incremental_gc_forward_mlog_phase(MutationLog * from_mlog, + MutationLog * to_mlog, + MutationLog * defer_mlog, + ObjectStatistics * per_type_stats); private: /** garbage collector configuration **/ @@ -262,11 +349,11 @@ namespace xo { /** contains allocated objects, along with unreachable garbage to be collected. * roles reverse after each incremental, or full, collection. **/ - std::array, static_cast(role::N)> nursery_; + std::array, role2int(role::N)> nursery_; /** empty space, destination for objects that survive collection. * roles reverse after each full collection. **/ - std::array, static_cast(role::N)> tenured_; + std::array, role2int(role::N)> tenured_; /** current state of GC activity. * @text @@ -286,6 +373,13 @@ namespace xo { **/ std::vector gc_root_v_; + /** log cross-generational and cross-checkpoint mutations. + * These need to be adjusted on next incremental collection + **/ + std::array, role2int(role::N)> mutation_log_; + /** temporary mutation log (for deferred entries) **/ + up defer_mutation_log_; + /** allocation/collection counters **/ GcStatistics gc_statistics_; diff --git a/xo-alloc/include/xo/alloc/IAlloc.hpp b/xo-alloc/include/xo/alloc/IAlloc.hpp index 2f759c53..272fe9ad 100644 --- a/xo-alloc/include/xo/alloc/IAlloc.hpp +++ b/xo-alloc/include/xo/alloc/IAlloc.hpp @@ -12,6 +12,8 @@ namespace xo { template using up = std::unique_ptr; + class Object; + namespace gc { /** @class IAllocator * @brief memory allocation interface with limited garbaga collector support @@ -27,6 +29,8 @@ namespace xo { /** z + alloc_padding(z) **/ static std::size_t with_padding(std::size_t z); + /** optional name for this allocator; labelling for diagnostics **/ + virtual const std::string & name() const = 0; /** allocator size in bytes (up to soft limit). * Includes unallocated mmeory **/ @@ -47,15 +51,25 @@ namespace xo { virtual std::size_t before_checkpoint() const = 0; /** number of bytes allocated since @ref checkpoint **/ virtual std::size_t after_checkpoint() const = 0; + /** @return true iff debug logging enabled **/ + virtual bool debug_flag() const { return false; } /** reset allocator to empty state. **/ virtual void clear() = 0; - /** remember allocator state. All currently-allocated addresses x + /** remember allocator state. All currently-allocated addresses xo * will satisfy is_before_checkpoint(x). Subsequent allocations x * will fail is_before_checkpoint(x), until checkpoint superseded * by @ref clear or another call to @ref checkpoint **/ virtual void checkpoint() = 0; + /** perform assignment + * @code + * *lhs = rhs + * @endcode + * plus additional book keeping if needed (e.g. in @ref GC) + * Default implementation just does the assignment. + **/ + virtual void assign_member(Object * parent, Object ** lhs, Object * rhs); /** allocate @p z bytes of memory. returns pointer to first address **/ virtual std::byte * alloc(std::size_t z) = 0; /** allocate @p z bytes for copy of object at @p src. diff --git a/xo-alloc/include/xo/alloc/ListAlloc.hpp b/xo-alloc/include/xo/alloc/ListAlloc.hpp index 8d27e6b4..db1a2df7 100644 --- a/xo-alloc/include/xo/alloc/ListAlloc.hpp +++ b/xo-alloc/include/xo/alloc/ListAlloc.hpp @@ -45,18 +45,19 @@ namespace xo { // inherited from IAlloc.. - virtual std::size_t size() const override; - virtual std::size_t available() const override; - virtual std::size_t allocated() const override; - virtual bool contains(const void * x) const override; - virtual bool is_before_checkpoint(const void * x) const override; - virtual std::size_t before_checkpoint() const override; - virtual std::size_t after_checkpoint() const override; + virtual const std::string & name() const final override; + virtual std::size_t size() const final override; + virtual std::size_t available() const final override; + virtual std::size_t allocated() const final override; + virtual bool contains(const void * x) const final override; + virtual bool is_before_checkpoint(const void * x) const final override; + virtual std::size_t before_checkpoint() const final override; + virtual std::size_t after_checkpoint() const final override; - virtual void clear() override; - virtual void checkpoint() override; - virtual std::byte * alloc(std::size_t z) override; - virtual void release_redline_memory() override; + virtual void clear() final override; + virtual void checkpoint() final override; + virtual std::byte * alloc(std::size_t z) final override; + virtual void release_redline_memory() final override; private: /** **/ diff --git a/xo-alloc/include/xo/alloc/Object.hpp b/xo-alloc/include/xo/alloc/Object.hpp index f66e8e9a..f8e45e84 100644 --- a/xo-alloc/include/xo/alloc/Object.hpp +++ b/xo-alloc/include/xo/alloc/Object.hpp @@ -15,8 +15,6 @@ namespace xo { class ObjectStatistics; }; - class Object; - template class gc_ptr; @@ -88,6 +86,13 @@ namespace xo { **/ static gc::IAlloc * mm; + /** assign value @p rhs to member @p *lhs of @p parent. + * if assignment creates a cross-generational or cross-checkpoint pointer, + * add mutation log entry + **/ + template + static void assign_member(gp parent, gp * lhs, gp rhs); + /** use from GC aux functions **/ static gc::GC * _gc() { return reinterpret_cast(mm); } @@ -127,6 +132,13 @@ namespace xo { * initially all reachable objects are black. * GC is complete when all reachable objects are white. * GC needs a variable amount of temporary storage to keep track of all gray objects + * + * Evacuate reachable object graph rooted at @p src to to-space. + * On return all objects reachable from @p src are white + * + * @param src address of object to evacuate + * @param gc garbage collector + * @param stats per-object-type GC statistics **/ static Object * _deep_move(Object * src, gc::GC * gc, gc::ObjectStatistics * stats); @@ -213,6 +225,15 @@ namespace xo { virtual std::size_t _forward_children() = 0; }; + template + void + Object::assign_member(gp parent, gp * lhs, gp rhs) + { + Object::mm->assign_member(parent.ptr(), + reinterpret_cast(lhs->ptr_address()), + rhs.ptr()); + } + /** @class Cpof * @brief argument to operator new used for garbage collector evacuation phase * diff --git a/xo-alloc/src/alloc/Forwarding1.cpp b/xo-alloc/src/alloc/Forwarding1.cpp index 825115a2..ca4f051b 100644 --- a/xo-alloc/src/alloc/Forwarding1.cpp +++ b/xo-alloc/src/alloc/Forwarding1.cpp @@ -21,6 +21,11 @@ namespace xo { return dest_.ptr() + offset; } + Object * + Forwarding1::_destination() { + return dest_.ptr(); + } + std::size_t Forwarding1::_shallow_size() const { assert(false); diff --git a/xo-alloc/src/alloc/GC.cpp b/xo-alloc/src/alloc/GC.cpp index ec4aa647..6dd8a929 100644 --- a/xo-alloc/src/alloc/GC.cpp +++ b/xo-alloc/src/alloc/GC.cpp @@ -71,6 +71,51 @@ namespace xo { << ">"; } + bool + MutationLogEntry::is_child_forwarded() const + { + assert(!parent_->_is_forwarded()); + + return (*lhs_)->_is_forwarded(); + } + + bool + MutationLogEntry::is_parent_forwarded() const + { + return parent_->_is_forwarded(); + } + + Object * + MutationLogEntry::parent_destination() const + { + //const bool c_debug_flag = true; + //scope log(XO_DEBUG(c_debug_flag)); + + if (parent_->_is_forwarded()) { + //log && log("parent is forwarded", xtag("parent", (void*)parent_)); + + return parent_->_destination(); + } else { + //log && log("parent is ordinary", xtag("parent", (void*)parent_)); + + return parent_; + } + } + + MutationLogEntry + MutationLogEntry::update_parent_moved(Object * parent_to) const + { + std::byte * parent_from = reinterpret_cast(parent_); + std::byte * lhs_from = reinterpret_cast(lhs_); + + std::ptrdiff_t offset = (lhs_from - parent_from); + + std::byte * lhs_to = reinterpret_cast(parent_to) + offset; + + return MutationLogEntry(parent_to, + reinterpret_cast(lhs_to)); + } + GC::GC(const Config & config) : config_{config} { @@ -89,6 +134,10 @@ namespace xo { tenured_[role2int(role::to_space) ] = ListAlloc::make("TB", tenured_size, 2 * tenured_size, config.debug_flag_); + mutation_log_[role2int(role::from_space)] = std::make_unique(); + mutation_log_[role2int(role::to_space)] = std::make_unique(); + defer_mutation_log_ = std::make_unique(); + this->checkpoint(); } @@ -100,6 +149,13 @@ namespace xo { return up{gc}; } + const std::string & + GC::name() const + { + static std::string s_default_name = "GC"; + return s_default_name; + } + std::size_t GC::size() const { @@ -151,22 +207,34 @@ namespace xo { return nursery_[role2int(role::to_space)]->after_checkpoint(); } - generation + bool + GC::debug_flag() const + { + return config_.debug_flag_; + } + + generation_result GC::fromspace_generation_of(const void * x) const { if (tenured_[role2int(role::from_space)]->contains(x)) - return generation::tenured; + return generation_result::tenured; - return generation::nursery; + if (nursery_[role2int(role::from_space)]->contains(x)) + return generation_result::nursery; + + return generation_result::not_found; } - generation - GC::generation_of(const void * x) const + generation_result + GC::tospace_generation_of(const void * x) const { if (tenured_[role2int(role::to_space)]->contains(x)) - return generation::tenured; + return generation_result::tenured; - return generation::nursery; + if (nursery_[role2int(role::to_space)]->contains(x)) + return generation_result::nursery; + + return generation_result::not_found; } std::byte * @@ -184,6 +252,11 @@ namespace xo { return nullptr; } + std::size_t + GC::mlog_size() const { + return mutation_log_[role2int(role::to_space)]->size(); + } + void GC::clear() { @@ -231,39 +304,55 @@ namespace xo { { scope log(XO_DEBUG(config_.debug_flag_), xtag("z", z), xtag("+pad", IAlloc::alloc_padding(z))); - generation g = this->fromspace_generation_of(src); + generation_result gr = this->fromspace_generation_of(src); std::byte * retval = nullptr; - if (g == generation::tenured) - { - log && log("tenured"); + switch (gr) { + case generation_result::tenured: + { + log && log("tenured"); - retval = tenured_[role2int(role::to_space)]->alloc(z); - } else if (nursery_[role2int(role::from_space)]->is_before_checkpoint(src)) - { - log && log("promote"); - - /* nursery object has survived 2nd collection cycle - * -> promote into tenured generation - */ - retval = tenured_[role2int(role::to_space)]->alloc(z); - - this->gc_statistics_.total_promoted_ += IAlloc::with_padding(z); - } else { - log && log("nursery"); - - retval = nursery_[role2int(role::to_space)]->alloc(z); - - if (!retval) { - /* nursery space exhausted */ - - this->request_gc(generation::nursery); - - nursery_[role2int(role::to_space)]->release_redline_memory(); - - retval = nursery_[role2int(role::to_space)]->alloc(z); + retval = tenured_[role2int(role::to_space)]->alloc(z); } + break; + case generation_result::nursery: + { + if (nursery_[role2int(role::from_space)]->is_before_checkpoint(src)) + { + /* nursery object has survived 2nd collection cycle + * -> promote into tenured generation + */ + retval = tenured_[role2int(role::to_space)]->alloc(z); + + log && log("promote", xtag("addr", (void*)retval)); + + assert(this->tospace_generation_of(retval) == generation_result::tenured); + + this->gc_statistics_.total_promoted_ += IAlloc::with_padding(z); + } else { + log && log("nursery"); + + retval = nursery_[role2int(role::to_space)]->alloc(z); + + if (!retval) { + /* nursery space exhausted !? */ + + this->request_gc(generation::nursery); + + nursery_[role2int(role::to_space)]->release_redline_memory(); + + retval = nursery_[role2int(role::to_space)]->alloc(z); + } + } + } + break; + case generation_result::not_found: + /* something wrong -- we only copy objects that are known to be in from-space + */ + + assert(false); + break; } assert(retval); @@ -271,6 +360,63 @@ namespace xo { return retval; } + void + GC::assign_member(Object * parent, Object ** lhs, Object * rhs) + { + ++gc_statistics_.n_mutation_; + + *lhs = rhs; + + if (runstate_.in_progress()) { + /* don't log mutations (if any) during GC */ + return; + } + + if (!config_.allow_incremental_gc_) { + /* full GCs don't need mutation log, since no cross-generational pointers */ + return; + } + + switch (tospace_generation_of(rhs)) + { + case generation_result::tenured: + /* only need to log mutations that create tenured->nursery pointers */ + return; + + case generation_result::nursery: + switch (tospace_generation_of(parent)) { + case generation_result::nursery: + if (is_before_checkpoint(parent)) { + // N1->N0, so must log + this->mutation_log_[role2int(role::to_space)]->push_back(MutationLogEntry(parent, lhs)); + ++(this->gc_statistics_.n_logged_mutation_); + ++(this->gc_statistics_.n_xckp_mutation_); + } else { + // parent in N0, not an xckp mutation + return; + } + break; + case generation_result::tenured: + // T->N, so must log + this->mutation_log_[role2int(role::to_space)]->push_back(MutationLogEntry(parent, lhs)); + ++(this->gc_statistics_.n_logged_mutation_); + ++(this->gc_statistics_.n_xgen_mutation_); + break; + case generation_result::not_found: + // parent is global + // This may be ok (provided lhs is a gc root) + break; + } + break; + + case generation_result::not_found: + + // child is global; + // logging not required + break; + } + } + void GC::release_redline_memory() { @@ -293,10 +439,20 @@ namespace xo { tenured_[role2int(role::from_space)] = std::move(tmp); } + void + GC::swap_mutation_log() + { + up tmp = std::move(mutation_log_[role2int(role::to_space)]); + mutation_log_[role2int(role::to_space)] = std::move(mutation_log_[role2int(role::from_space)]); + mutation_log_[role2int(role::from_space)] = std::move(tmp); + } + void GC::swap_spaces(generation target) { - // will be copying into storage currently labelled FromSpace + scope log(XO_DEBUG(this->debug_flag())); + + // will be copying into the memory regions currently labelled FromSpace /* gc will copy some to-be-determined amount in [0..promote_z] from nursery->tenured generation. @@ -321,6 +477,14 @@ namespace xo { - promote_z + incr_gc_threshold_); this->swap_nursery(); + + this->swap_mutation_log(); + + log && log(xtag("nursery.from", nursery_[role2int(role::from_space)]->name())); + log && log(xtag("nursery.to", nursery_[role2int(role::to_space) ]->name())); + log && log(xtag("tenured.from", tenured_[role2int(role::from_space)]->name())); + log && log(xtag("tenured.to", tenured_[role2int(role::to_space) ]->name())); + } /*swap_spaces*/ void @@ -351,6 +515,242 @@ namespace xo { } } + void + GC::incremental_gc_forward_mlog_phase(MutationLog * from_mlog, + MutationLog * to_mlog, + MutationLog * defer_mlog, + ObjectStatistics * per_type_stats) + { + scope log(XO_DEBUG(config_.debug_flag_), xtag("from_mlog.size", from_mlog->size())); + + /* categorize pointers based on combination of {source address, destination address}, + * only care about the generation associated with an address. + * + * N0 : nursery(from), before checkpoint + * N0': nursery(to), before checkpoint + * N1 : nursery(from), after checkpoint + * N1': nursery(to), after checkpoint + * T : tenured(to) + * + * loc(P): parent region before GC + * loc(C): child region before GC + * + * | | forwarded | loc now post | loc after | + * | | already? | root copy | action | + * | loc(P) loc(C) | P C | P' C' | P' C' | defer | action + * ----|---------------+--------------+---------------+---------------+-------+--------------- + * (a) | T N0 | no no | T N0 | T N1' | | C->N1', +mlog + * (b) | | yes | N1' | N1' | | +mlog + * (c) | T N1 | no no | T N1 | T T | | C->T, -mlog + * (d) | | yes | T T | T T | | -mlog + * (e) | N1 N0 | no no | N1 N0 | N1 N0 | P ->C | defer + * (f) | | yes | N1 N1' | N1 N1' | P ->C'| defer + * (g) | | yes yes | T N1' | T N1' | | +mlog + * + * notes: + * (a) C survives due to xgen ptr {T -> N0}; after collection have xgen ptr {T -> N1}. + * (b) C already evac'd; after collection stil have xgen ptr {T -> N1} + * (c) C survives due to xgen ptr (T -> N1): promote to T, so no longer xgen + * (d) C already evac'd: after collection no longer xgen (T -> T) + * (e) P,C maybe garbage. don't move either, but defer mlog incase P saved by a subsequent mutation. + * in that case C saved alto, + will still have an xgen ptr, so still need an mlog entry + * (f) P maybe garbage, C survives. defer mlog incase P saved+promoted by a subsequent mutation; + * in that case will still have an xgen (T -> N) ptr, so still need an mlog entry. + */ + + std::size_t i_from = 0; + // number of rescued subgraphs via mutation log entries + std::size_t n_rescue = 0; + + for (MutationLogEntry & from_entry : *from_mlog) + { + if (log) { + if (i_from % 10000 == 0) + log(xtag("i_from", i_from)); + } + + void * parent = from_entry.parent(); + + if (tospace_generation_of(parent) == generation_result::tenured) + { + // cases (a)(b)(c)(d) + // loc(P) is T. T didn't move b/c incremental gc. + + if (from_entry.is_dead()) { + // obsolete mutation -- no longer belongs to parent, discard + } else { + // note: child obtained (as it must be) by reading from parent's memory _now_. + Object * child_from = from_entry.child(); + + if (child_from) { + if (!child_from->_is_forwarded()) { + // P->C*. + // either: + // - C*=C in from-space, so needs evac + // - C*=C' in to-space, P already updated b/c of another mutation + // + if (fromspace_generation_of(child_from) != generation_result::not_found) { + // C*=C in from-space. needs evac, along with reachable descendants + // + // Includes cases: + // (a) T->N0 + // (c) T->N1 + + ++n_rescue; + + Object::_deep_move(child_from, this, per_type_stats); + + // C forwards to C', fall thru to parent fixup below + // (a) T->N1' + // (c) T->T + } else { + // P updated via some other mutation + // so don't need this mlog + ; + } + } + + // re-test, state may have changed above + if (from_entry.is_child_forwarded()) { + // P->C, C moved to C' + // Includes cases (a),(c) from above + + Object * child_to = child_from->_destination(); + + from_entry.fixup_parent_child_moved(child_to); + + // P->C', loc(C') in {N1', T'} + + if (tospace_generation_of(child_to) == generation_result::nursery) { + // (b) loc(P)=T, loc(C')=N1'; also case (a) + + // still have xgen pointer, so need mlog for it + to_mlog->push_back(from_entry); + } else { + // (d) loc(P)=T, loc(C')=T; also case (c) + // no longer xgen, so does not require mlog + } + } + + } else { + // nullptr child, discard + } + } + } else if (from_entry.is_parent_forwarded()) { + // Must have: + // loc(P) = N1, because: + // loc(P)=N0 -> ineligible for mlog; + // loc(P)=T -> not moved on incr GC + // + // follows that loc(P') = T + // already have P'->C' when parent moved separately + + Object * parent_to = from_entry.parent_destination(); + + log(xtag("parent_to", (void*)parent_to)); + + assert(tospace_generation_of(parent_to) == generation_result::tenured); + + MutationLogEntry to_entry = from_entry.update_parent_moved(parent_to); + + Object * child_to = to_entry.child(); // after moving + + if (tospace_generation_of(child_to) == generation_result::nursery) { + if (to_entry.is_dead()) { + ; + } else { + // (g) loc(P)=N1, loc(C)=N0, loc(P')=T, loc(C')=N1 + to_mlog->push_back(to_entry); + } + + } + } else { + // loc(P) = N1, loc(C) = N0, P may be garbage + // Includes cases: + // (e) P->C, C not moved + // (f) P->C, C moved to C' + // + // P may yet be rescued by another mlog entry, so defer + + if (!from_entry.is_dead()) { + defer_mlog->push_back(from_entry); + } + } + + ++i_from; + } + + from_mlog->clear(); + + if (n_rescue == 0) { + // if we didn't rescue any objects + // then we now confirm that otherwise-unreachable parents in defer_mlog + // are garbage + + defer_mlog->clear(); + } + } + + void + GC::incremental_gc_forward_mlog(ObjectStatistics * per_type_stats) + { + /* control here: + * - incremental gc. + * - gc roots have been copied, along with everything reachable from them. + * + * plan: + * - forward mutation in *from_mutation_log, writing them to + * *to_mutationlog and/or *defer_mutation_log. + * Use defer when mutation P->C encountered, but P was not copied. + * P appears to be garbage, but may turn out to be live if encountered + * in another mutation. + * + */ + + MutationLog * to_mlog = mutation_log_[role2int(role::to_space)].get(); + + for (;;) { + MutationLog * from_mlog = mutation_log_[role2int(role::from_space)].get(); + MutationLog * defer_mlog = defer_mutation_log_.get(); + + this->incremental_gc_forward_mlog_phase(from_mlog, + to_mlog, + defer_mlog, + per_type_stats); + + assert(from_mlog->empty()); + + if (defer_mlog->empty()) { + /* fixpoint reached */ + break; + } + + /* control here: + * 1. at least one mlog triggered a rescue + * 2. at least one mlog was deferred (b/c otherwise-unreachable parent) + * + * it's conceivable deferred parent now reachable thanks to rescues; + * revisit entries in defer_mlog, + * + * using now-empty from_mlog as scratch for any remaining deferred entries + */ + + std::swap(mutation_log_[role2int(role::from_space)], defer_mutation_log_); + } + } + + void + GC::forward_mutation_log(generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (upto == generation::tenured) { + log && log("TODO: forward mutation log for full GC"); + } else { + this->incremental_gc_forward_mlog(&gc_statistics_.per_type_stats_); + } + } + void GC::cleanup_phase(generation upto) { @@ -401,11 +801,11 @@ namespace xo { } void - GC::execute_gc(generation target) + GC::execute_gc(generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); - bool full_move = (target == generation::tenured); + bool full_move = (upto == generation::tenured); // TODO: RAII version in case of exceptions this->runstate_ = GCRunstate(true /*in_progress*/, full_move); @@ -415,7 +815,7 @@ namespace xo { /* new allocation since last GC */ std::size_t new_alloc = this->after_checkpoint(); - ++(gc_statistics_.gen_v_[static_cast(target)].n_gc_); + ++(gc_statistics_.gen_v_[static_cast(upto)].n_gc_); gc_statistics_.total_allocated_ += new_alloc; gc_statistics_.total_promoted_sab_ = gc_statistics_.total_promoted_; @@ -423,15 +823,17 @@ namespace xo { log && log("step 1: swap to/from roles"); - this->swap_spaces(target); + this->swap_spaces(upto); log && log("step 2a: copy globals"); - this->copy_globals(target); + this->copy_globals(upto); log && log("step 2b: TODO: copy pinned"); - log && log("step 3: TODO: forward mutation log"); + log && log("step 3: forward mutation log"); + + this->forward_mutation_log(upto); log && log("step 4: TODO: notify destructor log"); @@ -439,7 +841,7 @@ namespace xo { log && log("step 6: cleanup"); - this->cleanup_phase(target); + this->cleanup_phase(upto); this->runstate_ = GCRunstate(); diff --git a/xo-alloc/src/alloc/IAlloc.cpp b/xo-alloc/src/alloc/IAlloc.cpp index 4fbdd556..6e06a644 100644 --- a/xo-alloc/src/alloc/IAlloc.cpp +++ b/xo-alloc/src/alloc/IAlloc.cpp @@ -41,6 +41,12 @@ namespace xo { return z + alloc_padding(z); } + void + IAlloc::assign_member(Object * /*parent*/, Object ** lhs, Object * rhs) + { + *lhs = rhs; + } + std::byte * IAlloc::alloc_gc_copy(std::size_t /*z*/, const void * /*src*/) { diff --git a/xo-alloc/src/alloc/ListAlloc.cpp b/xo-alloc/src/alloc/ListAlloc.cpp index 76da8b19..a5ae38e7 100644 --- a/xo-alloc/src/alloc/ListAlloc.cpp +++ b/xo-alloc/src/alloc/ListAlloc.cpp @@ -50,6 +50,16 @@ namespace xo { return retval; } + const std::string & + ListAlloc::name() const { + if (hd_) { + return hd_->name(); + } + + static std::string s_default_name = "ListAlloc"; + return s_default_name; + } + std::size_t ListAlloc::size() const { return total_z_; diff --git a/xo-alloc/src/alloc/Object.cpp b/xo-alloc/src/alloc/Object.cpp index 7aa1a8d0..d3772003 100644 --- a/xo-alloc/src/alloc/Object.cpp +++ b/xo-alloc/src/alloc/Object.cpp @@ -34,7 +34,7 @@ namespace xo { bool full_move = gc->runstate().full_move(); - if (!full_move && (gc->generation_of(src) == gc::generation::tenured)) { + if (!full_move && (gc->tospace_generation_of(src) == gc::generation_result::tenured)) { /* don't move tenured objects during incremental collection */ return src; } @@ -61,7 +61,7 @@ namespace xo { bool full_move = gc->runstate().full_move(); - if (!full_move && gc->generation_of(from_src) == generation::tenured) { + if (!full_move && gc->tospace_generation_of(from_src) == gc::generation_result::tenured) { /** incremental collection does not move already-tenured objects **/ return from_src; } diff --git a/xo-alloc/utest/CMakeLists.txt b/xo-alloc/utest/CMakeLists.txt index d37786e3..50882bba 100644 --- a/xo-alloc/utest/CMakeLists.txt +++ b/xo-alloc/utest/CMakeLists.txt @@ -1,11 +1,13 @@ # build unittest alloc/utest +# +# NOTE: more GC tests in xo-object/utest -set(SELF_EXE utest.alloc) -set(SELF_SRCS +set(UTEST_EXE utest.alloc) +set(UTEST_SRCS alloc_utest_main.cpp ArenaAlloc.test.cpp GC.test.cpp) -xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS}) -xo_self_dependency(${SELF_EXE} xo_alloc) -xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2) +xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) +xo_self_dependency(${UTEST_EXE} xo_alloc) +xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) diff --git a/xo-alloc/utest/GC.test.cpp b/xo-alloc/utest/GC.test.cpp index dc175615..12445624 100644 --- a/xo-alloc/utest/GC.test.cpp +++ b/xo-alloc/utest/GC.test.cpp @@ -65,5 +65,8 @@ namespace xo { REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1); } } + } /*namespace ut*/ } /*namespace xo*/ + +/* GC.test.cpp */ diff --git a/xo-expression/include/xo/expression/Primitive.hpp b/xo-expression/include/xo/expression/Primitive.hpp index fbcd201c..6c1c12bc 100644 --- a/xo-expression/include/xo/expression/Primitive.hpp +++ b/xo-expression/include/xo/expression/Primitive.hpp @@ -103,39 +103,6 @@ namespace xo { refrtag("name", name_), rtag("type", print::quot(this->valuetype()->short_name())), refrtag("value", (void*)(this->value()))); - -#ifdef OBSOLETE - ppstate * pps = ppii.pps(); - - if (ppii.upto()) { - if (!pps->print_upto("print_upto_tag("name", name_)) - return false; - - if (!pps->print_upto_tag("type", print::quot(this->value_td()->short_name()))) - return false; - - if (!pps->print_upto_tag("value", (void*)(this->value()))) - return false; - - pps->write(">"); - - return true; - } else { - pps->write("newline_pretty_tag(ppii.ci1(), "name", name_); - pps->newline_pretty_tag(ppii.ci1(), "type", print::quot(this->value_td()->short_name())); - /* don't have pretty printer for native function pointers anyway - * + simplifies ppdetail_atomic - */ - pps->newline_pretty_tag(ppii.ci1(), "value", (void*)this->value()); - pps->write(">"); - - return false; - } -#endif } private: diff --git a/xo-object/include/xo/object/BooleanObj.hpp b/xo-object/include/xo/object/BooleanObj.hpp deleted file mode 100644 index 3d9a0455..00000000 --- a/xo-object/include/xo/object/BooleanObj.hpp +++ /dev/null @@ -1,37 +0,0 @@ -/* @file BooleanObj.hpp - * - * author: Roland Conybeare, Aug 2025 - */ - -#include "xo/alloc/Object.hpp" - -namespace xo { - namespace obj { - /** @class BooleanObj - * @brief Boxed wrapper for a boolean value - **/ - class BooleanObj : public Object { - public: - /** @return instance representing boolean with truth-value @p x **/ - static gp boolean_obj(bool x); - static gp true_obj(); - static gp false_obj(); - - bool value() const { return value_; } - - // inherited from Object.. - - virtual std::size_t _shallow_size() const override; - virtual Object * _shallow_copy() const override; - virtual std::size_t _forward_children() override; - - private: - explicit BooleanObj(bool x) : value_{x} {} - - private: - bool value_; - }; - } -} - -/* end BooleanObj.hpp */ diff --git a/xo-object/include/xo/object/Integer.hpp b/xo-object/include/xo/object/Integer.hpp index 1db629f7..ad9f80e3 100644 --- a/xo-object/include/xo/object/Integer.hpp +++ b/xo-object/include/xo/object/Integer.hpp @@ -15,7 +15,12 @@ namespace xo { Integer() = default; explicit Integer(int_type x); + /** create instance holding integer value @p x **/ static gp make(int_type x); + /** downcast from @p x iff x is actually an Integer. Otherwise nullptr **/ + static gp from(gp x); + + int_type value() const { return value_; } // inherited from Object.. virtual std::size_t _shallow_size() const override; diff --git a/xo-object/include/xo/object/List.hpp b/xo-object/include/xo/object/List.hpp index 51a2d877..9c8cd31a 100644 --- a/xo-object/include/xo/object/List.hpp +++ b/xo-object/include/xo/object/List.hpp @@ -18,14 +18,36 @@ namespace xo { /** @return list with first element @p car, and tail @p cdr **/ static gp cons(gp car, gp cdr); + /** @return list with single element @p x1 **/ + template + static gp list(T && x1) { + return List::cons(x1, nil); + } + + /** @return list with elements @p x1, ..., @p rest in argument order **/ + template + static gp list(T && x1, Rest &&... rest) { + return List::cons(x1, list(rest...)); + } + + /** @return true iff list is empty **/ bool is_nil() const { return this == nil.ptr(); } gp head() const { return head_; } - gp tail() const { return tail_; } + gp rest() const { return rest_; } + /** @return first element in list; synonym for @ref head **/ + gp car() const { return head_; } + /** @return remainder of list after first element; synonym for @ref rest **/ + gp cdr() const { return rest_; } + + /** @return number of top-level elements in this list **/ std::size_t size() const; gp list_ref(std::size_t i) const; + void assign_head(gp head); + void assign_rest(gp rest); + // inherited from Object.. virtual std::size_t _shallow_size() const override; @@ -33,11 +55,11 @@ namespace xo { virtual std::size_t _forward_children() override; private: - List(gp head, gp tail); + List(gp head, gp rest); private: gp head_; - gp tail_; + gp rest_; }; } } /*namespace xo*/ diff --git a/xo-object/include/xo/object/String.hpp b/xo-object/include/xo/object/String.hpp index 596879b9..a96cdd20 100644 --- a/xo-object/include/xo/object/String.hpp +++ b/xo-object/include/xo/object/String.hpp @@ -12,7 +12,7 @@ namespace xo { public: enum Owner { unique, shared }; - /** donwcase from @p x iff x is actually a String. Otherwise nullptr **/ + /** donwcast from @p x iff x is actually a String. Otherwise nullptr **/ static gp from(gp x); /** create copy of string @p s, using allocator @ref Object::mm **/ diff --git a/xo-object/src/object/Integer.cpp b/xo-object/src/object/Integer.cpp index a0ff8bd6..fe4454da 100644 --- a/xo-object/src/object/Integer.cpp +++ b/xo-object/src/object/Integer.cpp @@ -15,6 +15,11 @@ namespace xo { return new (MMPtr(mm)) Integer(x); } + gp + Integer::from(gp x) { + return dynamic_cast(x.ptr()); + } + std::size_t Integer::_shallow_size() const { return sizeof(Integer); @@ -23,7 +28,6 @@ namespace xo { Object * Integer::_shallow_copy() const { Cpof cpof(this); - return new (cpof) Integer(*this); } diff --git a/xo-object/src/object/List.cpp b/xo-object/src/object/List.cpp index 19778b73..446aa919 100644 --- a/xo-object/src/object/List.cpp +++ b/xo-object/src/object/List.cpp @@ -4,13 +4,14 @@ **/ #include "List.hpp" +#include "xo/indentlog/scope.hpp" #include #include namespace xo { namespace obj { - List::List(gp head, gp tail) - : head_{head}, tail_{tail} {} + List::List(gp head, gp rest) + : head_{head}, rest_{rest} {} gp List::nil = new List(nullptr, nullptr); @@ -27,7 +28,7 @@ namespace xo { gp l(this); while (!l->is_nil()) { ++retval; - l = l->tail(); + l = l->rest(); } return retval; @@ -40,7 +41,7 @@ namespace xo { while (i > 0) { assert(!(rem->is_nil())); - rem = rem->tail(); + rem = rem->rest(); --i; } @@ -48,6 +49,18 @@ namespace xo { } + void + List::assign_head(gp head) + { + Object::assign_member(this, &(this->head_), head); + } + + void + List::assign_rest(gp tail) + { + Object::assign_member(this, &(this->rest_), tail); + } + std::size_t List::_shallow_size() const { return sizeof(List); @@ -55,6 +68,8 @@ namespace xo { Object * List::_shallow_copy() const { + scope log(XO_DEBUG(Object::mm->debug_flag())); + assert(!(this->is_nil())); Cpof cpof(this); @@ -65,7 +80,7 @@ namespace xo { std::size_t List::_forward_children() { Object::_forward_inplace(head_); - Object::_forward_inplace(tail_); + Object::_forward_inplace(rest_); return List::_shallow_size(); } } diff --git a/xo-object/src/object/String.cpp b/xo-object/src/object/String.cpp index 32b75a74..f1e097bd 100644 --- a/xo-object/src/object/String.cpp +++ b/xo-object/src/object/String.cpp @@ -31,14 +31,12 @@ namespace xo { } gp - String::from(gp x) - { + String::from(gp x) { return dynamic_cast(x.ptr()); } gp - String::copy(const char * s) - { + String::copy(const char * s) { return copy(Object::mm, s); } diff --git a/xo-object/utest/CMakeLists.txt b/xo-object/utest/CMakeLists.txt index aa6d934c..90f33f9b 100644 --- a/xo-object/utest/CMakeLists.txt +++ b/xo-object/utest/CMakeLists.txt @@ -4,7 +4,8 @@ set(SELF_EXE utest.object) set(SELF_SRCS object_utest_main.cpp String.test.cpp - List.test.cpp) + List.test.cpp + GC.test.cpp) xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS}) xo_self_dependency(${SELF_EXE} xo_object) diff --git a/xo-object/utest/List.test.cpp b/xo-object/utest/List.test.cpp index 78ffc606..862b6ae5 100644 --- a/xo-object/utest/List.test.cpp +++ b/xo-object/utest/List.test.cpp @@ -17,6 +17,7 @@ namespace xo { using xo::obj::List; using xo::obj::String; using xo::gc::GC; + using xo::gc::generation_result; using xo::gc::generation; namespace { @@ -117,8 +118,8 @@ namespace xo { REQUIRE(s.ptr()); REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); - REQUIRE(gc->generation_of(reinterpret_cast(s.ptr())) - == generation::nursery); + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::nursery); } } @@ -142,8 +143,8 @@ namespace xo { REQUIRE(s.ptr()); REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); - REQUIRE(gc->generation_of(reinterpret_cast(s.ptr())) - == generation::tenured); + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::tenured); } } @@ -168,8 +169,8 @@ namespace xo { REQUIRE(s.ptr()); REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); - REQUIRE(gc->generation_of(reinterpret_cast(s.ptr())) - == generation::tenured); + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::tenured); } }