+ xo-alloc + xo-object + xo-alloc docs + GC utests

This commit is contained in:
Roland Conybeare 2025-08-03 15:59:38 -05:00
commit 5f46b51f12
32 changed files with 2903 additions and 82 deletions

View file

@ -0,0 +1,58 @@
/* AllocPolicy.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#include <cstdint>
namespace xo {
/** Tag class, drives overload of operator new.
* See also: xoglobal, xocopy
**/
struct xolib {
xolib() = default;
};
/** @brief opt-in allocator for XO libraries.
*
* By default delegates to vanilla operator new/delete,
* but can set alloc/free functions at runtime to
* adopt a different implementation.
*
* Intending this to op-in to garbage-collector?
* Not sure if we actually need this
*
* Use:
* struct Foo { .. };
* auto p = new (xo) Foo(..);
**/
class XoAllocPolicy {
public:
using AllocFn = void* (*)(std::size_t);
using FreeFn = void (*)(void *);
public:
XoAllocPolicy() = default;
static void * global_alloc(std::size_t z) { return ::operator new(z); }
static void global_free(void * x) { ::operator delete(x); }
void * alloc(std::size_t z) { return (*alloc_)(z); }
void free(void * x) { (*free_)(x); }
private:
AllocFn alloc_ = global_alloc;
FreeFn free_ = global_free;
};
/** singleton xolib instance **/
static XoAllocPolicy xo;
}
inline void * operator new(std::size_t z, xo::xolib) {
return xo::xo.alloc(z);
}
void operator delete(void * ptr) noexcept;
/* end AllocPolicy.hpp */

View file

@ -1,4 +1,4 @@
/* file LinearAlloc.hpp
/* file ArenaAlloc.hpp
*
* author: Roland Conybeare, Jul 2025
*/
@ -9,7 +9,7 @@
namespace xo {
namespace gc {
/** @class LinearAlloc
/** @class ArenaAlloc
* @brief Bump allocator with fixed capacity
*
* @text
@ -33,34 +33,39 @@ namespace xo {
*
* TODO: rename to ArenaAlloc
**/
class LinearAlloc : public IAlloc {
class ArenaAlloc : public IAlloc {
public:
~LinearAlloc();
~ArenaAlloc();
/** create allocator with capacity @p z,
* with reserved capacity @p redline_z.
**/
static up<LinearAlloc> make(std::size_t redline_z, std::size_t z);
static up<ArenaAlloc> make(const std::string & name,
std::size_t redline_z,
std::size_t z,
bool debug_flag);
std::uint8_t * free_ptr() const { return free_ptr_; }
void set_free_ptr(std::uint8_t * x);
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 is_before_checkpoint(const std::uint8_t * x) 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 void clear() override;
virtual void checkpoint() override;
virtual std::uint8_t * alloc(std::size_t z) override;
virtual std::byte * alloc(std::size_t z) override;
virtual void release_redline_memory() override;
private:
LinearAlloc(std::size_t rz, std::size_t z);
ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag);
private:
/**
@ -68,23 +73,30 @@ namespace xo {
* - @ref free_ always a multiple of word size (assumed to be sizeof(void*))
**/
/** optional instance name, for diagnostics **/
std::string name_;
/** allocator owns memory in range [@ref lo_, @ref hi_) **/
std::uint8_t * lo_ = nullptr;
std::byte * lo_ = nullptr;
/** checkpoint (for GC support); divides objects into
* older (addresses below checkpoint)
* and younger (addresses above checkpoint)
**/
std::uint8_t * checkpoint_;
std::byte * checkpoint_;
/** free pointer. memory in range [@ref free_, @ref limit_) available **/
std::uint8_t * free_ptr_ = nullptr;
std::byte * free_ptr_ = nullptr;
/** soft limit: end of released memory **/
std::uint8_t * limit_ = nullptr;
std::byte * limit_ = nullptr;
/** amount of last-resort memory to reserve **/
std::size_t redline_z_ = 0;
/** hard limit: end of allocated memory **/
std::uint8_t * hi_ = nullptr;
std::byte * hi_ = nullptr;
/** true to enable detailed debug logging **/
bool debug_flag_ = false;
};
} /*namespace gc*/
} /*namespace xo*/
/* end LinearAlloc.hpp */
/* end ArenaAlloc.hpp */

View file

