git subrepo clone git@github.com:Rconybea/xo-alloc.git xo-alloc

subrepo:
  subdir:   "xo-alloc"
  merged:   "fc656313"
upstream:
  origin:   "git@github.com:Rconybea/xo-alloc.git"
  branch:   "main"
  commit:   "fc656313"
git-subrepo:
  version:  "0.4.9"
  origin:   "???"
  commit:   "???"
This commit is contained in:
Roland Conybeare 2026-06-06 22:03:21 -04:00
commit 2c8faf6e43
49 changed files with 7196 additions and 0 deletions

12
xo-alloc/.gitrepo Normal file
View file

@ -0,0 +1,12 @@
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme
;
[subrepo]
remote = git@github.com:Rconybea/xo-alloc.git
branch = main
commit = fc656313e9582957f13446364299a8e79cbd51f0
parent = d16545d815d055837e0973cca8483277a925d7fb
method = merge
cmdver = 0.4.9

32
xo-alloc/CMakeLists.txt Normal file
View file

@ -0,0 +1,32 @@
# xo-alloc/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(xo_alloc VERSION 0.1)
include(GNUInstallDirs)
include(cmake/xo-bootstrap-macros.cmake)
xo_cxx_toplevel_options3()
# ----------------------------------------------------------------
# c++ settings
set(PROJECT_CXX_FLAGS "")
#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") # gcc-only!
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)
# end CmakeLists.txt

16
xo-alloc/README.md Normal file
View file

@ -0,0 +1,16 @@
# xo-alloc -- arena allocator and incremental garbage collector
# Rules for writing garbage-collected classes.
Topics
* allocation - allocate Objects (inheriting xo::Object) before owned scratch space.
Can relax this if/when abandon the bad-for-locality use of two pointers
into to-space to keep track of grey objects. Want to use stack anyway
so we can do depth-first search.
* destructors - can omit except for finalization
* assignment - MUST USE Object::assign_member() to assign pointers to gc-owned memory.
Only necessary for old->new pointers, so don't need to worry about this
for initialization.
* finalization - not supported (yet)
- padding - use IAlloc::with_padding(z) for hand-allocated objects.

View file

@ -0,0 +1,41 @@
# ----------------------------------------------------------------
# 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 (XO_SUBMODULE_BUILD)
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
# local version of xo-cmake macros
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/xo-cmake/cmake")
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
endif()
else()
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,11 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(xo_allocutil)
find_dependency(xo_unit)
find_dependency(indentlog)
find_dependency(reflect)
find_dependency(callback)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake")
check_required_components("@PROJECT_NAME@")

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,28 @@
.. _glossary:
Glossary
--------
.. glossary::
GC
| garbage collector
mlog
| mutation log.
| Remembers cross-generation and cross-checkpoint pointers
nursery
| in garbage collector, memory region dedicated to young objects.
| These are objects that have survived less than 2 incremental collection cycles.
tenured
| in garbage collector, memory region dedicated to older objects.
| These are defined as objects that have survived 2 or more incremental collection cycles.
xgen
| cross-generation tenured->nursery pointer; requires special GC bookkeeping
xckp
| cross-checkpoint pointer; requires special GC bookkeeping
.. toctree::

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

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

@ -0,0 +1,17 @@
# 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
glossary
genindex
search

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 rest 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

@ -0,0 +1,231 @@
/* file ArenaAlloc.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#pragma once
#include "xo/allocutil/IAlloc.hpp"
#include "ObjectStatistics.hpp"
namespace xo {
namespace gc {
/** @class ArenaAlloc
* @brief Bump allocator with fixed capacity with dynamic virtual memory commitment.
*
* @text
*
* allocation order:
* ----------------------->
*
* <----------------- .size(), .reserved() --------------------------->
* <----------------- .committed() ------------->
*
* <-------allocated------><--------free--------><-----uncommitted---->
* XXXXXXXXXXXXXXXXXXXXXXXX______________________......................
* ^ ^ ^ ^ ^
* lo checkpoint free limit hi
*
* +- .alloc() ->
* +-- .expand() -->
* > < .before_checkpoint()
* > < .after_checkpoint()
*
* lifetime:
*
* 1. initial state after ctor
*
* >< committed()=0
* <---------------------------uncommitted---------------------------->
* ....................................................................
* ^ ^
* lo hi
* checkpoint
* free
* limit
*
* 1a. one call to ::mmap()
* 1b. vm address space [lo,hi) is reserved
* 1c. address space [lo,hi) is inaccessible. no read|write|execute permission
*
* 2. after first allocation of n bytes
*
* <--committed--->
* <--free--><--------------------uncommitted-------------------->
* > <- allocated
* XXXXXX__________.....................................................
* ^ ^ ^ ^
* lo lo+n limit hi
* ^ free
* checkpoint
*
* 2a. committed just enough hugepages (2mb each) to accomodate n,
* i.e. expand-on-demand:
* - one call to ::mprotect()
* - .limit = .lo + (k+1) * .hugepage_z for some integer k>=0
* - k * .page_z <= n < (k+1) * .hugepage_z
* 2b. expect immediate cost 1-5us, includes:
* - TLB flush
* invalidate TLB entries for committed range on all cores that this
* process' threads have run on since process inception.
* Also, if a kernel thread has run on one of said cores, it may
* have borrowed our TLB entries
* - page table update
* write to entry for each vm page
* - kernel overhead 100-1000 cycles (< 1us)
* 2c. expect deferred cost 1us-2us per hugepage:
* - committed pages aren't backed by physical memory until
* first touched; minor page fault on first access for each page.
* - so about 256-512us for 1MB
* 3. after .expand(z)
*
* <-------------committed------------>
* <------------free------------><----------uncomitted----------->
* > <- allocated
* XXXXXX______________________________.................................
* ^ ^ ^ ^
* lo lo+n limit hi
* ^ free
* checkpoint
*
* 3a. same as case 2. but without advancing .free pointer.
*
* 4. after dtor
*
* 4a. all memory returned to o/s, no longer reserved.
* - one call to ::munmap()
*
* @endtext
*
* Design Notes:
* - non-copyable, non-moveable
* - @ref lo_ <= @ref checkpoint_ <= @ref free_ <= @ref limit_ <= @ref hi_
* - memory for ArenaAlloc itself (not the memory it allocates), ~100 bytes
* always heap allocated. Use ArenaAlloc::make()
* - memory obtained from mmap(), not heap
* - memory addresses are stable. Expand storage by committing VM pages.
* - @ref lo_ is aligned on VM page size (guaranteed by mmap())
* - @ref lo_ + @ref committed_z_ <= @ref hi_
* - @ref limit_ <= @ref lO_ + @ref committed_z_
* - @ref committed_z_ is always a multiple of VM page size
* - @ref limit_ is not guaranteed to be aligned with VM page size.
* - @ref expand increases @ref limit_ and @ref committed_z_ as needed.
*
**/
class ArenaAlloc : public IAlloc {
public:
ArenaAlloc(const ArenaAlloc &) = delete;
ArenaAlloc(ArenaAlloc &&) = delete;
~ArenaAlloc();
/** Create allocator with capacity @p z,
* Reserve memory addresses for @p z bytes,
* (but don't commit them until needed)
**/
static up<ArenaAlloc> make(const std::string & name,
std::size_t z,
bool debug_flag);
/** size of virtual address range reserved for this allocator **/
std::size_t reserved() const { return hi_ - lo_; };
std::size_t page_size() const { return page_z_; }
std::size_t hugepage_z() const { return hugepage_z_; }
std::byte * free_ptr() const { return free_ptr_; }
void set_free_ptr(std::byte * x);
/** if address @p x is allocated from this arena,
* return true along with offset relative to base address @ref lo_
* otherwise return false with 0
**/
std::pair<bool, std::size_t> location_of(const void * x) const;
/** allocated span **/
std::pair<const std::byte *, const std::byte *> allocated_span() const {
return std::make_pair(lo_, free_ptr_);
}
/** Reset to empty state; provision at least @p need_z bytes of (committed) space **/
void reset(std::size_t need_z);
/** gc support: If used for storing xo::Object instances, scan allocated memory
* to populate @p *p_dest.
**/
void capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const;
/** expand available (i.e. committed) space to size at least @p z
* In practice will round up to a multiple of @ref page_z_.
**/
bool expand(std::size_t z);
// inherited from IAlloc...
virtual const std::string & name() const final override;
virtual std::size_t size() const final override;
virtual std::size_t committed() const final override;
virtual std::size_t available() const final override;
virtual std::size_t allocated() const final override;
virtual bool contains(const void * x) const final override;
virtual bool is_before_checkpoint(const void * x) const final override;
virtual std::size_t before_checkpoint() const final override;
virtual std::size_t after_checkpoint() const final override;
virtual bool debug_flag() const final override;
virtual void clear() final override;
virtual void checkpoint() final override;
virtual std::byte * alloc(std::size_t z) final override;
virtual bool check_owned(IObject * src) const final override;
ArenaAlloc & operator=(const ArenaAlloc &) = delete;
ArenaAlloc & operator=(ArenaAlloc &&) = delete;
private:
ArenaAlloc(const std::string & name,
std::size_t z, bool debug_flag);
private:
/**
* Invariants:
* - @ref free_ always a multiple of word size (assumed to be sizeof(void*))
**/
/** optional instance name, for diagnostics **/
std::string name_;
/** size of a VM page (from getpagesize()) **/
std::size_t page_z_ = 0;
/** size of a huge VM page. hardwiring this in ctor (to 2MB).
* larger pages relieve pressure on TLB, but suboptimal if use << 2MB
**/
std::size_t hugepage_z_ = 0;
/** allocator owns memory in range [@ref lo_, @ref hi_) **/
std::byte * lo_ = nullptr;
/** prefix of this size is actually committed.
* Remainder uses uncommitted virtual address space
**/
std::size_t committed_z_ = 0;
/** checkpoint (for GC support); divides objects into
* older (addresses below checkpoint)
* and younger (addresses above checkpoint)
**/
std::byte * checkpoint_ = nullptr;
/** free pointer. memory in range [@ref free_, @ref limit_) available **/
std::byte * free_ptr_ = nullptr;
/** soft limit: end of committed virtual memory
* invariant: @ref limit_ = @ref lo_ + @ref committed_z_
**/
std::byte * limit_ = nullptr;
/** hard limit: end of reserved virtual memory **/
std::byte * hi_ = nullptr;
/** true to enable detailed debug logging **/
bool debug_flag_ = false;
};
} /*namespace gc*/
} /*namespace xo*/
/* end ArenaAlloc.hpp */

View file

@ -0,0 +1,40 @@
/** @file Blob.hpp
*
* @author Roland Conybeare, Nov 2025
**/
#pragma once
#include "Object.hpp"
#include <xo/allocutil/IAlloc.hpp>
namespace xo {
/** Use to allocate opaque binary data,
* with object header.
*
* Not sure if we want to bother implementing reflection for this...
**/
class Blob : public Object {
public:
Blob(std::size_t z) : z_{z} {};
static gp<Blob> make(gc::IAlloc * mm, std::size_t z);
std::size_t size() const { return z_; }
std::byte * data() { return data_; }
virtual TaggedPtr self_tp() const final override;
virtual void display(std::ostream & os) const final override;
virtual std::size_t _shallow_size() const final override;
virtual Object * _shallow_copy(gc::IAlloc * gc) const final override;
virtual std::size_t _forward_children(gc::IAlloc * gc) final override;
private:
std::size_t z_ = 0;
/** flexible array, with @ref z_ bytes **/
std::byte data_[];
};
}
/* end Blob.hpp */

View file

@ -0,0 +1,245 @@
/* CircularBuffer.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#pragma once
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/tostr.hpp"
#include "xo/indentlog/print/tag.hpp"
#include <vector>
#include <cstdint>
#include <cassert>
//#include <concepts>
namespace xo {
namespace gc {
/** @class CircularBuffer
* @brief A circular buffer
*
* push operations may overwrite prior contents,
* i.e. buffer behavior on overflow
* old
*
* @tparam T is type for buffer elements.
**/
template <typename T>
class CircularBuffer {
public:
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using reference = value_type &;
using const_reference = const value_type &;
using pointer = value_type *;
using const_pointer = const value_type *;
template <typename Parent, typename TT>
class _iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = TT;
using difference_type = std::ptrdiff_t;
using pointer = value_type *;
using reference = value_type &;
_iterator(Parent * p, std::int64_t ix) : parent_{p}, index_{ix} {}
reference operator* () const { return (*parent_)[index_]; }
pointer operator->() const { return &(*parent_)[index_]; }
_iterator & operator++() { ++index_; return *this; }
_iterator operator++(int) { _iterator retval(parent_, index_); ++index_; return retval; }
auto operator<=>(const _iterator & other) {
if (parent_ == other.parent_)
return index_ <=> other.index_;
else
return std::partial_ordering::unordered;
}
bool operator==(const _iterator & other) const = default;
private:
Parent * parent_ = nullptr;
/** index position
* (-1 = just before front = rend, 0 = front, z-1 = back, z = just after back = end)
**/
std::int64_t index_ = 0;
};
using iterator = _iterator<CircularBuffer<T>, T>;
using const_iterator = _iterator<const CircularBuffer<T>, const T>;
public:
explicit CircularBuffer(std::size_t capacity = 0, bool debug_flag = false);
CircularBuffer(const CircularBuffer& other) = default;
CircularBuffer(CircularBuffer&& other) noexcept = default;
~CircularBuffer() = default;
static constexpr std::int64_t npos = -1;
/** @return location of i'th element. i: 0=front, 1=second etc **/
std::size_t location_of(std::size_t i) const;
/** @return ordinal index (relative to front) of location @p loc;
* npos if not used
**/
//std::int64_t index_of(std::size_t loc) const; // not implemented yet
// standard container methods
bool empty() const noexcept { return size_ == 0; }
size_type size() const noexcept { return size_; }
size_type max_size() const noexcept { return contents_.size(); }
// void reserve(size_type new_capacity); // not implemented
size_type capacity() const noexcept { return contents_.size(); }
// void shrink_to_fit(); // not implemented
reference at(size_type pos) {
if ((pos < 0) || (pos >= size_)) {
throw std::out_of_range(tostr("CircularBuffer::at: index out of range",
xtag("pos", pos), xtag("size", size_)));
}
return contents_[this->location_of(pos)];
}
const_reference at(size_type pos) const {
reference retval = const_cast<CircularBuffer*>(this)->at(pos);
return retval;
}
reference operator[](size_type pos) {
return contents_[this->location_of(pos)];
}
const_reference operator[](size_type pos) const {
return contents_[this->location_of(pos)];
}
reference front() { return contents_[front_ix_]; }
const_reference front() const {
reference retval = const_cast<CircularBuffer*>(this)->front();
return retval;
}
reference back() { return contents_[location_of(size_ - 1)]; }
const_reference back() const {
reference retval = const_cast<CircularBuffer*>(this)->back();
return retval;
}
iterator begin() { return iterator(this, 0); }
iterator end() { return iterator(this, size_); }
const_iterator begin() const { return const_iterator(this, 0); }
const_iterator end() const { return const_iterator(this, size_); }
// reverse_iterator rbegin();
// reverse_iterator rend();
// const_reverse_iterator rbegin() const;
// const_reverse_iterator rend() const;
// General Methods
void clear() {
size_ = 0;
front_ix_ = 0;
std::size_t capacity = contents_.size();
contents_.clear();
contents_.resize(capacity);
}
/** push @p x on to the end of this buffer.
* If buffer is at capacity, overwrites the oldest element
**/
CircularBuffer & push_back(const T & x);
// template<typename... Args>
//reference emplace_back(Args&&... args);
CircularBuffer & pop_back();
// push_front();
// pop_front();
CircularBuffer& operator=(const CircularBuffer& other) = default;
CircularBuffer& operator=(CircularBuffer&& other) noexcept = default;
private:
/** number of elements in buffer. Not the same as @code contents_.size();
* the latter represents buffer capacity.
*
* Promise:
* size_ <= contents_.size()
**/
std::size_t size_ = 0;
/** first element is @code contents_.at(front_ix_) **/
std::size_t front_ix_ = 0;
/** buffer contents. contents_.size() represents buffer capacity
* first element stored in @code contents_.at(front_)
* last element stored in @code contents_.at((front_ + size_ - 1) % contents_.size())
**/
std::vector<T> contents_;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
template <typename T>
CircularBuffer<T>::CircularBuffer(std::size_t capacity, bool debug_flag)
: size_{0}, front_ix_{0}, contents_{capacity}, debug_flag_{debug_flag}
{
}
template <typename T>
std::size_t
CircularBuffer<T>::location_of(std::size_t i) const
{
if (size_ == 0)
return 0;
else
return (front_ix_ + i) % size_;
}
template <typename T>
CircularBuffer<T> &
CircularBuffer<T>::push_back(const T & x) {
scope log(XO_DEBUG(debug_flag_), rtag("x", x), xrtag("size", size_));
if (size_ < contents_.size()) {
++size_;
/* _after_ incr .size_ */
std::size_t back_ix = location_of(size_ - 1);
this->contents_[back_ix] = x;
log && log(xtag("back_ix", back_ix), xtag("+size", size_));
} else {
std::size_t back_ix = location_of(size_);
this->contents_[back_ix] = x;
/* buffer was full, so oldest element replaced */
this->front_ix_ = (this->front_ix_ + 1) % contents_.size();
log && log(xtag("back_ix", back_ix), xtag("+front", front_ix_));
}
return *this;
}
template <typename T>
CircularBuffer<T> &
CircularBuffer<T>::pop_back() {
if (size_ > 0) {
std::size_t back_ix = location_of(size_ - 1);
this->contents_[back_ix] = T();
--(this->size_);
} else {
assert(false);
}
return *this;
}
} /*namespace gc*/
} /*namespace xo*/
/* CircularBuffer.hpp */

