+ 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 e1d5ae46d2
58 changed files with 3948 additions and 83 deletions

View file

@ -69,6 +69,7 @@ set(DOX_EXCLUDE_PATTERNS [=[
# ----------------------------------------------------------------
# xo satellite projects
# in reverse topological order i.e. dependencies first
add_subdirectory(xo-cmake)
add_subdirectory(xo-indentlog)
@ -86,6 +87,7 @@ add_subdirectory(xo-unit)
add_subdirectory(xo-pyunit)
#
add_subdirectory(xo-alloc)
add_subdirectory(xo-object)
#
add_subdirectory(xo-callback)
add_subdirectory(xo-webutil)
@ -118,6 +120,6 @@ add_subdirectory(xo-pyjit)
# ----------------------------------------------------------------
# documentation. must follow add_subdirectory() for satellite projects
xo_umbrella_doxygen_deps(xo_flatstring xo_ratio xo_unit xo_tokenizer xo_reader xo_jit)
xo_umbrella_doxygen_deps(xo_alloc xo_flatstring xo_ratio xo_unit xo_tokenizer xo_reader xo_jit)
xo_umbrella_doxygen_config()
xo_umbrella_sphinx_config(index.rst docs/install.rst docs/glossary.rst)

View file

@ -206,6 +206,7 @@ pkgs.mkShell {
pkgs.catch2
pkgs.zlib
pkgs.unzip
pkgs.libbsd
pkgs.cmake
pkgs.pkg-config

View file

@ -12,6 +12,7 @@ Some features: kalman filters, stochastic processes, complex event processing, s
:caption: XO contents
docs/install
xo-alloc/docs/index
xo-indentlog/docs/index
xo-flatstring/docs/index
xo-ratio/docs/index

View file

@ -20,9 +20,13 @@ add_definitions(${PROJECT_CXX_FLAGS})
# must complete definition of expression lib before configuring examples
add_subdirectory(src/alloc)
add_subdirectory(utest)
xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets)
# ----------------------------------------------------------------
# docs targets depend on other library/utest/exec targets above,
# --> must come after them.
#
add_subdirectory(docs)
add_subdirectory(utest)
# end CmakeLists.txt

View file

@ -0,0 +1,9 @@
# xo-alloc/docs/CMakeLists.txt
xo_doxygen_collect_deps()
xo_docdir_doxygen_config()
xo_docdir_sphinx_config(
index.rst install.rst introduction.rst implementation.rst)
# see xo-reader/doc or xo-unit/doc for working examples
# example.rst install.rst implementation.rst

41
xo-alloc/docs/README Normal file
View file

@ -0,0 +1,41 @@
standalone build
+-----------------------------------------------+
| cmake |
| CMakeLists.txt |
| $PREFIX/share/cmake/xo_macros/xo_cxx.cmake |
+-----------------------------------------------+
|
| +----------------------+
+------------------------------------------------->| .build/docs/Doxyfile |
| +----------------------+
| ^
| (cmake) |
| /------------/
| |
| +---------------------------------------+ +-----------------+
+---->| doxygen |--------->| .build/docs/dox |
| | $PREFIX/share/xo-macros/Doxyfile.in | (doxygen)| +- html/ |
| +---------------------------------------+ | +- xml/ |
| +-----------------+
| |
| |(sphinx)
| |
| v
| +---------------------------------------+ +--------------------+
\---->| sphinx |------->| .build/docs/sphinx |
| +- conf.py | | +- html/ |
| +- _static/ | +--------------------+
| +- *.rst |
+---------------------------------------+
umbrella build relies on top-level cmake macros
files
README this file
CMakeLists.txt build entry point
conf.py sphinx config
_static static files for sphinx
index.rst toplevel sphinx document; entry point

1
xo-alloc/docs/_static/README vendored Normal file
View file

@ -0,0 +1 @@
add any static {.html, .js, ..} files for sphinx to pickup here

BIN
xo-alloc/docs/_static/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

39
xo-alloc/docs/conf.py Normal file
View file

@ -0,0 +1,39 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'xo alloc documentation'
copyright = '2025, Roland Conybeare'
author = 'Roland Conybeare'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
#extensions = []
extensions = [ "breathe",
"sphinx.ext.mathjax", # inline math
"sphinx.ext.autodoc", # generate info from docstrings
"sphinxcontrib.ditaa", # diagrams-through-ascii-art
"sphinxcontrib.plantuml" # text -> uml diagrams
]
# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in
# match project name in Doxyfile.in
breathe_default_project = "xodoxxml"
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
#html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_favicon = '_static/img/favicon.ico'

View file

@ -0,0 +1,202 @@
.. _implementation:
.. toctree::
:maxdepth: 2
Library
=======
Library dependency tower for *xo-alloc*:
.. ditaa::
+------------------------------------------+
| xo_alloc |
+------------------------------------------+
| xo_indentlog |
+------------------------------------------+
Install instructions :doc:`here<install>`
Components
==========
Abstraction tower for *xo-alloc* components:
.. ditaa::
:--scale: 0.85
+----------------+-------------+
| IAlloc | Object |
+----------------+-------------+
+-------------+ +-------------+
| GC | | Forwarding1 |
+-------------+ +-------------+
| ListAlloc |
+-------------+
| ArenaAlloc |
+-------------+
* *IAlloc*
Allocator interface.
* *Object*
Root Object Interface for types participating in garbage collection
* *GC*
Incremental compacting garbage collector.
* *ListAlloc*
Auto-expanding allocator. Contains a collection of ArenaAllocs
* *ArenaAlloc*
Arena allocator (a.k.a bump allocator).
* *Object*
Interface for types that participate in garbage collection
* *Forwarding1*
Forwarding pointer. Supports the Object interface;
used internally by GC during evacuation.
Key Points
----------
* Allocators can be reset, but do not support freeing of individual allocs.
* GC works with types that implement auxiliary GC-support methods.
Such types must inherit Object.
* A region may uses multiple arenas, but because of allocation activity
since the last GC. If necessary, GC will allocate a new to-space with a
single arena that's large enough to accomodate all objects that might survive
from a from-space that has acquired multiple arenas.
Intent is to scale up to find application's working set size, then stabilize
Components
==========
Allocators
----------
Inheritance
^^^^^^^^^^^
.. uml::
:caption: allocators
:scale: 99%
:align: center
class IAlloc {
+ alloc()
+ alloc_gc_copy()
+ checkpoint()
+ clear()
}
class ArenaAlloc {
+ free_ptr()
- lo_ : byte*
- checkpoint_ : byte*
- limit_ : byte*
}
IAlloc <|-- ArenaAlloc
class ListAlloc {
+ expand()
+ free_ptr()
- start_z_
- hd_
- full_l_
}
IAlloc <|-- ListAlloc
class GC {
+ add_gc_root()
+ request_gc()
+ gc_statistics()
- gc_root_v_[] : Object**
- nursery_[2] : ListAlloc*
- tenured_[2] : ListAlloc*
}
IAlloc <|-- GC
Composition
^^^^^^^^^^^
.. uml::
:caption: allocator composition
:scale: 99%
:align: center
object gc<<GC>>
gc : nursery[from] = n0
gc : nursery[to] = n1
gc : tenured[from] = t0
gc : tenured[to] = t1
object n0<<ListAlloc>>
object n1<<ListAlloc>>
object t0<<ListAlloc>>
object t1<<ListAlloc>>
gc o-- n0
gc o-- n1
gc o-- t0
gc o-- t1
Each ListAlloc composes like this:
.. uml::
:caption: ListAlloc composition
:scale: 99%
:align: center
object x<<ListAlloc>>
x : hd_ = a0
x : full_l = {a1, a2}
object a0<<ArenaAlloc>>
a0 : lo_ = 0
a0 : free_ = 12345
a0 : hi_ = 1000000
object a1<<ArenaAlloc>>
object a2<<ArenaAlloc>>
x o-- a0
x o-- a1
x o-- a2
Here *a1* and *a2* are full, while *a0* can still allocate memory.
Objects
.. uml::
:caption: objects
:scale: 99%
:align: center
class Object {
+ _is_forwarded()
+ _offset_destination()
+ _forward_to()
+ _destination()
+ _shallow_size()
+ _shallow_copy()
+ _forward_children()
}
class Forwarding1 {
- dest_ : Object*
}
Object <|-- Forwarding1

14
xo-alloc/docs/index.rst Normal file
View file

@ -0,0 +1,14 @@
# xo-alloc documentation master file
xo-alloc documentation
======================
xo-alloc provides arena allocators and a generation garbage collector
.. toctree::
:maxdepth: 2
:caption: xo-alloc contents
install
introduction
implementation

120
xo-alloc/docs/install.rst Normal file
View file

@ -0,0 +1,120 @@
.. _install:
.. toctree::
:maxdepth: 2
Source
======
Source code lives on github `here`_
.. _here: https://github.com/rconybea/xo-alloc
To clone from git:
.. code-block:: bash
git clone https://github.com/rconybea/xo-alloc
Tested with gcc 13.3
Install
=======
One-step Install
----------------
Install along with the reset of *XO* from `xo-umbrella2 source`_
.. _xo-umbrella2 source: https://github.com/rconybea/xo-umbrella2
Minimal Install
---------------
To build+install just required dependencies:
``xo-alloc`` uses several supporting libraries from the *XO* project:
- `xo-indentlog source`_ (structured logging)
- `xo-cmake source`_ (shared cmake macros)
.. _xo-indentlog source: https://github.com/rconybea/indentlog
.. _xo-cmake source: https://github.com/rconybea/xo-cmake
Building from source
--------------------
Install scripts for XO libraries depend on helper scripts installed from `xo-cmake`.
Preamble:
.. code-block:: bash
mkdir -p ~/proj/xo
cd ~/proj/xo
git clone https://github.com/rconybea/xo-cmake
PREFIX=/usr/local # ..or desired installation prefix
# want PREFIX/bin in PATH to use xo-cmake helpers
PATH=$PREFIX/bin:$PATH
Install `xo-cmake`:
.. code-block:: bash
cmake -B xo-cmake/.build -S xo-cmake
cmake --install xo-cmake/.build
Install remaining dependencie(s) in topological order:
.. code-block:: bash
xo-build --clone --configure --build --install xo-indentlog
xo-build --clone --configure --build --install xo-alloc
Directories under ``PREFIX`` will then contain:
.. code-block::
PREFIX
+- bin
| +- xo-build
| +- xo-cmake-config
| \- xo-cmake-lcov-harness
+- include
| \- xo
| +- alloc/
| \- indentlog/
+- lib
| +- cmake
| | +- xo_alloc/
| | \- indentlog/
| +- lib*.so
+- share
+- cmake
| \- xo_macros
| +- code-coverage.cmake
| +- xo-project-macros.cmake
| \- xo_cxx.cmake
+- etc
| \- xo
| \- subsystem-list
\- xo-macros
+- Doxyfile.in
+- gen-ccov.in
\- xo-bootstrap-macros.cmake
CMake Support
-------------
To use built-in cmake support, when using ``xo-alloc`` from another project:
Make sure ``PREFIX/lib/cmake`` is searched by cmake (for example include it in ``CMAKE_PREFIX_PATH``)
Add to your ``CMakeLists.txt``:
.. code-block:: cmake
FindPackage(xo_alloc CONFIG REQUIRED)
target_link_libraries(mytarget INTERFACE xo_alloc)

View file

@ -0,0 +1,268 @@
.. _introduction:
.. toctree
:maxdepth: 2
Introduction
============
The ``xo-alloc`` library provides a in incremental, generational collector for c++ code.
Features:
* *incremental* - can reasonably expect short pause times.
* *generational* - focuses effort on collecting young objects,
on the basis that they're more likely to be garbage.
* *compacting* - each garbage collection cycle evacuates survivors to contiguous memory,
so effect is to defragment.
* *collects cycles* - collection algorithm naturally collects cyclic references
Tradeoffs:
* Application is responsible for spilling register values and protecting hardware stack,
since garbage collector cannot indepndently distinguish collectable object pointers from
non-pointer values.
* GC will not spontaneously run without permission. Instead will set a pending bit, with GC
occurring only when application releases it (e.g. when stack+registers are known to be empty of values
subject to GC).
* GC implementation is single-threaded. It cannot run in parallel with the mutator (i.e. application code)
In return this allows GC to be only lightly coupled with application.
* GC divides each generation into separate from- and to- spaces. A collection cycle copies surviving
objects out of from-space. Once complete, the entire from-space is treated as empty, and available to
become to-space on a future cycle. This means that at any time only half of allocated memory is available
to the application; the rest is waiting to receive survivors from the next GC cycle.
Design
------
Garbage Collector
^^^^^^^^^^^^^^^^^
The garbage collector supports two generations, labelled *nursery* and *tenured*.
Nursery objects that survive two collection cycles are promoted to tenured space.
Nursery and tenured objects are kept in separate memory areas, instead of being interspersed.
Collection cycles come in two flavors:
1. *incremental* collections - these collect only the nursery space.
2. *full* collections - these collect both nursery and tenured spaces.
Full collection may incur noticeable GC pauses.
Application Interaction
^^^^^^^^^^^^^^^^^^^^^^^
Application code that interacts with GC has several responsibilities.
1. application must explicitly invoke GC, when convenient. Since in general any GC-eligible object
may get moved by the collector: once a collection cycle completes,
it's up to the application to re-load pointers from memory addresses
(GC roots) that have been shared with the collector.
2. application must identify a set of GC roots. GC preserves everything reachable from any GC root
3. The collector needs to know how to traverse GC-managed objects.
We teach it this by requiring that such objects inherit the ``xo::Object`` interface,
and implement auxiliary function detailed below.
4. GC also needs to know when a mutation alters a pointer from one GC-managed object to another.
In particular, GC needs to track pointers from tenured space into nursery space,
and update them when an incremental collection moves nursery objects.
We do this by requiring application code use a GC-provided assignment primitive
on GC-eligible pointers.
Example GC Use
--------------
.. code-block:: cpp
:linenos:
#include "xo/object/List.hpp" // polymorphic List with GC support
#include "xo/object/String.hpp" // string type with GC support
#include "xo/alloc/GC.hpp"
int main() {
using xo::gc::Config;
using xo::obj::String;
using xo::obj::List;
using xo::gp;
Config config = { .initial_nursery_z_ = 50*1000,
.initial_tenured_z_ = 10*1000*1000,
.debug_flag_ = false };
up<GC> gc = GC::make(config);
Object::mm = gc; // use GC for allocation of Object (+ derived classes)
gc->disable_gc(); // gc forbidden
// tiny example data structure
gp<String> s1 = String::copy("hello");
gp<String> s2 = String::copy(", ");
gp<String> s3 = String::copy("world!");
gp<List> list = List::cons(s1, List::cons(s2, List::cons(s3, List::nil)));
// tell GC what to preserve
gc->add_gc_root(reinterpret_cast<Object **>(list.ptr_address());
gc->enable_gc(); // triggers immediate gc
// s1, s2, s3 invalid.
// list at new address
std::cout << "list.size=" << list->size << std::endl;
}
GC-Eligible Types
-----------------
Or, how to inherit ``xo::Object`` and provide GC support
A type Foo that inherits ``xo::Object`` needs to provide overrides for Object methods ``_shallow_size()``,
``_shallow_copy()`` and ``_forward_children()``:
Typical Pattern
^^^^^^^^^^^^^^^
GC support methods look something like this:
* class definition
.. code-block:: cpp
:linenos:
#include "xo/alloc/Object.hpp"
namespace xo {
class Foo : public xo::Object {
public:
...
virtual std::size_t _shallow_size() const override;
virtual Object * _shallow_copy() const override;
virtual std::size_t _forward_children() override;
};
}
* use overloaded ``operator new``
A GC-eligible class will allocate instances using the ``MMPtr`` overload.
This allocates memory in GC-owned space
.. code-block::cpp
:linenos:
gp<Foo> Foo::make(...) {
...
return new MMPtr(mm) Foo(...);
}
* ``_shallow_size()`` returns the amount of memory used by the subject:
.. code-block:: cpp
:linenos:
std::size_t Foo::_shallow_size() const { return sizeof(Foo); }
* ``_shallow_copy()`` is invoked during GC to create a copy of the subject
It should use the ``xo::Cpof`` argument to ``operator new``.
.. code-block:: cpp
:linenos:
Object *
Foo::_shallow_copy() const;
* ``_forward_children()`` is invoked during GC to vist child ``xo::Object`` pointers
to make sure they survive
.. code-block:: cpp
:linenos:
std::size_t
Foo::_forward_children();
Atomic Types Without Object Pointers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Plain-old-data classes without embedded pointers
.. code-block:: cpp
:linenos:
Object *
Foo::_shallow_copy() const {
return new (Cpof(this)) Foo(*this);
}
.. code-block:: cpp
:linenos:
std::size_t
Foo::_forward_children() { return Foo::_shallow_size(); }
For example see ``xo::obj::String`` in ``xo-object``
Non-GC Objects
^^^^^^^^^^^^^^
A class *Foo* that inherits ``xo::Object`` can opt-out of garbage collection by
omitting the ``MMptr(mm)`` overload.
In that case `Foo::_shallow_size()`, `Foo::_shallow_copy()` and `Foo::_forward_children()`
will not be called:
.. code-block:: cpp
:linenos:
std::size_t Foo::_shallow_size() const { return sizeof(Foo); }
Object * Foo::_shallow_copy() const { assert(false); return nullptr; }
std::size_t Foo::_forward_children() { assert(false); return 0; }
For example see ``xo::obj::Boolean`` in ``xo-object``
Structs Containing Object Pointers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A class with object pointers needs to tell GC how to traverse them
.. code-block:: cpp
:linenos:
#include "xo/alloc/Object.hpp"
namespace xo {
class Foo : public xo::Object {
public:
...
virtual std::size_t _shallow_size() const override;
virtual Object * _shallow_copy() const override;
virtual std::size_t _forward_children() override;
private:
gp<Object> bar_;
gp<Object> quux_;
};
}
* ``_forward_children()`` is invoked during GC to fixup child pointers
that refer to forwarding objects:
.. code-block:: cpp
:linenos:
std::size_t
Foo::_forward_children()
{
Object::_forward_inplace(bar_);
Object::_forward_inplace(quux_);
return Foo::_shallow_size();
}
For example see ``xo::obj::List`` in ``xo-object``

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 */

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 */

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 */

View file

@ -0,0 +1,13 @@
/* AllocPolicy.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "AllocPolicy.hpp"
/* note: inline/.hpp definition not allowed for operator delete */
void operator delete(void * ptr) noexcept {
xo::xo.free(ptr);
}
/* end AllocPolicy.cpp */

View file

@ -1,29 +1,33 @@
/* file LinearAlloc.cpp
/* file ArenaAlloc.cpp
*
* author: Roland Conybeare
*/
#include "LinearAlloc.hpp"
#include "ArenaAlloc.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/tag.hpp"
#include <cassert>
namespace xo {
namespace gc {
LinearAlloc::LinearAlloc(std::size_t rz, std::size_t z)
ArenaAlloc::ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag)
{
this->lo_ = (new std::uint8_t [rz + z]);
this->name_ = name;
this->lo_ = (new std::byte [rz + z]);
this->checkpoint_ = lo_;
this->free_ptr_ = lo_;
this->limit_ = lo_ + z;
this->redline_z_ = rz;
this->hi_ = limit_ + rz;
this->debug_flag_ = debug_flag;
if (!lo_) {
throw std::runtime_error(tostr("LinearAlloc: allocation failed",
throw std::runtime_error(tostr("ArenaAlloc: allocation failed",
xtag("size", rz + z)));
}
}
LinearAlloc::~LinearAlloc()
ArenaAlloc::~ArenaAlloc()
{
delete [] this->lo_;
@ -33,17 +37,19 @@ namespace xo {
this->checkpoint_ = nullptr;
this->free_ptr_ = nullptr;
this->limit_ = nullptr;
this->redline_z_ = 0;
this->hi_ = nullptr;
this->debug_flag_ = false;
}
up<LinearAlloc>
LinearAlloc::make(std::size_t rz, std::size_t z)
up<ArenaAlloc>
ArenaAlloc::make(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag)
{
return up<LinearAlloc>(new LinearAlloc(rz, z));
return up<ArenaAlloc>(new ArenaAlloc(name, rz, z, debug_flag));
}
void
LinearAlloc::set_free_ptr(std::uint8_t * x)
ArenaAlloc::set_free_ptr(std::byte * x)
{
assert(lo_ <= x);
assert(x < limit_);
@ -57,70 +63,79 @@ namespace xo {
}
std::size_t
LinearAlloc::size() const {
ArenaAlloc::size() const {
return limit_ - lo_;
}
std::size_t
LinearAlloc::available() const {
ArenaAlloc::available() const {
return limit_ - free_ptr_;
}
std::size_t
LinearAlloc::allocated() const {
ArenaAlloc::allocated() const {
return free_ptr_ - lo_;
}
bool
LinearAlloc::is_before_checkpoint(const std::uint8_t * x) const {
ArenaAlloc::contains(const void * x) const {
return (lo_ <= x) && (x < hi_);
}
bool
ArenaAlloc::is_before_checkpoint(const void * x) const {
return (lo_ <= x) && (x < checkpoint_);
}
std::size_t
LinearAlloc::before_checkpoint() const
ArenaAlloc::before_checkpoint() const
{
return checkpoint_ - lo_;
}
std::size_t
LinearAlloc::after_checkpoint() const
ArenaAlloc::after_checkpoint() const
{
return free_ptr_ - checkpoint_;
}
void
LinearAlloc::clear()
ArenaAlloc::clear()
{
this->checkpoint_ = lo_;
this->free_ptr_ = lo_;
this->limit_ = lo_;
this->limit_ = hi_ - redline_z_;
}
void
LinearAlloc::checkpoint()
ArenaAlloc::checkpoint()
{
this->checkpoint_ = this->free_ptr_;
}
std::uint8_t *
LinearAlloc::alloc(std::size_t z)
std::byte *
ArenaAlloc::alloc(std::size_t z0)
{
scope log(XO_DEBUG(debug_flag_));
/* word size for alignment */
constexpr uint32_t c_bpw = sizeof(void*);
constexpr uint32_t c_bpw = sizeof(std::uintptr_t);
std::uintptr_t free_u64 = reinterpret_cast<std::uintptr_t>(free_ptr_);
assert(free_u64 % c_bpw == 0ul);
/* round up to multiple of c_bpw */
std::uint32_t dz = (c_bpw - (z % c_bpw));
z += dz;
std::uint32_t dz = alloc_padding(z0);
assert(z % c_bpw == 0ul);
std::size_t z1 = z0 + dz;
std::uint8_t * retval = this->free_ptr_;
assert(z1 % c_bpw == 0ul);
this->free_ptr_ += z;
std::byte * retval = this->free_ptr_;
this->free_ptr_ += z1;
log && log(xtag("self", name_), xtag("z0", z0), xtag("+pad", dz), xtag("z1", z1));
if (free_ptr_ > limit_) {
return nullptr;
@ -128,8 +143,14 @@ namespace xo {
return retval;
}
void
ArenaAlloc::release_redline_memory() {
this->limit_ = this->hi_;
}
} /*namespace gc*/
} /*namespace xo*/
/* end LinearAlloc.cpp */
/* end ArenaAlloc.cpp */

View file

@ -2,7 +2,12 @@
set(SELF_LIB xo_alloc)
set(SELF_SRCS
LinearAlloc.cpp
IAlloc.cpp
ArenaAlloc.cpp
ListAlloc.cpp
GC.cpp
Object.cpp
Forwarding1.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})

View file

@ -0,0 +1,45 @@
/* file Forwarding1.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Forwarding1.hpp"
#include <cstddef>
#include <cassert>
namespace xo {
namespace obj {
Forwarding1::Forwarding1(gp<Object> dest)
: dest_{dest}
{}
Object *
Forwarding1::_offset_destination(Object * src) const
{
intptr_t offset = src - static_cast<const Object *>(this);
return dest_.ptr() + offset;
}
std::size_t
Forwarding1::_shallow_size() const {
assert(false);
return 0;
}
Object *
Forwarding1::_shallow_copy() const {
assert(false);
return nullptr;
}
std::size_t
Forwarding1::_forward_children() {
assert(false);
return 0;
}
} /*namespace obj*/
} /*namespace xo*/
/* end Forwarding1.cpp */

492
xo-alloc/src/alloc/GC.cpp Normal file
View file

@ -0,0 +1,492 @@
/* GC.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "GC.hpp"
#include "Object.hpp"
#include "xo/indentlog/scope.hpp"
#include <cassert>
#include <cstddef>
namespace xo {
namespace gc {
void
PerGenerationStatistics::include_gc(std::size_t alloc_z,
std::size_t before_z,
std::size_t after_z,
std::size_t promote_z)
{
this->update_snapshot(after_z);
new_alloc_z_ += alloc_z;
scanned_z_ += before_z;
survive_z_ += after_z;
promote_z_ += promote_z;
}
void
PerGenerationStatistics::update_snapshot(std::size_t after_z)
{
used_z_ = after_z;
}
void
PerGenerationStatistics::display(std::ostream & os) const
{
os << "<PerGenerationStatistics"
<< xtag("used", used_z_)
<< xtag("n_gc", n_gc_)
<< xtag("new_alloc_z", new_alloc_z_)
<< xtag("scanned_z", scanned_z_)
<< xtag("survive_z", survive_z_)
<< xtag("promote_z", promote_z_)
<< ">";
}
void
GcStatistics::include_gc(generation upto,
std::size_t alloc_z,
std::size_t before_z,
std::size_t after_z,
std::size_t promote_z)
{
gen_v_[static_cast<std::size_t>(upto)].include_gc(alloc_z, before_z, after_z, promote_z);
}
void
GcStatistics::update_snapshot(generation upto,
std::size_t after_z)
{
gen_v_[static_cast<std::size_t>(upto)].update_snapshot(after_z);
}
void
GcStatistics::display(std::ostream & os) const
{
os << "<GcStatistics"
<< xtag("gen_v", gen_v_)
<< xtag("total_allocated", total_allocated_)
// << xtag("per_type_stats", per_type_stats_)
<< ">";
}
GC::GC(const Config & config)
: config_{config}
{
enum { NurseryFrom, NurseryTo, TenuredFrom, TenuredTo };
std::size_t nursery_size = config.initial_nursery_z_;
std::size_t tenured_size = config.initial_tenured_z_;
nursery_[role2int(role::from_space)]
= ListAlloc::make("NA", nursery_size, 2 * nursery_size, config.debug_flag_);
nursery_[role2int(role::to_space) ]
= ListAlloc::make("NB", nursery_size, 2 * nursery_size, config.debug_flag_);
tenured_[role2int(role::from_space)]
= ListAlloc::make("TA", tenured_size, 2 * tenured_size, config.debug_flag_);
tenured_[role2int(role::to_space) ]
= ListAlloc::make("TB", tenured_size, 2 * tenured_size, config.debug_flag_);
this->checkpoint();
}
up<GC>
GC::make(const Config & config)
{
GC * gc = new GC(config);
return up<GC>{gc};
}
std::size_t
GC::size() const
{
return nursery_[role2int(role::to_space)]->size() + tenured_[role2int(role::to_space)]->size();
}
std::size_t
GC::allocated() const
{
return (nursery_[role2int(role::to_space)]->allocated()
+ tenured_[role2int(role::to_space)]->allocated());
}
std::size_t
GC::available() const
{
return nursery_[role2int(role::to_space)]->available();
}
bool
GC::fromspace_contains(const void * x) const
{
return (nursery_[role2int(role::from_space)]->contains(x)
|| tenured_[role2int(role::from_space)]->contains(x));
}
bool
GC::contains(const void * x) const
{
return (nursery_[role2int(role::to_space)]->contains(x)
|| tenured_[role2int(role::to_space)]->contains(x));
}
bool
GC::is_before_checkpoint(const void * x) const
{
return nursery_[role2int(role::to_space)]->is_before_checkpoint(x);
}
std::size_t
GC::before_checkpoint() const
{
return nursery_[role2int(role::to_space)]->before_checkpoint();
}
std::size_t
GC::after_checkpoint() const
{
return nursery_[role2int(role::to_space)]->after_checkpoint();
}
generation
GC::fromspace_generation_of(const void * x) const
{
if (tenured_[role2int(role::from_space)]->contains(x))
return generation::tenured;
return generation::nursery;
}
generation
GC::generation_of(const void * x) const
{
if (tenured_[role2int(role::to_space)]->contains(x))
return generation::tenured;
return generation::nursery;
}
std::byte *
GC::free_ptr(generation gen)
{
switch(gen) {
case generation::nursery:
return nursery_[role2int(role::to_space)]->free_ptr();
case generation::tenured:
return tenured_[role2int(role::to_space)]->free_ptr();
case generation::N:
assert(false);
}
return nullptr;
}
void
GC::clear()
{
nursery_[role2int(role::from_space)]->clear();
nursery_[role2int(role::to_space) ]->clear();
tenured_[role2int(role::from_space)]->clear();
tenured_[role2int(role::to_space) ]->clear();
}
void
GC::add_gc_root(Object ** addr)
{
gc_root_v_.push_back(addr);
}
void
GC::checkpoint()
{
nursery_[role2int(role::to_space) ]->checkpoint();
}
std::byte *
GC::alloc(std::size_t z)
{
std::byte * x = nursery_[role2int(role::to_space)]->alloc(z);
if (!x) {
this->request_gc(generation::nursery);
if (incr_gc_pending_ || full_gc_pending_)
nursery_[role2int(role::to_space)]->release_redline_memory();
/* try (just once) more, maybe request fits in redline space */
x = nursery_[role2int(role::to_space)]->alloc(z);
assert(x);
}
return x;
}
std::byte *
GC::alloc_gc_copy(std::size_t z, const void * src)
{
scope log(XO_DEBUG(config_.debug_flag_), xtag("z", z), xtag("+pad", IAlloc::alloc_padding(z)));
generation g = this->fromspace_generation_of(src);
std::byte * retval = nullptr;
if (g == generation::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);
}
}
assert(retval);
return retval;
}
void
GC::release_redline_memory()
{
// not supported feature for GC
}
void
GC::swap_nursery()
{
up<ListAlloc> tmp = std::move(nursery_[role2int(role::to_space)]);
nursery_[role2int(role::to_space)] = std::move(nursery_[role2int(role::from_space)]);
nursery_[role2int(role::from_space)] = std::move(tmp);
}
void
GC::swap_tenured()
{
up<ListAlloc> tmp = std::move(tenured_[role2int(role::to_space)]);
tenured_[role2int(role::to_space)] = std::move(tenured_[role2int(role::from_space)]);
tenured_[role2int(role::from_space)] = std::move(tmp);
}
void
GC::swap_spaces(generation target)
{
// will be copying into storage currently labelled FromSpace
/* gc will copy some to-be-determined amount in [0..promote_z]
from nursery->tenured generation.
*/
std::size_t promote_z = nursery_[role2int(role::to_space)]->before_checkpoint();
if (target == generation::tenured) {
/* gc on tenured generation may need this much space */
std::size_t tenured_z = (tenured_[role2int(role::to_space)]->allocated()
+ promote_z
+ full_gc_threshold_);
tenured_[role2int(role::from_space)]->reset(tenured_z);
this->swap_tenured();
} else {
if (tenured_[role2int(role::to_space)]->available() < promote_z) {
tenured_[role2int(role::to_space)]->expand(promote_z);
}
}
nursery_[role2int(role::from_space)]->reset(nursery_[role2int(role::to_space)]->allocated()
- promote_z
+ incr_gc_threshold_);
this->swap_nursery();
} /*swap_spaces*/
void
GC::copy_object(Object ** pp_object, generation upto, ObjectStatistics * object_stats)
{
void * object_address = *pp_object;
if (nursery_[role2int(role::to_space)]->contains(object_address)
|| ((upto == generation::tenured)
&& tenured_[role2int(role::to_space)]->contains(object_address)))
{
/* global is already in to-space */
;
} else if((upto == generation::nursery) && tenured_[role2int(role::to_space)]->contains(object_address))
{
/* skip tenured objects when incremental collection */
;
} else {
*pp_object = Object::_deep_move(*pp_object, this, object_stats);
}
}
void
GC::copy_globals(generation upto)
{
for (Object ** pp_root : gc_root_v_) {
this->copy_object(pp_root, upto, &gc_statistics_.per_type_stats_);
}
}
void
GC::cleanup_phase(generation upto)
{
scope log(XO_DEBUG(config_.debug_flag_));
std::size_t N_allocated = nursery_[role2int(role::from_space)]->after_checkpoint();
std::size_t T_allocated = tenured_[role2int(role::from_space)]->after_checkpoint();
std::size_t N_before_gc = nursery_[role2int(role::from_space)]->allocated();
std::size_t T_before_gc = tenured_[role2int(role::from_space)]->allocated();
std::size_t N_after_gc = nursery_[role2int(role::to_space)]->allocated();
std::size_t T_after_gc = tenured_[role2int(role::to_space)]->allocated();
//std::byte * N_free_ptr = nursery_[role2int(role::to_space)]->free_ptr();
std::size_t promote_z = gc_statistics_.total_promoted_ - gc_statistics_.total_promoted_sab_;
this->nursery_[role2int(role::from_space)]->reset(0);
this->tenured_[role2int(role::from_space)]->reset(0);
/* objects currenty in to-space nursery have survived one collection */
this->nursery_[role2int(role::to_space)]->checkpoint();
// nursery_[role2int(role::to_space)]->set_redline(nursery_[role2int(role::to_space)]->allocated() + incr_gc_threshold_)
if (upto == generation::tenured)
this->tenured_[role2int(role::to_space)]->checkpoint();
if (log) {
log(xtag("N_allocated", N_allocated));
log(xtag("N_before_gc", N_before_gc));
log(xtag("N_after_gc", N_after_gc));
log(xtag("T_allocated", T_allocated));
log(xtag("T_before_gc", T_before_gc));
log(xtag("T_after_gc", T_after_gc));
}
this->incr_gc_pending_ = false;
this->gc_statistics_.include_gc(generation::nursery, N_allocated, N_before_gc, N_after_gc, promote_z);
if (upto == generation::tenured) {
this->full_gc_pending_ = false;
this->gc_statistics_.include_gc(generation::tenured, T_allocated, T_before_gc, T_after_gc, 0);
} else {
// still want to update tenured stats for current alloc size
this->gc_statistics_.update_snapshot(generation::tenured, T_after_gc);
}
}
void
GC::execute_gc(generation target)
{
scope log(XO_DEBUG(config_.debug_flag_));
bool full_move = (target == generation::tenured);
// TODO: RAII version in case of exceptions
this->runstate_ = GCRunstate(true /*in_progress*/, full_move);
log && log("step 0: snapshot alloc stats");
/* new allocation since last GC */
std::size_t new_alloc = this->after_checkpoint();
++(gc_statistics_.gen_v_[static_cast<std::size_t>(target)].n_gc_);
gc_statistics_.total_allocated_ += new_alloc;
gc_statistics_.total_promoted_sab_ = gc_statistics_.total_promoted_;
log && log(xtag("new_alloc", new_alloc));
log && log("step 1: swap to/from roles");
this->swap_spaces(target);
log && log("step 2a: copy globals");
this->copy_globals(target);
log && log("step 2b: TODO: copy pinned");
log && log("step 3: TODO: forward mutation log");
log && log("step 4: TODO: notify destructor log");
log && log("step 5: TODO: keep reachable weak pointers");
log && log("step 6: cleanup");
this->cleanup_phase(target);
this->runstate_ = GCRunstate();
log && log("statistics:");
log && log(gc_statistics_);
}
void
GC::request_gc(generation target)
{
if (!runstate_.in_progress() && (gc_enabled_ == 0)) {
if (!config_.allow_incremental_gc_)
target = generation::tenured;
if ((target == generation::nursery)
&& (tenured_[role2int(role::to_space)]->after_checkpoint() > full_gc_threshold_))
{
/** full collection when >= @ref full_gc_threshold_ bytes added to tenured
* generation, since last full collection
**/
target = generation::tenured;
}
this->execute_gc(target);
} else {
this->incr_gc_pending_ = true;
if (target == generation::tenured)
this->full_gc_pending_ = true;
}
}
void
GC::disable_gc() {
--gc_enabled_;
}
void
GC::enable_gc() {
++gc_enabled_;
if (gc_enabled_ == 0) {
/* unblock gc */
if (incr_gc_pending_)
this->request_gc(full_gc_pending_ ? generation::tenured : generation::nursery);
}
}
} /*namespace gc*/
} /*namespace xo*/
/* end GC.cpp */

View file

@ -0,0 +1,54 @@
/* @file IAlloc.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "IAlloc.hpp"
#include <cassert>
#include <cstddef>
namespace xo {
namespace gc {
std::uint32_t
IAlloc::alloc_padding(std::size_t z)
{
/* word size for alignment */
constexpr uint32_t c_bpw = sizeof(std::uintptr_t);
/* round up to multiple of c_bpw, but map 0 -> 0
* (table assuming c_bpw==8)
*
* z%c_bpw dz
* ------------
* 0 0
* 1 7
* 2 6
* .. ..
* 7 1
*/
std::uint32_t dz = (c_bpw - (z % c_bpw)) % c_bpw;
z += dz;
assert(z % c_bpw == 0ul);
return dz;
}
std::size_t
IAlloc::with_padding(std::size_t z)
{
return z + alloc_padding(z);
}
std::byte *
IAlloc::alloc_gc_copy(std::size_t /*z*/, const void * /*src*/)
{
assert(false);
return nullptr;
}
} /*namespace gc*/
} /*namespace xo*/
/* end IAlloc.cpp */

View file

@ -0,0 +1,318 @@
/* file ListAlloc.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "ListAlloc.hpp"
#include "ArenaAlloc.hpp"
#include <cassert>
#include <cstddef>
namespace xo {
namespace gc {
ListAlloc::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)
: start_z_{cz},
hd_{std::move(hd)},
marked_{marked},
full_l_{},
current_z_{cz},
next_z_{nz},
total_z_{tz},
use_redline_{use_redline},
debug_flag_{debug_flag}
{}
ListAlloc::~ListAlloc()
{
this->clear();
}
up<ListAlloc>
ListAlloc::make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag)
{
std::unique_ptr<ArenaAlloc> hd{ArenaAlloc::make(name, 0, cz, debug_flag)};
if (!hd)
return nullptr;
ArenaAlloc * marked = nullptr;
up<ListAlloc> retval{new ListAlloc(std::move(hd),
marked,
cz, nz, cz,
false /*!use_redline*/,
debug_flag)};
return retval;
}
std::size_t
ListAlloc::size() const {
return total_z_;
}
std::byte *
ListAlloc::free_ptr() const {
return hd_->free_ptr();
}
std::size_t
ListAlloc::available() const {
if (hd_) {
/* can only allocate from @ref hd_,
* so even if there were available space in @ref full_l_,
* it's not accessible to ListAlloc.
*/
return hd_->available();
}
return 0;
}
std::size_t
ListAlloc::allocated() const {
std::size_t total = 0;
if (hd_)
total += hd_->allocated();
for (const auto & alloc : full_l_)
total += alloc->allocated();
return total;
}
bool
ListAlloc::contains(const void * x) const {
if (hd_ && hd_->contains(x))
return true;
for (const auto & alloc : full_l_) {
if (alloc->contains(x))
return true;
}
return false;
}
bool
ListAlloc::is_before_checkpoint(const void * x) const {
if (!marked_)
return false;
if ((marked_ == hd_.get()) && hd_->contains(x))
return hd_->is_before_checkpoint(x);
/*
* 1. allocs in full_l_ appear in youngest-to-oldest order
* 2. allocators that appear before marked_ in full_l_ count as 'after checkpoint'
* 3. allocators that appear after marked_ in full_l_ count as 'before checkpoint'
*/
bool younger_than_marked = true;
for (const auto & alloc : full_l_) {
if (younger_than_marked) {
if (alloc.get() == marked_) {
/* nothing else to test on this iteration,
* already checked .marked_ specifically
*/
younger_than_marked = false;
} else {
/* after checkpoint */
if (alloc->contains(x))
return false;
}
} else {
if (alloc->contains(x))
return true;
}
}
return false;
}
std::size_t
ListAlloc::before_checkpoint() const
{
if (marked_) {
if (full_l_.empty()) {
assert(marked_ == hd_.get());
return marked_->before_checkpoint();
}
} else {
/* count everything allocated */
return this->allocated();
}
std::size_t z = 0;
/* control here: .marked & .full_l non-empty. */
if (hd_.get() == marked_) {
z += hd_->before_checkpoint();
/* anything in .full_l older than marked .hd */
for (const auto & alloc : full_l_) {
z += alloc->allocated();
}
return z;
} else {
/* messiest case: .marked is true,
* and not the youngest arena
*/
bool younger_than_marked = true;
for (const auto & alloc : full_l_) {
if (younger_than_marked) {
if (alloc.get() == marked_) {
younger_than_marked = false;
z += marked_->before_checkpoint();
} else {
;
}
} else {
z += alloc->allocated();
}
}
}
return z;
}
std::size_t
ListAlloc::after_checkpoint() const
{
if (!marked_)
return 0;
if (full_l_.empty()) {
assert(marked_ == hd_.get());
return marked_->after_checkpoint();
}
bool younger_than_marked = true;
std::size_t z = 0;
for (const auto & alloc : full_l_) {
if (younger_than_marked) {
if (alloc.get() == marked_) {
younger_than_marked = false;
z += marked_->after_checkpoint();
break;
} else {
z += alloc->allocated();
}
}
}
return z;
}
void
ListAlloc::clear() {
// general hygiene
start_z_ = 0;
hd_.reset();
marked_ = nullptr;
full_l_.clear();
current_z_ = 0;
next_z_ = 0;
total_z_ = 0;
use_redline_ = false;
}
bool
ListAlloc::reset(std::size_t z)
{
// warning: hd_->size() does not include redline memory
hd_->release_redline_memory();
bool recycle_head_bucket = hd_ && (z <= hd_->size());
this->full_l_.clear();
this->marked_ = nullptr;
this->redlined_flag_ = false;
if (recycle_head_bucket) {
this->hd_->clear();
this->total_z_ = hd_->size();
return true;
} else {
this->hd_.reset(nullptr);
this->total_z_ = 0;
return this->expand(z);
}
}
bool
ListAlloc::expand(std::size_t z)
{
std::size_t cz = current_z_;
std::size_t nz = next_z_;
std::size_t tz;
do {
tz = cz + nz;
cz = nz;
nz = tz;
} while (cz < z);
std::string name = hd_->name() + "+exp";
std::unique_ptr<ArenaAlloc> new_alloc = ArenaAlloc::make(name, 0, cz, debug_flag_);
if (!new_alloc)
return false;
this->current_z_ = cz;
this->next_z_ = nz;
this->total_z_ += cz;
this->hd_ = std::move(new_alloc);
return true;
}
void
ListAlloc::checkpoint() {
hd_->checkpoint();
this->marked_ = hd_.get();
}
std::byte *
ListAlloc::alloc(std::size_t z) {
std::byte * retval = hd_->alloc(z);
if (retval)
return retval;
if (this->expand(z))
return hd_->alloc(z);
return nullptr;
}
void
ListAlloc::release_redline_memory()
{
if (use_redline_)
redlined_flag_ = true;
this->hd_->release_redline_memory();
}
} /*namespace gc*/
} /*namespace xo*/
/* end ListAlloc.cpp */