@ -0,0 +1,28 @@
/* Forwarding.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#pragma once
#include "Object.hpp"
namespace xo {
namespace gc {
class Forwarding : public Object {
public:
Forwarding() = default;
// inherited from Object..
#ifdef NOT_USING
virtual bool _is_forwarded() const override final { return true; }
#endif
virtual Object * _destination() override final { return destination_.ptr(); }
private:
gp<Object> destination_;
};
} /*namespace gc*/
} /*namespace xo*/
/* end Forwarding.hpp */

View file

@ -0,0 +1,40 @@
/* file Forwarding1.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Object.hpp"
namespace xo {
namespace obj {
class Forwarding1 : public Object {
public:
explicit Forwarding1(gp<Object> 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;
private:
/** the object that used to be located at this address (i.e. @c this)
* has been moved to @ref destination_ ,
* with original location overwritten by a forwarding pointer
*
* Require:
* - can only use Forwarding with types that have a single vtable.
* To forward a multiply-inheriting class with two vtables, use Forwarding2.
* - if you try to use Forwarding for an object with multiple vtables,
* one of the vtable pointers will be replaced by @ref destination_.
* UB revealed when GC traverses a pointer that relies on the 2nd
* vtable to index virtual methods.
**/
gp<Object> dest_;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Forwarding1.hpp */

310
include/xo/alloc/GC.hpp Normal file
View file