View file

@ -0,0 +1,56 @@
/* file Forwarding1.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Object.hpp"
namespace xo {
namespace obj {
/** @class Forwarding1
* @brief forwarding pointer for garbage collector.
*
* Used internally by garbage collector (see @ref GC).
* During evacuate phase overwrite from-space objects in-place
* with an instance of this class.
*
* This class suitable only for singly-inheriting objects,
* i.e. those that have exactly one vtable.
**/
class Forwarding1 : public Object {
public:
explicit Forwarding1(gp<IObject> dest);
// inherited from Object..
virtual TaggedPtr self_tp() const final override;
virtual void display(std::ostream & os) const final override;
virtual bool _is_forwarded() const final override { return true; }
virtual IObject * _offset_destination(IObject * src) const final override;
virtual IObject * _destination() final override;
/** required by Object i/face, but never called on Forwarding1 **/
virtual std::size_t _shallow_size() const final override;
/** required by Object i/face, but never called on Forwarding1 **/
virtual IObject * _shallow_copy(gc::IAlloc * mm) const final override;
/** required by Object i/face, but never called on Forwarding1 **/
virtual std::size_t _forward_children(gc::IAlloc * mm) final 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<IObject> dest_;
};
} /*namespace obj*/
} /*namespace xo*/
/* end Forwarding1.hpp */

View file

@ -0,0 +1,496 @@
/* GC.hpp
*
* author: Roland Conybeare, jul 2025
*/
#pragma once
#include "ArenaAlloc.hpp"
#include "GcStatistics.hpp"
#include "Object.hpp"
#include "xo/callback/UpCallbackSet.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 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.
* This number represents reserved address space.
* pages are committed on demand.
* Initial committment will be up to @ref incr_gc_threshold_
**/
std::size_t initial_nursery_z_ = 64*1024*1024;
/** initial size in bytes for oldest (Tenured) generation.
* GC allocates two tenured spaces of this size.
* This number represents reserved address space.
* pages are committed on demand.
* Initial committment will be up to @ref full_gc_threshold_
**/
std::size_t initial_tenured_z_ = 128*1024*1024;
/** trigger incremental GC after this many bytes allocated in nursery **/
std::size_t incr_gc_threshold_ = 64*1024;
/** trigger full GC after this many bytes promoted to tenured **/
std::size_t full_gc_threshold_ = 512*1024;
/** true to permit incremental garbage collection **/
bool allow_incremental_gc_ = true;
/** true to report statistics **/
bool stats_flag_ = false;
/** true to capture per-type object statistics **/
bool object_stats_flag_ = false;
/** remember basic gc statistics for this many GC's; separately for incremental + full GCs **/
std::size_t stats_history_z_ = 256;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
/** @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 MutationLogEntry {
public:
MutationLogEntry(IObject * parent, IObject ** lhs)
: parent_{parent}, lhs_{lhs} {}
IObject * parent() const { return parent_; }
IObject ** lhs() const { return lhs_; }
IObject * child() const { return *lhs_; }
bool is_child_forwarded() const;
bool is_parent_forwarded() const;
IObject * parent_destination() const;
/** Flag obsolete mutation.
* Future proofing, never happens for regular objects
**/
bool is_dead() const { return false; }
MutationLogEntry update_parent_moved(IObject * parent_to) const;
void fixup_parent_child_moved(IObject * child_to);
private:
IObject * parent_ = nullptr;
IObject ** lhs_ = nullptr;
};
using MutationLog = std::vector<MutationLogEntry>;
/** @class GcCopyCallback
* @brief optional callback to observe individual copy operations during GC
*
* For viz
**/
class GcCopyCallback {
public:
virtual ~GcCopyCallback() = default;
virtual void notify_gc_copy(std::size_t z, const void * src_addr, const void * dest_addr,
generation src_gen, generation dest_gen) = 0;
/** invoked when added to callback set (i.e. @ref GC::GcCopyCallbackSet) **/
void notify_add_callback() {}
/** invoked when removed from callback set **/
void notify_remove_callback() {}
};
/** @class GC
* @brief generational garbage collector
*
* Works with objects of type @ref xo::Object
**/
class GC : public IAlloc {
public:
using CallbackId = xo::fn::CallbackId;
using GcCopyCallbackSet = xo::fn::UpCallbackSet<GcCopyCallback>;
using nanos = decltype(xo::qty::qty::nanosecond);
/** rebind is for typed allocators. since IAlloc is untyped,
* we want degenerate version
**/
template <typename U>
struct rebind { using other = GC; };
public:
/** create new GC instance with configuration @p config **/
explicit GC(const Config & config);
/** noncopyable **/
GC(const GC & other) = delete;
virtual ~GC();
/** create GC allocator.
*
* Initial memory consumption:
* approximately 2x @ref Config::nursery_size_ + 2x @ref Config::tenured_size_
**/
static up<GC> make(const Config & config);
/** runtime downcast **/
static GC * from(IAlloc * mm);
const Config & config() const { return config_; }
std::uint8_t nursery_polarity() const { return nursery_polarity_; }
std::uint8_t tenured_polarity() const { return tenured_polarity_; }
const GCRunstate & runstate() const { return runstate_; }
const GcStatistics & native_gc_statistics() const { return gc_statistics_; }
GcStatisticsExt get_gc_statistics() const;
const GcStatisticsHistory & gc_history() const { return gc_history_; }
/** true iff GC permitted in current state **/
bool is_gc_enabled() const { return gc_enabled_ == 0; }
/** true iff GC has been requested **/
bool is_gc_pending() const { return incr_gc_pending_ || full_gc_pending_; }
/** true iff full GC pending **/
bool is_full_gc_pending() const { return full_gc_pending_; }
/** true during (and only during) a GC cycle **/
bool gc_in_progress() const { return runstate_.in_progress(); }
/** @return pagesize (will be the same for {nursery, tenured} spaces) **/
std::size_t pagesize() const;
/** @return hugepage size (will be the same for {nursery, tenured} spaces) **/
std::size_t hugepage_z() const;
/** @return allocation portion of Nursery to-space **/
std::size_t nursery_to_allocated() const;
/** @return reserved size of Nursery to-space **/
std::size_t nursery_to_reserved() const;
/** @return committed size of Nursery to-space **/
std::size_t nursery_to_committed() const;
/** @return nursery bytes used before checkpoint **/
std::size_t nursery_before_checkpoint() const;
/** @return nursery bytes used after checkpoint **/
std::size_t nursery_after_checkpoint() const;
/** @return allocated memory range for nursery **/
std::pair<const std::byte *, const std::byte *> nursery_span(role role) const;
/** @return nursery bytes used in from-space
* (only interesting during GC copy phase, e.g. during scope of a GcCopyCallback call)
**/
std::size_t nursery_from_allocated() const;
/** @return reserved size of Tenured to-space **/
std::size_t tenured_to_reserved() const;
/** @return committed size of Tenured to-space **/
std::size_t tenured_to_committed() const;
/** @return tenured bytes used before checkpoint **/
std::size_t tenured_before_checkpoint() const;
/** @return tenured bytes used after checkpoint = promoted since last GC **/
std::size_t tenured_after_checkpoint() const;
/** @return generation to which object at @p x belongs **/
generation_result tospace_generation_of(const void * x) const;
/** @return generation to which object at @p x belongs,
* location relative to base address for that generation,
* and allocated size of that generation
* @p role chooses between to-space and from-space
**/
std::tuple<generation_result, std::size_t, std::size_t, std::size_t> location_of(role role, const void * x) const;
/** @return generation to which object at @p x belongs,
* location relative to base address for @p x,
* and allocated size of generation
**/
std::tuple<generation_result, std::size_t, std::size_t, std::size_t> tospace_location_of(const void * x) const;
/** @return generation that contains @p x, given it's in from-space **/
generation_result fromspace_generation_of(const void * x) const;
/** @return generation to which object at @p x belongs,
* location relative to base address for @p x,
* and allocated size of generation
**/
std::tuple<generation_result, std::size_t, std::size_t, std::size_t> fromspace_location_of(const void * x) const;
/** true iff from-space contains @p x **/
bool fromspace_contains(const void * x) const;
/** @return free pointer for generation @p gen, i.e. nursery or tenured space **/
std::byte * free_ptr(generation gen);
/** @return current size of (number of entries in) mutation log **/
std::size_t mlog_size() const;
/** add gc root at address @p addr . Gc will keep alive anything reachable
* from @c *addr
**/
void add_gc_root(IObject ** addr);
/** reverse the effect of previous call to @ref add_gc_root **/
void remove_gc_root(IObject ** addr);
/** convenience wrapper **/
template <typename T>
void add_gc_root_dwim(gp<T> * p) {
static_assert(std::is_convertible_v<T*, IObject*>);
this->add_gc_root(reinterpret_cast<IObject**>(p->ptr_address()));
}
template <typename T>
void remove_gc_root_dwim(gp<T> * p) {
static_assert(std::is_convertible_v<T*, IObject*>);
this->remove_gc_root(reinterpret_cast<IObject**>(p->ptr_address()));
}
/** may optionally use this to observe GC copy phase.
* Will be invoked once _per surviving object_, so not cheap.
* Intended for GC visualization.
**/
CallbackId add_gc_copy_callback(up<GcCopyCallback> fn);
/** request garbage collection.
* If GC currently disabled, collection will be deferred until the next time GC
* is in an enabled state. See @ref disable_gc and @ref enable_gc
**/
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.
*
* @return true iff GC performed
**/
bool enable_gc();
/** same as @c this->enable_gc() followed by @c this->disable_gc()
* @return true iff GC performed
**/
bool enable_gc_once();
// inherited from IAlloc..
virtual const std::string & name() const final override;
/** capacity in bytes (counting both free+allocated) for object storage.
* only counts one of {to-space, from-space},
* since one role is always held empty between collections.
**/
virtual std::size_t size() const final override;
/** for committed count both to-space and from-space **/
virtual std::size_t committed() const final override;
virtual std::size_t allocated() const final override;
virtual std::size_t available() const final override;
/** only tests to-space **/
virtual bool contains(const void * x) const final override;
virtual bool is_before_checkpoint(const void * x) const final override;
virtual std::size_t before_checkpoint() const final override;
virtual std::size_t after_checkpoint() const final override;
virtual bool debug_flag() const final override;
virtual void clear() final override;
virtual void checkpoint() final override;
/** GC bookkeeping for an assignment that modifes an Object reference.
* Whenever an @ref Object instance P contains a member variable that can refer
* to another @ref Object, then we need to involve GC to perform the assignment.
* In particular a side-effect that changes the target of such reference to Q after P
* has been promoted, may lead to a tenured->nursery cross-generational pointer.
* GC needs to know about such pointers to it can update them as part of subsequent
* incremental collections.
*
* @param parent. object with member variable being modified
* @param lhs. address of a member variable within the allocation of @p parent.
* @param rhs. new target for @p *lhs
**/
virtual void assign_member(IObject * parent, IObject ** lhs, IObject* rhs) final override;
/** evacuate @p *lhs and replace with forwarding pointer **/
virtual void forward_inplace(IObject ** lhs) final override;
/** during GC check for source objects owned by GC.
* See Object::_shallow_move.
**/
virtual bool check_owned(IObject * src) const final override;
/** queries during GC to determine if object at address @p src should move:
* - full GC -> always
* - incr GC -> if not tenured
**/
virtual bool check_move(IObject * src) const final override;
/** if src is cross-generational (or cross-checkpoint), verify that it
* is recorded in mutation log,
* given an object @p parent that contains object pointer @p lhs
**/
virtual bool check_write_barrier(const void * parent, const void * const * lhs, bool may_throw) const final;
virtual std::byte * alloc(std::size_t z) final override;
virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) final override;
private:
ArenaAlloc * nursery_to() const { return nursery(role::to_space); }
ArenaAlloc * nursery_from() const { return nursery(role::from_space); }
ArenaAlloc * tenured_to() const { return tenured(role::to_space); }
ArenaAlloc * tenured_from() const { return tenured(role::from_space); }
ArenaAlloc * nursery(role r) const { return nursery_[role2int(r)].get(); }
ArenaAlloc * tenured(role r) const { return tenured_[role2int(r)].get(); }
MutationLog * mutation_log(role r) const { return mutation_log_[role2int(r)].get(); }
/** begin GC now **/
void execute_gc(generation g);
/** cleanup phase. aux function for @ref execute_gc **/
void cleanup_phase(generation g, nanos dt);
/** 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 From/To spaces for mutation log **/
void swap_mutation_log();
/** swap roles of FromSpace/ToSpace **/
void swap_spaces(generation g);
/** scan to-space for object statistics before GC */
void capture_object_statistics(generation upto, capture_phase phase);
/** copy object **/
void copy_object(IObject ** addr, generation upto, ObjectStatistics * object_stats);
/** copy everything reachable from global gc roots **/
void copy_globals(generation g);
/** review mutation log; may discover+rescue reachable objects.
**/
void forward_mutation_log(generation upto);
/** Aux function for @ref execute_gc. Updates bookkeeping for cross-generational
* (T->N, aka xgen) and (N1->N0, aka xckp) pointers
**/
void incremental_gc_forward_mlog(ObjectStatistics * per_type_stats);
/**
* Aux function for @ref incremental_gc_forward_mlog. Calls this function until
* fixpoint.
*
* @param from_mlog incoming mutation log. Contains {xgen,xckp} pointers before GC.
* Contents of this log is consumed (+discarded) before method returns.
* @param to_mlog outgoing mutation log. Will contain {xgen,xckp} pointers after GC.
* @param defer_mlog contains log entries associated with possible garbage.
**/
void incremental_gc_forward_mlog_phase(MutationLog * from_mlog,
MutationLog * to_mlog,
MutationLog * defer_mlog,
ObjectStatistics * per_type_stats);
/** Aux function for @ref execute_gc. Updates bookkeeping for cross-generational
* (T->N, aka xgen) and (N1->N0, aka xckcp) pointers on full gc
**/
void full_gc_forward_mlog(ObjectStatistics * per_type_stats);
/**
* Aux function for @ref full_gc_forward_mlog. Calls this function until fixpoint.
*
* @param from_mlog incoming mutation log. Contains {xgen,xckp} pointers before GC.
* Contents of this log is consumed (+discarded) before method returns.
* @param to_mlog outgoing mutation log. Will contain {xgen,xckp} pointers after GC.
* @param defer_mlog contains log entries associated with possible garbage.
*
**/
void full_gc_forward_mlog_phase(MutationLog * from_mlog,
MutationLog * to_mlog,
MutationLog * defer_mlog,
ObjectStatistics * per_type_stats);
private:
/** garbage collector configuration **/
Config config_;
/** keep track of the identity of from-space and to-space.
* assist for animation (see xo-imgui/example/ex2).
* polarity alternates between 0 and 1 on each GC
**/
std::uint8_t nursery_polarity_ = 0;
std::uint8_t tenured_polarity_ = 0;
/** contains allocated objects, along with unreachable garbage to be collected.
* roles reverse after each incremental, or full, collection.
**/
std::array<up<ArenaAlloc>, role2int(role::N)> nursery_;
/** empty space, destination for objects that survive collection.
* roles reverse after each full collection.
**/
std::array<up<ArenaAlloc>, role2int(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<IObject**> gc_root_v_;
/** log cross-generational and cross-checkpoint mutations.
* These need to be adjusted on next incremental collection.
*
* mutation_log_[tospace] accumulates {xgen,xckp} pointers until
* the next GC.
*
* See GC aux functions
* @ref incremental_gc_forward_mlog
* @ref full_gc_forward_mlog
*
**/
std::array<up<MutationLog>, role2int(role::N)> mutation_log_;
/** temporary mutation log (for deferred entries) **/
up<MutationLog> defer_mutation_log_;
/** allocation/collection counters **/
GcStatistics gc_statistics_;
/** optional per-object-type counters. snapshot at beginning of collection cycle **/
std::array<ObjectStatistics, gen2int(generation::N)> object_statistics_sab_;
/** optional per-object-type counters. snapshot at end of collection cycle **/
std::array<ObjectStatistics, gen2int(generation::N)> object_statistics_sae_;
/** 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;
/** rotating per-gc statistics history **/
GcStatisticsHistory gc_history_;
/** for (optional) viz: invoke when copying individual objects **/
GcCopyCallbackSet gc_copy_cbset_;
};
} /*namespace gc*/
} /*namespace xo*/
/* end GC.hpp */