View file

@ -0,0 +1,196 @@
/* Object.cpp
*
* author: Roalnd Conybeare, Jul 2025
*/
#include "Object.hpp"
#include "GC.hpp"
#include "Forwarding1.hpp"
using xo::obj::Forwarding1;
void *
operator new (std::size_t z, const xo::Cpof & cpof)
{
using xo::gc::GC;
GC * gc = reinterpret_cast<GC *>(xo::Object::mm);
return gc->alloc_gc_copy(z, cpof.src_);
}
namespace xo {
gc::IAlloc *
Object::mm = nullptr;
Object *
Object::_forward(Object * src, gc::GC * gc)
{
if (!src)
return src;
if (src->_is_forwarded())
return src->_offset_destination(src);
bool full_move = gc->runstate().full_move();
if (!full_move && (gc->generation_of(src) == gc::generation::tenured)) {
/* don't move tenured objects during incremental collection */
return src;
}
Object::_shallow_move(src, gc);
/* *src is now a forwarding pointer to copy in to-space */
return src->_offset_destination(src);
}
Object *
Object::_deep_move(Object * from_src, gc::GC * gc, gc::ObjectStatistics * /*stats*/)
{
using gc::generation;
if (!from_src)
return nullptr;
Object * retval = from_src->_destination();
if (retval)
return retval;
bool full_move = gc->runstate().full_move();
if (!full_move && gc->generation_of(from_src) == generation::tenured) {
/** incremental collection does not move already-tenured objects **/
return from_src;
}
/**
* To-space:
*
* to_lo = start of to-space
* w,W = white objects. An object x is white if x + all immediate children of x are in to-space
* (also implies this GC cycle put it there)
* g,G = grey objects. An object x is gray if it's in to-space,
* but possibly has >0 black children
* _ = free to-space memory
* N = nursery space
* T = tenured space
*
* wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________...
* ^ ^ ^
* to_lo grey_lo(N) free_ptr(N)
*
* After moving children of one object, advancing {nursery_grey_lo, nursery_free_ptr}
*
* wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______...
* ^ ^ ^
* to_lo grey_lo(N) free_ptr(N)
*
* Invariant:
*
* objects in [to_lo, gray_lo) are white.
* all gray objects are in [gray_lo, free_ptr)
* memory starting at free_ptr is free.
*
* deep_move terminates when gray_lo catches up to free_ptr
*
* Above is simplified. Complication is that GC (including incremental) may
* promote objects from nursery (N) to tenured (T)
*
* So more accurate before/after picture
*
* N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________...
* ^ ^ ^
* to_lo(N) grey_lo(N) free_ptr(N)
*
* T wwwwwwwwwwwwwwgggggggggggg_______________________________...
* ^ ^ ^
* to_lo(T) grey_lo(T) free_ptr(N)
*
* After moving children of one object, advancing {nursery_grey_lo, nursery_free_ptr}
*
* N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____...
* ^ ^ ^
* to_lo(N) grey_lo(N) free_ptr(N)
*
* T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________...
* ^ ^ ^
* to_lo(T) grey_lo(T) free_ptr(T)
*
* deep_move terminates when both:
* - gray_lo(N) catches up with free_ptr(N)
* - gray_lo(T) catches up with free_ptr(T)
*
**/
std::array<std::byte *, gen2int(generation::N)> gray_lo_v
= { gc->free_ptr(generation::nursery), gc->free_ptr(generation::tenured) };
Object * to_src = Object::_shallow_move(from_src, gc);
std::size_t fixup_work = 0;
do {
fixup_work = 0;
auto fixup_generation = [gc, &gray_lo_v](generation gen) {
std::size_t work = 0;
while(gray_lo_v[gen2int(gen)] < gc->free_ptr(gen)) {
Object * x = reinterpret_cast<Object *>(gray_lo_v[gen2int(gen)]);
// update per-class stats here
std::size_t xz = x->_forward_children();
// must pad xz to multiple of word size,
// to match behavior of LinearAlloc::alloc()
//
xz += gc::IAlloc::alloc_padding(xz);
gray_lo_v[gen2int(gen)] += xz;
++work;
}
return work;
};
fixup_work += fixup_generation(generation::nursery);
fixup_work += fixup_generation(generation::tenured);
} while (fixup_work > 0);
return to_src;
} /*deep_move*/
Object *
Object::_shallow_move(Object * src, gc::GC * gc)
{
/* filter for source objects that are owned by GC.
* Care required though -- during GC from/to spaces have been swapped already
*/
if (gc->fromspace_contains(src))
{
Object * dest = src->_shallow_copy();
if (dest != src)
src->_forward_to(dest);
return dest;
} else {
return src;
}
}
void
Object::_forward_to(Object * dest)
{
char * mem = reinterpret_cast<char *>(this);
Forwarding1 * fwd = new (mem) Forwarding1(dest);
(void)fwd;
}
} /*namespace xo*/
/* end Object.cpp*/

