+ xo-alloc + xo-object + xo-alloc docs + GC utests
This commit is contained in:
parent
8970f51dbd
commit
5f46b51f12
32 changed files with 2903 additions and 82 deletions
58
include/xo/alloc/AllocPolicy.hpp
Normal file
58
include/xo/alloc/AllocPolicy.hpp
Normal 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 */
|
||||
|
|
@ -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 */
|
||||
28
include/xo/alloc/Forwarding.hpp
Normal file
28
include/xo/alloc/Forwarding.hpp
Normal 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 */
|
||||
40
include/xo/alloc/Forwarding1.hpp
Normal file
40
include/xo/alloc/Forwarding1.hpp
Normal 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
310
include/xo/alloc/GC.hpp
Normal 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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
232
include/xo/alloc/Object.hpp
Normal 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 */
|
||||
49
include/xo/alloc/Stack.hpp
Normal file
49
include/xo/alloc/Stack.hpp
Normal 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 */
|
||||
Loading…
Add table
Add a link
Reference in a new issue