View file

@ -0,0 +1,287 @@
/** @file GcStatistics.hpp
*
* @author Roland Conybeare, Aug 2025
**/
#pragma once
#include "generation.hpp"
#include "CircularBuffer.hpp"
#include <xo/reflect/TypeDescr.hpp>
#include <xo/unit/quantity.hpp>
#include <xo/unit/quantity_iostream.hpp>
#include <xo/indentlog/print/pretty.hpp>
#include <ostream>
#include <array>
namespace xo {
namespace gc {
/** @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:
GcStatistics() = default;
/** update statistics at beginning of a GC cycle
* @param upto. nursery -> incremental collection; tenured -> full collection
* @param alloc_z. new allocations (since preceding GC)
**/
void begin_gc(generation upto,
std::size_t alloc_z);
/** 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);
/** number of collection cycles, whether full or incremental **/
std::size_t n_gc() const { return gen_v_[gen2int(generation::nursery)].n_gc_; }
/** @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;
/** total number of mutations to already-allocated objects,
* whether or not GC needs to log them.
**/
std::size_t n_mutation_ = 0;
/** total number of mutation eligible for logging (cumulative across GCs) **/
std::size_t n_logged_mutation_ = 0;
/** total number of cross-generation mutations
* (tenured->nursery when reported; cumulative across GCs) **/
std::size_t n_xgen_mutation_ = 0;
/** total number of cross-checkpoint mutations
* (N0 -> N1 when reported; cumulative across GCs)
**/
std::size_t n_xckp_mutation_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatistics & x) {
x.display(os);
return os;
}
/** @class GcStatisticsExt
* @brief extend GcStatistics for application convenience
**/
class GcStatisticsExt : public GcStatistics {
public:
GcStatisticsExt() = default;
explicit GcStatisticsExt(const GcStatistics & x) : GcStatistics{x} {}
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** current capacity of nursery generation **/
std::size_t nursery_z_ = 0;
/** current nursery survivor size **/
std::size_t nursery_before_checkpoint_z_ = 0;
/** current nursery new alloc size **/
std::size_t nursery_after_checkpoint_z_ = 0;
/** current capacity of tenured generation **/
std::size_t tenured_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatisticsExt & x) {
x.display(os);
return os;
}
/** @class GcStatisticsHistoryItem
* @brief info we want to record over time (won't have cumulative things in it)
**/
class GcStatisticsHistoryItem {
public:
using nanos = xo::qty::type::nanoseconds<std::int64_t>;
public:
GcStatisticsHistoryItem() = default;
constexpr GcStatisticsHistoryItem(std::size_t gc_seq,
generation upto,
std::size_t new_alloc_z,
std::size_t survive_z,
std::size_t promote_z,
std::size_t persist_z,
std::size_t effort_z,
std::size_t garbage0_z,
std::size_t garbage1_z,
std::size_t garbageN_z,
nanos dt,
std::size_t sum_effort_z,
std::size_t sum_garbage_z)
: gc_seq_{gc_seq},
upto_{upto},
new_alloc_z_{new_alloc_z},
survive_z_{survive_z},
promote_z_{promote_z},
persist_z_{persist_z},
effort_z_{effort_z},
garbage0_z_{garbage0_z},
garbage1_z_{garbage1_z},
garbageN_z_{garbageN_z},
dt_{dt},
sum_effort_z_{sum_effort_z},
sum_garbage_z_{sum_garbage_z}
{}
constexpr GcStatisticsHistoryItem(const GcStatisticsHistoryItem &) = default;
std::size_t garbage_z() const { return garbage0_z_ + garbage1_z_ + garbageN_z_; }
float efficiency() const {
std::size_t gz = this->garbage_z();
return gz / static_cast<float>(effort_z_ + gz);
}
/** lifetime byte-weighted average collection efficiency. Always in [0.0, 1.0] **/
float average_efficiency() const {
return sum_garbage_z_ / static_cast<float>(sum_effort_z_ + sum_garbage_z_);
}
/** collection rate, in bytes/sec **/
float collection_rate() const;
GcStatisticsHistoryItem & operator=(const GcStatisticsHistoryItem & x) {
gc_seq_ = x.gc_seq_;
upto_ = x.upto_;
new_alloc_z_ = x.new_alloc_z_;
survive_z_ = x.survive_z_;
promote_z_ = x.promote_z_;
persist_z_ = x.persist_z_;
effort_z_ = x.effort_z_;
garbage0_z_ = x.garbage0_z_;
garbage1_z_ = x.garbage1_z_;
garbageN_z_ = x.garbageN_z_;
this->dt_.scale_ = x.dt_.scale_;
sum_effort_z_ = x.sum_effort_z_;
sum_garbage_z_ = x.sum_garbage_z_;
return *this;
}
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** sequence number for collection being reported **/
std::size_t gc_seq_ = 0;
/** type of GC that generated this record **/
generation upto_;
/** #of bytes new allocation **/
std::size_t new_alloc_z_ = 0;
/** #of bytes surviving their first collection (i.e. N0->N1) **/
std::size_t survive_z_ = 0;
/** #of bytes promoted to tenured.
* Comprises all objects surviving their 2nd collection (i.e. N1->T)
**/
std::size_t promote_z_ = 0;
/** #of bytes surviving 3rd of later collection **/
std::size_t persist_z_ = 0;
/** #of bytes copied **/
std::size_t effort_z_ = 0;
/** #of bytes garbage from N0 (i.e. survived 0 GCs) **/
std::size_t garbage0_z_ = 0;
/** #of bytes garbage from N1 (i.e. survived 1 GCs) **/
std::size_t garbage1_z_ = 0;
/** #of bytes garbage from T (i.e. survived 2+ GCs) **/
std::size_t garbageN_z_ = 0;
/** elapsed time for this GC (see @ref GC::execute_gc) **/
nanos dt_;
// ----- cumulative statistics -----
/** sum (in bytes) copied by collections since inception **/
std::size_t sum_effort_z_ = 0;
/** sum (in bytes) of garbage collected since inception **/
std::size_t sum_garbage_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatisticsHistoryItem & x) {
x.display(os);
return os;
}
using GcStatisticsHistory = CircularBuffer<GcStatisticsHistoryItem>;
} /*namespace gc*/
namespace print {
template <>
struct ppdetail<xo::gc::PerGenerationStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::PerGenerationStatistics &);
};
template<>
struct ppdetail<xo::gc::GcStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatistics &);
};
template<>
struct ppdetail<xo::gc::GcStatisticsExt> {
static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatisticsExt &);
};
template<>
struct ppdetail<xo::gc::GcStatisticsHistoryItem> {
static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatisticsHistoryItem &);
};
} /*namespace print*/
} /*namespace xo*/
/* end GcStatistics.hpp */

View file

@ -0,0 +1,100 @@
/* file ListAlloc.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#pragma once
#include "IAlloc.hpp"
#include "ObjectStatistics.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.
*
* TODO: reserve address space using mmap,
* but don't commit until alloc requires it.
**/
class ListAlloc : public IAlloc {
public:
ListAlloc(std::unique_ptr<ArenaAlloc> hd,
ArenaAlloc * marked,
std::size_t cz, std::size_t nz, std::size_t tz,
bool debug_flag);
~ListAlloc();
static up<ListAlloc> make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag);
/** page size used by underlying ArenaAlloc **/
std::size_t page_size() const;
/** hugepage size used by underlying ArenaAlloc **/
std::size_t hugepage_z() const;
/** reset to have at least @p z bytes of storage **/
bool reset(std::size_t z);
/** expand bucket list to accomodate a request of size @p z **/
bool expand(std::size_t z, const std::string & name);
/** current free pointer **/
std::byte * free_ptr() const;
/** scan space (must not contain forwarding pointers, because loses size info)
* + gather stats by object type
*
* See @ref Object::self_tp
**/
void capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const;
// inherited from IAlloc..
virtual const std::string & name() const final override;
virtual std::size_t size() const final override;
virtual std::size_t committed() const final override;
virtual std::size_t available() const final override;
virtual std::size_t allocated() const final override;
virtual bool contains(const void * x) const final override;
virtual bool is_before_checkpoint(const void * x) const final override;
virtual std::size_t before_checkpoint() const final override;
virtual std::size_t after_checkpoint() const final override;
virtual bool debug_flag() const final override;
virtual void clear() final override;
virtual void checkpoint() final override;
virtual std::byte * alloc(std::size_t z) final override;
private:
/** **/
std::size_t start_z_ = 0;
/** 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_;
/** size of current arena @ref hd_ **/
std::size_t current_z_ = 0;
/** if @ref hd_ fills, size of next arena to allocate **/
std::size_t next_z_ = 0;
/** total size of @ref hd_ + contents of @ref full_l_ **/
std::size_t total_z_ = 0;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
} /*namespace gc*/
} /*namespace xo*/
/* end ListAlloc.hpp */

View file