View file

@ -0,0 +1,87 @@
/* @file ArenaAlloc.test.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "xo/alloc/ArenaAlloc.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::ArenaAlloc;
namespace ut {
namespace {
struct testcase_alloc {
testcase_alloc(std::size_t rz, std::size_t z)
: redline_z_{rz}, arena_z_{z} {}
std::size_t redline_z_;
std::size_t arena_z_;
};
std::vector<testcase_alloc>
s_testcase_v = {
testcase_alloc(0, 4096)
};
}
TEST_CASE("linearalloc", "[alloc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const testcase_alloc & tc = s_testcase_v[i_tc];
constexpr bool c_debug_flag = false;
auto alloc = ArenaAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag);
REQUIRE(alloc.get());
REQUIRE(alloc->name() == "linearalloc");
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == tc.arena_z_);
REQUIRE(alloc->allocated() == 0);
REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
auto free0 = alloc->free_ptr();
auto mem = alloc->alloc(tc.arena_z_);
REQUIRE(mem != nullptr);
REQUIRE(mem == free0);
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == 0);
REQUIRE(alloc->allocated() == tc.arena_z_);
REQUIRE(alloc->is_before_checkpoint(mem) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == tc.arena_z_);
alloc->clear();
REQUIRE(alloc->free_ptr() == free0);
REQUIRE(alloc->available() == tc.arena_z_);
REQUIRE(alloc->allocated() == 0);
REQUIRE(alloc->is_before_checkpoint(free0) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
mem = alloc->alloc(1);
auto used = sizeof(void*);
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == tc.arena_z_ - used);
REQUIRE(alloc->allocated() == used);
REQUIRE(alloc->is_before_checkpoint(free0) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == used);
}
}
} /*namespace ut */
} /*namespace xo*/