@ -0,0 +1,310 @@
/* GC.hpp
*
* author: Roland Conybeare, jul 2025
*/
#pragma once
#include "ListAlloc.hpp"
#include "xo/indentlog/print/array.hpp"
#include <vector>
#include <array>
namespace xo {
/** types that can participate in GC inherit from this base class. See Object.hpp in this directory **/
class Object;
namespace gc {
enum class generation {
nursery,
tenured,
N
};
constexpr std::size_t gen2int(generation x) { return static_cast<std::size_t>(x); }
enum class role {
/** nursery: generation for new objects **/
from_space,
/** tenured: generation for objects that have survived two collections **/
to_space,
N,
};
constexpr std::size_t role2int(role x) { return static_cast<std::size_t>(x); }
/** @class Config
* @brief garbage collector configuration
**/
struct Config {
/** initial size in bytes for youngest (Nursery) generation.
* GC allocates two nursery spaces of this size.
* Will allocate more space as needed
**/
std::size_t initial_nursery_z_ = 0;
/** initial size in bytes for oldest (Tenured) generation.
* GC allocates two tenured spaces of this size
* Will allocate more space as needed
**/
std::size_t initial_tenured_z_ = 0;
/** true to permit incremental garbage collection **/
bool allow_incremental_gc_ = true;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
/** @class ObjectStatistics
* @brief placeholder for type-driven allocation statistics
*
* Passed to @ref Object::deep_move for example
**/
class ObjectStatistics {
};
/** @class PerGenerationStatistics
* @brief garbage collection statistics for particular GC generation
**/
class PerGenerationStatistics {
public:
/** update statistics after a GC cycle
* @param alloc_z. new allocations (since preceding GC)
* @param before_z. generation size (bytes allocated) before collection
* @param after_z. generation size after collection
* @param promote_z. bytes promoted to next generation
**/
void include_gc(std::size_t alloc_z, std::size_t before_z, std::size_t after_z,
std::size_t promote_z);
/** update with current state (use at end of gc cycle) **/
void update_snapshot(std::size_t after_z);
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** number of bytes currently in use **/
std::size_t used_z_ = 0;
/** number of collection cycles completed **/
std::size_t n_gc_ = 0;
/** sum of new alloc bytes, sampled at start of each collection cycle **/
std::size_t new_alloc_z_ = 0;
/** sum of allocated bytes sampled at beginning of each collection cycle **/
std::size_t scanned_z_ = 0;
/** sum of bytes remaining after collection cycle **/
std::size_t survive_z_ = 0;
/** sum of bytes promoted to next generation **/
std::size_t promote_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const PerGenerationStatistics & x) {
x.display(os);
return os;
}
/** @class GcStatistics
* @brief garbage collection statistics
**/
class GcStatistics {
public:
/** update statistics after a GC cycle
* @param upto. nursery -> incremental collection; tenured -> full collection
* @param alloc_z. new allocations (since preceding GC)
* @param before_z. generation size (bytes allocated) before collection
* @param after_z. generation size after collection
* @param promote_z. bytes promoted to next generation
**/
void include_gc(generation upto, std::size_t alloc_z,
std::size_t before_z, std::size_t after_z, std::size_t promote_z);
/** update snapshot for current state.
* Use with tenured stats after incremental gc
**/
void update_snapshot(generation upto, std::size_t after_z);
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** statistics gathered across {incr, full} GCs respectively **/
std::array<PerGenerationStatistics, static_cast<std::size_t>(generation::N)> gen_v_;
/** total bytes allocated since inception **/
std::size_t total_allocated_ = 0;
/** snapshot of total bytes promoted asof beginning of last gc cycle **/
std::size_t total_promoted_sab_ = 0;
/** total bytes promoted from nursery->tenured since inception **/
std::size_t total_promoted_ = 0;
/** per-type statistics (placeholder) **/
ObjectStatistics per_type_stats_;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatistics & x) {
x.display(os);
return os;
}
/** @class GCRunstate
* @brief encapsulate state needed while GC is running
*
* state pertaining to a single GC invocation.
* We stash an instance of this in @ref GC as context,
* so that per-Object-derived-type auxiliary functions can be slightly streamlined
**/
class GCRunstate {
public:
GCRunstate() = default;
explicit GCRunstate(bool in_progress, bool full_move)
: in_progress_{in_progress}, full_move_{full_move} {}
bool in_progress() const { return in_progress_; }
bool full_move() const { return full_move_; }
private:
/** true when GC begins; remains true until GC cycle complete **/
bool in_progress_ = false;
/** true for full GC; false for incremental GC **/
bool full_move_ = false;
};
/** @class GC
* @brief generational garbage collector
*
* Works with objects of type @ref xo::Object
**/
class GC : public IAlloc {
public:
/** create new GC instance with configuration @p config **/
explicit GC(const Config & config);
/** create GC allocator.
*
* Initial memory consumption:
* approximately 2x @ref Config::nursery_size_ + 2x @ref Config::tenured_size_
**/
static up<GC> make(const Config & config);
const GCRunstate & runstate() const { return runstate_; }
const GcStatistics & gc_statistics() const { return gc_statistics_; }
/** 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 **/
std::byte * free_ptr(generation gen);
/** add gc root at address @p addr . Gc will keep alive anything reachable
* from @c *addr
**/
void add_gc_root(Object ** addr);
/** request garbage collection. **/
void request_gc(generation g);
/** disable garbage collection until matching call to @ref enable_gc.
*
* GC is disabled when number of calls to @ref disable_gc exceeds number of
* calls to @ref enable_gc.
**/
void disable_gc();
/** enable garbage collection
*
* GC is enabled when number of calls to @ref enable_gc is at least as large
* as number of calls to @ref disable_gc.
**/
void enable_gc();
// inherited from IAlloc..
/** 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 allocated() const override;
virtual std::size_t available() const 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 void clear() override;
virtual void checkpoint() override;
virtual std::byte * alloc(std::size_t z) override;
virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) override;
virtual void release_redline_memory() override;
private:
/** begin GC now **/
void execute_gc(generation g);
/** cleanup phase. aux function for @ref execute_gc **/
void cleanup_phase(generation g);
/** swap roles of From/To spaces for nursery generation **/
void swap_nursery();
/** swap roles of From/To spaces for tenured generation **/
void swap_tenured();
/** 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);
private:
/** garbage collector configuration **/
Config config_;
/** contains allocated objects, along with unreachable garbage to be collected.
* roles reverse after each incremental, or full, collection.
**/
std::array<up<ListAlloc>, static_cast<std::size_t>(role::N)> nursery_;
/** empty space, destination for objects that survive collection.
* roles reverse after each full collection.
**/
std::array<up<ListAlloc>, static_cast<std::size_t>(role::N)> tenured_;
/** current state of GC activity.
* @text
* in_progress full_move descr
* -----------------------------------------
* false * gc not running
* true false incremental gc
* true true full gc
* -----------------------------------------
* @endtext
**/
GCRunstate runstate_;
/** root object handles: targets of handles in this vector are always preserved by GC.
* Application can introduce new root object pointers at any time provided GC not running,
* but cannot withdraw them.
**/
std::vector<Object**> gc_root_v_;
/** allocation/collection counters **/
GcStatistics gc_statistics_;
/** trigger full GC whenever this much data arrives in tenured generation **/
std::size_t full_gc_threshold_ = 0;
/** trigger incr GC whenever this much data arrives in nuresery generation **/
std::size_t incr_gc_threshold_ = 0;
/** true when GC requested,
* remains true until GC.. completes? begins?
**/
bool incr_gc_pending_ = false;
bool full_gc_pending_ = false;
/** enabled when 0. disabled when <0 **/
int gc_enabled_ = 0;
};
} /*namespace gc*/
} /*namespace xo*/
/* end GC.hpp */

View file

@ -1,20 +0,0 @@
/* file GCAlloc.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#pragma once
namespace xo {
namespace gc {
class GC : public IAlloc {
enum class Space { A, B, N_Space };
enum class Gen { Nursery, Tenured };
};
} /*namespace mem */
} /*namespace xo*/
/* end GCAlloc.hpp */