@ -0,0 +1,170 @@
/* Object.hpp
*
* author: Roland Conybeare, Jul 2025
*/
#pragma once
#include "xo/allocutil/IObject.hpp"
#include "xo/reflect/TaggedPtr.hpp"
#include "xo/allocutil/ObjectVisitor.hpp"
#include "xo/allocutil/gc_ptr.hpp"
#include <concepts>
#include <cstdint>
namespace xo {
namespace gc {
class IAlloc;
class GC;
class ObjectStatistics;
};
/** Root class for all xo GC-collectable objects.
*
* Design notes:
* 1. xo::IObject -> xo-allocutil header-only library
* xo::Object -> xo-alloc ordinary library
* 2. 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.
* 3. Could adapt a gc-aware XO library (such as xo-ordinaltree)
* to a non-xo garbage collector.
* Would still need to use xo::IObject and xo::gc::gc_allocator_traits,
* but not necessarily xo::Object
* 4. Would be feasible to relax the must-inherit-from-Object constraint
* by having GC use its own wrapper, at cost of an extra layer of indirection
**/
class Object : public IObject {
public:
using TaggedPtr = xo::reflect::TaggedPtr;
public:
static gp<Object> from(gp<IObject> x) {
return dynamic_cast<Object*>(x.ptr());
}
virtual ~Object() noexcept = default;
/** memory allocator for objects. Likely this will be a GC instance,
* but simple arena also supported.
*
* Load-bearing for .assign_member()
**/
static gc::IAlloc * mm;
/** assign value @p rhs to member @p *lhs of @p parent.
* if assignment creates a cross-generational or cross-checkpoint pointer,
* add mutation log entry.
*
* DEPRECATED. prefer IObject::_gc_assign_member, for explicit alloc
**/
template <typename T>
static void assign_member(gp<IObject> parent, gp<T> * lhs, gp<IObject> rhs);
/** 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. allocator (poassibly garbage collector)
*/
static IObject * _forward(IObject * src, gc::IAlloc * gc);
template <typename T>
static void _forward_inplace(T ** src_addr, gc::IAlloc * gc) {
IObject * fwd = _forward(*src_addr, gc);
*src_addr = reinterpret_cast<T *>(fwd);
}
template <typename T>
static void _forward_inplace(gp<T> & src, gc::IAlloc * gc) {
_forward_inplace<T>(src.ptr_address(), gc);
}
/** 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
*
* Evacuate reachable object graph rooted at @p src to to-space.
* On return all objects reachable from @p src are white
*
* @param src address of object to evacuate
* @param gc garbage collector
* @param stats per-object-type GC statistics
**/
static IObject * _deep_move(IObject * src, gc::GC * gc, gc::ObjectStatistics * stats);
/** copy @p src to to-space. Overwrite original with forwarding pointer to new location.
* return the new location
**/
static IObject * _shallow_move(IObject * src, gc::IAlloc * gc);
// Reflection support
/** tagged pointer with runtime type information
**/
virtual TaggedPtr self_tp() const;
/** print on stream @p os **/
virtual void display(std::ostream & os) const;
// Inherited from IObject..
//virtual bool _is_forwarded() const override { return false; }
//virtual IObject * _offset_destination(IObject * src) const override { return src; };
virtual void _forward_to(IObject * dest) override;
//virtual IObject * _destination() override { return nullptr; }
virtual std::size_t _shallow_size() const override = 0;
virtual IObject * _shallow_copy(gc::IAlloc * gc) const override = 0;
virtual std::size_t _forward_children(gc::IAlloc * gc) override = 0;
};
static_assert(std::is_destructible_v<Object>, "Object must be destructible");
static_assert(std::is_nothrow_destructible_v<Object>, "Object must be noexcept destructible");
template <typename T>
void
Object::assign_member(gp<IObject> parent, gp<T> * lhs, gp<IObject> rhs)
{
Object::mm->assign_member(reinterpret_cast<IObject *>(parent.ptr()),
reinterpret_cast<IObject **>(lhs->ptr_address()),
reinterpret_cast<IObject *>(rhs.ptr()));
}
namespace gc {
template <typename T>
class ObjectVisitor<gp<T>> {
public:
static void forward_children(gp<T> & target,
IAlloc * gc)
{
Object::_forward_inplace(target, gc);
}
};
}
std::ostream &
operator<< (std::ostream & os, gp<Object> x);
} /*namespace xo*/
void * operator new (std::size_t z, const xo::Cpof & copy);
/* end Object.hpp */

View file

@ -0,0 +1,87 @@
/* file ObjectStatistics.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#pragma once
#include "xo/indentlog/print/pretty.hpp"
#include <vector>
#include <cstdint>
namespace xo {
namespace reflect { class TypeDescrBase; }
namespace gc {
enum class capture_phase {
/** snapshot-at-beginning **/
sab,
/** snapshot-at-end **/
sae,
};
/** @class PerObjectTypeStatistics
* @brief statistics for a particular object type
*
* Gathered for each leaf type descended from xo::obj::Object.
* See @ref xo::obj::Object::self_tp
*
* See @ref GC::capture_object_statistics
* (gathers @ref scanned_n_, @ref scanned_z_)
**/
struct PerObjectTypeStatistics {
using TypeDescr = xo::reflect::TypeDescrBase const *;
void display(std::ostream & os) const;
/** stats here are for objects of this type **/
TypeDescr td_ = nullptr;
/** number of objects scanned **/
std::size_t scanned_n_ = 0;
/** number of bytes scanned **/
std::size_t scanned_z_ = 0;
/** number of objects surviving **/
std::size_t survive_n_ = 0;
/** number of bytes from surviving objects **/
std::size_t survive_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const PerObjectTypeStatistics & x) {
x.display(os);
return os;
}
/** @class ObjectStatistics
* @brief placeholder for type-driven allocation statistics
*
* Passed to @ref Object::deep_move for example
**/
class ObjectStatistics {
public:
void display(std::ostream & os) const;
/** per-object-type statistics, indexed by TypeId **/
std::vector<PerObjectTypeStatistics> per_type_stats_v_;
};
inline std::ostream & operator<< (std::ostream & os, const ObjectStatistics & x) {
x.display(os);
return os;
}
} /*namespace gc*/
namespace print {
template <>
struct ppdetail<xo::gc::PerObjectTypeStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::PerObjectTypeStatistics &);
};
template <>
struct ppdetail<xo::gc::ObjectStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::ObjectStatistics &);
};
} /*namespace print*/
} /*namespace xo*/
/* end ObjectStatistics.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,54 @@
/* generation.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#pragma once
#include <ostream>
#include <cstdint>
#include <cassert>
namespace xo {
namespace gc {
enum class generation {
nursery,
tenured,
N
};
constexpr std::size_t gen2int(generation x) { return static_cast<std::size_t>(x); }
const char * gen2str(generation x);
inline std::ostream & operator<<(std::ostream & os, generation x) {
os << gen2str(x);
return os;
}
enum class generation_result {
nursery,
tenured,
not_found
};
inline generation valid_genresult2gen(generation_result x) {
assert(x != generation_result::not_found);
if (x == generation_result::nursery)
return generation::nursery;
else
return generation::tenured;
}
const char * genresult2str(generation_result x);
inline std::ostream & operator<<(std::ostream & os, generation_result x) {
os << genresult2str(x);
return os;
}
} /*namespace gc*/
} /*namespace xo*/
/* end generation.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

@ -0,0 +1,426 @@
/* file ArenaAlloc.cpp
*
* author: Roland Conybeare
*/
#include "ArenaAlloc.hpp"
#include "Object.hpp"
#include "ObjectStatistics.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/tag.hpp"
#include <sys/mman.h>
#include <unistd.h> // for getpagesize() on OSX
#include <cassert>
namespace xo {
using std::byte;
namespace gc {
namespace {
/* alignment better be a power of 2 */
std::size_t
align_lub(std::size_t x, std::size_t align)
{
/* e.g:
* align = 4096, x%align = 100 -> dx = 3996
* align = 4096, x%align = 0 -> dx = 0
*/
std::size_t dx = (align - (x % align)) % align;
return x + dx;
}
}
ArenaAlloc::ArenaAlloc(const std::string & name,
std::size_t z,
bool debug_flag)
{
scope log(XO_DEBUG(debug_flag), xtag("name", name));
constexpr size_t c_hugepage_z = 2 * 1024 * 1024;
this->name_ = name;
this->page_z_ = getpagesize();
this->hugepage_z_ = c_hugepage_z;
// 1. need k pagetable entries where k is lub {k | k * .page_z >= z}
// 2. base will be aligned with .page_z but likely not with .hugepage_z
// 3. bad to have misalignment, because misaligned {prefix, suffix} of [base, base+z)
// will use 4k pages instead of 2mb pages
//
// strategy:
// 4. round up z to multiple of c_hugepage_z
// 5. over-request so reserved range contains an aligned subrange of size z
// 6. unmap misaligned prefix
// 7. unmap misaligned suffix.
// 8. enable huge pages for now-aligned remainder of reserved range
//
// Z. note: rejecting inferior MAP_HUGETLB|MAP_HUGE_2MB flags on ::mmap here:
// Za. requires previously-reserved memory in /proc/sys/vm/nr_hugepages
// Zb. reserved pages permenently resident in RAM, never swapped
// Zc. memory cost incurred even if no application is using said pages
z = align_lub(z, c_hugepage_z); // 4.
// 5.
byte * base = reinterpret_cast<byte *>(::mmap(nullptr,
z + c_hugepage_z,
PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0));
log && log("acquired memory [lo,hi) using mmap",
xtag("lo", base),
xtag("z", z),
xtag("hi", reinterpret_cast<byte *>(base) + z));
if (base == MAP_FAILED) {
throw std::runtime_error(tostr("ArenaAlloc: uncommitted allocation failed",
xtag("size", z)));
}
byte * aligned_base = reinterpret_cast<byte *>(align_lub(reinterpret_cast<size_t>(base),
c_hugepage_z));
assert(reinterpret_cast<size_t>(aligned_base) % c_hugepage_z == 0);
assert(aligned_base >= base);
assert(aligned_base < base + c_hugepage_z);
if (base < aligned_base) {
size_t prefix = aligned_base - base;
::munmap(base, prefix); // 6.
}
byte * aligned_hi = aligned_base + z;
byte * hi = base + z + c_hugepage_z;
if (aligned_hi < hi) {
size_t suffix = hi - aligned_hi;
::munmap(aligned_hi, suffix); // 7.
}
#ifdef __linux__
::madvise(aligned_base, z, MADV_HUGEPAGE); // 8.
#endif
// TODO: for OSX -> need something else here.
// MAP_ALIGNED_SUPER with mmap() and/or
// use mach_vm_allocate()
//
this->lo_ = aligned_base;
this->committed_z_ = 0;
this->checkpoint_ = lo_;
this->free_ptr_ = lo_;
this->limit_ = lo_;
this->hi_ = lo_ + z;
this->debug_flag_ = debug_flag;
if (!lo_) {
throw std::runtime_error(tostr("ArenaAlloc: allocation failed",
xtag("size", z)));
}
log && log(xtag("lo", (void*)lo_),
xtag("page_z", page_z_),
xtag("hugepage_z", hugepage_z_));
}
ArenaAlloc::~ArenaAlloc()
{
scope log(XO_DEBUG(debug_flag_));
// hygiene..
if (lo_) {
log && log("unmap [lo,hi)", xtag("lo", lo_), xtag("z", hi_ - lo_), xtag("hi", hi_));
::munmap(lo_, hi_ - lo_);
}
// could use this as fallback if we dropped the uncommitted technique
//delete [] this->lo_;
this->lo_ = nullptr;
this->committed_z_ = 0;
this->checkpoint_ = nullptr;
this->free_ptr_ = nullptr;
this->limit_ = nullptr;
this->hi_ = nullptr;
this->debug_flag_ = false;
}
up<ArenaAlloc>
ArenaAlloc::make(const std::string & name,
std::size_t z, bool debug_flag)
{
return up<ArenaAlloc>(new ArenaAlloc(name,
z, debug_flag));
}
bool
ArenaAlloc::expand(size_t offset_z)
{
scope log(XO_DEBUG(debug_flag_), xtag("offset_z", offset_z), xtag("committed_z", committed_z_));
if (offset_z <= committed_z_) {
log && log("trivial success, offset within committed range",
xtag("offset_z", offset_z),
xtag("committed_z", committed_z_));
return true;
}
if (lo_ + offset_z > hi_) {
throw std::runtime_error(tostr("ArenaAlloc::expand: requested size exceeds reserved size",
xtag("requested", offset_z), xtag("reserved", reserved())));
}
/*
* pre:
*
* _______________...................................
* ^ ^ ^
* lo limit hi
*
* < committed_z >
* <----------offset_z----------->
* > <- z: 0 <= z < hugepage_z
* <---------aligned_offset_z--------->
* <--- add_commit_z -->
*
* post:
* ____________________________________..............
* ^ ^ ^
* lo limit hi
*
*/
std::size_t aligned_offset_z = align_lub(offset_z, hugepage_z_);
std::byte * commit_start = lo_ + committed_z_;
std::size_t add_commit_z = aligned_offset_z - committed_z_;
assert(limit_ == lo_ + committed_z_);
log && log(xtag("aligned_offset_z", aligned_offset_z),
xtag("add_commit_z", add_commit_z));
log && log("expand committed range",
xtag("commit_start", commit_start),
xtag("add_commit_z", add_commit_z),
xtag("commit_end", commit_start + add_commit_z));
if (::mprotect(commit_start, add_commit_z, PROT_READ | PROT_WRITE) != 0) {
throw std::runtime_error(tostr("ArenaAlloc::expand: commit failure",
xtag("committed_z", committed_z_),
xtag("add_commit_z", add_commit_z)));
}
this->committed_z_ = aligned_offset_z;
this->limit_ = this->lo_ + committed_z_;
assert(committed_z_ % hugepage_z_ == 0);
assert(reinterpret_cast<size_t>(limit_) % hugepage_z_ == 0);
return true;
}
void
ArenaAlloc::set_free_ptr(std::byte * x)
{
assert(lo_ <= x);
assert(x < limit_);
if (lo_ <= x && x < limit_) {
this->free_ptr_ = x;
if (checkpoint_ > free_ptr_)
this->checkpoint_ = free_ptr_;
} else {
throw std::runtime_error(tostr("LinearAllog::set_free_ptr(x): expected lo <= x < limit",
xtag("lo", lo_), xtag("x", x), xtag("limit", limit_)));
}
}
std::pair<bool, std::size_t>
ArenaAlloc::location_of(const void * x) const
{
if ((lo_ <= x) && (x < hi_)) {
return std::make_pair(true, reinterpret_cast<const std::byte *>(x) - lo_);
} else {
return std::make_pair(false, 0);
}
}
void
ArenaAlloc::reset(std::size_t need_z) {
this->clear();
this->expand(need_z);
}
void
ArenaAlloc::capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const
{
scope log(XO_DEBUG(debug_flag_),
xtag("name", name_),
xtag("capacity", limit_ - lo_),
xtag("alloc", free_ptr_ - lo_),
xtag("lo", (void*)lo_),
xtag("free_ptr", (void*)free_ptr_));
using xo::reflect::TaggedPtr;
std::byte * p = lo_;
while (p < free_ptr_) {
log && log(xtag("p", (void *)p));
Object * obj = reinterpret_cast<Object *>(p);
TaggedPtr tp = obj->self_tp();
std::size_t z = obj->_shallow_size();
std::uint32_t id = tp.td()->id().id();
log && log(xtag("obj", (void*)obj),
xtag("z", z),
xtag("typeid", id));
if (p_dest->per_type_stats_v_.size() < id + 1)
p_dest->per_type_stats_v_.resize(id + 1);
PerObjectTypeStatistics & dest = p_dest->per_type_stats_v_.at(id);
dest.td_ = tp.td();
log && log(xtag("td", tp.td()->short_name()));
switch (phase) {
case capture_phase::sab:
++dest.scanned_n_;
dest.scanned_z_ += z;
break;
case capture_phase::sae:
++dest.survive_n_;
dest.survive_z_ += z;
break;
}
p += z;
}
assert(p == free_ptr_);
}
const std::string &
ArenaAlloc::name() const {
return name_;
}
std::size_t
ArenaAlloc::size() const {
return limit_ - lo_;
}
std::size_t
ArenaAlloc::committed() const {
return committed_z_;
}
std::size_t
ArenaAlloc::available() const {
return limit_ - free_ptr_;
}
std::size_t
ArenaAlloc::allocated() const {
return free_ptr_ - lo_;
}
bool
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
ArenaAlloc::before_checkpoint() const
{
return checkpoint_ - lo_;
}
std::size_t
ArenaAlloc::after_checkpoint() const
{
return free_ptr_ - checkpoint_;
}
bool
ArenaAlloc::check_owned(IObject * src) const
{
byte * addr = reinterpret_cast<byte *>(src);
return (lo_ <= addr) && (addr < hi_);
}
bool
ArenaAlloc::debug_flag() const
{
return debug_flag_;
}
void
ArenaAlloc::clear()
{
this->set_free_ptr(lo_);
//this->limit_ = hi_;
}
void
ArenaAlloc::checkpoint()
{
this->checkpoint_ = this->free_ptr_;
}
std::byte *
ArenaAlloc::alloc(std::size_t z0)
{
scope log(XO_DEBUG(debug_flag_));
/* word size for alignment */
constexpr uint32_t c_bpw = sizeof(std::uintptr_t);
(void)c_bpw;
std::uintptr_t free_u64 = reinterpret_cast<std::uintptr_t>(free_ptr_);
(void)free_u64;
assert(free_u64 % c_bpw == 0ul);
std::uint32_t dz = alloc_padding(z0);
std::size_t z1 = z0 + dz;
assert(z1 % c_bpw == 0ul);
this->expand(this->allocated() + z1);
std::byte * retval = this->free_ptr_;
log && log(xtag("self", name_),
xtag("z0", z0),
xtag("+pad", dz),
xtag("z1", z1),
xtag("size", this->size()),
xtag("avail", this->available()));
this->free_ptr_ += z1;
return retval;
}
} /*namespace gc*/
} /*namespace xo*/
/* end ArenaAlloc.cpp */