View file

@ -3,7 +3,8 @@
set(SELF_EXE utest.alloc)
set(SELF_SRCS
alloc_utest_main.cpp
LinearAlloc.test.cpp)
ArenaAlloc.test.cpp
GC.test.cpp)
xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS})
xo_self_dependency(${SELF_EXE} xo_alloc)

View file

@ -0,0 +1,69 @@
/* @file GC.test.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "xo/alloc/GC.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::GC;
using xo::gc::generation;
using xo::gc::Config;
namespace ut {
namespace {
struct testcase_gc {
testcase_gc(std::size_t nz, std::size_t tz) : nursery_z_{nz}, tenured_z_{tz} {}
std::size_t nursery_z_;
std::size_t tenured_z_;
};
std::vector<testcase_gc>
s_testcase_v = {
testcase_gc(1024, 4096)
};
}
TEST_CASE("gc", "[alloc][gc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const testcase_gc & tc = s_testcase_v[i_tc];
up<GC> gc = GC::make(
{.initial_nursery_z_ = tc.nursery_z_,
.initial_tenured_z_ = tc.tenured_z_});
REQUIRE(gc.get());
REQUIRE(gc->size() == tc.nursery_z_ + tc.tenured_z_);
REQUIRE(gc->allocated() == 0);
REQUIRE(gc->available() == tc.nursery_z_);
REQUIRE(gc->before_checkpoint() == 0);
// ListAlloc model is that nothing is before checkpoint
// until it's first established
REQUIRE(gc->after_checkpoint() == 0);
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->is_gc_enabled() == true);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 0);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
/* gc with empty state */
gc->request_gc(generation::nursery);
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
/* still empty state */
gc->request_gc(generation::tenured);
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
}
}
} /*namespace ut*/
} /*namespace xo*/