View file

@ -20,6 +20,13 @@ namespace xo {
public:
virtual ~IAlloc() {}
/** compute padding to add to an allocation of size z to bring it up to
* a multiple of word size (8 bytes on x86_64)
**/
static std::uint32_t alloc_padding(std::size_t z);
/** z + alloc_padding(z) **/
static std::size_t with_padding(std::size_t z);
/** allocator size in bytes (up to soft limit).
* Includes unallocated mmeory
**/
@ -30,10 +37,12 @@ namespace xo {
virtual std::size_t available() const = 0;
/** number of bytes allocated from this allocator **/
virtual std::size_t allocated() const = 0;
/** true iff pointer x comes from this allocator **/
virtual bool contains(const void * x) const = 0;
/** true iff object at address @p x was allocated by this allocator,
* and before checkpoint
**/
virtual bool is_before_checkpoint(const std::uint8_t * x) const = 0;
virtual bool is_before_checkpoint(const void * x) const = 0;
/** number of bytes allocated before @ref checkpoint **/
virtual std::size_t before_checkpoint() const = 0;
/** number of bytes allocated since @ref checkpoint **/
@ -48,10 +57,39 @@ namespace xo {
**/
virtual void checkpoint() = 0;
/** allocate @p z bytes of memory. returns pointer to first address **/
virtual std::uint8_t * alloc(std::size_t z) = 0;
virtual std::byte * alloc(std::size_t z) = 0;
/** allocate @p z bytes for copy of object at @p src.
* Only used in @ref GC. Default implementation asserts and returns nullptr
**/
virtual std::byte * alloc_gc_copy(std::size_t z, const void * src);
/** release last-resort reserved memory **/
virtual void release_redline_memory() = 0;
};
} /*namespace gc*/
class MMPtr {
public:
explicit MMPtr(gc::IAlloc * mm) : mm_{mm} {}
gc::IAlloc * mm_ = nullptr;
};
} /*namespace xo*/
inline void * operator new (std::size_t z, const xo::MMPtr & mmp) {
return mmp.mm_->alloc(z);
}
//inline void operator delete (void * p, const MMPtr & mmp) {
// mmp.mm_->free(reinterpret_cast<std::byte *>(p));
//}
inline void * operator new[] (std::size_t z, const xo::MMPtr & mmp) {
return mmp.mm_->alloc(z);
}
//inline void operator delete[] (void * p, const MMPtr & mmp) {
// mmp.mm_->free(reinterpret_cast<std::byte *>(p));
//}
/* end IAlloc.hpp */

View file