View file

@ -0,0 +1,57 @@
/** @file Blob.cpp
*
* @author Roland Conybeare, Nov 2025
**/
#include "Blob.hpp"
#include "xo/reflect/Reflect.hpp"
#include "xo/allocutil/IAlloc.hpp"
namespace xo {
using xo::reflect::Reflect;
using xo::reflect::TaggedPtr;
gp<Blob>
Blob::make(gc::IAlloc * mm, std::size_t z) {
std::byte * mem = mm->alloc(sizeof(Blob) + z);
return new (mem) Blob(z);
}
TaggedPtr
Blob::self_tp() const
{
return Reflect::make_tp(const_cast<Blob*>(this));
}
void
Blob::display(std::ostream & os) const
{
os << "<blob" << xtag("z", z_) << ">";
}
std::size_t
Blob::_shallow_size() const {
return sizeof(Blob) + z_;
}
Object *
Blob::_shallow_copy(gc::IAlloc * mm) const {
Cpof cpof(mm, this);
std::byte * cp_mem = mm->alloc_gc_copy(sizeof(Blob) + z_, this);
gp<Blob> copy = new (cp_mem) Blob(z_);
::memcpy(copy->data(), data_, z_);
return copy.get();
}
std::size_t
Blob::_forward_children(gc::IAlloc *)
{
return this->_shallow_size();
}
}
/* end Blob.cpp */

View file

@ -0,0 +1,24 @@
# alloc/CMakeLists.txt
set(SELF_LIB xo_alloc)
set(SELF_SRCS
ArenaAlloc.cpp
ListAlloc.cpp
GC.cpp
GcStatistics.cpp
ObjectStatistics.cpp
Object.cpp
Blob.cpp
Forwarding1.cpp
generation.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
xo_headeronly_dependency(${SELF_LIB} xo_allocutil)
# xo-unit used for time measurement
xo_headeronly_dependency(${SELF_LIB} xo_unit)
xo_dependency(${SELF_LIB} indentlog)
xo_dependency(${SELF_LIB} reflect)
xo_headeronly_dependency(${SELF_LIB} callback)
#end CMakeLists.txt

View file

@ -0,0 +1,79 @@
/* file Forwarding1.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Forwarding1.hpp"
#include "xo/reflect/Reflect.hpp"
#include <cstddef>
#include <cassert>
namespace xo {
using xo::reflect::Reflect;
using xo::reflect::TaggedPtr;
namespace obj {
Forwarding1::Forwarding1(gp<IObject> dest)
: dest_{dest}
{}
TaggedPtr
Forwarding1::self_tp() const
{
return Reflect::make_tp(const_cast<Forwarding1*>(this));
}
void
Forwarding1::display(std::ostream & os) const
{
os << "<fwd"
<< xtag("dest", (void*)dest_.ptr())
// << xtag("dest-td", dest_->self_tp().td()->short_name())
<< ">";
}
IObject *
Forwarding1::_offset_destination(IObject * src) const
{
intptr_t offset = src - static_cast<const IObject *>(this);
return dest_.ptr() + offset;
}
IObject *
Forwarding1::_destination() {
return dest_.ptr();
}
// LCOV_EXCL_START
std::size_t
Forwarding1::_shallow_size() const {
assert(false);
return 0;
}
// LCOV_EXCL_STOP
// LCOV_EXCL_START
IObject *
Forwarding1::_shallow_copy(gc::IAlloc *) const {
/* forwarding objects are never copied */
assert(false);
return nullptr;
}
// LCOV_EXCL_STOP
// LCOV_EXCL_START
std::size_t
Forwarding1::_forward_children(gc::IAlloc *) {
/* forwarding objects are never traced */
assert(false);
return 0;
}
// LCOV_EXCL_STOP
} /*namespace obj*/
} /*namespace xo*/
/* end Forwarding1.cpp */

1526
xo-alloc/src/alloc/GC.cpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,214 @@
/* GcStatistics.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "GcStatistics.hpp"
#include "xo/indentlog/print/pretty_vector.hpp"
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);
//++n_gc_;
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"
<< xrtag("used", used_z_)
<< xrtag("n_gc", n_gc_)
<< xrtag("new_alloc_z", new_alloc_z_)
<< xrtag("scanned_z", scanned_z_)
<< xrtag("survive_z", survive_z_)
<< xrtag("promote_z", promote_z_)
<< ">";
}
void
GcStatistics::begin_gc(generation upto,
std::size_t new_alloc)
{
++(this->gen_v_[static_cast<std::size_t>(upto)].n_gc_);
this->total_allocated_ += new_alloc;
this->total_promoted_sab_ = total_promoted_;
}
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"
<< xrtag("gen_v", gen_v_)
<< xrtag("total_allocated", total_allocated_)
<< xrtag("total_promoted_sab", total_promoted_sab_)
// total_promoted
// n_mtuation
// n_logged_mutation
// n_xgen_mutation
// n_xckp_mutation
// << xtag("per_type_stats", per_type_stats_)
<< ">";
}
void
GcStatisticsExt::display(std::ostream & os) const
{
os << "<GcStatisticsExt"
<< xrtag("gen_v", gen_v_)
<< xrtag("total_allocated", total_allocated_)
<< xrtag("total_promoted_sab", total_promoted_)
<< xrtag("nursery_z", nursery_z_)
<< xrtag("nursery_before_ckp_z", nursery_before_checkpoint_z_)
<< xrtag("nursery_after_ckp_z", nursery_after_checkpoint_z_)
<< xrtag("tenured_z", tenured_z_)
<< xrtag("n_mutation", n_mutation_)
<< xrtag("n_logged_mutation", n_logged_mutation_)
<< xrtag("n_xgen_mutation", n_xgen_mutation_)
<< xrtag("n_xckp_mutation", n_xckp_mutation_)
// << xtag("per_type_stats", per_type_stats_)
<< ">";
}
float
GcStatisticsHistoryItem::collection_rate() const {
using namespace xo::qty::qty;
float gz = this->garbage_z();
auto dt_nanos = this->dt_.with_repr<float>();
auto dt_sec = dt_nanos.rescale_ext<xo::qty::u::second>();
auto rate = gz / dt_sec;
float retval = rate.scale();
//scope log(XO_DEBUG(true));
//log && log(xtag("gz", gz), xtag("dt_sec", dt_sec), xtag("rate", rate), xtag("rate/sec", retval));
return retval;
}
void
GcStatisticsHistoryItem::display(std::ostream & os) const
{
os << "<GcStatisticsHistoryItem"
<< xrtag("upto", upto_)
<< xrtag("survive_z", survive_z_)
<< xrtag("promote_z", promote_z_)
<< xrtag("persist_z", persist_z_)
<< xrtag("effort_z", effort_z_)
<< xrtag("garbage0_z", garbage0_z_)
<< xrtag("garbage1_z", garbage1_z_)
<< xrtag("garbageN_z", garbageN_z_)
<< xrtag("dt", dt_)
<< ">";
}
} /*namespace gc*/
namespace print {
bool
ppdetail<xo::gc::PerGenerationStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::PerGenerationStatistics & x)
{
return ppii.pps()->pretty_struct(ppii,
"PerGenerationStatistics",
refrtag("used_z", x.used_z_),
refrtag("n_gc", x.n_gc_),
refrtag("new_alloc_z", x.new_alloc_z_),
refrtag("scanned_z", x.scanned_z_),
refrtag("survive_z", x.survive_z_),
refrtag("promote_z", x.promote_z_)
);
}
bool
ppdetail<xo::gc::GcStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::GcStatistics & x)
{
return ppii.pps()->pretty_struct(ppii,
"GcStatistics",
refrtag("gen_v", x.gen_v_),
refrtag("total_allocated", x.total_allocated_),
refrtag("total_promoted_sab", x.total_promoted_sab_),
refrtag("total_promoted", x.total_promoted_),
refrtag("n_mutation", x.n_mutation_),
refrtag("n_logged_mutation", x.n_logged_mutation_),
refrtag("n_xgen_mutation", x.n_xgen_mutation_),
refrtag("n_xckp_mutation", x.n_xckp_mutation_)
);
}
bool
ppdetail<xo::gc::GcStatisticsExt>::print_pretty(const ppindentinfo & ppii,
const xo::gc::GcStatisticsExt & x)
{
return ppii.pps()->pretty_struct(ppii,
"GcStatisticsExt",
refrtag("gen_v", x.gen_v_),
refrtag("total_allocated", x.total_allocated_),
refrtag("total_promoted_sab", x.total_promoted_sab_),
refrtag("total_promoted", x.total_promoted_),
refrtag("n_mutation", x.n_mutation_),
refrtag("n_logged_mutation", x.n_logged_mutation_),
refrtag("n_xgen_mutation", x.n_xgen_mutation_),
refrtag("n_xckp_mutation", x.n_xckp_mutation_),
refrtag("nursery_z", x.nursery_z_),
refrtag("nursery_before_checkpoint_z", x.nursery_before_checkpoint_z_),
refrtag("nursery_after_checkpoint_z", x.nursery_after_checkpoint_z_),
refrtag("tenured_z", x.tenured_z_));
}
bool
ppdetail<xo::gc::GcStatisticsHistoryItem>::print_pretty(const ppindentinfo & ppii,
const xo::gc::GcStatisticsHistoryItem & x)
{
return ppii.pps()->pretty_struct(ppii,
"GcStatisticsHistoryItem",
refrtag("upto", gen2str(x.upto_)),
refrtag("survive_z", x.survive_z_),
refrtag("promote_z", x.promote_z_),
refrtag("persist_z", x.persist_z_),
refrtag("effort_z", x.effort_z_),
refrtag("garbage0_z", x.garbage0_z_),
refrtag("garbage1_z", x.garbage1_z_),
refrtag("garbageN_z", x.garbageN_z_),
refrtag("dt", x.dt_));
}
} /*namespace print*/
} /*namespace xo*/
/* end GcStatistics.cpp */

View file