View file

@ -3,7 +3,7 @@
* author: Roland Conybeare, Jul 2025
*/
#include "xo/alloc/LinearAlloc.hpp"
#include "xo/alloc/ArenaAlloc.hpp"
#include <catch2/catch.hpp>
namespace xo {
@ -33,15 +33,53 @@ namespace xo {
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const testcase_alloc & tc = s_testcase_v[i_tc];
auto alloc = LinearAlloc::make(tc.redline_z_, tc.arena_z_);
constexpr bool c_debug_flag = false;
auto alloc = LinearAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag);
REQUIRE(alloc.get());
REQUIRE(alloc->name() == "linearalloc");
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == tc.arena_z_);
REQUIRE(alloc->allocated() == 0);
REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
auto free0 = alloc->free_ptr();
auto mem = alloc->alloc(tc.arena_z_);
REQUIRE(mem != nullptr);
REQUIRE(mem == free0);
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == 0);
REQUIRE(alloc->allocated() == tc.arena_z_);
REQUIRE(alloc->is_before_checkpoint(mem) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == tc.arena_z_);
alloc->clear();
REQUIRE(alloc->free_ptr() == free0);
REQUIRE(alloc->available() == tc.arena_z_);
REQUIRE(alloc->allocated() == 0);
REQUIRE(alloc->is_before_checkpoint(free0) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
mem = alloc->alloc(1);
auto used = sizeof(void*);
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == tc.arena_z_ - used);
REQUIRE(alloc->allocated() == used);
REQUIRE(alloc->is_before_checkpoint(free0) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == used);
}
}