@ -6,13 +6,17 @@
#pragma once
#include "IAlloc.hpp"
#include <list>
#include <memory>
#include <cstdint>
namespace xo {
namespace gc {
class ArenaAlloc;
/** GC-compatible allocator using a linked list of buckets.
*
* - all allocs done from first allocator in list
* GC Support:
* - reserved memory, released after call to @ref release_redline_memory.
*
@ -21,27 +25,60 @@ namespace xo {
**/
class ListAlloc : public IAlloc {
public:
ListAlloc(LinearAlloc* hd,
std::size_t cz, std::size_t nz; std::size_tz,
LinearAlloc* marked, bool use_redline,
bool redlined_flag, OnEmptyFn on_overflow);
ListAlloc(std::unique_ptr<ArenaAlloc> hd,
ArenaAlloc * marked,
std::size_t cz, std::size_t nz, std::size_t tz,
bool use_redline,
bool debug_flag);
~ListAlloc();
static up<ListAlloc> make(std::size_t cz, std::size_t nz,
OnEmptyFn on_overflow);
static up<ListAlloc> make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag);
/** reset to have at least @p z bytes of storage **/
bool reset(std::size_t z);
/** expand bucket list to accomodate a requrest of size @p z **/
bool expand(std::size_t z);
/** current free pointer **/
std::byte * free_ptr() const;
// 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 void clear() override;
virtual void checkpoint() override;
virtual std::byte * alloc(std::size_t z) override;
virtual void release_redline_memory() override;
private:
/** **/
std::size_t start_z_ = 0;
LinearAlloc* hd_ = nullptr;
/** all new allocs from this list **/
std::unique_ptr<ArenaAlloc> hd_;
/** allocator that was in @ref hd_ when @ref checkpoint last called **/
ArenaAlloc * marked_ = nullptr;
/** overflow allocs (expect list to be short);
* from trying to converge on app working set size
**/
std::list<std::unique_ptr<ArenaAlloc>> full_l_;
std::size_t current_z_ = 0;;
std::size_t next_z_ = 0;;
std::size_t total_z_ = 0;
bool use_redline_ = false;
bool redlined_flag_ = false;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
} /*namespace gc*/
} /*namespace xo*/
/* end ListAlloc.hpp */

232
include/xo/alloc/Object.hpp Normal file
View file