@ -0,0 +1,400 @@
/* file ListAlloc.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "ListAlloc.hpp"
#include "ArenaAlloc.hpp"
#include "xo/indentlog/scope.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 debug_flag)
: start_z_{cz},
hd_{std::move(hd)},
marked_{marked},
full_l_{},
current_z_{cz},
next_z_{nz},
total_z_{tz},
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,
cz, debug_flag)};
if (!hd)
return nullptr;
ArenaAlloc * marked = nullptr;
up<ListAlloc> retval{new ListAlloc(std::move(hd),
marked,
cz, nz, cz,
debug_flag)};
return retval;
}
void
ListAlloc::capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const
{
hd_->capture_object_statistics(phase, p_dest);
for (const auto & arena : full_l_)
arena->capture_object_statistics(phase, p_dest);
}
const std::string &
ListAlloc::name() const {
if (hd_) {
return hd_->name();
}
static std::string s_default_name = "ListAlloc";
return s_default_name;
}
std::size_t
ListAlloc::page_size() const {
return hd_->page_size();
}
std::size_t
ListAlloc::hugepage_z() const {
return hd_->hugepage_z();
}
std::size_t
ListAlloc::size() const {
return total_z_;
}
std::size_t
ListAlloc::committed() const {
std::size_t z = 0;
if (hd_)
z += hd_->committed();
for (const auto & a : full_l_)
z += a->committed();
return 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 true;
if (marked_ && marked_->contains(x))
return marked_->is_before_checkpoint(x);
/*
* 1. allocs in full_l_ appear in oldest-to-youngest order
* 2. allocators that appear before marked_ in full_l_ count as 'before checkpoint'
* 3. allocators that appear after marked_ in full_l_ count as 'after checkpoint'
*/
bool older_than_marked = true;
for (const auto & alloc : full_l_) {
if (older_than_marked) {
if (alloc.get() == marked_) {
/* nothing else to test on this iteration,
* already checked .marked_ specifically
*/
break;
} else {
/* before checkpoint */
if (alloc->contains(x))
return true;
}
}
}
return false;
}
std::size_t
ListAlloc::before_checkpoint() const
{
scope log(XO_DEBUG(false && debug_flag_), xtag("marked", marked_ ? marked_->name() : ""));
if (marked_) {
if (full_l_.empty()) {
assert(marked_ == hd_.get());
return marked_->before_checkpoint();
} else {
std::size_t z = 0;
/* control here: .marked & .full_l non-empty. */
if (hd_.get() == marked_) {
z += hd_->before_checkpoint();
/* anything in .full_l is 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
*/
/* full_l always in increasing time order: oldest-to-youngest order */
size_t i_alloc = 0;
for (const auto & alloc : full_l_) {
log && log(xtag("i_alloc", i_alloc),
xtag("alloc", alloc->name()),
xtag("z", z));
if (alloc.get() == marked_) {
log && log("marked", xtag("+z", marked_->before_checkpoint()));
z += marked_->before_checkpoint();
break;
} else {
log && log("older than marked", xtag("+z", alloc->allocated()));
z += alloc->allocated();
}
++i_alloc;
}
}
return z;
}
} else {
/* count *everything* allocated */
return this->allocated();
}
}
std::size_t
ListAlloc::after_checkpoint() const
{
scope log(XO_DEBUG(false && debug_flag_), xtag("marked", marked_ ? marked_->name() : ""));
if (!marked_)
return 0;
if (full_l_.empty()) {
assert(marked_ == hd_.get());
return marked_->after_checkpoint();
}
bool older_than_marked = true;
std::size_t z = 0;
std::size_t i_alloc = 0;
for (const auto & alloc : full_l_) {
log && log(xtag("i_alloc", i_alloc),
xtag("alloc", alloc->name()),
xtag("z", z));
if (older_than_marked) {
if (alloc.get() == marked_) {
log && log("marked", xtag("+z", marked_->after_checkpoint()));
older_than_marked = false;
z += marked_->after_checkpoint();
}
} else {
/* younger than marked */
log && log("younger", xtag("+z", alloc->allocated()));
z += alloc->allocated();
}
++i_alloc;
}
/** head must be included, since it's always the youngest bucket **/
z += hd_->after_checkpoint();
log && log("z", z);
return z;
}
bool
ListAlloc::debug_flag() const {
return debug_flag_;
}
void
ListAlloc::clear() {
// general hygiene
start_z_ = 0;
hd_.reset();
marked_ = nullptr;
full_l_.clear();
current_z_ = 0;
next_z_ = 0;
total_z_ = 0;
}
bool
ListAlloc::reset(std::size_t z)
{
scope log(XO_DEBUG(debug_flag_), xtag("z", z));
bool recycle_head_bucket = hd_ && (z <= hd_->size());
this->full_l_.clear();
this->marked_ = nullptr;
if (recycle_head_bucket) {
this->hd_->clear();
this->total_z_ = hd_->size();
return true;
} else {
std::string old_name = this->hd_->name();
this->hd_.reset(nullptr);
this->total_z_ = 0;
return this->expand(z, old_name + "+");
}
}
bool
ListAlloc::expand(std::size_t z, const std::string & name)
{
scope log(XO_DEBUG(debug_flag_), xtag("name", name));
//log && log("before", xtag("before_ckp", this->before_checkpoint()));
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);
log && log("expand to", xtag("cz", cz));
std::unique_ptr<ArenaAlloc> new_alloc = ArenaAlloc::make(name,
cz, debug_flag_);
cz = new_alloc->size();
if (!new_alloc)
return false;
this->current_z_ = cz;
this->next_z_ = nz;
this->total_z_ += cz;
if (hd_)
this->full_l_.push_back(std::move(hd_));
this->hd_ = std::move(new_alloc);
//log && log("after", xtag("before_ckp", this->before_checkpoint()));
return true;
}
void
ListAlloc::checkpoint() {
scope log(XO_DEBUG(debug_flag_));
hd_->checkpoint();
this->marked_ = hd_.get();
log && log(xtag("hd", (void*)hd_.get()), xtag("marked", (void*)marked_));
}
std::byte *
ListAlloc::alloc(std::size_t z) {
scope log(XO_DEBUG(debug_flag_));
/* ArenaAlloc::alloc() may modify its own size */
std::size_t z_pre = hd_->size();
std::byte * retval = hd_->alloc(z);
if (retval) {
std::size_t z_post = hd_->size();
this->total_z_ += (z_post - z_pre);
return retval;
}
log && log("space exhausted -> expand");
if (this->expand(z, hd_->name() + "+"))
return hd_->alloc(z);
return nullptr;
}
} /*namespace gc*/
} /*namespace xo*/
/* end ListAlloc.cpp */

View file

@ -0,0 +1,230 @@
/* 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 *>(cpof.mm_);
return cpof.mm_->alloc_gc_copy(z, cpof.src_);
}
namespace xo {
using xo::reflect::TaggedPtr;
gc::IAlloc *
Object::mm = nullptr;
TaggedPtr
Object::self_tp() const
{
assert(false);
return TaggedPtr::universal_null();
}
void
Object::display(std::ostream & os) const
{
os << "<Object>";
}
IObject *
Object::_forward(IObject * src,
gc::IAlloc * gc)
{
scope log(XO_DEBUG(gc->debug_flag()), xtag("src", src));
if (!src)
return src;
if (src->_is_forwarded()) {
log && log("already forwarded", xtag("dest", src->_offset_destination(src)));
return src->_offset_destination(src);
}
if (gc->check_move(src)) {
log && log("needs forwarding");
Object::_shallow_move(src, gc);
/* *src is now a forwarding pointer to a copy in to-space */
return src->_offset_destination(src);
} else {
log && log("already tenured + incr collection");
/* don't move tenured objects during incremental collection */
return src;
}
}
IObject *
Object::_deep_move(IObject * from_src, gc::GC * gc, gc::ObjectStatistics * /*stats*/)
{
scope log(XO_DEBUG(gc->config().debug_flag_));
using gc::generation;
if (!from_src)
return nullptr;
IObject * retval = from_src->_destination();
if (retval)
return retval;
if (!gc->check_move(from_src)) {
/** 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) };
IObject * to_src = Object::_shallow_move(from_src, gc);
std::size_t fixup_work = 0;
do {
fixup_work = 0;
auto fixup_generation = [gc, &log, &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
log && log("fwd children", xtag("x", x));
std::size_t xz = x->_forward_children(gc);
// 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*/
IObject *
Object::_shallow_move(IObject * src, gc::IAlloc * gc)
{
/* filter for source objects that are owned by GC.
* Care required though -- during GC from/to spaces have been swapped already
*/
if (gc->check_owned(src))
{
IObject * dest = src->_shallow_copy(gc);
if (dest != src)
src->_forward_to(dest);
return dest;
} else {
return src;
}
}
void
Object::_forward_to(IObject * dest)
{
char * mem = reinterpret_cast<char *>(this);
Forwarding1 * fwd = new (mem) Forwarding1(dest);
(void)fwd;
}
std::ostream &
operator<< (std::ostream & os, gp<Object> x)
{
if (x.ptr()) {
x->display(os);
} else {
os << "<nullptr>";
}
return os;
}
} /*namespace xo*/
/* end Object.cpp*/

View file

@ -0,0 +1,73 @@
/* file ObjectStatistics.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "ObjectStatistics.hpp"
#include "xo/reflect/TypeDescr.hpp"
#include "xo/indentlog/print/pretty_vector.hpp"
namespace xo {
namespace gc {
void
PerObjectTypeStatistics::display(std::ostream & os) const
{
os << "<PerObjectTypeStatistics";
if (td_)
os << xrtag("td", td_->short_name());
else
os << xrtag("td", "nullptr");
os << xrtag("scanned_n", scanned_n_)
<< xrtag("scanned_z", scanned_z_)
<< xrtag("survive_n", survive_n_)
<< xrtag("survive_z", survive_z_)
<< ">";
}
void
ObjectStatistics::display(std::ostream & os) const
{
os << "<ObjectStatistics";
std::size_t i = 0;
for (const auto & x : per_type_stats_v_) {
os << " :[" << i << "] " << x;
}
os << ">";
}
} /*namespace gc*/
namespace print {
bool
ppdetail<xo::gc::PerObjectTypeStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::PerObjectTypeStatistics & x)
{
static constexpr std::string_view c_nullptr_str = "nullptr";
if (x.td_) {
return ppii.pps()->pretty_struct(ppii,
"PerObjectTypeStatistics",
refrtag("td", x.td_ ? x.td_->short_name() : c_nullptr_str),
refrtag("scanned_n", x.scanned_n_),
refrtag("scanned_z", x.scanned_z_),
refrtag("survive_n", x.survive_n_),
refrtag("survive_z", x.survive_z_));
} else {
/* print nothing -- empty struct */
return true;
}
}
bool
ppdetail<xo::gc::ObjectStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::ObjectStatistics & x)
{
return ppii.pps()->pretty_struct(ppii,
"ObjectTypeStatistics",
refrtag("per_type_stats_v", x.per_type_stats_v_));
}
} /*namespace gc*/
} /*namespace xo*/
/* end ObjectStatistics.cpp */

View file

@ -0,0 +1,31 @@
/* generation.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "generation.hpp"
namespace xo {
namespace gc {
const char * gen2str(generation x) {
switch (x) {
case generation::nursery: return "nursery";
case generation::tenured: return "tenured";
case generation::N: break;
}
return "?generation";
}
const char * genresult2str(generation_result x) {
switch (x) {
case generation_result::nursery: return "nursery";
case generation_result::tenured: return "tenured";
case generation_result::not_found: return "not-found";
}
return "?generation_result";
}
} /*namespace gc*/
} /*namespace xo*/
/* generation.cpp */

View file

@ -0,0 +1,88 @@
/* @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 {
explicit testcase_alloc(std::size_t z)
:
arena_z_{z} {}
std::size_t arena_z_;
};
std::vector<testcase_alloc>
s_testcase_v = {
testcase_alloc(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.arena_z_, c_debug_flag);
alloc->expand(tc.arena_z_);
REQUIRE(alloc.get());
REQUIRE(alloc->name() == "linearalloc");
REQUIRE(alloc->size() == std::max(tc.arena_z_, alloc->hugepage_z()));
REQUIRE(alloc->available() == std::max(tc.arena_z_, alloc->hugepage_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(std::max(tc.arena_z_, alloc->hugepage_z()));
REQUIRE(mem != nullptr);
REQUIRE(mem == free0);
REQUIRE(alloc->size() == std::max(tc.arena_z_, alloc->hugepage_z()));
REQUIRE(alloc->available() == 0);
REQUIRE(alloc->allocated() == std::max(tc.arena_z_, alloc->hugepage_z()));
REQUIRE(alloc->is_before_checkpoint(mem) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == std::max(tc.arena_z_, alloc->hugepage_z()));
alloc->clear();
REQUIRE(alloc->free_ptr() == free0);
REQUIRE(alloc->available() == std::max(tc.arena_z_, alloc->hugepage_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() == std::max(tc.arena_z_, alloc->hugepage_z()));
REQUIRE(alloc->available() == std::max(tc.arena_z_, alloc->hugepage_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

@ -0,0 +1,26 @@
# xo-alloc/utest/CMakeLists.txt
#
# NOTE: more GC tests in xo-object/utest
set(UTEST_EXE utest.alloc)
set(UTEST_SRCS
alloc_utest_main.cpp
IAlloc.test.cpp
ArenaAlloc.test.cpp
ListAlloc.test.cpp
GC.test.cpp
GcStatistics.test.cpp
ObjectStatistics.test.cpp
Forwarding1.test.cpp
CircularBuffer.test.cpp
generation.test.cpp
)
if (ENABLE_TESTING)
xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS})
xo_self_dependency(${UTEST_EXE} xo_alloc)
xo_dependency(${UTEST_EXE} reflect)
xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2)
endif()
# end CmakeLists.txt

View file

@ -0,0 +1,174 @@
/* CircularBuffer.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/CircularBuffer.hpp"
#include "xo/indentlog/print/vector.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::CircularBuffer;
namespace ut {
TEST_CASE("circular_buffer_0", "[circular_buffer]")
{
CircularBuffer<std::string> q(10, false /*debug_flag*/);
q.push_back("a");
REQUIRE(q.back() == "a");
q.push_back("b");
REQUIRE(q.back() == "b");
q.push_back("c");
REQUIRE(q.back() == "c");
REQUIRE(q.location_of(0) == 0);
REQUIRE(q.location_of(1) == 1);
REQUIRE(q.location_of(2) == 2);
//REQUIRE(q.index_of(0) == 0);
REQUIRE(q.size() == 3);
REQUIRE(q.front() == "a");
REQUIRE(q.at(0) == "a");
REQUIRE(q.at(1) == "b");
REQUIRE(q.at(2) == "c");
CircularBuffer<std::string> q2;
q2 = q;
q.clear();
REQUIRE(q2.size() == 3);
REQUIRE(q2.front() == "a");
REQUIRE(q2.at(0) == "a");
REQUIRE(q2.at(1) == "b");
REQUIRE(q2.at(2) == "c");
}
TEST_CASE("circular_buffer_1", "[circular_buffer]")
{
CircularBuffer<std::string> q(2, false /*debug_flag*/);
q.push_back("a");
REQUIRE(q.back() == "a");
q.push_back("b");
REQUIRE(q.back() == "b");
q.push_back("c");
REQUIRE(q.back() == "c");
REQUIRE(q.location_of(0) == 1);
REQUIRE(q.location_of(1) == 0);
//REQUIRE(q.index_of(0) == 0);
REQUIRE(q.size() == 2);
REQUIRE(q.front() == "b");
REQUIRE(q.at(0) == "b");
REQUIRE(q.at(1) == "c");
{
std::size_t i = 0;
for (const auto & qi : q) {
REQUIRE(qi == q.at(i));
++i;
}
}
CircularBuffer<std::string> q2 = q;
q.clear();
REQUIRE(q2.size() == 2);
REQUIRE(q2.front() == "b");
REQUIRE(q2.at(0) == "b");
REQUIRE(q2.at(1) == "c");
{
std::size_t i = 0;
for (const auto & qi : q) {
REQUIRE(qi == q2.at(i));
++i;
}
}
}
}
namespace {
struct Testcase_CircularBuffer {
explicit Testcase_CircularBuffer(std::size_t capacity,
const std::vector<std::string> & contents)
: capacity_{capacity},
contents_{contents} {}
std::size_t capacity_ = 0;
std::vector<std::string> contents_;
};
std::vector<Testcase_CircularBuffer>
s_testcase_v = {
Testcase_CircularBuffer(0, {}),
Testcase_CircularBuffer(1, {"a"}),
Testcase_CircularBuffer(2, {"a", "b"}),
Testcase_CircularBuffer(2, {"a", "b", "c", "d"})
};
}
namespace ut {
TEST_CASE("circular_buffer_2", "[circular_buffer]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const Testcase_CircularBuffer & tc = s_testcase_v[i_tc];
INFO(tostr(xtag("i_tc", i_tc),
xtag("capacity", tc.capacity_),
xrtag("contents", tc.contents_)));
for (std::size_t j_phase = 0; j_phase < 2; ++j_phase) {
constexpr bool c_debug_flag = false;
CircularBuffer<std::string> q(tc.capacity_, false /*debug_flag*/);
REQUIRE(q.empty());
REQUIRE(q.size() == 0);
REQUIRE(q.begin() == q.end());
REQUIRE(q.capacity() == tc.capacity_);
std::size_t n = 0;
for (const auto & s : tc.contents_) {
INFO(tostr(xtag("n0", n), xtag("s", s)));
++n;
INFO(xtag("n1", n));
q.push_back(s);
REQUIRE(q.back() == s);
REQUIRE(q.capacity() == tc.capacity_);
REQUIRE(q.size() == std::min(n, tc.capacity_));
std::size_t i = 0;
for (const auto & qi : q) {
INFO(tostr(xtag("i", i), xtag("qi", qi)));
if (n <= tc.capacity_) {
REQUIRE(qi == tc.contents_.at(i));
REQUIRE(qi == tc.contents_[i]);
} else {
REQUIRE(qi == tc.contents_.at(n - tc.capacity_ + i));
REQUIRE(qi == tc.contents_[n - tc.capacity_ + i]);
}
++i;
}
REQUIRE(i == std::min(n, tc.capacity_));
if (tc.contents_.size() <= tc.capacity_)
REQUIRE(q.front() == tc.contents_.at(0));
}
q.clear();
REQUIRE(q.size() == 0);
REQUIRE(q.capacity() == tc.capacity_);
}
}
}
}
}
/* CircularBuffer.test.cpp */