View file

@ -1,5 +1,7 @@
xo-cmake
xo-indentlog
xo-alloc
xo-object
xo-refcnt
xo-subsys
xo-randomgen

26
xo-object/CMakeLists.txt Normal file
View file

@ -0,0 +1,26 @@
# object/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(xo_object VERSION 0.1)
include(GNUInstallDirs)
include(cmake/xo-bootstrap-macros.cmake)
xo_cxx_toplevel_options3()
# ----------------------------------------------------------------
# c++ settings
set(PROJECT_CXX_FLAGS "")
add_definitions(${PROJECT_CXX_FLAGS})
# ----------------------------------------------------------------
add_subdirectory(src/object)
xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets)
# ----------------------------------------------------------------
add_subdirectory(utest)

View file

@ -0,0 +1,35 @@
# ----------------------------------------------------------------
# for example:
# $ PREFIX=/usr/local # for example
# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build
#
# will get
# CMAKE_MODULE_PATH
# from xo-cmake-config --cmake-module-path
#
# and expect .cmake macros in
# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake
# ----------------------------------------------------------------
find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED)
if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND")
message(FATAL "could not find xo-cmake-config executable")
endif()
message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}")
if (NOT XO_SUBMODULE_BUILD)
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
# default to typical install location for xo-project-macros
execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH)
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
endif()
endif()
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
#
include(xo_macros/xo_cxx)
xo_cxx_bootstrap_message()

View file

@ -0,0 +1,8 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# reminder: deps here must also appear in xo-object/src/object/CMakeLists.txt
find_dependency(xo_alloc)
#find_dependency(xo_flatstring)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
check_required_components("@PROJECT_NAME@")

View file

@ -0,0 +1,37 @@
/* @file Boolean.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/Object.hpp"
namespace xo {
namespace obj {
/** @class Boolean
* @brief Boxed wrapper for a boolean value
**/
class Boolean : public Object {
public:
/** @return instance representing boolean with truth-value @p x **/
static gp<Boolean> boolean_obj(bool x);
static gp<Boolean> true_obj();
static gp<Boolean> 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 Boolean(bool x) : value_{x} {}
private:
bool value_;
};
}
}
/* end Boolean.hpp */

View file

@ -0,0 +1,37 @@
/* @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<BooleanObj> boolean_obj(bool x);
static gp<BooleanObj> true_obj();
static gp<BooleanObj> 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 */

View file

@ -0,0 +1,21 @@
/* @file Collection.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/Object.hpp"
namespace xo {
namespace obj {
class Collection : public Object {
// inherited from Object..
//virtual std::size_t _shallow_size() const override;
//virtual Object * _shallow_copy() const override;
//virtual std::size_t _forward_children() override;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Collection.hpp */

View file

@ -0,0 +1,31 @@
/* @file Integer.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Number.hpp"
namespace xo {
namespace obj {
class Integer : public Number {
public:
using int_type = long long;
public:
Integer() = default;
explicit Integer(int_type x);
static gp<Integer> make(int_type x);
// 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:
int_type value_ = 0;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Integer.hpp */

View file