@ -0,0 +1,232 @@
/* Object.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#pragma once
#include "IAlloc.hpp"
#include <concepts>
#include <cstdint>
namespace xo {
namespace gc {
class GC;
class ObjectStatistics;
};
class Object;
template <typename T>
class gc_ptr;
template <typename T>
using gp = gc_ptr<T>;
/** wrapper for a pointer to garbage-collector-eligible T.
* Application code will usually use the alias template gp<T>
**/
template <typename T>
class gc_ptr {
public:
using element_type = T;
public:
gc_ptr() = default;
gc_ptr(T * p) : ptr_{p} {}
gc_ptr(const gc_ptr & x) : ptr_{x.ptr_} {}
/** create from gc_ptr to some related type @tparam S **/
template <typename S>
gc_ptr(const gc_ptr<S> & x) : ptr_{x.ptr()} {}
static bool is_eq(gc_ptr x1, gc_ptr x2) {
std::uintptr_t u1 = reinterpret_cast<std::uintptr_t>(x1.ptr());
std::uintptr_t u2 = reinterpret_cast<std::uintptr_t>(x2.ptr());
// multiple inheritance shenanigans.
// (allow interface pointers separated by one pointer)
if (u1 >= u2)
return (u1 <= u2 + sizeof(std::uintptr_t));
else
return (u2 <= u1 + sizeof(std::uintptr_t));
}
T * ptr() const { return ptr_; }
T ** ptr_address() { return &ptr_; }
bool is_null() const { return ptr_ == nullptr; }
void make_null() { ptr_ = nullptr; }
void assign_ptr(T * x) { ptr_ = x; }
gc_ptr & operator=(const gc_ptr & x) { ptr_ = x.ptr(); return *this; }
T * operator->() const { return ptr_; }
private:
T * ptr_ = nullptr;
};
/** Root class for all xo GC-collectable objects.
*
* Design note:
*
* relying on inheritance means we insist that GC traits
* for a type appear directly in that type's vtable, and at specific locations.
* This implies one level of indirection when GC traverses an instance.
*
* Would be feasible to relax the must-inherit-from-Object constraint,
* but cost would be an extra layer of indirection
**/
class Object {
public:
virtual ~Object() = default;
/** memory allocator for objects. Likely this will be a GC instance,
* but simple arena also supported.
**/
static gc::IAlloc * mm;
/** use from GC aux functions **/
static gc::GC * _gc() { return reinterpret_cast<gc::GC*>(mm); }
/** during GC
* 1. copy destination object @p *addr to (new) to-space.
* 2. overwrite existing object @p *addr with a forwarding pointer to
* copy made in step 1.
* 3. return the location of the copy make in step 1.
*
* @p src. source object to be forwarded
* @p gc. garbage collector
*/
static Object * _forward(Object * src, gc::GC * gc);
template <typename T>
static void _forward_inplace(T ** src_addr) {
Object * fwd = _forward(*src_addr, _gc());
*src_addr = reinterpret_cast<T *>(fwd);
}
template <typename T>
static void _forward_inplace(gp<T> & src) {
_forward_inplace<T>(src.ptr_address());
}
/** primary workhorse for garbage collection.
*
* we assign each object one of three colors: black|gray|white.
*
* color | location | children | action |
* ------+------------+------------+-------------------------+
* black | from-space | any | move to to-space |
* gray | to-space | any | move remaining children |
* white | to-space | white/gray | done |
*
* 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
**/
static Object * _deep_move(Object * src, gc::GC * gc, gc::ObjectStatistics * stats);
/** copy @p src to to-space, and replace original with forwarding pointer to new location.
* return the new location
**/
static Object * _shallow_move(Object * src, gc::GC * gc);
// GC support
/** true iff this object represents a forwarding pointer.
* Forwarding pointers are exclusively created by the garbage collector;
* forwarding pointers (and only forwarding pointers) return true here.
**/
virtual bool _is_forwarded() const { return false; }
/** offset for uncommon situation where pointer address is offset from object
* base address
**/
virtual Object * _offset_destination(Object * src) const { return src; };
/** replace this object with a forwarding pointer referring to @p dest.
**/
virtual void _forward_to(Object * dest);
/** if this object represents a forwarding pointer, return its new location.
* forwarding pointers belong to the garbage collector implementation.
* (if you have to ask -- no, your class is not a forwarding pointer)
* all other objects return nullptr here.
**/
virtual Object * _destination() { return nullptr; }
/** return amount of storage (including padding) consumed by this object,
* excluding immediate Object-pointer children
**/
virtual std::size_t _shallow_size() const = 0;
// TODO: _shallow_move() also overwrite *this with gc-only forwarding object point to C
/** if subject is allocated by GC:
* - create copy C in to-space
* - destination C will be nursery|tenured depending on location of this.
* else
* - return this to disengage from GC
*
* Require: @ref mm is an instance of @ref gc::GC
**/
virtual Object * _shallow_copy() const = 0;
/** update child pointers that refer to forwarding pointers,
* replacing them with the correct destination.
* See @ref Object::deep_move
*
* this gray object, located in to-space.
* fwd1 forwarding objects.
* Located in from-space. Invalid at end of GC cycle.
* p1,p2 source pointers.
* D1,D2 already-forwarded objects. located in to-space.
*
* before:
* this fwd1
* +----+ +-+
* | p1 ----->|x|-------> D1
* | | +-+
* | |
* | p2 ----------------> D2
* +----+
*
* after:
* this
* +----+
* | p1 ----------------> D1
* | |
* | |
* | p2 ----------------> D2
* +----+
*
* this is now white
*
* @return shallow size of *this. Must exactly match the amount of memory in to-space
* allocated by @ref _shallow_move
*
**/
virtual std::size_t _forward_children() = 0;
};
/** @class Cpof
* @brief argument to operator new used for garbage collector evacuation phase
*
* Tag overloaded operator new to activate allocation policy based on location
* in memory of source object.
**/
class Cpof {
public:
explicit Cpof(const Object * src) : src_{src} {}
const void * src_ = nullptr;
};
} /*namespace xo*/
void * operator new (std::size_t z, const xo::Cpof & copy);
/* end Object.hpp */

View file

@ -0,0 +1,49 @@
/* Stack.hpp
*
* author: Roland Conybeare, jul 2025
*/
#pragma once
#include <vector>
namespace xo {
namespace gc {
/** Simple stack implementation
**/
template <typename T>
class Stack {
public:
explicit Stack(std::size_t capacity) {
this->contents_.reserve(capacity);
}
bool is_empty() const { return contents_.empty(); }
std::size_t available() const { return contents_.capacity() - contents_.size(); }
void drop() { contents_.resize(contents_.size() - 1); }
void push(const T & x) { contents_.push_back(x); }
T pop() {
T retval = contents_[contents_.size() - 1];
this->drop();
return retval;
}
const T & top() const {
return this->lookup(0);
}
const T & lookup(std::size_t i) const {
return contents_.at(contents_.size() - 1 - i);
}
void clear() { contents_.clear(); }
void reset_to(std::size_t z) { contents_.resize(z); }
std::size_t n_elements() const { return contents_.size(); }
std::size_t capacity() const { return contents_.capacity(); }
private:
std::vector<T> contents_;
};
} /*namespace gc*/
} /*namespace xo*/
/* end Stack.hpp */