View file

@ -0,0 +1,93 @@
/* Forwarding1.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "Forwarding1.hpp"
#include "ArenaAlloc.hpp"
#include "xo/reflect/Reflect.hpp"
#include <catch2/catch.hpp>
#include <regex>
#include <cstring>
namespace xo {
using xo::reflect::Reflect;
using xo::obj::Forwarding1;
namespace gc {
namespace {
class DummyObject : public Object {
public:
explicit DummyObject(const char * data) {
::strncpy(data_.data(), data, 128);
}
gp<Object> member() const { return member_; }
void assign_member(Object * x) {
Object::mm->assign_member(this, reinterpret_cast<IObject**>(member_.ptr_address()), x);
}
TaggedPtr self_tp() const final override {
return Reflect::make_tp(const_cast<DummyObject*>(this));
}
void display(std::ostream & os) const final override { os << data_; }
virtual std::size_t _shallow_size() const final override { return sizeof(*this); }
virtual IObject * _shallow_copy(gc::IAlloc * mm) const final override { return new (Cpof(mm, this)) DummyObject(*this); }
virtual std::size_t _forward_children(gc::IAlloc * gc) final override { return _shallow_size(); }
private:
std::array<char, 128> data_;
gp<Object> member_;
};
}
TEST_CASE("Forwarding1", "[gc][alloc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
gp<Object> obj = new DummyObject("Well, I wasn't expecting that!");
gp<Forwarding1> fwd = new Forwarding1(obj);
REQUIRE(fwd->_destination() == obj.ptr());
REQUIRE(fwd->_offset_destination(fwd.ptr()) == obj.ptr());
REQUIRE(fwd->self_tp().td()->short_name() == "Forwarding1");
std::stringstream ss;
ss << fwd;
// forwarding printer looks like
// "<fwd :dest 0x1ef49c20>"
//
std::regex pattern(R"(<fwd :dest 0x[0-9a-f]+>)");
REQUIRE(std::regex_match(ss.str(), pattern));
//REQUIRE(ss.str() == "<fwd :dest DummyObject>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("IAlloc.assign_member", "[gc][alloc]")
{
/* not giving this nit it's own translation unit.
*/
gp<DummyObject> obj = new DummyObject("This also a surprise..");
up<ArenaAlloc> arena = ArenaAlloc::make("test", 1024, false);
Object::mm = arena.get();
obj->assign_member(obj.ptr());
REQUIRE(obj->member().ptr() == obj.ptr());
}
} /*namespace gc*/
} /*namespace xo*/
/* end Forwarding1.test.cpp */

395
xo-alloc/utest/GC.test.cpp Normal file
View file

@ -0,0 +1,395 @@
/* @file GC.test.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "xo/alloc/GC.hpp"
#include "xo/allocutil/gc_allocator_traits.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::IAlloc;
using xo::gc::GC;
using xo::gc::gc_allocator_traits;
using xo::gc::generation;
using xo::gc::Config;
using xo::reflect::TaggedPtr;
namespace ut {
namespace {
struct testcase_gc {
testcase_gc(std::size_t nz, std::size_t tz, std::size_t n_gct, std::size_t t_gct)
: nursery_z_{nz}, tenured_z_{tz}, incr_gc_threshold_{n_gct}, full_gc_threshold_{t_gct} {}
std::size_t nursery_z_;
std::size_t tenured_z_;
std::size_t incr_gc_threshold_;
std::size_t full_gc_threshold_;
};
std::vector<testcase_gc>
s_testcase_v = {
// n_gct: nursery gc threshold
// t_gct: tenured gc threshold
//
// nz tz n_gct t_gct
testcase_gc(1024, 4096, 1024, 1024)
};
}
TEST_CASE("gc", "[alloc][gc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
try {
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_,
.incr_gc_threshold_ = tc.incr_gc_threshold_,
.full_gc_threshold_ = tc.full_gc_threshold_,
});
REQUIRE(gc.get());
REQUIRE(gc->name() == "GC");
REQUIRE(gc->nursery_to_allocated() == 0);
REQUIRE(gc->nursery_to_committed() >= tc.nursery_z_);
REQUIRE(gc->nursery_to_reserved() >= tc.nursery_z_);
REQUIRE(gc->nursery_to_reserved() < tc.nursery_z_ + gc->hugepage_z());
REQUIRE(gc->size() >= tc.nursery_z_ + tc.tenured_z_);
REQUIRE(gc->size() < tc.nursery_z_ + gc->hugepage_z() + tc.tenured_z_ + gc->hugepage_z());
REQUIRE(gc->allocated() == 0);
REQUIRE(gc->available() == gc->nursery_to_reserved());
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->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 0);
REQUIRE(gc->native_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->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->native_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->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
} catch (std::exception & ex) {
std::cerr << "caught exception: " << ex.what() << std::endl;
REQUIRE(false);
}
}
}
/** gc-enabled allocator **/
namespace {
/** Setup test with custom allocator
*
**/
template <typename Nested, typename GcObjectInterface>
struct TestClass : public GcObjectInterface {
TestClass() = default;
explicit TestClass(const Nested & member1) : member1_{member1} {}
// using allocator_type = Allocator;
// using allocator_traits = xo::gc::gc_allocator_traits<Allocator>;
/** stage1 - just allocates some memory using allocator **/
template <typename Allocator>
static TestClass * make_0(Allocator & alloc) {
TestClass * mem = alloc.allocate(sizeof(TestClass));
/* but ctor will not have run, so ub to visit object */
return mem;
}
/** stage2 - use allocator_traits construct **/
template <typename Allocator>
static TestClass * make_1(Allocator & alloc) {
using traits = gc_allocator_traits<Allocator>;
TestClass * mem = traits::allocate(alloc, 1);
/* ctor will not have run here either */
return mem;
}
/** stage3 - invoke construct **/
template <typename Allocator>
static TestClass * make_2(Allocator & alloc) {
using traits = gc_allocator_traits<Allocator>;
TestClass * obj = traits::allocate(alloc, 1);
try {
// placement new
traits::construct(alloc, obj);
return obj;
} catch(...) {
traits::deallocate(alloc, obj, 1);
throw;
}
}
/** stage4 - init nested type **/
template <typename Allocator>
static TestClass * make_3(Allocator & alloc) {
using traits = gc_allocator_traits<Allocator>;
TestClass * obj = traits::allocate(alloc, 1);
try {
Nested nested;
// placemenet new
traits::construct(alloc, obj);
return obj;
} catch(...) {
traits::deallocate(alloc, obj, 1);
throw;
}
}
// ----- inherited from Object -----
virtual TaggedPtr self_tp() const final override {
assert(false); return TaggedPtr::universal_null();
}
virtual void display(std::ostream & os) const final override {
os << "<TestClass>";
}
virtual std::size_t _shallow_size() const final override {
assert(false); return sizeof(*this);
}
virtual IObject * _shallow_copy(IAlloc * gc) const final override {
assert(false); return nullptr;
}
virtual std::size_t _forward_children(IAlloc * gc) final override {
assert(false); return _shallow_size();
}
Nested member1_;
};
//template <typename Allocator>
struct MemberType {
public:
//using allocator_type = Allocator;
//using vector_allocator_type = typename std::allocator_traits<Allocator>::template rebind_alloc<gp<Object>>;
using vector_type = std::vector<gp<Object>>;
//using vector_type = std::vector<gp<Object>, vector_allocator_type>;
public:
MemberType() : ctor_ran_{true} {}
//explicit MemberType(const Allocator & alloc)
//: member2_{vector_allocator_type(alloc)}, ctor_ran_{true} {}
explicit MemberType(const vector_type & mem2) : member2_{mem2}, ctor_ran_{true} {}
//MemberType(const vector_type & mem2, const Allocator & alloc)
//: member2_{mem2, vector_allocator_type(alloc)}, ctor_ran_{true} {}
vector_type member2_;
bool ctor_ran_ = false;
};
#ifdef NOT_YET
struct MemberType2 {
public:
MemberType2() = default;
/** GC hooks rely on copy constructor. But can't write it without allocator state.
* Therefore: need copy-like constructor that takes allocator argument
**/
template <typename Allocator>
explicit MemberType2(Allocator & alloc, uint64 payload) {
using traits = gc_allocator_traits<Allocator>;
uint64_t * ptr = traits::allocate(alloc, 1);
this->payload_ = payload;
this->ctor_ran_ = true;
}
uint64_t * payload_ = nullptr;
bool ctor_ran_ = false;
}
#endif
}
TEST_CASE("vector_custom_allocator", "[alloc][vector]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
try {
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_,
.incr_gc_threshold_ = tc.incr_gc_threshold_,
.full_gc_threshold_ = tc.full_gc_threshold_,
});
REQUIRE(gc.get());
REQUIRE(gc->name() == "GC");
using NestedElementAllocator = xo::gc::allocator<gp<Object>>;
NestedElementAllocator alloc(gc.get());
/** testv will use GC to allocaate element storage
* Attempt to gc will fail, because memory iteration
* won't work.
**/
std::vector<gp<Object>,
NestedElementAllocator> testv(alloc);
testv.push_back(gp<Object>());
#ifdef NOPE
using ex_allocator = xo::gc::allocator<int>;
using MyObjectInterface = gc_allocator_traits<ex_allocator>::template object_interface<ex_allocator>;
using NestedType = MemberType;
//using NestedType = MemberType<NestedElementAllocator>;
using MyType = TestClass<NestedType, MyObjectInterface>;
using MyAllocator = xo::gc::allocator<MyType>;
MyAllocator alloc(gc.get());
{
/* verify that MyType is constructible */
MyType obj0;
REQUIRE(obj0.member1_.ctor_ran_ == true);
}
{
MyType * mem0 = MyType::make_0(alloc);
REQUIRE(mem0 != nullptr);
REQUIRE(mem0->member1_.ctor_ran_ == false);
}
{
MyType * mem1 = MyType::make_1(alloc);
REQUIRE(mem1 != nullptr);
REQUIRE(mem1->member1_.ctor_ran_ == false);
}
{
MyType * mem2 = MyType::make_2(alloc);
REQUIRE(mem2 != nullptr);
REQUIRE(mem2->member1_.ctor_ran_ == true);
}
{
MyType * mem3 = MyType::make_3(alloc);
REQUIRE(mem3 != nullptr);
REQUIRE(mem3->member1_.ctor_ran_ == true);
}
gp<MyType> ptr;
{
REQUIRE(ptr.is_null());
//ptr = MyType::make_0();
}
#endif
} catch (std::exception & ex) {
std::cerr << "caught exception: " << ex.what() << std::endl;
REQUIRE(false);
}
}
}
TEST_CASE("gc_allocator_traits", "[alloc][gc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
try {
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_,
.incr_gc_threshold_ = tc.incr_gc_threshold_,
.full_gc_threshold_ = tc.full_gc_threshold_,
});
REQUIRE(gc.get());
REQUIRE(gc->name() == "GC");
using ex_allocator = xo::gc::allocator<int>;
using MyObjectInterface = gc_allocator_traits<ex_allocator>::template object_interface<ex_allocator>;
using NestedElementAllocator = xo::gc::allocator<gp<Object>>;
using NestedType = MemberType;
//using NestedType = MemberType<NestedElementAllocator>;
using MyType = TestClass<NestedType, MyObjectInterface>;
using MyAllocator = xo::gc::allocator<MyType>;
MyAllocator alloc(gc.get());
{
/* verify that MyType is constructible */
MyType obj0;
REQUIRE(obj0.member1_.ctor_ran_ == true);
}
{
MyType * mem0 = MyType::make_0(alloc);
REQUIRE(mem0 != nullptr);
REQUIRE(mem0->member1_.ctor_ran_ == false);
}
{
MyType * mem1 = MyType::make_1(alloc);
REQUIRE(mem1 != nullptr);
REQUIRE(mem1->member1_.ctor_ran_ == false);
}
{
MyType * mem2 = MyType::make_2(alloc);
REQUIRE(mem2 != nullptr);
REQUIRE(mem2->member1_.ctor_ran_ == true);
}
{
MyType * mem3 = MyType::make_3(alloc);
REQUIRE(mem3 != nullptr);
REQUIRE(mem3->member1_.ctor_ran_ == true);
}
gp<MyType> ptr;
{
REQUIRE(ptr.is_null());
//ptr = MyType::make_0();
}
} catch (std::exception & ex) {
std::cerr << "caught exception: " << ex.what() << std::endl;
REQUIRE(false);
}
}
}
} /*namespace ut*/
} /*namespace xo*/
/* GC.test.cpp */

View file