@ -0,0 +1,43 @@
/* @file List.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Sequence.hpp"
namespace xo {
namespace obj {
/** @class List
* @brief A list element -- aka cons cell
**/
class List : public Sequence {
public:
/** the empty list. unique sentinel object **/
static gp<List> nil;
/** @return list with first element @p car, and tail @p cdr **/
static gp<List> cons(gp<Object> car, gp<List> cdr);
bool is_nil() const { return this == nil.ptr(); }
gp<Object> head() const { return head_; }
gp<List> tail() const { return tail_; }
std::size_t size() const;
gp<Object> list_ref(std::size_t i) const;
// 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:
List(gp<Object> head, gp<List> tail);
private:
gp<Object> head_;
gp<List> tail_;
};
}
} /*namespace xo*/

View file

@ -0,0 +1,20 @@
/* @file Number.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Scalar.hpp"
namespace xo {
namespace obj {
class Number : public Scalar {
// inherited from Object..
//virtual std::size_t _shallow_size() const override;
//virtual Object * _shallow_copy() override;
//virtual std::size_t _forward_children() override;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Number.hpp */

View file

@ -0,0 +1,20 @@
/* @file Numeric.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/Object.hpp"
namespace xo {
namespace obj {
class Numeric : public Object {
// inherited from Object..
//virtual std::size_t _shallow_size() const override;
//virtual Object * _shallow_copy() override;
//virtual std::size_t _forward_children() override;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Numeric.hpp */

View file

@ -0,0 +1,20 @@
/* @file Scalar.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Numeric.hpp"
namespace xo {
namespace obj {
class Scalar : public Numeric {
// inherited from Object..
//virtual std::size_t _shallow_size() const override;
//virtual Object * _shallow_copy() override;
//virtual std::size_t _forward_children() override;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Scalar.hpp */

View file

@ -0,0 +1,20 @@
/* @file Sequence.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Collection.hpp"
namespace xo {
namespace obj {
class Sequence : public Collection {
// inherited from Object..
//virtual std::size_t _shallow_size() const override;
//virtual Object * _shallow_copy() const override;
//virtual std::size_t _fixup_forwarded_children() override;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Sequence.hpp */

View file

@ -0,0 +1,54 @@
/* @file String.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/IAlloc.hpp"
#include "xo/alloc/Object.hpp"
namespace xo {
namespace obj {
class String : public Object {
public:
enum Owner { unique, shared };
/** donwcase from @p x iff x is actually a String. Otherwise nullptr **/
static gp<String> from(gp<Object> x);
/** create copy of string @p s, using allocator @ref Object::mm **/
static gp<String> copy(const char * s);
/** create copy of string @p s, using allocator @p mm **/
static gp<String> copy(gc::IAlloc * mm, const char * s);
/** create empty string with @p z bytes of string space **/
static gp<String> allocate(std::size_t z);
/** create string containing contents of @p s1 follwed by contents of @p s2 **/
static gp<String> append(gp<String> s1, gp<String> s2);
const char * c_str() const { return chars_; }
std::size_t length() const;
// 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:
String(Owner owner, std::size_t z, char * s);
/** create instance, copying string contents (when @p copy_flag is true) using allocator @p mm **/
String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy);
private:
/** true iff storage in @ref chars_ is owned by this String.
**/
Owner owner_ = Owner::shared;
/** length of @ref chars_ in bytes (storage allocated, not necessarily string length) **/
std::size_t z_chars_ = 0;
/** string contents. always null-terminated **/
char * chars_ = nullptr;
};
} /*namespace obj*/
} /*namespace xo*/
/* end String.hpp */

View file

@ -0,0 +1,52 @@
/* @file Boolean.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Boolean.hpp"
#include <array>
#include <cassert>
#include <cstddef>
namespace xo {
namespace obj {
gp<Boolean>
Boolean::boolean_obj(bool x)
{
static std::array<gp<Boolean>, 2> s_boolean_v
= {{ new Boolean{false}, new Boolean{true} }};
return s_boolean_v[static_cast<std::size_t>(x)];
}
std::size_t
Boolean::_shallow_size() const
{
return sizeof(Boolean);
}
Object *
Boolean::_shallow_copy() const
{
/* Boolean instances not created in GC-owned space,
* so GC will not traverse them.
*
* If we wanted booleans in GC-owned space, would need
* to pad Boolean::value_ with enough space to hold a forwarding
* pointer
*/
assert(false);
return nullptr;
}
std::size_t
Boolean::_forward_children()
{
assert(false);
return 0;
}
}
} /*namespace xo*/
/* end Boolean.cpp */

View file

@ -0,0 +1,11 @@
# object/CMakeLists.txt
set(SELF_LIB xo_object)
set(SELF_SRCS
Boolean.cpp
String.cpp
List.cpp
Integer.cpp)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
xo_dependency(${SELF_LIB} xo_alloc)

View file

@ -0,0 +1,38 @@
/* @file Integer.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Integer.hpp"
#include <cstddef>
namespace xo {
namespace obj {
Integer::Integer(int_type x) : value_{x} {}
gp<Integer>
Integer::make(int_type x) {
return new (MMPtr(mm)) Integer(x);
}
std::size_t
Integer::_shallow_size() const {
return sizeof(Integer);
}
Object *
Integer::_shallow_copy() const {
Cpof cpof(this);
return new (cpof) Integer(*this);
}
std::size_t
Integer::_forward_children() {
return Integer::_shallow_size();
}
} /*namespace obj*/
} /*namespace xo*/
/* end Integer.cpp */

View file

@ -0,0 +1,74 @@
/** @file List.cpp
*
* author: Roland Conybeare, Aug 2025
**/
#include "List.hpp"
#include <cassert>
#include <cstddef>
namespace xo {
namespace obj {
List::List(gp<Object> head, gp<List> tail)
: head_{head}, tail_{tail} {}
gp<List>
List::nil = new List(nullptr, nullptr);
gp<List>
List::cons(gp<Object> car, gp<List> cdr) {
return new (MMPtr(mm)) List(car, cdr);
}
std::size_t
List::size() const {
std::size_t retval = 0;
gp<const List> l(this);
while (!l->is_nil()) {
++retval;
l = l->tail();
}
return retval;
}
gp<Object>
List::list_ref(std::size_t i) const {
gp<const List> rem(this);
while (i > 0) {
assert(!(rem->is_nil()));
rem = rem->tail();
--i;
}
return rem->head();
}
std::size_t
List::_shallow_size() const {
return sizeof(List);
}
Object *
List::_shallow_copy() const {
assert(!(this->is_nil()));
Cpof cpof(this);
return new (cpof) List(*this);
}
std::size_t
List::_forward_children() {
Object::_forward_inplace(head_);
Object::_forward_inplace(tail_);
return List::_shallow_size();
}
}
}
/* end List.cpp */

View file

@ -0,0 +1,133 @@
/* @file String.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "String.hpp"
#include "GC.hpp"
#include <bsd/string.h>
#include <cstddef>
#include <cstring>
#include <cassert>
namespace xo {
namespace obj {
String::String(Owner owner, std::size_t z, char * s)
: owner_{owner}, z_chars_{z}, chars_{s}
{}
String::String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy)
: owner_{owner}, z_chars_{z}
{
if (copy) {
chars_ = reinterpret_cast<char *>(mm->alloc(z));
assert(chars_);
strlcpy(chars_, s, z);
} else {
chars_ = s;
}
}
gp<String>
String::from(gp<Object> x)
{
return dynamic_cast<String*>(x.ptr());
}
gp<String>
String::copy(const char * s)
{
return copy(Object::mm, s);
}
gp<String>
String::copy(gc::IAlloc * mm, const char * s)
{
std::size_t z = 1 + (s ? ::strlen(s) : 0);
const char * chars = s ? s : "";
// const-cast ok since chars copied with Owner::unique
return new (MMPtr(mm)) String(mm, Owner::unique, z, const_cast<char *>(chars), true /*copy*/);
}
gp<String>
String::allocate(std::size_t z)
{
return new (MMPtr(Object::mm)) String(mm, Owner::unique, z, const_cast<char *>(""), true /*copy*/);
}
gp<String>
String::append(gp<String> s1, gp<String> s2)
{
std::size_t z1 = s1->length();
std::size_t z2 = s2->length();
std::size_t z = z1 + z2;
gp<String> retval = allocate(z);
strlcpy(retval->chars_, s1->chars_, z1);
strlcpy(retval->chars_ + z1, s2->chars_, z2);
return retval;
}
std::size_t
String::length() const
{
return ::strlen(chars_);
}
// ----- GC support -----
std::size_t
String::_shallow_size() const
{
/* no child Object* pointers to fixup,
* but must count for amount of storage used by _shallow_move()
*/
std::size_t retval = gc::IAlloc::with_padding(sizeof(String));
if (owner_ == Owner::unique)
retval += gc::IAlloc::with_padding(z_chars_);
return retval;
}
Object *
String::_shallow_copy() const
{
// Reminder: String must come before secondary allocation,
Cpof cpof(this);
// might expect to write:
// gp<String> copy = new (gcm) String(Object::mm, owner_, z_chars_, chars_);
// but this would always put string contents in nursery to-space.
//
// We need to choose nursery/tenured based on location of this,
// achieved by calling GC::alloc_copy() instead of GC::alloc()
//
gp<String> copy = new (cpof) String(owner_, z_chars_, chars_);
if (owner_ == Owner::unique) {
std::byte * mem = reinterpret_cast<std::byte *>(chars_);
copy->chars_ = reinterpret_cast<char *>(Object::mm->alloc_gc_copy(z_chars_, mem));
strlcpy(copy->chars_, chars_, z_chars_);
}
return copy.ptr();
}
std::size_t
String::_forward_children()
{
return this->_shallow_size();
}
} /*namespace obj*/
} /*namespace xo*/
/* end String.cpp */

