diff --git a/CMakeLists.txt b/CMakeLists.txt index f93e8b0c..eebf3aff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,9 +20,13 @@ add_definitions(${PROJECT_CXX_FLAGS}) # must complete definition of expression lib before configuring examples add_subdirectory(src/alloc) - +add_subdirectory(utest) xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) # ---------------------------------------------------------------- +# docs targets depend on other library/utest/exec targets above, +# --> must come after them. +# +add_subdirectory(docs) -add_subdirectory(utest) +# end CmakeLists.txt diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt new file mode 100644 index 00000000..e13b26a0 --- /dev/null +++ b/docs/CMakeLists.txt @@ -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 diff --git a/docs/README b/docs/README new file mode 100644 index 00000000..6aff5d41 --- /dev/null +++ b/docs/README @@ -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 diff --git a/docs/_static/README b/docs/_static/README new file mode 100644 index 00000000..7297d046 --- /dev/null +++ b/docs/_static/README @@ -0,0 +1 @@ +add any static {.html, .js, ..} files for sphinx to pickup here diff --git a/docs/_static/img/favicon.ico b/docs/_static/img/favicon.ico new file mode 100644 index 00000000..4163dd69 Binary files /dev/null and b/docs/_static/img/favicon.ico differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..1ca83ac9 --- /dev/null +++ b/docs/conf.py @@ -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' diff --git a/docs/implementation.rst b/docs/implementation.rst new file mode 100644 index 00000000..e325f88c --- /dev/null +++ b/docs/implementation.rst @@ -0,0 +1,202 @@ +.. _implementation: + +.. toctree:: + :maxdepth: 2 + +Library +======= + +Library dependency tower for *xo-alloc*: + +.. ditaa:: + + +------------------------------------------+ + | xo_alloc | + +------------------------------------------+ + | xo_indentlog | + +------------------------------------------+ + +Install instructions :doc:`here` + +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 : nursery[from] = n0 + gc : nursery[to] = n1 + gc : tenured[from] = t0 + gc : tenured[to] = t1 + + object n0<> + + object n1<> + + object t0<> + + object t1<> + + 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<> + x : hd_ = a0 + x : full_l = {a1, a2} + + object a0<> + a0 : lo_ = 0 + a0 : free_ = 12345 + a0 : hi_ = 1000000 + + object a1<> + + object a2<> + + 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 diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..198cf01c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,14 @@ +# xo-alloc documentation master file + +xo-alloc documentation +====================== + +xo-alloc provides arena allocators and a generation garbage collector + +.. toctree:: + :maxdepth: 2 + :caption: xo-alloc contents + + install + introduction + implementation diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 00000000..ab356be5 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,120 @@ +.. _install: + +.. toctree:: + :maxdepth: 2 + +Source +====== + +Source code lives on github `here`_ + +.. _here: https://github.com/rconybea/xo-alloc + +To clone from git: + +.. code-block:: bash + + git clone https://github.com/rconybea/xo-alloc + +Tested with gcc 13.3 + +Install +======= + +One-step Install +---------------- + +Install along with the reset of *XO* from `xo-umbrella2 source`_ + +.. _xo-umbrella2 source: https://github.com/rconybea/xo-umbrella2 + +Minimal Install +--------------- + +To build+install just required dependencies: +``xo-alloc`` uses several supporting libraries from the *XO* project: + +- `xo-indentlog source`_ (structured logging) +- `xo-cmake source`_ (shared cmake macros) + +.. _xo-indentlog source: https://github.com/rconybea/indentlog +.. _xo-cmake source: https://github.com/rconybea/xo-cmake + +Building from source +-------------------- + +Install scripts for XO libraries depend on helper scripts installed from `xo-cmake`. + +Preamble: + +.. code-block:: bash + + mkdir -p ~/proj/xo + cd ~/proj/xo + + git clone https://github.com/rconybea/xo-cmake + + PREFIX=/usr/local # ..or desired installation prefix + + # want PREFIX/bin in PATH to use xo-cmake helpers + PATH=$PREFIX/bin:$PATH + +Install `xo-cmake`: + +.. code-block:: bash + + cmake -B xo-cmake/.build -S xo-cmake + cmake --install xo-cmake/.build + +Install remaining dependencie(s) in topological order: + +.. code-block:: bash + + xo-build --clone --configure --build --install xo-indentlog + xo-build --clone --configure --build --install xo-alloc + +Directories under ``PREFIX`` will then contain: + +.. code-block:: + + PREFIX + +- bin + | +- xo-build + | +- xo-cmake-config + | \- xo-cmake-lcov-harness + +- include + | \- xo + | +- alloc/ + | \- indentlog/ + +- lib + | +- cmake + | | +- xo_alloc/ + | | \- indentlog/ + | +- lib*.so + +- share + +- cmake + | \- xo_macros + | +- code-coverage.cmake + | +- xo-project-macros.cmake + | \- xo_cxx.cmake + +- etc + | \- xo + | \- subsystem-list + \- xo-macros + +- Doxyfile.in + +- gen-ccov.in + \- xo-bootstrap-macros.cmake + +CMake Support +------------- + +To use built-in cmake support, when using ``xo-alloc`` from another project: + +Make sure ``PREFIX/lib/cmake`` is searched by cmake (for example include it in ``CMAKE_PREFIX_PATH``) + +Add to your ``CMakeLists.txt``: + +.. code-block:: cmake + + FindPackage(xo_alloc CONFIG REQUIRED) + target_link_libraries(mytarget INTERFACE xo_alloc) diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 00000000..7b5333cf --- /dev/null +++ b/docs/introduction.rst @@ -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::make(config); + + Object::mm = gc; // use GC for allocation of Object (+ derived classes) + + gc->disable_gc(); // gc forbidden + + // tiny example data structure + gp s1 = String::copy("hello"); + gp s2 = String::copy(", "); + gp s3 = String::copy("world!"); + gp list = List::cons(s1, List::cons(s2, List::cons(s3, List::nil))); + + // tell GC what to preserve + gc->add_gc_root(reinterpret_cast(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::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 bar_; + gp 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`` diff --git a/include/xo/alloc/AllocPolicy.hpp b/include/xo/alloc/AllocPolicy.hpp new file mode 100644 index 00000000..53f758ee --- /dev/null +++ b/include/xo/alloc/AllocPolicy.hpp @@ -0,0 +1,58 @@ +/* AllocPolicy.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include + +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 */ diff --git a/include/xo/alloc/LinearAlloc.hpp b/include/xo/alloc/ArenaAlloc.hpp similarity index 61% rename from include/xo/alloc/LinearAlloc.hpp rename to include/xo/alloc/ArenaAlloc.hpp index e1390dfc..e2e74e8f 100644 --- a/include/xo/alloc/LinearAlloc.hpp +++ b/include/xo/alloc/ArenaAlloc.hpp @@ -1,4 +1,4 @@ -/* file LinearAlloc.hpp +/* file ArenaAlloc.hpp * * author: Roland Conybeare, Jul 2025 */ @@ -9,7 +9,7 @@ namespace xo { namespace gc { - /** @class LinearAlloc + /** @class ArenaAlloc * @brief Bump allocator with fixed capacity * * @text @@ -33,34 +33,39 @@ namespace xo { * * TODO: rename to ArenaAlloc **/ - class LinearAlloc : public IAlloc { + class ArenaAlloc : public IAlloc { public: - ~LinearAlloc(); + ~ArenaAlloc(); /** create allocator with capacity @p z, * with reserved capacity @p redline_z. **/ - static up make(std::size_t redline_z, std::size_t z); + static up make(const std::string & name, + std::size_t redline_z, + std::size_t z, + bool debug_flag); - std::uint8_t * free_ptr() const { return free_ptr_; } - void set_free_ptr(std::uint8_t * x); + const std::string & name() const { return name_; } + std::byte * free_ptr() const { return free_ptr_; } + void set_free_ptr(std::byte * x); // inherited from IAlloc... virtual std::size_t size() const override; virtual std::size_t available() const override; virtual std::size_t allocated() const override; - virtual bool is_before_checkpoint(const std::uint8_t * x) const override; + virtual bool contains(const void * x) const override; + virtual bool is_before_checkpoint(const void * x) const override; virtual std::size_t before_checkpoint() const override; virtual std::size_t after_checkpoint() const override; virtual void clear() override; virtual void checkpoint() override; - virtual std::uint8_t * alloc(std::size_t z) override; - + virtual std::byte * alloc(std::size_t z) override; + virtual void release_redline_memory() override; private: - LinearAlloc(std::size_t rz, std::size_t z); + ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag); private: /** @@ -68,23 +73,30 @@ namespace xo { * - @ref free_ always a multiple of word size (assumed to be sizeof(void*)) **/ + /** optional instance name, for diagnostics **/ + std::string name_; + /** allocator owns memory in range [@ref lo_, @ref hi_) **/ - std::uint8_t * lo_ = nullptr; + std::byte * lo_ = nullptr; /** checkpoint (for GC support); divides objects into * older (addresses below checkpoint) * and younger (addresses above checkpoint) **/ - std::uint8_t * checkpoint_; + std::byte * checkpoint_; /** free pointer. memory in range [@ref free_, @ref limit_) available **/ - std::uint8_t * free_ptr_ = nullptr; + std::byte * free_ptr_ = nullptr; /** soft limit: end of released memory **/ - std::uint8_t * limit_ = nullptr; + std::byte * limit_ = nullptr; + /** amount of last-resort memory to reserve **/ + std::size_t redline_z_ = 0; /** hard limit: end of allocated memory **/ - std::uint8_t * hi_ = nullptr; + std::byte * hi_ = nullptr; + /** true to enable detailed debug logging **/ + bool debug_flag_ = false; }; } /*namespace gc*/ } /*namespace xo*/ -/* end LinearAlloc.hpp */ +/* end ArenaAlloc.hpp */ diff --git a/include/xo/alloc/Forwarding.hpp b/include/xo/alloc/Forwarding.hpp new file mode 100644 index 00000000..47a555da --- /dev/null +++ b/include/xo/alloc/Forwarding.hpp @@ -0,0 +1,28 @@ +/* Forwarding.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "Object.hpp" + +namespace xo { + namespace gc { + class Forwarding : public Object { + public: + Forwarding() = default; + + // inherited from Object.. +#ifdef NOT_USING + virtual bool _is_forwarded() const override final { return true; } +#endif + virtual Object * _destination() override final { return destination_.ptr(); } + + private: + gp destination_; + }; + } /*namespace gc*/ +} /*namespace xo*/ + +/* end Forwarding.hpp */ diff --git a/include/xo/alloc/Forwarding1.hpp b/include/xo/alloc/Forwarding1.hpp new file mode 100644 index 00000000..62536651 --- /dev/null +++ b/include/xo/alloc/Forwarding1.hpp @@ -0,0 +1,40 @@ +/* file Forwarding1.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "Object.hpp" + +namespace xo { + namespace obj { + class Forwarding1 : public Object { + public: + explicit Forwarding1(gp dest); + + // inherited from Object.. + virtual bool _is_forwarded() const override { return true; } + virtual Object * _offset_destination(Object * src) const; + virtual std::size_t _shallow_size() const override; + virtual Object * _shallow_copy() const override; + virtual std::size_t _forward_children() override; + + private: + /** the object that used to be located at this address (i.e. @c this) + * has been moved to @ref destination_ , + * with original location overwritten by a forwarding pointer + * + * Require: + * - can only use Forwarding with types that have a single vtable. + * To forward a multiply-inheriting class with two vtables, use Forwarding2. + * - if you try to use Forwarding for an object with multiple vtables, + * one of the vtable pointers will be replaced by @ref destination_. + * UB revealed when GC traverses a pointer that relies on the 2nd + * vtable to index virtual methods. + **/ + gp dest_; + }; + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Forwarding1.hpp */ diff --git a/include/xo/alloc/GC.hpp b/include/xo/alloc/GC.hpp new file mode 100644 index 00000000..f42864b7 --- /dev/null +++ b/include/xo/alloc/GC.hpp @@ -0,0 +1,310 @@ +/* GC.hpp + * + * author: Roland Conybeare, jul 2025 + */ + +#pragma once + +#include "ListAlloc.hpp" +#include "xo/indentlog/print/array.hpp" +#include +#include + +namespace xo { + /** types that can participate in GC inherit from this base class. See Object.hpp in this directory **/ + class Object; + + namespace gc { + enum class generation { + nursery, + tenured, + N + }; + + constexpr std::size_t gen2int(generation x) { return static_cast(x); } + + enum class role { + /** nursery: generation for new objects **/ + from_space, + /** tenured: generation for objects that have survived two collections **/ + to_space, + N, + }; + + constexpr std::size_t role2int(role x) { return static_cast(x); } + + /** @class Config + * @brief garbage collector configuration + **/ + struct Config { + /** initial size in bytes for youngest (Nursery) generation. + * GC allocates two nursery spaces of this size. + * Will allocate more space as needed + **/ + std::size_t initial_nursery_z_ = 0; + /** initial size in bytes for oldest (Tenured) generation. + * GC allocates two tenured spaces of this size + * Will allocate more space as needed + **/ + std::size_t initial_tenured_z_ = 0; + /** true to permit incremental garbage collection **/ + bool allow_incremental_gc_ = true; + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + /** @class ObjectStatistics + * @brief placeholder for type-driven allocation statistics + * + * Passed to @ref Object::deep_move for example + **/ + class ObjectStatistics { + }; + + /** @class PerGenerationStatistics + * @brief garbage collection statistics for particular GC generation + **/ + class PerGenerationStatistics { + public: + /** update statistics after a GC cycle + * @param alloc_z. new allocations (since preceding GC) + * @param before_z. generation size (bytes allocated) before collection + * @param after_z. generation size after collection + * @param promote_z. bytes promoted to next generation + **/ + void include_gc(std::size_t alloc_z, std::size_t before_z, std::size_t after_z, + std::size_t promote_z); + /** update with current state (use at end of gc cycle) **/ + void update_snapshot(std::size_t after_z); + + /** @param os. write stats on this output stream **/ + void display(std::ostream & os) const; + + /** number of bytes currently in use **/ + std::size_t used_z_ = 0; + + /** number of collection cycles completed **/ + std::size_t n_gc_ = 0; + /** sum of new alloc bytes, sampled at start of each collection cycle **/ + std::size_t new_alloc_z_ = 0; + /** sum of allocated bytes sampled at beginning of each collection cycle **/ + std::size_t scanned_z_ = 0; + /** sum of bytes remaining after collection cycle **/ + std::size_t survive_z_ = 0; + /** sum of bytes promoted to next generation **/ + std::size_t promote_z_ = 0; + }; + + inline std::ostream & operator<< (std::ostream & os, const PerGenerationStatistics & x) { + x.display(os); + return os; + } + + /** @class GcStatistics + * @brief garbage collection statistics + **/ + class GcStatistics { + public: + /** update statistics after a GC cycle + * @param upto. nursery -> incremental collection; tenured -> full collection + * @param alloc_z. new allocations (since preceding GC) + * @param before_z. generation size (bytes allocated) before collection + * @param after_z. generation size after collection + * @param promote_z. bytes promoted to next generation + **/ + void include_gc(generation upto, std::size_t alloc_z, + std::size_t before_z, std::size_t after_z, std::size_t promote_z); + /** update snapshot for current state. + * Use with tenured stats after incremental gc + **/ + void update_snapshot(generation upto, std::size_t after_z); + + /** @param os. write stats on this output stream **/ + void display(std::ostream & os) const; + + /** statistics gathered across {incr, full} GCs respectively **/ + std::array(generation::N)> gen_v_; + /** total bytes allocated since inception **/ + std::size_t total_allocated_ = 0; + /** snapshot of total bytes promoted asof beginning of last gc cycle **/ + std::size_t total_promoted_sab_ = 0; + /** total bytes promoted from nursery->tenured since inception **/ + std::size_t total_promoted_ = 0; + + /** per-type statistics (placeholder) **/ + ObjectStatistics per_type_stats_; + }; + + inline std::ostream & operator<< (std::ostream & os, const GcStatistics & x) { + x.display(os); + return os; + } + + /** @class GCRunstate + * @brief encapsulate state needed while GC is running + * + * state pertaining to a single GC invocation. + * We stash an instance of this in @ref GC as context, + * so that per-Object-derived-type auxiliary functions can be slightly streamlined + **/ + class GCRunstate { + public: + GCRunstate() = default; + explicit GCRunstate(bool in_progress, bool full_move) + : in_progress_{in_progress}, full_move_{full_move} {} + + bool in_progress() const { return in_progress_; } + bool full_move() const { return full_move_; } + + private: + /** true when GC begins; remains true until GC cycle complete **/ + bool in_progress_ = false; + /** true for full GC; false for incremental GC **/ + bool full_move_ = false; + }; + + /** @class GC + * @brief generational garbage collector + * + * Works with objects of type @ref xo::Object + **/ + class GC : public IAlloc { + public: + /** create new GC instance with configuration @p config **/ + explicit GC(const Config & config); + + /** create GC allocator. + * + * Initial memory consumption: + * approximately 2x @ref Config::nursery_size_ + 2x @ref Config::tenured_size_ + **/ + static up make(const Config & config); + + const GCRunstate & runstate() const { return runstate_; } + const GcStatistics & gc_statistics() const { return gc_statistics_; } + + /** true iff GC permitted in current state **/ + bool is_gc_enabled() const { return gc_enabled_ == 0; } + /** @return generation to which object at @p x belongs **/ + generation generation_of(const void * x) const; + /** @return generation that contains @p x, given it's in from-space **/ + generation fromspace_generation_of(const void * x) const; + /** true iff from-space contains @p x **/ + bool fromspace_contains(const void * x) const; + /** true during (and only during) a GC cycle **/ + bool gc_in_progress() const { return runstate_.in_progress(); } + /** return free pointer for generation @p gen, i.e. nursery or tenured space **/ + std::byte * free_ptr(generation gen); + + /** add gc root at address @p addr . Gc will keep alive anything reachable + * from @c *addr + **/ + void add_gc_root(Object ** addr); + /** request garbage collection. **/ + void request_gc(generation g); + /** disable garbage collection until matching call to @ref enable_gc. + * + * GC is disabled when number of calls to @ref disable_gc exceeds number of + * calls to @ref enable_gc. + **/ + void disable_gc(); + /** enable garbage collection + * + * GC is enabled when number of calls to @ref enable_gc is at least as large + * as number of calls to @ref disable_gc. + **/ + void enable_gc(); + + // inherited from IAlloc.. + + /** capacity in bytes (counting both free+allocated) for object storage. + * only counts one of {to-space, from-space}, + * since one role is always held empty between collections. + **/ + virtual std::size_t size() const override; + + virtual std::size_t allocated() const override; + virtual std::size_t available() const override; + /** only tests to-space **/ + virtual bool contains(const void * x) const override; + virtual bool is_before_checkpoint(const void * x) const override; + virtual std::size_t before_checkpoint() const override; + virtual std::size_t after_checkpoint() const override; + + virtual void clear() override; + virtual void checkpoint() override; + + virtual std::byte * alloc(std::size_t z) override; + virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) override; + + virtual void release_redline_memory() override; + + private: + /** begin GC now **/ + void execute_gc(generation g); + /** cleanup phase. aux function for @ref execute_gc **/ + void cleanup_phase(generation g); + /** swap roles of From/To spaces for nursery generation **/ + void swap_nursery(); + /** swap roles of From/To spaces for tenured generation **/ + void swap_tenured(); + /** swap roles of FromSpace/ToSpace **/ + void swap_spaces(generation g); + /** copy object **/ + void copy_object(Object ** addr, generation upto, ObjectStatistics * object_stats); + /** copy everything reachable from global gc roots **/ + void copy_globals(generation g); + + private: + /** garbage collector configuration **/ + Config config_; + + /** contains allocated objects, along with unreachable garbage to be collected. + * roles reverse after each incremental, or full, collection. + **/ + std::array, static_cast(role::N)> nursery_; + /** empty space, destination for objects that survive collection. + * roles reverse after each full collection. + **/ + std::array, static_cast(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 gc_root_v_; + + /** allocation/collection counters **/ + GcStatistics gc_statistics_; + + /** trigger full GC whenever this much data arrives in tenured generation **/ + std::size_t full_gc_threshold_ = 0; + /** trigger incr GC whenever this much data arrives in nuresery generation **/ + std::size_t incr_gc_threshold_ = 0; + + /** true when GC requested, + * remains true until GC.. completes? begins? + **/ + bool incr_gc_pending_ = false; + bool full_gc_pending_ = false; + + /** enabled when 0. disabled when <0 **/ + int gc_enabled_ = 0; + }; + } /*namespace gc*/ + +} /*namespace xo*/ + +/* end GC.hpp */ diff --git a/include/xo/alloc/GCAlloc.hpp b/include/xo/alloc/GCAlloc.hpp deleted file mode 100644 index e0c6ab7a..00000000 --- a/include/xo/alloc/GCAlloc.hpp +++ /dev/null @@ -1,20 +0,0 @@ -/* file GCAlloc.hpp - * - * author: Roland Conybeare, Jul 2025 - */ - -#pragma once - -namespace xo { - namespace gc { - class GC : public IAlloc { - enum class Space { A, B, N_Space }; - enum class Gen { Nursery, Tenured }; - - }; - - } /*namespace mem */ -} /*namespace xo*/ - - -/* end GCAlloc.hpp */ diff --git a/include/xo/alloc/IAlloc.hpp b/include/xo/alloc/IAlloc.hpp index 848f182c..2f759c53 100644 --- a/include/xo/alloc/IAlloc.hpp +++ b/include/xo/alloc/IAlloc.hpp @@ -20,6 +20,13 @@ namespace xo { public: virtual ~IAlloc() {} + /** compute padding to add to an allocation of size z to bring it up to + * a multiple of word size (8 bytes on x86_64) + **/ + static std::uint32_t alloc_padding(std::size_t z); + /** z + alloc_padding(z) **/ + static std::size_t with_padding(std::size_t z); + /** allocator size in bytes (up to soft limit). * Includes unallocated mmeory **/ @@ -30,10 +37,12 @@ namespace xo { virtual std::size_t available() const = 0; /** number of bytes allocated from this allocator **/ virtual std::size_t allocated() const = 0; + /** true iff pointer x comes from this allocator **/ + virtual bool contains(const void * x) const = 0; /** true iff object at address @p x was allocated by this allocator, * and before checkpoint **/ - virtual bool is_before_checkpoint(const std::uint8_t * x) const = 0; + virtual bool is_before_checkpoint(const void * x) const = 0; /** number of bytes allocated before @ref checkpoint **/ virtual std::size_t before_checkpoint() const = 0; /** number of bytes allocated since @ref checkpoint **/ @@ -48,10 +57,39 @@ namespace xo { **/ virtual void checkpoint() = 0; /** allocate @p z bytes of memory. returns pointer to first address **/ - virtual std::uint8_t * alloc(std::size_t z) = 0; + virtual std::byte * alloc(std::size_t z) = 0; + /** allocate @p z bytes for copy of object at @p src. + * Only used in @ref GC. Default implementation asserts and returns nullptr + **/ + virtual std::byte * alloc_gc_copy(std::size_t z, const void * src); + /** release last-resort reserved memory **/ + virtual void release_redline_memory() = 0; }; } /*namespace gc*/ + + class MMPtr { + public: + explicit MMPtr(gc::IAlloc * mm) : mm_{mm} {} + + gc::IAlloc * mm_ = nullptr; + }; } /*namespace xo*/ +inline void * operator new (std::size_t z, const xo::MMPtr & mmp) { + return mmp.mm_->alloc(z); +} + +//inline void operator delete (void * p, const MMPtr & mmp) { +// mmp.mm_->free(reinterpret_cast(p)); +//} + +inline void * operator new[] (std::size_t z, const xo::MMPtr & mmp) { + return mmp.mm_->alloc(z); +} + +//inline void operator delete[] (void * p, const MMPtr & mmp) { +// mmp.mm_->free(reinterpret_cast(p)); +//} + /* end IAlloc.hpp */ diff --git a/include/xo/alloc/ListAlloc.hpp b/include/xo/alloc/ListAlloc.hpp index 780d2bd2..8d27e6b4 100644 --- a/include/xo/alloc/ListAlloc.hpp +++ b/include/xo/alloc/ListAlloc.hpp @@ -6,13 +6,17 @@ #pragma once #include "IAlloc.hpp" +#include #include #include namespace xo { namespace gc { + class ArenaAlloc; + /** GC-compatible allocator using a linked list of buckets. * + * - all allocs done from first allocator in list * GC Support: * - reserved memory, released after call to @ref release_redline_memory. * @@ -21,27 +25,60 @@ namespace xo { **/ class ListAlloc : public IAlloc { public: - ListAlloc(LinearAlloc* hd, - std::size_t cz, std::size_t nz; std::size_tz, - LinearAlloc* marked, bool use_redline, - bool redlined_flag, OnEmptyFn on_overflow); + ListAlloc(std::unique_ptr hd, + ArenaAlloc * marked, + std::size_t cz, std::size_t nz, std::size_t tz, + bool use_redline, + bool debug_flag); ~ListAlloc(); - static up make(std::size_t cz, std::size_t nz, - OnEmptyFn on_overflow); + static up make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag); + + /** reset to have at least @p z bytes of storage **/ + bool reset(std::size_t z); + + /** expand bucket list to accomodate a requrest of size @p z **/ + bool expand(std::size_t z); + + /** current free pointer **/ + std::byte * free_ptr() const; + + // inherited from IAlloc.. + + virtual std::size_t size() const override; + virtual std::size_t available() const override; + virtual std::size_t allocated() const override; + virtual bool contains(const void * x) const override; + virtual bool is_before_checkpoint(const void * x) const override; + virtual std::size_t before_checkpoint() const override; + virtual std::size_t after_checkpoint() const override; + + virtual void clear() override; + virtual void checkpoint() override; + virtual std::byte * alloc(std::size_t z) override; + virtual void release_redline_memory() override; private: + /** **/ std::size_t start_z_ = 0; - LinearAlloc* hd_ = nullptr; + /** all new allocs from this list **/ + std::unique_ptr 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> full_l_; std::size_t current_z_ = 0;; std::size_t next_z_ = 0;; std::size_t total_z_ = 0; bool use_redline_ = false; bool redlined_flag_ = false; + /** true to enable debug logging **/ + bool debug_flag_ = false; }; } /*namespace gc*/ } /*namespace xo*/ - /* end ListAlloc.hpp */ diff --git a/include/xo/alloc/Object.hpp b/include/xo/alloc/Object.hpp new file mode 100644 index 00000000..f66e8e9a --- /dev/null +++ b/include/xo/alloc/Object.hpp @@ -0,0 +1,232 @@ +/* Object.hpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#pragma once + +#include "IAlloc.hpp" +#include +#include + +namespace xo { + namespace gc { + class GC; + class ObjectStatistics; + }; + + class Object; + + template + class gc_ptr; + + template + using gp = gc_ptr; + + /** wrapper for a pointer to garbage-collector-eligible T. + * Application code will usually use the alias template gp + **/ + template + class gc_ptr { + public: + using element_type = T; + + public: + gc_ptr() = default; + gc_ptr(T * p) : ptr_{p} {} + gc_ptr(const gc_ptr & x) : ptr_{x.ptr_} {} + + /** create from gc_ptr to some related type @tparam S **/ + template + gc_ptr(const gc_ptr & x) : ptr_{x.ptr()} {} + + static bool is_eq(gc_ptr x1, gc_ptr x2) { + std::uintptr_t u1 = reinterpret_cast(x1.ptr()); + std::uintptr_t u2 = reinterpret_cast(x2.ptr()); + + // multiple inheritance shenanigans. + // (allow interface pointers separated by one pointer) + + if (u1 >= u2) + return (u1 <= u2 + sizeof(std::uintptr_t)); + else + return (u2 <= u1 + sizeof(std::uintptr_t)); + } + + T * ptr() const { return ptr_; } + T ** ptr_address() { return &ptr_; } + + bool is_null() const { return ptr_ == nullptr; } + void make_null() { ptr_ = nullptr; } + + void assign_ptr(T * x) { ptr_ = x; } + + gc_ptr & operator=(const gc_ptr & x) { ptr_ = x.ptr(); return *this; } + T * operator->() const { return ptr_; } + + private: + T * ptr_ = nullptr; + }; + + /** Root class for all xo GC-collectable objects. + * + * Design note: + * + * relying on inheritance means we insist that GC traits + * for a type appear directly in that type's vtable, and at specific locations. + * This implies one level of indirection when GC traverses an instance. + * + * Would be feasible to relax the must-inherit-from-Object constraint, + * but cost would be an extra layer of indirection + **/ + class Object { + public: + virtual ~Object() = default; + + /** memory allocator for objects. Likely this will be a GC instance, + * but simple arena also supported. + **/ + static gc::IAlloc * mm; + + /** use from GC aux functions **/ + static gc::GC * _gc() { return reinterpret_cast(mm); } + + /** during GC + * 1. copy destination object @p *addr to (new) to-space. + * 2. overwrite existing object @p *addr with a forwarding pointer to + * copy made in step 1. + * 3. return the location of the copy make in step 1. + * + * @p src. source object to be forwarded + * @p gc. garbage collector + */ + static Object * _forward(Object * src, gc::GC * gc); + + template + static void _forward_inplace(T ** src_addr) { + Object * fwd = _forward(*src_addr, _gc()); + + *src_addr = reinterpret_cast(fwd); + } + + template + static void _forward_inplace(gp & src) { + _forward_inplace(src.ptr_address()); + } + + /** primary workhorse for garbage collection. + * + * we assign each object one of three colors: black|gray|white. + * + * color | location | children | action | + * ------+------------+------------+-------------------------+ + * black | from-space | any | move to to-space | + * gray | to-space | any | move remaining children | + * white | to-space | white/gray | done | + * + * initially all reachable objects are black. + * GC is complete when all reachable objects are white. + * GC needs a variable amount of temporary storage to keep track of all gray objects + **/ + static Object * _deep_move(Object * src, gc::GC * gc, gc::ObjectStatistics * stats); + + /** copy @p src to to-space, and replace original with forwarding pointer to new location. + * return the new location + **/ + static Object * _shallow_move(Object * src, gc::GC * gc); + + // GC support + + /** true iff this object represents a forwarding pointer. + * Forwarding pointers are exclusively created by the garbage collector; + * forwarding pointers (and only forwarding pointers) return true here. + **/ + virtual bool _is_forwarded() const { return false; } + + /** offset for uncommon situation where pointer address is offset from object + * base address + **/ + virtual Object * _offset_destination(Object * src) const { return src; }; + + /** replace this object with a forwarding pointer referring to @p dest. + **/ + virtual void _forward_to(Object * dest); + + /** if this object represents a forwarding pointer, return its new location. + * forwarding pointers belong to the garbage collector implementation. + * (if you have to ask -- no, your class is not a forwarding pointer) + * all other objects return nullptr here. + **/ + virtual Object * _destination() { return nullptr; } + + /** return amount of storage (including padding) consumed by this object, + * excluding immediate Object-pointer children + **/ + virtual std::size_t _shallow_size() const = 0; + + // TODO: _shallow_move() also overwrite *this with gc-only forwarding object point to C + + /** if subject is allocated by GC: + * - create copy C in to-space + * - destination C will be nursery|tenured depending on location of this. + * else + * - return this to disengage from GC + * + * Require: @ref mm is an instance of @ref gc::GC + **/ + virtual Object * _shallow_copy() const = 0; + + /** update child pointers that refer to forwarding pointers, + * replacing them with the correct destination. + * See @ref Object::deep_move + * + * this gray object, located in to-space. + * fwd1 forwarding objects. + * Located in from-space. Invalid at end of GC cycle. + * p1,p2 source pointers. + * D1,D2 already-forwarded objects. located in to-space. + * + * before: + * this fwd1 + * +----+ +-+ + * | p1 ----->|x|-------> D1 + * | | +-+ + * | | + * | p2 ----------------> D2 + * +----+ + * + * after: + * this + * +----+ + * | p1 ----------------> D1 + * | | + * | | + * | p2 ----------------> D2 + * +----+ + * + * this is now white + * + * @return shallow size of *this. Must exactly match the amount of memory in to-space + * allocated by @ref _shallow_move + * + **/ + virtual std::size_t _forward_children() = 0; + }; + + /** @class Cpof + * @brief argument to operator new used for garbage collector evacuation phase + * + * Tag overloaded operator new to activate allocation policy based on location + * in memory of source object. + **/ + class Cpof { + public: + explicit Cpof(const Object * src) : src_{src} {} + + const void * src_ = nullptr; + }; +} /*namespace xo*/ + +void * operator new (std::size_t z, const xo::Cpof & copy); + +/* end Object.hpp */ diff --git a/include/xo/alloc/Stack.hpp b/include/xo/alloc/Stack.hpp new file mode 100644 index 00000000..b894d853 --- /dev/null +++ b/include/xo/alloc/Stack.hpp @@ -0,0 +1,49 @@ +/* Stack.hpp + * + * author: Roland Conybeare, jul 2025 + */ + +#pragma once + +#include + +namespace xo { + namespace gc { + /** Simple stack implementation + **/ + template + 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 contents_; + }; + + } /*namespace gc*/ +} /*namespace xo*/ + +/* end Stack.hpp */ diff --git a/src/alloc/AllocPolicy.cpp b/src/alloc/AllocPolicy.cpp new file mode 100644 index 00000000..b1dc162f --- /dev/null +++ b/src/alloc/AllocPolicy.cpp @@ -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 */ diff --git a/src/alloc/LinearAlloc.cpp b/src/alloc/ArenaAlloc.cpp similarity index 52% rename from src/alloc/LinearAlloc.cpp rename to src/alloc/ArenaAlloc.cpp index 3ae57e70..227e2d63 100644 --- a/src/alloc/LinearAlloc.cpp +++ b/src/alloc/ArenaAlloc.cpp @@ -1,29 +1,33 @@ -/* file LinearAlloc.cpp +/* file ArenaAlloc.cpp * * author: Roland Conybeare */ -#include "LinearAlloc.hpp" +#include "ArenaAlloc.hpp" +#include "xo/indentlog/scope.hpp" #include "xo/indentlog/print/tag.hpp" #include namespace xo { namespace gc { - LinearAlloc::LinearAlloc(std::size_t rz, std::size_t z) + ArenaAlloc::ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag) { - this->lo_ = (new std::uint8_t [rz + z]); + this->name_ = name; + this->lo_ = (new std::byte [rz + z]); this->checkpoint_ = lo_; this->free_ptr_ = lo_; this->limit_ = lo_ + z; + this->redline_z_ = rz; this->hi_ = limit_ + rz; + this->debug_flag_ = debug_flag; if (!lo_) { - throw std::runtime_error(tostr("LinearAlloc: allocation failed", + throw std::runtime_error(tostr("ArenaAlloc: allocation failed", xtag("size", rz + z))); } } - LinearAlloc::~LinearAlloc() + ArenaAlloc::~ArenaAlloc() { delete [] this->lo_; @@ -33,17 +37,19 @@ namespace xo { this->checkpoint_ = nullptr; this->free_ptr_ = nullptr; this->limit_ = nullptr; + this->redline_z_ = 0; this->hi_ = nullptr; + this->debug_flag_ = false; } - up - LinearAlloc::make(std::size_t rz, std::size_t z) + up + ArenaAlloc::make(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag) { - return up(new LinearAlloc(rz, z)); + return up(new ArenaAlloc(name, rz, z, debug_flag)); } void - LinearAlloc::set_free_ptr(std::uint8_t * x) + ArenaAlloc::set_free_ptr(std::byte * x) { assert(lo_ <= x); assert(x < limit_); @@ -57,70 +63,79 @@ namespace xo { } std::size_t - LinearAlloc::size() const { + ArenaAlloc::size() const { return limit_ - lo_; } std::size_t - LinearAlloc::available() const { + ArenaAlloc::available() const { return limit_ - free_ptr_; } std::size_t - LinearAlloc::allocated() const { + ArenaAlloc::allocated() const { return free_ptr_ - lo_; } bool - LinearAlloc::is_before_checkpoint(const std::uint8_t * x) const { + ArenaAlloc::contains(const void * x) const { + return (lo_ <= x) && (x < hi_); + } + + bool + ArenaAlloc::is_before_checkpoint(const void * x) const { return (lo_ <= x) && (x < checkpoint_); } std::size_t - LinearAlloc::before_checkpoint() const + ArenaAlloc::before_checkpoint() const { return checkpoint_ - lo_; } std::size_t - LinearAlloc::after_checkpoint() const + ArenaAlloc::after_checkpoint() const { return free_ptr_ - checkpoint_; } void - LinearAlloc::clear() + ArenaAlloc::clear() { this->checkpoint_ = lo_; this->free_ptr_ = lo_; - this->limit_ = lo_; + this->limit_ = hi_ - redline_z_; } void - LinearAlloc::checkpoint() + ArenaAlloc::checkpoint() { this->checkpoint_ = this->free_ptr_; } - std::uint8_t * - LinearAlloc::alloc(std::size_t z) + std::byte * + ArenaAlloc::alloc(std::size_t z0) { + scope log(XO_DEBUG(debug_flag_)); + /* word size for alignment */ - constexpr uint32_t c_bpw = sizeof(void*); + constexpr uint32_t c_bpw = sizeof(std::uintptr_t); std::uintptr_t free_u64 = reinterpret_cast(free_ptr_); assert(free_u64 % c_bpw == 0ul); - /* round up to multiple of c_bpw */ - std::uint32_t dz = (c_bpw - (z % c_bpw)); - z += dz; + std::uint32_t dz = alloc_padding(z0); - assert(z % c_bpw == 0ul); + std::size_t z1 = z0 + dz; - std::uint8_t * retval = this->free_ptr_; + assert(z1 % c_bpw == 0ul); - this->free_ptr_ += z; + std::byte * retval = this->free_ptr_; + + this->free_ptr_ += z1; + + log && log(xtag("self", name_), xtag("z0", z0), xtag("+pad", dz), xtag("z1", z1)); if (free_ptr_ > limit_) { return nullptr; @@ -128,8 +143,14 @@ namespace xo { return retval; } + + void + ArenaAlloc::release_redline_memory() { + this->limit_ = this->hi_; + } + } /*namespace gc*/ } /*namespace xo*/ -/* end LinearAlloc.cpp */ +/* end ArenaAlloc.cpp */ diff --git a/src/alloc/CMakeLists.txt b/src/alloc/CMakeLists.txt index cc5768d8..bc0f919f 100644 --- a/src/alloc/CMakeLists.txt +++ b/src/alloc/CMakeLists.txt @@ -2,7 +2,12 @@ set(SELF_LIB xo_alloc) set(SELF_SRCS - LinearAlloc.cpp + IAlloc.cpp + ArenaAlloc.cpp + ListAlloc.cpp + GC.cpp + Object.cpp + Forwarding1.cpp ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) diff --git a/src/alloc/Forwarding1.cpp b/src/alloc/Forwarding1.cpp new file mode 100644 index 00000000..825115a2 --- /dev/null +++ b/src/alloc/Forwarding1.cpp @@ -0,0 +1,45 @@ +/* file Forwarding1.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "Forwarding1.hpp" +#include +#include + +namespace xo { + namespace obj { + Forwarding1::Forwarding1(gp dest) + : dest_{dest} + {} + + Object * + Forwarding1::_offset_destination(Object * src) const + { + intptr_t offset = src - static_cast(this); + + return dest_.ptr() + offset; + } + + std::size_t + Forwarding1::_shallow_size() const { + assert(false); + return 0; + } + + Object * + Forwarding1::_shallow_copy() const { + assert(false); + return nullptr; + } + + std::size_t + Forwarding1::_forward_children() { + assert(false); + return 0; + } + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Forwarding1.cpp */ diff --git a/src/alloc/GC.cpp b/src/alloc/GC.cpp new file mode 100644 index 00000000..ec4aa647 --- /dev/null +++ b/src/alloc/GC.cpp @@ -0,0 +1,492 @@ +/* GC.cpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include "GC.hpp" +#include "Object.hpp" +#include "xo/indentlog/scope.hpp" +#include +#include + +namespace xo { + namespace gc { + void + PerGenerationStatistics::include_gc(std::size_t alloc_z, + std::size_t before_z, + std::size_t after_z, + std::size_t promote_z) + { + this->update_snapshot(after_z); + + new_alloc_z_ += alloc_z; + scanned_z_ += before_z; + survive_z_ += after_z; + promote_z_ += promote_z; + } + + void + PerGenerationStatistics::update_snapshot(std::size_t after_z) + { + used_z_ = after_z; + } + + void + PerGenerationStatistics::display(std::ostream & os) const + { + os << ""; + } + + 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(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(upto)].update_snapshot(after_z); + } + + void + GcStatistics::display(std::ostream & os) const + { + os << ""; + } + + GC::GC(const Config & config) + : config_{config} + { + enum { NurseryFrom, NurseryTo, TenuredFrom, TenuredTo }; + + std::size_t nursery_size = config.initial_nursery_z_; + std::size_t tenured_size = config.initial_tenured_z_; + + nursery_[role2int(role::from_space)] + = ListAlloc::make("NA", nursery_size, 2 * nursery_size, config.debug_flag_); + nursery_[role2int(role::to_space) ] + = ListAlloc::make("NB", nursery_size, 2 * nursery_size, config.debug_flag_); + + tenured_[role2int(role::from_space)] + = ListAlloc::make("TA", tenured_size, 2 * tenured_size, config.debug_flag_); + tenured_[role2int(role::to_space) ] + = ListAlloc::make("TB", tenured_size, 2 * tenured_size, config.debug_flag_); + + this->checkpoint(); + } + + up + GC::make(const Config & config) + { + GC * gc = new GC(config); + + return up{gc}; + } + + std::size_t + GC::size() const + { + return nursery_[role2int(role::to_space)]->size() + tenured_[role2int(role::to_space)]->size(); + } + + std::size_t + GC::allocated() const + { + return (nursery_[role2int(role::to_space)]->allocated() + + tenured_[role2int(role::to_space)]->allocated()); + } + + std::size_t + GC::available() const + { + return nursery_[role2int(role::to_space)]->available(); + } + + bool + GC::fromspace_contains(const void * x) const + { + return (nursery_[role2int(role::from_space)]->contains(x) + || tenured_[role2int(role::from_space)]->contains(x)); + } + + bool + GC::contains(const void * x) const + { + return (nursery_[role2int(role::to_space)]->contains(x) + || tenured_[role2int(role::to_space)]->contains(x)); + } + + bool + GC::is_before_checkpoint(const void * x) const + { + return nursery_[role2int(role::to_space)]->is_before_checkpoint(x); + } + + std::size_t + GC::before_checkpoint() const + { + return nursery_[role2int(role::to_space)]->before_checkpoint(); + } + + std::size_t + GC::after_checkpoint() const + { + return nursery_[role2int(role::to_space)]->after_checkpoint(); + } + + generation + GC::fromspace_generation_of(const void * x) const + { + if (tenured_[role2int(role::from_space)]->contains(x)) + return generation::tenured; + + return generation::nursery; + } + + generation + GC::generation_of(const void * x) const + { + if (tenured_[role2int(role::to_space)]->contains(x)) + return generation::tenured; + + return generation::nursery; + } + + std::byte * + GC::free_ptr(generation gen) + { + switch(gen) { + case generation::nursery: + return nursery_[role2int(role::to_space)]->free_ptr(); + case generation::tenured: + return tenured_[role2int(role::to_space)]->free_ptr(); + case generation::N: + assert(false); + } + + return nullptr; + } + + void + GC::clear() + { + nursery_[role2int(role::from_space)]->clear(); + nursery_[role2int(role::to_space) ]->clear(); + + tenured_[role2int(role::from_space)]->clear(); + tenured_[role2int(role::to_space) ]->clear(); + } + + void + GC::add_gc_root(Object ** addr) + { + gc_root_v_.push_back(addr); + } + + void + GC::checkpoint() + { + nursery_[role2int(role::to_space) ]->checkpoint(); + } + + std::byte * + GC::alloc(std::size_t z) + { + std::byte * x = nursery_[role2int(role::to_space)]->alloc(z); + + if (!x) { + this->request_gc(generation::nursery); + + if (incr_gc_pending_ || full_gc_pending_) + nursery_[role2int(role::to_space)]->release_redline_memory(); + + /* try (just once) more, maybe request fits in redline space */ + x = nursery_[role2int(role::to_space)]->alloc(z); + + assert(x); + } + + return x; + } + + std::byte * + GC::alloc_gc_copy(std::size_t z, const void * src) + { + scope log(XO_DEBUG(config_.debug_flag_), xtag("z", z), xtag("+pad", IAlloc::alloc_padding(z))); + + generation g = this->fromspace_generation_of(src); + + std::byte * retval = nullptr; + + if (g == generation::tenured) + { + log && log("tenured"); + + retval = tenured_[role2int(role::to_space)]->alloc(z); + } else if (nursery_[role2int(role::from_space)]->is_before_checkpoint(src)) + { + log && log("promote"); + + /* nursery object has survived 2nd collection cycle + * -> promote into tenured generation + */ + retval = tenured_[role2int(role::to_space)]->alloc(z); + + this->gc_statistics_.total_promoted_ += IAlloc::with_padding(z); + } else { + log && log("nursery"); + + retval = nursery_[role2int(role::to_space)]->alloc(z); + + if (!retval) { + /* nursery space exhausted */ + + this->request_gc(generation::nursery); + + nursery_[role2int(role::to_space)]->release_redline_memory(); + + retval = nursery_[role2int(role::to_space)]->alloc(z); + } + } + + assert(retval); + + return retval; + } + + void + GC::release_redline_memory() + { + // not supported feature for GC + } + + void + GC::swap_nursery() + { + up tmp = std::move(nursery_[role2int(role::to_space)]); + nursery_[role2int(role::to_space)] = std::move(nursery_[role2int(role::from_space)]); + nursery_[role2int(role::from_space)] = std::move(tmp); + } + + void + GC::swap_tenured() + { + up tmp = std::move(tenured_[role2int(role::to_space)]); + tenured_[role2int(role::to_space)] = std::move(tenured_[role2int(role::from_space)]); + tenured_[role2int(role::from_space)] = std::move(tmp); + } + + void + GC::swap_spaces(generation target) + { + // will be copying into storage currently labelled FromSpace + + /* gc will copy some to-be-determined amount in [0..promote_z] + from nursery->tenured generation. + */ + std::size_t promote_z = nursery_[role2int(role::to_space)]->before_checkpoint(); + if (target == generation::tenured) { + /* gc on tenured generation may need this much space */ + std::size_t tenured_z = (tenured_[role2int(role::to_space)]->allocated() + + promote_z + + full_gc_threshold_); + + tenured_[role2int(role::from_space)]->reset(tenured_z); + + this->swap_tenured(); + } else { + if (tenured_[role2int(role::to_space)]->available() < promote_z) { + tenured_[role2int(role::to_space)]->expand(promote_z); + } + } + + nursery_[role2int(role::from_space)]->reset(nursery_[role2int(role::to_space)]->allocated() + - promote_z + + incr_gc_threshold_); + this->swap_nursery(); + } /*swap_spaces*/ + + void + GC::copy_object(Object ** pp_object, generation upto, ObjectStatistics * object_stats) + { + void * object_address = *pp_object; + + if (nursery_[role2int(role::to_space)]->contains(object_address) + || ((upto == generation::tenured) + && tenured_[role2int(role::to_space)]->contains(object_address))) + { + /* global is already in to-space */ + ; + } else if((upto == generation::nursery) && tenured_[role2int(role::to_space)]->contains(object_address)) + { + /* skip tenured objects when incremental collection */ + ; + } else { + *pp_object = Object::_deep_move(*pp_object, this, object_stats); + } + } + + void + GC::copy_globals(generation upto) + { + for (Object ** pp_root : gc_root_v_) { + this->copy_object(pp_root, upto, &gc_statistics_.per_type_stats_); + } + } + + void + GC::cleanup_phase(generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + std::size_t N_allocated = nursery_[role2int(role::from_space)]->after_checkpoint(); + std::size_t T_allocated = tenured_[role2int(role::from_space)]->after_checkpoint(); + + std::size_t N_before_gc = nursery_[role2int(role::from_space)]->allocated(); + std::size_t T_before_gc = tenured_[role2int(role::from_space)]->allocated(); + + std::size_t N_after_gc = nursery_[role2int(role::to_space)]->allocated(); + std::size_t T_after_gc = tenured_[role2int(role::to_space)]->allocated(); + //std::byte * N_free_ptr = nursery_[role2int(role::to_space)]->free_ptr(); + + std::size_t promote_z = gc_statistics_.total_promoted_ - gc_statistics_.total_promoted_sab_; + + this->nursery_[role2int(role::from_space)]->reset(0); + this->tenured_[role2int(role::from_space)]->reset(0); + + /* objects currenty in to-space nursery have survived one collection */ + this->nursery_[role2int(role::to_space)]->checkpoint(); + + // nursery_[role2int(role::to_space)]->set_redline(nursery_[role2int(role::to_space)]->allocated() + incr_gc_threshold_) + + if (upto == generation::tenured) + this->tenured_[role2int(role::to_space)]->checkpoint(); + + if (log) { + log(xtag("N_allocated", N_allocated)); + log(xtag("N_before_gc", N_before_gc)); + log(xtag("N_after_gc", N_after_gc)); + log(xtag("T_allocated", T_allocated)); + log(xtag("T_before_gc", T_before_gc)); + log(xtag("T_after_gc", T_after_gc)); + } + + this->incr_gc_pending_ = false; + this->gc_statistics_.include_gc(generation::nursery, N_allocated, N_before_gc, N_after_gc, promote_z); + + if (upto == generation::tenured) { + this->full_gc_pending_ = false; + this->gc_statistics_.include_gc(generation::tenured, T_allocated, T_before_gc, T_after_gc, 0); + } else { + // still want to update tenured stats for current alloc size + this->gc_statistics_.update_snapshot(generation::tenured, T_after_gc); + } + } + + void + GC::execute_gc(generation target) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + bool full_move = (target == generation::tenured); + + // TODO: RAII version in case of exceptions + this->runstate_ = GCRunstate(true /*in_progress*/, full_move); + + log && log("step 0: snapshot alloc stats"); + + /* new allocation since last GC */ + std::size_t new_alloc = this->after_checkpoint(); + + ++(gc_statistics_.gen_v_[static_cast(target)].n_gc_); + gc_statistics_.total_allocated_ += new_alloc; + gc_statistics_.total_promoted_sab_ = gc_statistics_.total_promoted_; + + log && log(xtag("new_alloc", new_alloc)); + + log && log("step 1: swap to/from roles"); + + this->swap_spaces(target); + + log && log("step 2a: copy globals"); + + this->copy_globals(target); + + log && log("step 2b: TODO: copy pinned"); + + log && log("step 3: TODO: forward mutation log"); + + log && log("step 4: TODO: notify destructor log"); + + log && log("step 5: TODO: keep reachable weak pointers"); + + log && log("step 6: cleanup"); + + this->cleanup_phase(target); + + this->runstate_ = GCRunstate(); + + log && log("statistics:"); + log && log(gc_statistics_); + } + + void + GC::request_gc(generation target) + { + if (!runstate_.in_progress() && (gc_enabled_ == 0)) { + if (!config_.allow_incremental_gc_) + target = generation::tenured; + + if ((target == generation::nursery) + && (tenured_[role2int(role::to_space)]->after_checkpoint() > full_gc_threshold_)) + { + /** full collection when >= @ref full_gc_threshold_ bytes added to tenured + * generation, since last full collection + **/ + target = generation::tenured; + } + + this->execute_gc(target); + } else { + this->incr_gc_pending_ = true; + if (target == generation::tenured) + this->full_gc_pending_ = true; + } + } + + void + GC::disable_gc() { + --gc_enabled_; + } + + void + GC::enable_gc() { + ++gc_enabled_; + + if (gc_enabled_ == 0) { + /* unblock gc */ + if (incr_gc_pending_) + this->request_gc(full_gc_pending_ ? generation::tenured : generation::nursery); + } + } + } /*namespace gc*/ +} /*namespace xo*/ + +/* end GC.cpp */ diff --git a/src/alloc/IAlloc.cpp b/src/alloc/IAlloc.cpp new file mode 100644 index 00000000..4fbdd556 --- /dev/null +++ b/src/alloc/IAlloc.cpp @@ -0,0 +1,54 @@ +/* @file IAlloc.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "IAlloc.hpp" +#include +#include + +namespace xo { + namespace gc { + + std::uint32_t + IAlloc::alloc_padding(std::size_t z) + { + /* word size for alignment */ + constexpr uint32_t c_bpw = sizeof(std::uintptr_t); + + /* round up to multiple of c_bpw, but map 0 -> 0 + * (table assuming c_bpw==8) + * + * z%c_bpw dz + * ------------ + * 0 0 + * 1 7 + * 2 6 + * .. .. + * 7 1 + */ + std::uint32_t dz = (c_bpw - (z % c_bpw)) % c_bpw; + z += dz; + + assert(z % c_bpw == 0ul); + + return dz; + } + + std::size_t + IAlloc::with_padding(std::size_t z) + { + return z + alloc_padding(z); + } + + std::byte * + IAlloc::alloc_gc_copy(std::size_t /*z*/, const void * /*src*/) + { + assert(false); + return nullptr; + } + + } /*namespace gc*/ +} /*namespace xo*/ + +/* end IAlloc.cpp */ diff --git a/src/alloc/ListAlloc.cpp b/src/alloc/ListAlloc.cpp new file mode 100644 index 00000000..76da8b19 --- /dev/null +++ b/src/alloc/ListAlloc.cpp @@ -0,0 +1,318 @@ +/* file ListAlloc.cpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include "ListAlloc.hpp" +#include "ArenaAlloc.hpp" +#include +#include + +namespace xo { + namespace gc { + ListAlloc::ListAlloc(std::unique_ptr hd, + ArenaAlloc * marked, + std::size_t cz, std::size_t nz, std::size_t tz, + bool use_redline, + bool debug_flag) + : start_z_{cz}, + hd_{std::move(hd)}, + marked_{marked}, + full_l_{}, + current_z_{cz}, + next_z_{nz}, + total_z_{tz}, + use_redline_{use_redline}, + debug_flag_{debug_flag} + {} + + ListAlloc::~ListAlloc() + { + this->clear(); + } + + up + ListAlloc::make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag) + { + std::unique_ptr hd{ArenaAlloc::make(name, 0, cz, debug_flag)}; + + if (!hd) + return nullptr; + + ArenaAlloc * marked = nullptr; + + up retval{new ListAlloc(std::move(hd), + marked, + cz, nz, cz, + false /*!use_redline*/, + debug_flag)}; + + return retval; + } + + std::size_t + ListAlloc::size() const { + return total_z_; + } + + std::byte * + ListAlloc::free_ptr() const { + return hd_->free_ptr(); + } + + std::size_t + ListAlloc::available() const { + if (hd_) { + /* can only allocate from @ref hd_, + * so even if there were available space in @ref full_l_, + * it's not accessible to ListAlloc. + */ + + return hd_->available(); + } + + return 0; + } + + std::size_t + ListAlloc::allocated() const { + std::size_t total = 0; + + if (hd_) + total += hd_->allocated(); + + for (const auto & alloc : full_l_) + total += alloc->allocated(); + + return total; + } + + bool + ListAlloc::contains(const void * x) const { + if (hd_ && hd_->contains(x)) + return true; + + for (const auto & alloc : full_l_) { + if (alloc->contains(x)) + return true; + } + + return false; + } + + bool + ListAlloc::is_before_checkpoint(const void * x) const { + if (!marked_) + return false; + + if ((marked_ == hd_.get()) && hd_->contains(x)) + return hd_->is_before_checkpoint(x); + + /* + * 1. allocs in full_l_ appear in youngest-to-oldest order + * 2. allocators that appear before marked_ in full_l_ count as 'after checkpoint' + * 3. allocators that appear after marked_ in full_l_ count as 'before checkpoint' + */ + + bool younger_than_marked = true; + + for (const auto & alloc : full_l_) { + if (younger_than_marked) { + if (alloc.get() == marked_) { + /* nothing else to test on this iteration, + * already checked .marked_ specifically + */ + younger_than_marked = false; + } else { + /* after checkpoint */ + if (alloc->contains(x)) + return false; + } + } else { + if (alloc->contains(x)) + return true; + } + } + + return false; + } + + std::size_t + ListAlloc::before_checkpoint() const + { + if (marked_) { + if (full_l_.empty()) { + assert(marked_ == hd_.get()); + + return marked_->before_checkpoint(); + } + } else { + /* count everything allocated */ + return this->allocated(); + } + + std::size_t z = 0; + + /* control here: .marked & .full_l non-empty. */ + if (hd_.get() == marked_) { + z += hd_->before_checkpoint(); + + /* anything in .full_l older than marked .hd */ + for (const auto & alloc : full_l_) { + z += alloc->allocated(); + } + + return z; + } else { + /* messiest case: .marked is true, + * and not the youngest arena + */ + bool younger_than_marked = true; + + for (const auto & alloc : full_l_) { + if (younger_than_marked) { + if (alloc.get() == marked_) { + younger_than_marked = false; + z += marked_->before_checkpoint(); + } else { + ; + } + } else { + z += alloc->allocated(); + } + } + } + + return z; + } + + std::size_t + ListAlloc::after_checkpoint() const + { + if (!marked_) + return 0; + + if (full_l_.empty()) { + assert(marked_ == hd_.get()); + + return marked_->after_checkpoint(); + } + + bool younger_than_marked = true; + + std::size_t z = 0; + + for (const auto & alloc : full_l_) { + if (younger_than_marked) { + if (alloc.get() == marked_) { + younger_than_marked = false; + z += marked_->after_checkpoint(); + break; + } else { + z += alloc->allocated(); + } + } + } + + return z; + } + + void + ListAlloc::clear() { + // general hygiene + start_z_ = 0; + hd_.reset(); + marked_ = nullptr; + full_l_.clear(); + current_z_ = 0; + next_z_ = 0; + total_z_ = 0; + use_redline_ = false; + } + + bool + ListAlloc::reset(std::size_t z) + { + // warning: hd_->size() does not include redline memory + hd_->release_redline_memory(); + + bool recycle_head_bucket = hd_ && (z <= hd_->size()); + + this->full_l_.clear(); + this->marked_ = nullptr; + this->redlined_flag_ = false; + + if (recycle_head_bucket) { + this->hd_->clear(); + this->total_z_ = hd_->size(); + + return true; + } else { + this->hd_.reset(nullptr); + this->total_z_ = 0; + + return this->expand(z); + } + } + + bool + ListAlloc::expand(std::size_t z) + { + std::size_t cz = current_z_; + std::size_t nz = next_z_; + std::size_t tz; + + do { + tz = cz + nz; + cz = nz; + nz = tz; + } while (cz < z); + + std::string name = hd_->name() + "+exp"; + + std::unique_ptr new_alloc = ArenaAlloc::make(name, 0, cz, debug_flag_); + + if (!new_alloc) + return false; + + this->current_z_ = cz; + this->next_z_ = nz; + this->total_z_ += cz; + + this->hd_ = std::move(new_alloc); + + return true; + } + + void + ListAlloc::checkpoint() { + hd_->checkpoint(); + + this->marked_ = hd_.get(); + } + + std::byte * + ListAlloc::alloc(std::size_t z) { + std::byte * retval = hd_->alloc(z); + + if (retval) + return retval; + + if (this->expand(z)) + return hd_->alloc(z); + + return nullptr; + } + + void + ListAlloc::release_redline_memory() + { + if (use_redline_) + redlined_flag_ = true; + + this->hd_->release_redline_memory(); + } + } /*namespace gc*/ +} /*namespace xo*/ + +/* end ListAlloc.cpp */ diff --git a/src/alloc/Object.cpp b/src/alloc/Object.cpp new file mode 100644 index 00000000..7aa1a8d0 --- /dev/null +++ b/src/alloc/Object.cpp @@ -0,0 +1,196 @@ +/* Object.cpp + * + * author: Roalnd Conybeare, Jul 2025 + */ + +#include "Object.hpp" +#include "GC.hpp" +#include "Forwarding1.hpp" + +using xo::obj::Forwarding1; + +void * +operator new (std::size_t z, const xo::Cpof & cpof) +{ + using xo::gc::GC; + + GC * gc = reinterpret_cast(xo::Object::mm); + + return gc->alloc_gc_copy(z, cpof.src_); +} + +namespace xo { + gc::IAlloc * + Object::mm = nullptr; + + Object * + Object::_forward(Object * src, gc::GC * gc) + { + if (!src) + return src; + + if (src->_is_forwarded()) + return src->_offset_destination(src); + + bool full_move = gc->runstate().full_move(); + + if (!full_move && (gc->generation_of(src) == gc::generation::tenured)) { + /* don't move tenured objects during incremental collection */ + return src; + } + + Object::_shallow_move(src, gc); + + /* *src is now a forwarding pointer to copy in to-space */ + + return src->_offset_destination(src); + } + + Object * + Object::_deep_move(Object * from_src, gc::GC * gc, gc::ObjectStatistics * /*stats*/) + { + using gc::generation; + + if (!from_src) + return nullptr; + + Object * retval = from_src->_destination(); + + if (retval) + return retval; + + bool full_move = gc->runstate().full_move(); + + if (!full_move && gc->generation_of(from_src) == generation::tenured) { + /** incremental collection does not move already-tenured objects **/ + return from_src; + } + + /** + * To-space: + * + * to_lo = start of to-space + * w,W = white objects. An object x is white if x + all immediate children of x are in to-space + * (also implies this GC cycle put it there) + * g,G = grey objects. An object x is gray if it's in to-space, + * but possibly has >0 black children + * _ = free to-space memory + * N = nursery space + * T = tenured space + * + * wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * After moving children of one object, advancing {nursery_grey_lo, nursery_free_ptr} + * + * wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * Invariant: + * + * objects in [to_lo, gray_lo) are white. + * all gray objects are in [gray_lo, free_ptr) + * memory starting at free_ptr is free. + * + * deep_move terminates when gray_lo catches up to free_ptr + * + * Above is simplified. Complication is that GC (including incremental) may + * promote objects from nursery (N) to tenured (T) + * + * So more accurate before/after picture + * + * N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwgggggggggggg_______________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(N) + * + * After moving children of one object, advancing {nursery_grey_lo, nursery_free_ptr} + * + * N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(T) + * + * deep_move terminates when both: + * - gray_lo(N) catches up with free_ptr(N) + * - gray_lo(T) catches up with free_ptr(T) + * + **/ + + std::array gray_lo_v + = { gc->free_ptr(generation::nursery), gc->free_ptr(generation::tenured) }; + + Object * to_src = Object::_shallow_move(from_src, gc); + + std::size_t fixup_work = 0; + do { + fixup_work = 0; + + auto fixup_generation = [gc, &gray_lo_v](generation gen) { + std::size_t work = 0; + while(gray_lo_v[gen2int(gen)] < gc->free_ptr(gen)) { + Object * x = reinterpret_cast(gray_lo_v[gen2int(gen)]); + + // update per-class stats here + + std::size_t xz = x->_forward_children(); + + // must pad xz to multiple of word size, + // to match behavior of LinearAlloc::alloc() + // + xz += gc::IAlloc::alloc_padding(xz); + + gray_lo_v[gen2int(gen)] += xz; + ++work; + } + + return work; + }; + + fixup_work += fixup_generation(generation::nursery); + fixup_work += fixup_generation(generation::tenured); + } while (fixup_work > 0); + + return to_src; + } /*deep_move*/ + + Object * + Object::_shallow_move(Object * src, gc::GC * gc) + { + /* filter for source objects that are owned by GC. + * Care required though -- during GC from/to spaces have been swapped already + */ + if (gc->fromspace_contains(src)) + { + Object * dest = src->_shallow_copy(); + + if (dest != src) + src->_forward_to(dest); + + return dest; + } else { + return src; + } + } + + void + Object::_forward_to(Object * dest) + { + char * mem = reinterpret_cast(this); + + Forwarding1 * fwd = new (mem) Forwarding1(dest); + + (void)fwd; + } + +} /*namespace xo*/ + +/* end Object.cpp*/ diff --git a/utest/ArenaAlloc.test.cpp b/utest/ArenaAlloc.test.cpp new file mode 100644 index 00000000..aae2695b --- /dev/null +++ b/utest/ArenaAlloc.test.cpp @@ -0,0 +1,87 @@ +/* @file ArenaAlloc.test.cpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include "xo/alloc/ArenaAlloc.hpp" +#include + +namespace xo { + using xo::gc::ArenaAlloc; + + namespace ut { + + namespace { + struct testcase_alloc { + testcase_alloc(std::size_t rz, std::size_t z) + : redline_z_{rz}, arena_z_{z} {} + + std::size_t redline_z_; + std::size_t arena_z_; + + }; + + std::vector + s_testcase_v = { + testcase_alloc(0, 4096) + }; + } + + + TEST_CASE("linearalloc", "[alloc]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const testcase_alloc & tc = s_testcase_v[i_tc]; + + constexpr bool c_debug_flag = false; + + auto alloc = ArenaAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag); + + REQUIRE(alloc.get()); + REQUIRE(alloc->name() == "linearalloc"); + REQUIRE(alloc->size() == tc.arena_z_); + REQUIRE(alloc->available() == tc.arena_z_); + REQUIRE(alloc->allocated() == 0); + REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == 0); + + auto free0 = alloc->free_ptr(); + + auto mem = alloc->alloc(tc.arena_z_); + + REQUIRE(mem != nullptr); + + REQUIRE(mem == free0); + + REQUIRE(alloc->size() == tc.arena_z_); + REQUIRE(alloc->available() == 0); + REQUIRE(alloc->allocated() == tc.arena_z_); + REQUIRE(alloc->is_before_checkpoint(mem) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == tc.arena_z_); + + alloc->clear(); + + REQUIRE(alloc->free_ptr() == free0); + REQUIRE(alloc->available() == tc.arena_z_); + REQUIRE(alloc->allocated() == 0); + REQUIRE(alloc->is_before_checkpoint(free0) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == 0); + + mem = alloc->alloc(1); + + auto used = sizeof(void*); + REQUIRE(alloc->size() == tc.arena_z_); + REQUIRE(alloc->available() == tc.arena_z_ - used); + REQUIRE(alloc->allocated() == used); + REQUIRE(alloc->is_before_checkpoint(free0) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == used); + + } + } + + } /*namespace ut */ +} /*namespace xo*/ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index e845f729..d37786e3 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -3,7 +3,8 @@ set(SELF_EXE utest.alloc) set(SELF_SRCS alloc_utest_main.cpp - LinearAlloc.test.cpp) + ArenaAlloc.test.cpp + GC.test.cpp) xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS}) xo_self_dependency(${SELF_EXE} xo_alloc) diff --git a/utest/GC.test.cpp b/utest/GC.test.cpp new file mode 100644 index 00000000..dc175615 --- /dev/null +++ b/utest/GC.test.cpp @@ -0,0 +1,69 @@ +/* @file GC.test.cpp + * + * author: Roland Conybeare, Jul 2025 + */ + +#include "xo/alloc/GC.hpp" +#include + +namespace xo { + using xo::gc::GC; + using xo::gc::generation; + using xo::gc::Config; + + namespace ut { + + namespace { + struct testcase_gc { + testcase_gc(std::size_t nz, std::size_t tz) : nursery_z_{nz}, tenured_z_{tz} {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + }; + + std::vector + s_testcase_v = { + testcase_gc(1024, 4096) + }; + } + + TEST_CASE("gc", "[alloc][gc]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const testcase_gc & tc = s_testcase_v[i_tc]; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_}); + + REQUIRE(gc.get()); + REQUIRE(gc->size() == tc.nursery_z_ + tc.tenured_z_); + REQUIRE(gc->allocated() == 0); + REQUIRE(gc->available() == tc.nursery_z_); + REQUIRE(gc->before_checkpoint() == 0); + // ListAlloc model is that nothing is before checkpoint + // until it's first established + REQUIRE(gc->after_checkpoint() == 0); + + REQUIRE(gc->gc_in_progress() == false); + REQUIRE(gc->is_gc_enabled() == true); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 0); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + + /* gc with empty state */ + gc->request_gc(generation::nursery); + + REQUIRE(gc->gc_in_progress() == false); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + + /* still empty state */ + gc->request_gc(generation::tenured); + + REQUIRE(gc->gc_in_progress() == false); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1); + } + } + } /*namespace ut*/ +} /*namespace xo*/ diff --git a/utest/LinearAlloc.test.cpp b/utest/LinearAlloc.test.cpp index 5d3a1fcd..b1909991 100644 --- a/utest/LinearAlloc.test.cpp +++ b/utest/LinearAlloc.test.cpp @@ -3,7 +3,7 @@ * author: Roland Conybeare, Jul 2025 */ -#include "xo/alloc/LinearAlloc.hpp" +#include "xo/alloc/ArenaAlloc.hpp" #include namespace xo { @@ -33,15 +33,53 @@ namespace xo { for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { const testcase_alloc & tc = s_testcase_v[i_tc]; - auto alloc = LinearAlloc::make(tc.redline_z_, tc.arena_z_); + constexpr bool c_debug_flag = false; + + auto alloc = LinearAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag); REQUIRE(alloc.get()); + REQUIRE(alloc->name() == "linearalloc"); REQUIRE(alloc->size() == tc.arena_z_); REQUIRE(alloc->available() == tc.arena_z_); REQUIRE(alloc->allocated() == 0); REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false); REQUIRE(alloc->before_checkpoint() == 0); REQUIRE(alloc->after_checkpoint() == 0); + + auto free0 = alloc->free_ptr(); + + auto mem = alloc->alloc(tc.arena_z_); + + REQUIRE(mem != nullptr); + + REQUIRE(mem == free0); + + REQUIRE(alloc->size() == tc.arena_z_); + REQUIRE(alloc->available() == 0); + REQUIRE(alloc->allocated() == tc.arena_z_); + REQUIRE(alloc->is_before_checkpoint(mem) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == tc.arena_z_); + + alloc->clear(); + + REQUIRE(alloc->free_ptr() == free0); + REQUIRE(alloc->available() == tc.arena_z_); + REQUIRE(alloc->allocated() == 0); + REQUIRE(alloc->is_before_checkpoint(free0) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == 0); + + mem = alloc->alloc(1); + + auto used = sizeof(void*); + REQUIRE(alloc->size() == tc.arena_z_); + REQUIRE(alloc->available() == tc.arena_z_ - used); + REQUIRE(alloc->allocated() == used); + REQUIRE(alloc->is_before_checkpoint(free0) == false); + REQUIRE(alloc->before_checkpoint() == 0); + REQUIRE(alloc->after_checkpoint() == used); + } }