@ -0,0 +1,216 @@
/* @file GcStatistics.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/GcStatistics.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/tostr.hpp"
#include "xo/indentlog/print/hex.hpp"
#include <catch2/catch.hpp>
#include <ranges>
namespace xo {
using xo::gc::GcStatistics;
using xo::gc::GcStatisticsExt;
using xo::gc::PerGenerationStatistics;
using xo::print::ppconfig;
namespace ut {
TEST_CASE("PerGenerationStatistics", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
PerGenerationStatistics stats;
std::string s = tostr(stats);
//std::cerr << hex_view(s.c_str(), s.c_str() + s.length(), true /*as_text*/) << std::endl;
REQUIRE(s == "<PerGenerationStatistics :used 0 :n_gc 0 :new_alloc_z 0 :scanned_z 0 :survive_z 0 :promote_z 0>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("GcStatistics", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
GcStatistics stats;
std::string s = tostr(stats);
REQUIRE(s ==
"<GcStatistics"
/**/" :gen_v ["
/***/ "<PerGenerationStatistics"
/****/ " :used 0"
/****/ " :n_gc 0"
/****/ " :new_alloc_z 0"
/****/ " :scanned_z 0"
/****/ " :survive_z 0"
/****/ " :promote_z 0"
/***/ ">"
/***/ " <PerGenerationStatistics"
/****/ " :used 0"
/****/ " :n_gc 0"
/****/ " :new_alloc_z 0"
/****/ " :scanned_z 0"
/****/ " :survive_z 0"
/****/ " :promote_z 0"
/***/ ">]"
/**/ " :total_allocated 0"
/**/ " :total_promoted_sab 0"
">");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("GcStatisticsExt", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
GcStatisticsExt stats;
std::string s = tostr(stats);
REQUIRE(s == "<GcStatisticsExt :gen_v [<PerGenerationStatistics :used 0 :n_gc 0 :new_alloc_z 0 :scanned_z 0 :survive_z 0 :promote_z 0> <PerGenerationStatistics :used 0 :n_gc 0 :new_alloc_z 0 :scanned_z 0 :survive_z 0 :promote_z 0>] :total_allocated 0 :total_promoted_sab 0 :nursery_z 0 :nursery_before_ckp_z 0 :nursery_after_ckp_z 0 :tenured_z 0 :n_mutation 0 :n_logged_mutation 0 :n_xgen_mutation 0 :n_xckp_mutation 0>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("GcStatistics-pretty", "[alloc][gc][pretty]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
std::stringstream ss;
ppconfig ppc;
GcStatistics stats;
std::string actual = toppstr2(ppc, stats);
std::string expected
= ("<GcStatistics\n"
" :gen_v\n"
" [ <PerGenerationStatistics\n"
" :used_z 0\n"
" :n_gc 0\n"
" :new_alloc_z 0\n"
" :scanned_z 0\n"
" :survive_z 0\n"
" :promote_z 0>,\n"
" <PerGenerationStatistics\n"
" :used_z 0\n"
" :n_gc 0\n"
" :new_alloc_z 0\n"
" :scanned_z 0\n"
" :survive_z 0\n"
" :promote_z 0> ]\n"
" :total_allocated 0\n"
" :total_promoted_sab 0\n"
" :total_promoted 0\n"
" :n_mutation 0\n"
" :n_logged_mutation 0\n"
" :n_xgen_mutation 0\n"
" :n_xckp_mutation 0>");
if (actual != expected) {
CHECK(actual == expected);
CHECK(actual.length() == expected.length());
auto a_ix = actual.begin();
auto e_ix = expected.begin();
std::size_t pos = 0;
while (a_ix != actual.end() && e_ix != expected.end()) {
INFO(xtag("pos", pos));
INFO(xtag("matching_prefix", std::string(actual.c_str(), pos)));
REQUIRE(*a_ix == *e_ix);
++a_ix;
++e_ix;
++pos;
}
}
REQUIRE(actual == expected);
tag_config::tag_color_enabled = saved;
}
TEST_CASE("GcStatisticsExt-pretty", "[alloc][gc][pretty]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
std::stringstream ss;
ppconfig ppc;
GcStatisticsExt stats;
std::string actual = toppstr2(ppc, stats);
std::string expected
= ("<GcStatisticsExt\n"
" :gen_v\n"
" [ <PerGenerationStatistics\n"
" :used_z 0\n"
" :n_gc 0\n"
" :new_alloc_z 0\n"
" :scanned_z 0\n"
" :survive_z 0\n"
" :promote_z 0>,\n"
" <PerGenerationStatistics\n"
" :used_z 0\n"
" :n_gc 0\n"
" :new_alloc_z 0\n"
" :scanned_z 0\n"
" :survive_z 0\n"
" :promote_z 0> ]\n"
" :total_allocated 0\n"
" :total_promoted_sab 0\n"
" :total_promoted 0\n"
" :n_mutation 0\n"
" :n_logged_mutation 0\n"
" :n_xgen_mutation 0\n"
" :n_xckp_mutation 0\n"
" :nursery_z 0\n"
" :nursery_before_checkpoint_z 0\n"
" :nursery_after_checkpoint_z 0\n"
" :tenured_z 0>");
if (actual != expected) {
CHECK(actual == expected);
CHECK(actual.length() == expected.length());
auto a_ix = actual.begin();
auto e_ix = expected.begin();
std::size_t pos = 0;
while (a_ix != actual.end() && e_ix != expected.end()) {
INFO(xtag("pos", pos));
INFO(xtag("matching_prefix", std::string(actual.c_str(), pos)));
REQUIRE(*a_ix == *e_ix);
++a_ix;
++e_ix;
++pos;
}
}
REQUIRE(actual == expected);
tag_config::tag_color_enabled = saved;
}
}
} /*namespace xo*/
/* GcStatistics.test.cpp */

View file

@ -0,0 +1,127 @@
/* @file IAlloc.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
//#include "xo/allocutil/IAlloc.hpp"
#include "xo/alloc/ArenaAlloc.hpp"
#include "xo/indentlog/print/tag.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::IAlloc;
using xo::gc::ArenaAlloc;
namespace ut {
TEST_CASE("ialloc", "[alloc]")
{
static_assert((sizeof(std::uintptr_t) == 8) && "possibly fine if this fails, but would want to know");
REQUIRE(IAlloc::alloc_padding(0) == 0);
for (std::size_t i = 1; i < sizeof(std::uintptr_t); ++i) {
REQUIRE(IAlloc::alloc_padding(i) + i == IAlloc::c_alloc_alignment);
}
REQUIRE(IAlloc::alloc_padding(IAlloc::c_alloc_alignment) == 0);
for (std::size_t i = 1; i < sizeof(std::uintptr_t); ++i) {
REQUIRE(IAlloc::alloc_padding(IAlloc::c_alloc_alignment + i) + i == IAlloc::c_alloc_alignment);
}
}
/* although xo::gc::allocator<T> is intended for
* IAlloc derivatives (so T is ArenaAlloc | GC),
*
* it only relies on allocate() and deallocate() methods
*/
namespace {
struct TestCase {
explicit TestCase(size_t arena_z, size_t n, size_t n2) : arena_z_{arena_z}, n_{n}, n2_{n2} {}
size_t arena_z_ = 0;
size_t n_ = 0;
size_t n2_ = 0;
};
std::vector<TestCase> s_testcase_v = { TestCase{1024*1024, 9, 13} };
}
TEST_CASE("gc.allocator", "[alloc]")
{
using xo::gc::allocator;
constexpr bool c_debug_flag = false;
for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
INFO(xtag("i_tc", i_tc));
const TestCase & tc = s_testcase_v[i_tc];
up<ArenaAlloc> mm1 = ArenaAlloc::make("arena1",
tc.arena_z_,
c_debug_flag);
up<ArenaAlloc> mm2 = ArenaAlloc::make("arena2",
tc.arena_z_,
c_debug_flag);
REQUIRE(mm1.get());
REQUIRE(mm1->allocated() == 0);
allocator<int32_t> alloc1(mm1.get());
allocator<double> alloc1a(mm1.get());
REQUIRE(mm2.get());
REQUIRE(mm2->allocated() == 0);
allocator<int32_t> alloc2(mm2.get());
SECTION("IAlloc identity determines allocator equality") {
REQUIRE(alloc1 == alloc1a);
REQUIRE(alloc1 != alloc2);
}
int * p1 = nullptr;
size_t z1 = 0;
SECTION("alloc space for ints") {
p1 = alloc1.allocate(tc.n_);
REQUIRE(p1 != nullptr);
// note: allowing for alignment
REQUIRE(mm1->allocated() >= sizeof(int32_t) * tc.n_);
REQUIRE(mm1->allocated() < sizeof(int32_t) * tc.n_ + IAlloc::c_alloc_alignment);
z1 = mm1->allocated();
// deallocate exists..
alloc1.deallocate(p1, tc.n_);
// ..but is a no-op
REQUIRE(mm1->allocated() == z1);
}
int * p2 = nullptr;
SECTION("allocator independence") {
REQUIRE(mm2->allocated() == 0);
p2 = alloc2.allocate(tc.n2_);
REQUIRE(p2 != nullptr);
REQUIRE(p1 != p2);
REQUIRE(mm2->allocated() >= sizeof(int32_t) * tc.n2_);
REQUIRE(mm2->allocated() < sizeof(int32_t) * tc.n2_ + IAlloc::c_alloc_alignment);
// mm1 unaffected by mm2 allocation
REQUIRE(mm1->allocated() == z1);
}
}
}
} /*namespace ut*/
} /*namespace xo*/
/* end IAlloc.test.cpp */

View file

@ -0,0 +1,64 @@
/* ListAlloc.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/ListAlloc.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::ListAlloc;
namespace ut {
#ifdef NOT_USING // ListAlloc probably permanently retired. Not maintaining
TEST_CASE("ListAlloc", "[alloc][gc]")
{
/** teeny weeny allocator.
* but underlying ArenaAlloc works in multiples of VM page size
* (most likely 4k)
**/
up<ListAlloc> alloc = ListAlloc::make("test", 16, 32, false);
REQUIRE(alloc->name() == "test");
REQUIRE(alloc->size() == 16);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
/* will expand */
std::byte * mem1 = alloc->alloc(20);
REQUIRE(mem1);
REQUIRE(alloc->size() == std::max(alloc->page_size(), alloc->hugepage_z()));
/* round up to multiple of 8 */
REQUIRE(alloc->before_checkpoint() == 24);
REQUIRE(alloc->after_checkpoint() == 0);
alloc->checkpoint();
std::byte * mem2 = alloc->alloc(30);
REQUIRE(mem2);
REQUIRE(alloc->size() == alloc->page_size());
REQUIRE(alloc->before_checkpoint() == 24);
/* round up to multiple of 8 */
REQUIRE(alloc->after_checkpoint() == 32);
std::byte * mem3 = alloc->alloc(40);
REQUIRE(mem3);
REQUIRE(alloc->size() == alloc->page_size());
REQUIRE(alloc->before_checkpoint() == 24);
/* already multiple of 8 */
REQUIRE(alloc->after_checkpoint() == 32 + 40);
REQUIRE(alloc->is_before_checkpoint(mem1) == true);
REQUIRE(alloc->is_before_checkpoint(mem2) == false);
REQUIRE(alloc->is_before_checkpoint(mem3) == false);
}
#endif
} /*namespace ut*/
} /*namespace xo*/
/* ListAlloc.test.cpp */

View file

@ -0,0 +1,185 @@
/* @file ObjectStatistics.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/ObjectStatistics.hpp"
#include "xo/reflect/Reflect.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/ppstr.hpp"
#include "xo/indentlog/print/tostr.hpp"
#include "xo/indentlog/print/hex.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::ObjectStatistics;
using xo::gc::PerObjectTypeStatistics;
using xo::reflect::Reflect;
using xo::print::ppconfig;
namespace ut {
TEST_CASE("PerObjectTypeStatistics", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
PerObjectTypeStatistics stats;
std::string s = tostr(stats);
//std::cerr << hex_view(s.c_str(), s.c_str() + s.length(), true /*as_text*/) << std::endl;
REQUIRE(s == "<PerObjectTypeStatistics :td nullptr :scanned_n 0 :scanned_z 0 :survive_n 0 :survive_z 0>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("PerObjectTypeStatistics-1", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
PerObjectTypeStatistics stats;
stats.td_ = Reflect::require<bool>();
stats.scanned_n_ = 4;
stats.scanned_z_ = 16;
stats.survive_n_ = 2;
stats.survive_z_ = 8;
std::string s = tostr(stats);
//std::cerr << hex_view(s.c_str(), s.c_str() + s.length(), true /*as_text*/) << std::endl;
REQUIRE(s == "<PerObjectTypeStatistics :td bool :scanned_n 4 :scanned_z 16 :survive_n 2 :survive_z 8>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("ObjectStatistics", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
ObjectStatistics stats;
std::string s = tostr(stats);
REQUIRE(s == "<ObjectStatistics>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("ObjectStatistics-1", "[alloc][gc]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
ObjectStatistics stats;
stats.per_type_stats_v_.push_back(PerObjectTypeStatistics());
std::string s = tostr(stats);
REQUIRE(s == "<ObjectStatistics :[0] <PerObjectTypeStatistics :td nullptr :scanned_n 0 :scanned_z 0 :survive_n 0 :survive_z 0>>");
tag_config::tag_color_enabled = saved;
}
TEST_CASE("ObjectStatistics-pretty", "[alloc][gc][pretty]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
std::stringstream ss;
ppconfig ppc;
ObjectStatistics stats;
std::string actual = toppstr2(ppc, stats);
std::string expected
= ("<ObjectTypeStatistics :per_type_stats_v []>");
if (actual != expected) {
CHECK(actual == expected);
CHECK(actual.length() == expected.length());
auto a_ix = actual.begin();
auto e_ix = expected.begin();
std::size_t pos = 0;
while (a_ix != actual.end() && e_ix != expected.end()) {
INFO(xtag("pos", pos));
INFO(xtag("matching_prefix", std::string(actual.c_str(), pos)));
REQUIRE(*a_ix == *e_ix);
++a_ix;
++e_ix;
++pos;
}
}
REQUIRE(actual == expected);
tag_config::tag_color_enabled = saved;
}
TEST_CASE("ObjectStatistics-pretty-1", "[alloc][gc][pretty]")
{
bool saved = tag_config::tag_color_enabled;
tag_config::tag_color_enabled = false;
PerObjectTypeStatistics objstats;
objstats.td_ = Reflect::require<bool>();
objstats.scanned_n_ = 4;
objstats.scanned_z_ = 16;
objstats.survive_n_ = 2;
objstats.survive_z_ = 8;
std::stringstream ss;
ppconfig ppc;
ObjectStatistics stats;
stats.per_type_stats_v_.push_back(objstats);
std::string actual = toppstr2(ppc, stats);
std::string expected
= ("<ObjectTypeStatistics\n"
" :per_type_stats_v\n"
" [ <PerObjectTypeStatistics\n"
" :td bool\n"
" :scanned_n 4\n"
" :scanned_z 16\n"
" :survive_n 2\n"
" :survive_z 8> ]>");
if (actual != expected) {
CHECK(actual == expected);
CHECK(actual.length() == expected.length());
auto a_ix = actual.begin();
auto e_ix = expected.begin();
std::size_t pos = 0;
while (a_ix != actual.end() && e_ix != expected.end()) {
INFO(xtag("pos", pos));
INFO(xtag("matching_prefix", std::string(actual.c_str(), pos)));
REQUIRE(*a_ix == *e_ix);
++a_ix;
++e_ix;
++pos;
}
}
tag_config::tag_color_enabled = saved;
}
}
} /*namespace xo*/
/* ObjectStatistics.test.cpp */

View file

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

View file

@ -0,0 +1,39 @@
/* generation.test.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "xo/alloc/generation.hpp"
#include <sstream>
#include <catch2/catch.hpp>
#include <cstring>
namespace xo {
namespace gc {
TEST_CASE("generation", "[gc]") {
REQUIRE(::strcmp(gen2str(generation::nursery), "nursery") == 0);
REQUIRE(::strcmp(gen2str(generation::tenured), "tenured") == 0);
REQUIRE(::strcmp(gen2str(generation::N), "?generation") == 0);
{
std::stringstream ss;
ss << generation::nursery;
REQUIRE(ss.str() == "nursery");
}
{
std::stringstream ss;
ss << generation::tenured;
REQUIRE(ss.str() == "tenured");
}
{
std::stringstream ss;
ss << generation::N;
REQUIRE(ss.str() == "?generation");
}
}
} /*namespace gc*/
} /*namespace xo*/
/* generation.test.cpp */