View file

@ -0,0 +1,11 @@
# build unittest object/utest
set(SELF_EXE utest.object)
set(SELF_SRCS
object_utest_main.cpp
String.test.cpp
List.test.cpp)
xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS})
xo_self_dependency(${SELF_EXE} xo_object)
xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2)

View file

@ -0,0 +1,184 @@
/* @file List.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/object/List.hpp"
#include "xo/object/String.hpp"
#include "xo/alloc/GC.hpp"
#include "xo/indentlog/scope.hpp"
#include <catch2/catch.hpp>
#include <ranges>
#include <vector>
#include <string>
namespace xo {
namespace ut {
using xo::obj::List;
using xo::obj::String;
using xo::gc::GC;
using xo::gc::generation;
namespace {
struct Testcase_List {
Testcase_List(std::size_t nz, std::size_t tz,
const std::vector<std::vector<std::string>> & v)
: nursery_z_{nz}, tenured_z_{tz}, v_{v}
{}
std::size_t nursery_z_;
std::size_t tenured_z_;
std::vector<std::vector<std::string>> v_;
};
std::vector<Testcase_List>
s_testcase_v = {
Testcase_List( 512, 1024, {{}}),
Testcase_List( 512, 1024, {{"hello", ", ", " world!"}}),
Testcase_List(1024, 2048, {{"the", " quick", " brown", "fox", "jumps"},
{"over", " the", " lazy", " dog!"}})
};
}
TEST_CASE("List", "[List][gc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const Testcase_List & tc = s_testcase_v[i_tc];
constexpr bool c_debug_flag = false;
up<GC> gc = GC::make(
{.initial_nursery_z_ = tc.nursery_z_,
.initial_tenured_z_ = tc.tenured_z_,
.debug_flag_ = c_debug_flag});
REQUIRE(gc.get());
/* use gc for all Object allocs */
Object::mm = gc.get();
{
scope log(XO_DEBUG(c_debug_flag));
log && log(xtag("i_tc", i_tc), xtag("tc.v_.size", tc.v_.size()));
std::vector<gp<List>> root_v(tc.v_.size());
std::size_t i = 0;
std::size_t expected_alloc_z = 0;
/* construct example Lists from testcase info */
for (const std::vector<std::string> & v : tc.v_)
{
/* building l1 in reverse order */
gp<List> l1 = List::nil;
for (std::size_t ip1 = v.size(); ip1 > 0; --ip1) {
const std::string & si = v.at(ip1 - 1);
log && log(xtag("i", ip1-1), xtag("si", si));
gp<String> sobj = String::copy(si.c_str());
l1 = List::cons(sobj, l1);
log && log(xtag("l1.size", l1->size()));
std::size_t alloc_z = l1->_shallow_size() + l1->head()->_shallow_size();
expected_alloc_z += alloc_z;
}
REQUIRE(l1->is_nil() == (v.size() == 0));
REQUIRE(l1->size() == v.size());
root_v[i] = l1;
gc->add_gc_root(reinterpret_cast<Object **>(root_v[i].ptr_address()));
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
REQUIRE(gc->allocated() == expected_alloc_z);
++i;
}
/* gc responsible for a bunch of list objects;
* all are roots and should be preserved
*/
gc->request_gc(generation::nursery);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
REQUIRE(gc->allocated() == expected_alloc_z);
/* verify GC preserved list structure and contents */
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
std::size_t nj = tc.v_.at(i).size();
REQUIRE(root_v.at(i)->size() == nj);
if (!(root_v.at(i)->is_nil()))
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
for (std::size_t j = 0; j < nj; ++j) {
gp<String> s = String::from(root_v.at(i)->list_ref(j));
REQUIRE(s.ptr());
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
REQUIRE(gc->generation_of(reinterpret_cast<std::byte*>(s.ptr()))
== generation::nursery);
}
}
/* every has survived one GC cycle. collect again should promote */
gc->request_gc(generation::nursery);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 2);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
REQUIRE(gc->allocated() == expected_alloc_z);
/* verify GC preserved list structure and contents */
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
std::size_t nj = tc.v_.at(i).size();
REQUIRE(root_v.at(i)->size() == nj);
if (!(root_v.at(i)->is_nil()))
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
for (std::size_t j = 0; j < nj; ++j) {
gp<String> s = String::from(root_v.at(i)->list_ref(j));
REQUIRE(s.ptr());
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
REQUIRE(gc->generation_of(reinterpret_cast<std::byte*>(s.ptr()))
== generation::tenured);
}
}
REQUIRE(gc->gc_statistics().total_promoted_ == gc->allocated());
gc->request_gc(generation::tenured);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 2);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
REQUIRE(gc->allocated() == expected_alloc_z);
/* verify GC preserved list structure and contents */
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
std::size_t nj = tc.v_.at(i).size();
REQUIRE(root_v.at(i)->size() == nj);
if (!(root_v.at(i)->is_nil()))
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
for (std::size_t j = 0; j < nj; ++j) {
gp<String> s = String::from(root_v.at(i)->list_ref(j));
REQUIRE(s.ptr());
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
REQUIRE(gc->generation_of(reinterpret_cast<std::byte*>(s.ptr()))
== generation::tenured);
}
}
log && log("stats", gc->gc_statistics());
}
}
} /*TEST_CASE(List, ..)*/
} /*namespace ut*/
} /*namespace xo*/
/* end List.test.cpp */

View file

@ -0,0 +1,157 @@
/* @file String.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/object/String.hpp"
#include "xo/alloc/GC.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/quoted.hpp"
#include <catch2/catch.hpp>
#include <cstring>
#include <cstdint>
namespace xo {
using xo::gc::IAlloc;
using xo::gc::GC;
using xo::gc::generation;
using xo::obj::String;
namespace ut {
namespace {
struct Testcase_String {
Testcase_String(std::size_t nz, std::size_t tz,
const std::vector<std::string> & v)
: nursery_z_{nz}, tenured_z_{tz}, v_{v} {}
std::size_t nursery_z_;
std::size_t tenured_z_;
std::vector<std::string> v_;
};
std::vector<Testcase_String>
s_testcase_v = {
Testcase_String(1024, 4096, {"hello"}),
Testcase_String(1024, 4096, {"hello", ", world!"})
};
}
TEST_CASE("String", "[String][gc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const Testcase_String & tc = s_testcase_v[i_tc];
up<GC> gc = GC::make(
{.initial_nursery_z_ = tc.nursery_z_,
.initial_tenured_z_ = tc.tenured_z_,
.debug_flag_ = false});
REQUIRE(gc.get());
/* use gc for all Object allocs */
Object::mm = gc.get();
{
std::size_t n_string = 0;
std::size_t expected_alloc_z = 0;
for (const std::string & s_str : tc.v_)
{
gp<String> s1 = String::copy(s_str.c_str());
++n_string;
/* 1+ for mandatory null terminator */
expected_alloc_z += (IAlloc::with_padding(sizeof(String))
+ IAlloc::with_padding(1 + s_str.length()));
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
REQUIRE(gc->allocated() == expected_alloc_z);
REQUIRE(s1->length() == s_str.length());
REQUIRE(strcmp(s1->c_str(), s_str.c_str()) == 0);
}
/* gc has n_string objects. Nothing refers to them, so gc going to kill all */
gc->request_gc(generation::nursery);
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
REQUIRE(gc->allocated() == 0);
REQUIRE(gc->gc_statistics().total_allocated_ == expected_alloc_z);
}
}
}
TEST_CASE("String2", "[String][gc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const Testcase_String & tc = s_testcase_v[i_tc];
up<GC> gc = GC::make(
{.initial_nursery_z_ = tc.nursery_z_,
.initial_tenured_z_ = tc.tenured_z_,
.debug_flag_ = false});
REQUIRE(gc.get());
/* use gc for all Object allocs */
Object::mm = gc.get();
{
scope log(XO_DEBUG(false));
std::size_t n_string = 0;
std::size_t expected_alloc_z = 0;
std::vector<gp<String>> sv(tc.v_.size());
std::size_t i = 0;
for (const std::string & s_str : tc.v_)
{
sv[i] = String::copy(s_str.c_str());
++n_string;
/* 1+ for mandatory null terminator */
std::size_t alloc_z = (IAlloc::with_padding(sizeof(String))
+ IAlloc::with_padding(1 + s_str.length()));
expected_alloc_z += alloc_z;
log && log(xtag("s_str", xo::print::unq(s_str)),
xtag("s_str.length", s_str.length()),
xtag("alloc_z", alloc_z));
log && log(xtag("expected_alloc_z", expected_alloc_z));
gc->add_gc_root(reinterpret_cast<Object **>(sv[i].ptr_address()));
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
REQUIRE(gc->allocated() == expected_alloc_z);
REQUIRE(sv[i]->length() == s_str.length());
REQUIRE(strcmp(sv[i]->c_str(), s_str.c_str()) == 0);
++i;
}
/* gc has a bunch of string objects; all are roots + should be preserved */
gc->request_gc(generation::nursery);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
REQUIRE(gc->allocated() == expected_alloc_z);
for (std::size_t i = 0, n = sv.size(); i < n; ++i) {
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(sv.at(i).ptr())));
REQUIRE(strcmp(sv.at(i)->c_str(), tc.v_[i].c_str()) == 0);
}
}
}
}
} /*namespace ut*/
} /*namespace xo*/
/* end String.test.cpp */

View file

@ -0,0 +1,6 @@
/* file object_utest_main.cpp */
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
/* end object_utest_main.cpp */