+ xo-alloc + xo-object + xo-alloc docs + GC utests
This commit is contained in:
parent
03c8d66401
commit
e1d5ae46d2
58 changed files with 3948 additions and 83 deletions
|
|
@ -69,6 +69,7 @@ set(DOX_EXCLUDE_PATTERNS [=[
|
|||
|
||||
# ----------------------------------------------------------------
|
||||
# xo satellite projects
|
||||
# in reverse topological order i.e. dependencies first
|
||||
|
||||
add_subdirectory(xo-cmake)
|
||||
add_subdirectory(xo-indentlog)
|
||||
|
|
@ -86,6 +87,7 @@ add_subdirectory(xo-unit)
|
|||
add_subdirectory(xo-pyunit)
|
||||
#
|
||||
add_subdirectory(xo-alloc)
|
||||
add_subdirectory(xo-object)
|
||||
#
|
||||
add_subdirectory(xo-callback)
|
||||
add_subdirectory(xo-webutil)
|
||||
|
|
@ -118,6 +120,6 @@ add_subdirectory(xo-pyjit)
|
|||
# ----------------------------------------------------------------
|
||||
# documentation. must follow add_subdirectory() for satellite projects
|
||||
|
||||
xo_umbrella_doxygen_deps(xo_flatstring xo_ratio xo_unit xo_tokenizer xo_reader xo_jit)
|
||||
xo_umbrella_doxygen_deps(xo_alloc xo_flatstring xo_ratio xo_unit xo_tokenizer xo_reader xo_jit)
|
||||
xo_umbrella_doxygen_config()
|
||||
xo_umbrella_sphinx_config(index.rst docs/install.rst docs/glossary.rst)
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ pkgs.mkShell {
|
|||
pkgs.catch2
|
||||
pkgs.zlib
|
||||
pkgs.unzip
|
||||
pkgs.libbsd
|
||||
|
||||
pkgs.cmake
|
||||
pkgs.pkg-config
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Some features: kalman filters, stochastic processes, complex event processing, s
|
|||
:caption: XO contents
|
||||
|
||||
docs/install
|
||||
xo-alloc/docs/index
|
||||
xo-indentlog/docs/index
|
||||
xo-flatstring/docs/index
|
||||
xo-ratio/docs/index
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
9
xo-alloc/docs/CMakeLists.txt
Normal file
9
xo-alloc/docs/CMakeLists.txt
Normal 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
41
xo-alloc/docs/README
Normal 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
1
xo-alloc/docs/_static/README
vendored
Normal 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
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
39
xo-alloc/docs/conf.py
Normal 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'
|
||||
202
xo-alloc/docs/implementation.rst
Normal file
202
xo-alloc/docs/implementation.rst
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
.. _implementation:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Library
|
||||
=======
|
||||
|
||||
Library dependency tower for *xo-alloc*:
|
||||
|
||||
.. ditaa::
|
||||
|
||||
+------------------------------------------+
|
||||
| xo_alloc |
|
||||
+------------------------------------------+
|
||||
| xo_indentlog |
|
||||
+------------------------------------------+
|
||||
|
||||
Install instructions :doc:`here<install>`
|
||||
|
||||
Components
|
||||
==========
|
||||
|
||||
Abstraction tower for *xo-alloc* components:
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.85
|
||||
|
||||
+----------------+-------------+
|
||||
| IAlloc | Object |
|
||||
+----------------+-------------+
|
||||
|
||||
+-------------+ +-------------+
|
||||
| GC | | Forwarding1 |
|
||||
+-------------+ +-------------+
|
||||
| ListAlloc |
|
||||
+-------------+
|
||||
| ArenaAlloc |
|
||||
+-------------+
|
||||
|
||||
* *IAlloc*
|
||||
Allocator interface.
|
||||
|
||||
* *Object*
|
||||
Root Object Interface for types participating in garbage collection
|
||||
|
||||
* *GC*
|
||||
Incremental compacting garbage collector.
|
||||
|
||||
* *ListAlloc*
|
||||
Auto-expanding allocator. Contains a collection of ArenaAllocs
|
||||
|
||||
* *ArenaAlloc*
|
||||
Arena allocator (a.k.a bump allocator).
|
||||
|
||||
* *Object*
|
||||
Interface for types that participate in garbage collection
|
||||
|
||||
* *Forwarding1*
|
||||
Forwarding pointer. Supports the Object interface;
|
||||
used internally by GC during evacuation.
|
||||
|
||||
Key Points
|
||||
----------
|
||||
|
||||
* Allocators can be reset, but do not support freeing of individual allocs.
|
||||
* GC works with types that implement auxiliary GC-support methods.
|
||||
Such types must inherit Object.
|
||||
* A region may uses multiple arenas, but because of allocation activity
|
||||
since the last GC. If necessary, GC will allocate a new to-space with a
|
||||
single arena that's large enough to accomodate all objects that might survive
|
||||
from a from-space that has acquired multiple arenas.
|
||||
Intent is to scale up to find application's working set size, then stabilize
|
||||
|
||||
Components
|
||||
==========
|
||||
|
||||
Allocators
|
||||
----------
|
||||
|
||||
Inheritance
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. uml::
|
||||
:caption: allocators
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
class IAlloc {
|
||||
+ alloc()
|
||||
+ alloc_gc_copy()
|
||||
+ checkpoint()
|
||||
+ clear()
|
||||
}
|
||||
|
||||
class ArenaAlloc {
|
||||
+ free_ptr()
|
||||
- lo_ : byte*
|
||||
- checkpoint_ : byte*
|
||||
- limit_ : byte*
|
||||
}
|
||||
|
||||
IAlloc <|-- ArenaAlloc
|
||||
|
||||
class ListAlloc {
|
||||
+ expand()
|
||||
+ free_ptr()
|
||||
- start_z_
|
||||
- hd_
|
||||
- full_l_
|
||||
}
|
||||
|
||||
IAlloc <|-- ListAlloc
|
||||
|
||||
class GC {
|
||||
+ add_gc_root()
|
||||
+ request_gc()
|
||||
+ gc_statistics()
|
||||
- gc_root_v_[] : Object**
|
||||
- nursery_[2] : ListAlloc*
|
||||
- tenured_[2] : ListAlloc*
|
||||
}
|
||||
|
||||
IAlloc <|-- GC
|
||||
|
||||
|
||||
Composition
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. uml::
|
||||
:caption: allocator composition
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
object gc<<GC>>
|
||||
gc : nursery[from] = n0
|
||||
gc : nursery[to] = n1
|
||||
gc : tenured[from] = t0
|
||||
gc : tenured[to] = t1
|
||||
|
||||
object n0<<ListAlloc>>
|
||||
|
||||
object n1<<ListAlloc>>
|
||||
|
||||
object t0<<ListAlloc>>
|
||||
|
||||
object t1<<ListAlloc>>
|
||||
|
||||
gc o-- n0
|
||||
gc o-- n1
|
||||
gc o-- t0
|
||||
gc o-- t1
|
||||
|
||||
|
||||
Each ListAlloc composes like this:
|
||||
|
||||
.. uml::
|
||||
:caption: ListAlloc composition
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
object x<<ListAlloc>>
|
||||
x : hd_ = a0
|
||||
x : full_l = {a1, a2}
|
||||
|
||||
object a0<<ArenaAlloc>>
|
||||
a0 : lo_ = 0
|
||||
a0 : free_ = 12345
|
||||
a0 : hi_ = 1000000
|
||||
|
||||
object a1<<ArenaAlloc>>
|
||||
|
||||
object a2<<ArenaAlloc>>
|
||||
|
||||
x o-- a0
|
||||
x o-- a1
|
||||
x o-- a2
|
||||
|
||||
Here *a1* and *a2* are full, while *a0* can still allocate memory.
|
||||
|
||||
Objects
|
||||
|
||||
.. uml::
|
||||
:caption: objects
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
class Object {
|
||||
+ _is_forwarded()
|
||||
+ _offset_destination()
|
||||
+ _forward_to()
|
||||
+ _destination()
|
||||
+ _shallow_size()
|
||||
+ _shallow_copy()
|
||||
+ _forward_children()
|
||||
}
|
||||
|
||||
class Forwarding1 {
|
||||
- dest_ : Object*
|
||||
}
|
||||
|
||||
Object <|-- Forwarding1
|
||||
14
xo-alloc/docs/index.rst
Normal file
14
xo-alloc/docs/index.rst
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# xo-alloc documentation master file
|
||||
|
||||
xo-alloc documentation
|
||||
======================
|
||||
|
||||
xo-alloc provides arena allocators and a generation garbage collector
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: xo-alloc contents
|
||||
|
||||
install
|
||||
introduction
|
||||
implementation
|
||||
120
xo-alloc/docs/install.rst
Normal file
120
xo-alloc/docs/install.rst
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
.. _install:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Source
|
||||
======
|
||||
|
||||
Source code lives on github `here`_
|
||||
|
||||
.. _here: https://github.com/rconybea/xo-alloc
|
||||
|
||||
To clone from git:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/rconybea/xo-alloc
|
||||
|
||||
Tested with gcc 13.3
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
One-step Install
|
||||
----------------
|
||||
|
||||
Install along with the reset of *XO* from `xo-umbrella2 source`_
|
||||
|
||||
.. _xo-umbrella2 source: https://github.com/rconybea/xo-umbrella2
|
||||
|
||||
Minimal Install
|
||||
---------------
|
||||
|
||||
To build+install just required dependencies:
|
||||
``xo-alloc`` uses several supporting libraries from the *XO* project:
|
||||
|
||||
- `xo-indentlog source`_ (structured logging)
|
||||
- `xo-cmake source`_ (shared cmake macros)
|
||||
|
||||
.. _xo-indentlog source: https://github.com/rconybea/indentlog
|
||||
.. _xo-cmake source: https://github.com/rconybea/xo-cmake
|
||||
|
||||
Building from source
|
||||
--------------------
|
||||
|
||||
Install scripts for XO libraries depend on helper scripts installed from `xo-cmake`.
|
||||
|
||||
Preamble:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mkdir -p ~/proj/xo
|
||||
cd ~/proj/xo
|
||||
|
||||
git clone https://github.com/rconybea/xo-cmake
|
||||
|
||||
PREFIX=/usr/local # ..or desired installation prefix
|
||||
|
||||
# want PREFIX/bin in PATH to use xo-cmake helpers
|
||||
PATH=$PREFIX/bin:$PATH
|
||||
|
||||
Install `xo-cmake`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cmake -B xo-cmake/.build -S xo-cmake
|
||||
cmake --install xo-cmake/.build
|
||||
|
||||
Install remaining dependencie(s) in topological order:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
xo-build --clone --configure --build --install xo-indentlog
|
||||
xo-build --clone --configure --build --install xo-alloc
|
||||
|
||||
Directories under ``PREFIX`` will then contain:
|
||||
|
||||
.. code-block::
|
||||
|
||||
PREFIX
|
||||
+- bin
|
||||
| +- xo-build
|
||||
| +- xo-cmake-config
|
||||
| \- xo-cmake-lcov-harness
|
||||
+- include
|
||||
| \- xo
|
||||
| +- alloc/
|
||||
| \- indentlog/
|
||||
+- lib
|
||||
| +- cmake
|
||||
| | +- xo_alloc/
|
||||
| | \- indentlog/
|
||||
| +- lib*.so
|
||||
+- share
|
||||
+- cmake
|
||||
| \- xo_macros
|
||||
| +- code-coverage.cmake
|
||||
| +- xo-project-macros.cmake
|
||||
| \- xo_cxx.cmake
|
||||
+- etc
|
||||
| \- xo
|
||||
| \- subsystem-list
|
||||
\- xo-macros
|
||||
+- Doxyfile.in
|
||||
+- gen-ccov.in
|
||||
\- xo-bootstrap-macros.cmake
|
||||
|
||||
CMake Support
|
||||
-------------
|
||||
|
||||
To use built-in cmake support, when using ``xo-alloc`` from another project:
|
||||
|
||||
Make sure ``PREFIX/lib/cmake`` is searched by cmake (for example include it in ``CMAKE_PREFIX_PATH``)
|
||||
|
||||
Add to your ``CMakeLists.txt``:
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
FindPackage(xo_alloc CONFIG REQUIRED)
|
||||
target_link_libraries(mytarget INTERFACE xo_alloc)
|
||||
268
xo-alloc/docs/introduction.rst
Normal file
268
xo-alloc/docs/introduction.rst
Normal 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``
|
||||
58
xo-alloc/include/xo/alloc/AllocPolicy.hpp
Normal file
58
xo-alloc/include/xo/alloc/AllocPolicy.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/* AllocPolicy.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
/** Tag class, drives overload of operator new.
|
||||
* See also: xoglobal, xocopy
|
||||
**/
|
||||
struct xolib {
|
||||
xolib() = default;
|
||||
};
|
||||
|
||||
/** @brief opt-in allocator for XO libraries.
|
||||
*
|
||||
* By default delegates to vanilla operator new/delete,
|
||||
* but can set alloc/free functions at runtime to
|
||||
* adopt a different implementation.
|
||||
*
|
||||
* Intending this to op-in to garbage-collector?
|
||||
* Not sure if we actually need this
|
||||
*
|
||||
* Use:
|
||||
* struct Foo { .. };
|
||||
* auto p = new (xo) Foo(..);
|
||||
**/
|
||||
class XoAllocPolicy {
|
||||
public:
|
||||
using AllocFn = void* (*)(std::size_t);
|
||||
using FreeFn = void (*)(void *);
|
||||
|
||||
public:
|
||||
XoAllocPolicy() = default;
|
||||
|
||||
static void * global_alloc(std::size_t z) { return ::operator new(z); }
|
||||
static void global_free(void * x) { ::operator delete(x); }
|
||||
|
||||
void * alloc(std::size_t z) { return (*alloc_)(z); }
|
||||
void free(void * x) { (*free_)(x); }
|
||||
|
||||
private:
|
||||
AllocFn alloc_ = global_alloc;
|
||||
FreeFn free_ = global_free;
|
||||
};
|
||||
|
||||
/** singleton xolib instance **/
|
||||
static XoAllocPolicy xo;
|
||||
}
|
||||
|
||||
inline void * operator new(std::size_t z, xo::xolib) {
|
||||
return xo::xo.alloc(z);
|
||||
}
|
||||
|
||||
void operator delete(void * ptr) noexcept;
|
||||
|
||||
/* end AllocPolicy.hpp */
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* file LinearAlloc.hpp
|
||||
/* file ArenaAlloc.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
/** @class LinearAlloc
|
||||
/** @class ArenaAlloc
|
||||
* @brief Bump allocator with fixed capacity
|
||||
*
|
||||
* @text
|
||||
|
|
@ -33,34 +33,39 @@ namespace xo {
|
|||
*
|
||||
* TODO: rename to ArenaAlloc
|
||||
**/
|
||||
class LinearAlloc : public IAlloc {
|
||||
class ArenaAlloc : public IAlloc {
|
||||
public:
|
||||
~LinearAlloc();
|
||||
~ArenaAlloc();
|
||||
|
||||
/** create allocator with capacity @p z,
|
||||
* with reserved capacity @p redline_z.
|
||||
**/
|
||||
static up<LinearAlloc> make(std::size_t redline_z, std::size_t z);
|
||||
static up<ArenaAlloc> make(const std::string & name,
|
||||
std::size_t redline_z,
|
||||
std::size_t z,
|
||||
bool debug_flag);
|
||||
|
||||
std::uint8_t * free_ptr() const { return free_ptr_; }
|
||||
void set_free_ptr(std::uint8_t * x);
|
||||
const std::string & name() const { return name_; }
|
||||
std::byte * free_ptr() const { return free_ptr_; }
|
||||
void set_free_ptr(std::byte * x);
|
||||
|
||||
// inherited from IAlloc...
|
||||
|
||||
virtual std::size_t size() const override;
|
||||
virtual std::size_t available() const override;
|
||||
virtual std::size_t allocated() const override;
|
||||
virtual bool is_before_checkpoint(const std::uint8_t * x) const override;
|
||||
virtual bool contains(const void * x) const override;
|
||||
virtual bool is_before_checkpoint(const void * x) const override;
|
||||
virtual std::size_t before_checkpoint() const override;
|
||||
virtual std::size_t after_checkpoint() const override;
|
||||
|
||||
virtual void clear() override;
|
||||
virtual void checkpoint() override;
|
||||
virtual std::uint8_t * alloc(std::size_t z) override;
|
||||
|
||||
virtual std::byte * alloc(std::size_t z) override;
|
||||
virtual void release_redline_memory() override;
|
||||
|
||||
private:
|
||||
LinearAlloc(std::size_t rz, std::size_t z);
|
||||
ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag);
|
||||
|
||||
private:
|
||||
/**
|
||||
|
|
@ -68,23 +73,30 @@ namespace xo {
|
|||
* - @ref free_ always a multiple of word size (assumed to be sizeof(void*))
|
||||
**/
|
||||
|
||||
/** optional instance name, for diagnostics **/
|
||||
std::string name_;
|
||||
|
||||
/** allocator owns memory in range [@ref lo_, @ref hi_) **/
|
||||
std::uint8_t * lo_ = nullptr;
|
||||
std::byte * lo_ = nullptr;
|
||||
/** checkpoint (for GC support); divides objects into
|
||||
* older (addresses below checkpoint)
|
||||
* and younger (addresses above checkpoint)
|
||||
**/
|
||||
std::uint8_t * checkpoint_;
|
||||
std::byte * checkpoint_;
|
||||
/** free pointer. memory in range [@ref free_, @ref limit_) available **/
|
||||
std::uint8_t * free_ptr_ = nullptr;
|
||||
std::byte * free_ptr_ = nullptr;
|
||||
/** soft limit: end of released memory **/
|
||||
std::uint8_t * limit_ = nullptr;
|
||||
std::byte * limit_ = nullptr;
|
||||
/** amount of last-resort memory to reserve **/
|
||||
std::size_t redline_z_ = 0;
|
||||
/** hard limit: end of allocated memory **/
|
||||
std::uint8_t * hi_ = nullptr;
|
||||
std::byte * hi_ = nullptr;
|
||||
/** true to enable detailed debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
};
|
||||
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
|
||||
/* end LinearAlloc.hpp */
|
||||
/* end ArenaAlloc.hpp */
|
||||
28
xo-alloc/include/xo/alloc/Forwarding.hpp
Normal file
28
xo-alloc/include/xo/alloc/Forwarding.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* Forwarding.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
class Forwarding : public Object {
|
||||
public:
|
||||
Forwarding() = default;
|
||||
|
||||
// inherited from Object..
|
||||
#ifdef NOT_USING
|
||||
virtual bool _is_forwarded() const override final { return true; }
|
||||
#endif
|
||||
virtual Object * _destination() override final { return destination_.ptr(); }
|
||||
|
||||
private:
|
||||
gp<Object> destination_;
|
||||
};
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Forwarding.hpp */
|
||||
40
xo-alloc/include/xo/alloc/Forwarding1.hpp
Normal file
40
xo-alloc/include/xo/alloc/Forwarding1.hpp
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* file Forwarding1.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Forwarding1 : public Object {
|
||||
public:
|
||||
explicit Forwarding1(gp<Object> dest);
|
||||
|
||||
// inherited from Object..
|
||||
virtual bool _is_forwarded() const override { return true; }
|
||||
virtual Object * _offset_destination(Object * src) const;
|
||||
virtual std::size_t _shallow_size() const override;
|
||||
virtual Object * _shallow_copy() const override;
|
||||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
/** the object that used to be located at this address (i.e. @c this)
|
||||
* has been moved to @ref destination_ ,
|
||||
* with original location overwritten by a forwarding pointer
|
||||
*
|
||||
* Require:
|
||||
* - can only use Forwarding with types that have a single vtable.
|
||||
* To forward a multiply-inheriting class with two vtables, use Forwarding2.
|
||||
* - if you try to use Forwarding for an object with multiple vtables,
|
||||
* one of the vtable pointers will be replaced by @ref destination_.
|
||||
* UB revealed when GC traverses a pointer that relies on the 2nd
|
||||
* vtable to index virtual methods.
|
||||
**/
|
||||
gp<Object> dest_;
|
||||
};
|
||||
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Forwarding1.hpp */
|
||||
310
xo-alloc/include/xo/alloc/GC.hpp
Normal file
310
xo-alloc/include/xo/alloc/GC.hpp
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
/* GC.hpp
|
||||
*
|
||||
* author: Roland Conybeare, jul 2025
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ListAlloc.hpp"
|
||||
#include "xo/indentlog/print/array.hpp"
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
namespace xo {
|
||||
/** types that can participate in GC inherit from this base class. See Object.hpp in this directory **/
|
||||
class Object;
|
||||
|
||||
namespace gc {
|
||||
enum class generation {
|
||||
nursery,
|
||||
tenured,
|
||||
N
|
||||
};
|
||||
|
||||
constexpr std::size_t gen2int(generation x) { return static_cast<std::size_t>(x); }
|
||||
|
||||
enum class role {
|
||||
/** nursery: generation for new objects **/
|
||||
from_space,
|
||||
/** tenured: generation for objects that have survived two collections **/
|
||||
to_space,
|
||||
N,
|
||||
};
|
||||
|
||||
constexpr std::size_t role2int(role x) { return static_cast<std::size_t>(x); }
|
||||
|
||||
/** @class Config
|
||||
* @brief garbage collector configuration
|
||||
**/
|
||||
struct Config {
|
||||
/** initial size in bytes for youngest (Nursery) generation.
|
||||
* GC allocates two nursery spaces of this size.
|
||||
* Will allocate more space as needed
|
||||
**/
|
||||
std::size_t initial_nursery_z_ = 0;
|
||||
/** initial size in bytes for oldest (Tenured) generation.
|
||||
* GC allocates two tenured spaces of this size
|
||||
* Will allocate more space as needed
|
||||
**/
|
||||
std::size_t initial_tenured_z_ = 0;
|
||||
/** true to permit incremental garbage collection **/
|
||||
bool allow_incremental_gc_ = true;
|
||||
/** true to enable debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
};
|
||||
|
||||
/** @class ObjectStatistics
|
||||
* @brief placeholder for type-driven allocation statistics
|
||||
*
|
||||
* Passed to @ref Object::deep_move for example
|
||||
**/
|
||||
class ObjectStatistics {
|
||||
};
|
||||
|
||||
/** @class PerGenerationStatistics
|
||||
* @brief garbage collection statistics for particular GC generation
|
||||
**/
|
||||
class PerGenerationStatistics {
|
||||
public:
|
||||
/** update statistics after a GC cycle
|
||||
* @param alloc_z. new allocations (since preceding GC)
|
||||
* @param before_z. generation size (bytes allocated) before collection
|
||||
* @param after_z. generation size after collection
|
||||
* @param promote_z. bytes promoted to next generation
|
||||
**/
|
||||
void include_gc(std::size_t alloc_z, std::size_t before_z, std::size_t after_z,
|
||||
std::size_t promote_z);
|
||||
/** update with current state (use at end of gc cycle) **/
|
||||
void update_snapshot(std::size_t after_z);
|
||||
|
||||
/** @param os. write stats on this output stream **/
|
||||
void display(std::ostream & os) const;
|
||||
|
||||
/** number of bytes currently in use **/
|
||||
std::size_t used_z_ = 0;
|
||||
|
||||
/** number of collection cycles completed **/
|
||||
std::size_t n_gc_ = 0;
|
||||
/** sum of new alloc bytes, sampled at start of each collection cycle **/
|
||||
std::size_t new_alloc_z_ = 0;
|
||||
/** sum of allocated bytes sampled at beginning of each collection cycle **/
|
||||
std::size_t scanned_z_ = 0;
|
||||
/** sum of bytes remaining after collection cycle **/
|
||||
std::size_t survive_z_ = 0;
|
||||
/** sum of bytes promoted to next generation **/
|
||||
std::size_t promote_z_ = 0;
|
||||
};
|
||||
|
||||
inline std::ostream & operator<< (std::ostream & os, const PerGenerationStatistics & x) {
|
||||
x.display(os);
|
||||
return os;
|
||||
}
|
||||
|
||||
/** @class GcStatistics
|
||||
* @brief garbage collection statistics
|
||||
**/
|
||||
class GcStatistics {
|
||||
public:
|
||||
/** update statistics after a GC cycle
|
||||
* @param upto. nursery -> incremental collection; tenured -> full collection
|
||||
* @param alloc_z. new allocations (since preceding GC)
|
||||
* @param before_z. generation size (bytes allocated) before collection
|
||||
* @param after_z. generation size after collection
|
||||
* @param promote_z. bytes promoted to next generation
|
||||
**/
|
||||
void include_gc(generation upto, std::size_t alloc_z,
|
||||
std::size_t before_z, std::size_t after_z, std::size_t promote_z);
|
||||
/** update snapshot for current state.
|
||||
* Use with tenured stats after incremental gc
|
||||
**/
|
||||
void update_snapshot(generation upto, std::size_t after_z);
|
||||
|
||||
/** @param os. write stats on this output stream **/
|
||||
void display(std::ostream & os) const;
|
||||
|
||||
/** statistics gathered across {incr, full} GCs respectively **/
|
||||
std::array<PerGenerationStatistics, static_cast<std::size_t>(generation::N)> gen_v_;
|
||||
/** total bytes allocated since inception **/
|
||||
std::size_t total_allocated_ = 0;
|
||||
/** snapshot of total bytes promoted asof beginning of last gc cycle **/
|
||||
std::size_t total_promoted_sab_ = 0;
|
||||
/** total bytes promoted from nursery->tenured since inception **/
|
||||
std::size_t total_promoted_ = 0;
|
||||
|
||||
/** per-type statistics (placeholder) **/
|
||||
ObjectStatistics per_type_stats_;
|
||||
};
|
||||
|
||||
inline std::ostream & operator<< (std::ostream & os, const GcStatistics & x) {
|
||||
x.display(os);
|
||||
return os;
|
||||
}
|
||||
|
||||
/** @class GCRunstate
|
||||
* @brief encapsulate state needed while GC is running
|
||||
*
|
||||
* state pertaining to a single GC invocation.
|
||||
* We stash an instance of this in @ref GC as context,
|
||||
* so that per-Object-derived-type auxiliary functions can be slightly streamlined
|
||||
**/
|
||||
class GCRunstate {
|
||||
public:
|
||||
GCRunstate() = default;
|
||||
explicit GCRunstate(bool in_progress, bool full_move)
|
||||
: in_progress_{in_progress}, full_move_{full_move} {}
|
||||
|
||||
bool in_progress() const { return in_progress_; }
|
||||
bool full_move() const { return full_move_; }
|
||||
|
||||
private:
|
||||
/** true when GC begins; remains true until GC cycle complete **/
|
||||
bool in_progress_ = false;
|
||||
/** true for full GC; false for incremental GC **/
|
||||
bool full_move_ = false;
|
||||
};
|
||||
|
||||
/** @class GC
|
||||
* @brief generational garbage collector
|
||||
*
|
||||
* Works with objects of type @ref xo::Object
|
||||
**/
|
||||
class GC : public IAlloc {
|
||||
public:
|
||||
/** create new GC instance with configuration @p config **/
|
||||
explicit GC(const Config & config);
|
||||
|
||||
/** create GC allocator.
|
||||
*
|
||||
* Initial memory consumption:
|
||||
* approximately 2x @ref Config::nursery_size_ + 2x @ref Config::tenured_size_
|
||||
**/
|
||||
static up<GC> make(const Config & config);
|
||||
|
||||
const GCRunstate & runstate() const { return runstate_; }
|
||||
const GcStatistics & gc_statistics() const { return gc_statistics_; }
|
||||
|
||||
/** true iff GC permitted in current state **/
|
||||
bool is_gc_enabled() const { return gc_enabled_ == 0; }
|
||||
/** @return generation to which object at @p x belongs **/
|
||||
generation generation_of(const void * x) const;
|
||||
/** @return generation that contains @p x, given it's in from-space **/
|
||||
generation fromspace_generation_of(const void * x) const;
|
||||
/** true iff from-space contains @p x **/
|
||||
bool fromspace_contains(const void * x) const;
|
||||
/** true during (and only during) a GC cycle **/
|
||||
bool gc_in_progress() const { return runstate_.in_progress(); }
|
||||
/** return free pointer for generation @p gen, i.e. nursery or tenured space **/
|
||||
std::byte * free_ptr(generation gen);
|
||||
|
||||
/** add gc root at address @p addr . Gc will keep alive anything reachable
|
||||
* from @c *addr
|
||||
**/
|
||||
void add_gc_root(Object ** addr);
|
||||
/** request garbage collection. **/
|
||||
void request_gc(generation g);
|
||||
/** disable garbage collection until matching call to @ref enable_gc.
|
||||
*
|
||||
* GC is disabled when number of calls to @ref disable_gc exceeds number of
|
||||
* calls to @ref enable_gc.
|
||||
**/
|
||||
void disable_gc();
|
||||
/** enable garbage collection
|
||||
*
|
||||
* GC is enabled when number of calls to @ref enable_gc is at least as large
|
||||
* as number of calls to @ref disable_gc.
|
||||
**/
|
||||
void enable_gc();
|
||||
|
||||
// inherited from IAlloc..
|
||||
|
||||
/** capacity in bytes (counting both free+allocated) for object storage.
|
||||
* only counts one of {to-space, from-space},
|
||||
* since one role is always held empty between collections.
|
||||
**/
|
||||
virtual std::size_t size() const override;
|
||||
|
||||
virtual std::size_t allocated() const override;
|
||||
virtual std::size_t available() const override;
|
||||
/** only tests to-space **/
|
||||
virtual bool contains(const void * x) const override;
|
||||
virtual bool is_before_checkpoint(const void * x) const override;
|
||||
virtual std::size_t before_checkpoint() const override;
|
||||
virtual std::size_t after_checkpoint() const override;
|
||||
|
||||
virtual void clear() override;
|
||||
virtual void checkpoint() override;
|
||||
|
||||
virtual std::byte * alloc(std::size_t z) override;
|
||||
virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) override;
|
||||
|
||||
virtual void release_redline_memory() override;
|
||||
|
||||
private:
|
||||
/** begin GC now **/
|
||||
void execute_gc(generation g);
|
||||
/** cleanup phase. aux function for @ref execute_gc **/
|
||||
void cleanup_phase(generation g);
|
||||
/** swap roles of From/To spaces for nursery generation **/
|
||||
void swap_nursery();
|
||||
/** swap roles of From/To spaces for tenured generation **/
|
||||
void swap_tenured();
|
||||
/** swap roles of FromSpace/ToSpace **/
|
||||
void swap_spaces(generation g);
|
||||
/** copy object **/
|
||||
void copy_object(Object ** addr, generation upto, ObjectStatistics * object_stats);
|
||||
/** copy everything reachable from global gc roots **/
|
||||
void copy_globals(generation g);
|
||||
|
||||
private:
|
||||
/** garbage collector configuration **/
|
||||
Config config_;
|
||||
|
||||
/** contains allocated objects, along with unreachable garbage to be collected.
|
||||
* roles reverse after each incremental, or full, collection.
|
||||
**/
|
||||
std::array<up<ListAlloc>, static_cast<std::size_t>(role::N)> nursery_;
|
||||
/** empty space, destination for objects that survive collection.
|
||||
* roles reverse after each full collection.
|
||||
**/
|
||||
std::array<up<ListAlloc>, static_cast<std::size_t>(role::N)> tenured_;
|
||||
|
||||
/** current state of GC activity.
|
||||
* @text
|
||||
* in_progress full_move descr
|
||||
* -----------------------------------------
|
||||
* false * gc not running
|
||||
* true false incremental gc
|
||||
* true true full gc
|
||||
* -----------------------------------------
|
||||
* @endtext
|
||||
**/
|
||||
GCRunstate runstate_;
|
||||
|
||||
/** root object handles: targets of handles in this vector are always preserved by GC.
|
||||
* Application can introduce new root object pointers at any time provided GC not running,
|
||||
* but cannot withdraw them.
|
||||
**/
|
||||
std::vector<Object**> gc_root_v_;
|
||||
|
||||
/** allocation/collection counters **/
|
||||
GcStatistics gc_statistics_;
|
||||
|
||||
/** trigger full GC whenever this much data arrives in tenured generation **/
|
||||
std::size_t full_gc_threshold_ = 0;
|
||||
/** trigger incr GC whenever this much data arrives in nuresery generation **/
|
||||
std::size_t incr_gc_threshold_ = 0;
|
||||
|
||||
/** true when GC requested,
|
||||
* remains true until GC.. completes? begins?
|
||||
**/
|
||||
bool incr_gc_pending_ = false;
|
||||
bool full_gc_pending_ = false;
|
||||
|
||||
/** enabled when 0. disabled when <0 **/
|
||||
int gc_enabled_ = 0;
|
||||
};
|
||||
} /*namespace gc*/
|
||||
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end GC.hpp */
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/* file GCAlloc.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
class GC : public IAlloc {
|
||||
enum class Space { A, B, N_Space };
|
||||
enum class Gen { Nursery, Tenured };
|
||||
|
||||
};
|
||||
|
||||
} /*namespace mem */
|
||||
} /*namespace xo*/
|
||||
|
||||
|
||||
/* end GCAlloc.hpp */
|
||||
|
|
@ -20,6 +20,13 @@ namespace xo {
|
|||
public:
|
||||
virtual ~IAlloc() {}
|
||||
|
||||
/** compute padding to add to an allocation of size z to bring it up to
|
||||
* a multiple of word size (8 bytes on x86_64)
|
||||
**/
|
||||
static std::uint32_t alloc_padding(std::size_t z);
|
||||
/** z + alloc_padding(z) **/
|
||||
static std::size_t with_padding(std::size_t z);
|
||||
|
||||
/** allocator size in bytes (up to soft limit).
|
||||
* Includes unallocated mmeory
|
||||
**/
|
||||
|
|
@ -30,10 +37,12 @@ namespace xo {
|
|||
virtual std::size_t available() const = 0;
|
||||
/** number of bytes allocated from this allocator **/
|
||||
virtual std::size_t allocated() const = 0;
|
||||
/** true iff pointer x comes from this allocator **/
|
||||
virtual bool contains(const void * x) const = 0;
|
||||
/** true iff object at address @p x was allocated by this allocator,
|
||||
* and before checkpoint
|
||||
**/
|
||||
virtual bool is_before_checkpoint(const std::uint8_t * x) const = 0;
|
||||
virtual bool is_before_checkpoint(const void * x) const = 0;
|
||||
/** number of bytes allocated before @ref checkpoint **/
|
||||
virtual std::size_t before_checkpoint() const = 0;
|
||||
/** number of bytes allocated since @ref checkpoint **/
|
||||
|
|
@ -48,10 +57,39 @@ namespace xo {
|
|||
**/
|
||||
virtual void checkpoint() = 0;
|
||||
/** allocate @p z bytes of memory. returns pointer to first address **/
|
||||
virtual std::uint8_t * alloc(std::size_t z) = 0;
|
||||
virtual std::byte * alloc(std::size_t z) = 0;
|
||||
/** allocate @p z bytes for copy of object at @p src.
|
||||
* Only used in @ref GC. Default implementation asserts and returns nullptr
|
||||
**/
|
||||
virtual std::byte * alloc_gc_copy(std::size_t z, const void * src);
|
||||
/** release last-resort reserved memory **/
|
||||
virtual void release_redline_memory() = 0;
|
||||
};
|
||||
} /*namespace gc*/
|
||||
|
||||
class MMPtr {
|
||||
public:
|
||||
explicit MMPtr(gc::IAlloc * mm) : mm_{mm} {}
|
||||
|
||||
gc::IAlloc * mm_ = nullptr;
|
||||
};
|
||||
} /*namespace xo*/
|
||||
|
||||
inline void * operator new (std::size_t z, const xo::MMPtr & mmp) {
|
||||
return mmp.mm_->alloc(z);
|
||||
}
|
||||
|
||||
//inline void operator delete (void * p, const MMPtr & mmp) {
|
||||
// mmp.mm_->free(reinterpret_cast<std::byte *>(p));
|
||||
//}
|
||||
|
||||
inline void * operator new[] (std::size_t z, const xo::MMPtr & mmp) {
|
||||
return mmp.mm_->alloc(z);
|
||||
}
|
||||
|
||||
//inline void operator delete[] (void * p, const MMPtr & mmp) {
|
||||
// mmp.mm_->free(reinterpret_cast<std::byte *>(p));
|
||||
//}
|
||||
|
||||
|
||||
/* end IAlloc.hpp */
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@
|
|||
#pragma once
|
||||
|
||||
#include "IAlloc.hpp"
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
class ArenaAlloc;
|
||||
|
||||
/** GC-compatible allocator using a linked list of buckets.
|
||||
*
|
||||
* - all allocs done from first allocator in list
|
||||
* GC Support:
|
||||
* - reserved memory, released after call to @ref release_redline_memory.
|
||||
*
|
||||
|
|
@ -21,27 +25,60 @@ namespace xo {
|
|||
**/
|
||||
class ListAlloc : public IAlloc {
|
||||
public:
|
||||
ListAlloc(LinearAlloc* hd,
|
||||
std::size_t cz, std::size_t nz; std::size_tz,
|
||||
LinearAlloc* marked, bool use_redline,
|
||||
bool redlined_flag, OnEmptyFn on_overflow);
|
||||
ListAlloc(std::unique_ptr<ArenaAlloc> hd,
|
||||
ArenaAlloc * marked,
|
||||
std::size_t cz, std::size_t nz, std::size_t tz,
|
||||
bool use_redline,
|
||||
bool debug_flag);
|
||||
~ListAlloc();
|
||||
|
||||
static up<ListAlloc> make(std::size_t cz, std::size_t nz,
|
||||
OnEmptyFn on_overflow);
|
||||
static up<ListAlloc> make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag);
|
||||
|
||||
/** reset to have at least @p z bytes of storage **/
|
||||
bool reset(std::size_t z);
|
||||
|
||||
/** expand bucket list to accomodate a requrest of size @p z **/
|
||||
bool expand(std::size_t z);
|
||||
|
||||
/** current free pointer **/
|
||||
std::byte * free_ptr() const;
|
||||
|
||||
// inherited from IAlloc..
|
||||
|
||||
virtual std::size_t size() const override;
|
||||
virtual std::size_t available() const override;
|
||||
virtual std::size_t allocated() const override;
|
||||
virtual bool contains(const void * x) const override;
|
||||
virtual bool is_before_checkpoint(const void * x) const override;
|
||||
virtual std::size_t before_checkpoint() const override;
|
||||
virtual std::size_t after_checkpoint() const override;
|
||||
|
||||
virtual void clear() override;
|
||||
virtual void checkpoint() override;
|
||||
virtual std::byte * alloc(std::size_t z) override;
|
||||
virtual void release_redline_memory() override;
|
||||
|
||||
private:
|
||||
/** **/
|
||||
std::size_t start_z_ = 0;
|
||||
LinearAlloc* hd_ = nullptr;
|
||||
/** all new allocs from this list **/
|
||||
std::unique_ptr<ArenaAlloc> hd_;
|
||||
/** allocator that was in @ref hd_ when @ref checkpoint last called **/
|
||||
ArenaAlloc * marked_ = nullptr;
|
||||
/** overflow allocs (expect list to be short);
|
||||
* from trying to converge on app working set size
|
||||
**/
|
||||
std::list<std::unique_ptr<ArenaAlloc>> full_l_;
|
||||
std::size_t current_z_ = 0;;
|
||||
std::size_t next_z_ = 0;;
|
||||
std::size_t total_z_ = 0;
|
||||
bool use_redline_ = false;
|
||||
bool redlined_flag_ = false;
|
||||
|
||||
/** true to enable debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
};
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
|
||||
/* end ListAlloc.hpp */
|
||||
|
|
|
|||
232
xo-alloc/include/xo/alloc/Object.hpp
Normal file
232
xo-alloc/include/xo/alloc/Object.hpp
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
/* Object.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IAlloc.hpp"
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
class GC;
|
||||
class ObjectStatistics;
|
||||
};
|
||||
|
||||
class Object;
|
||||
|
||||
template <typename T>
|
||||
class gc_ptr;
|
||||
|
||||
template <typename T>
|
||||
using gp = gc_ptr<T>;
|
||||
|
||||
/** wrapper for a pointer to garbage-collector-eligible T.
|
||||
* Application code will usually use the alias template gp<T>
|
||||
**/
|
||||
template <typename T>
|
||||
class gc_ptr {
|
||||
public:
|
||||
using element_type = T;
|
||||
|
||||
public:
|
||||
gc_ptr() = default;
|
||||
gc_ptr(T * p) : ptr_{p} {}
|
||||
gc_ptr(const gc_ptr & x) : ptr_{x.ptr_} {}
|
||||
|
||||
/** create from gc_ptr to some related type @tparam S **/
|
||||
template <typename S>
|
||||
gc_ptr(const gc_ptr<S> & x) : ptr_{x.ptr()} {}
|
||||
|
||||
static bool is_eq(gc_ptr x1, gc_ptr x2) {
|
||||
std::uintptr_t u1 = reinterpret_cast<std::uintptr_t>(x1.ptr());
|
||||
std::uintptr_t u2 = reinterpret_cast<std::uintptr_t>(x2.ptr());
|
||||
|
||||
// multiple inheritance shenanigans.
|
||||
// (allow interface pointers separated by one pointer)
|
||||
|
||||
if (u1 >= u2)
|
||||
return (u1 <= u2 + sizeof(std::uintptr_t));
|
||||
else
|
||||
return (u2 <= u1 + sizeof(std::uintptr_t));
|
||||
}
|
||||
|
||||
T * ptr() const { return ptr_; }
|
||||
T ** ptr_address() { return &ptr_; }
|
||||
|
||||
bool is_null() const { return ptr_ == nullptr; }
|
||||
void make_null() { ptr_ = nullptr; }
|
||||
|
||||
void assign_ptr(T * x) { ptr_ = x; }
|
||||
|
||||
gc_ptr & operator=(const gc_ptr & x) { ptr_ = x.ptr(); return *this; }
|
||||
T * operator->() const { return ptr_; }
|
||||
|
||||
private:
|
||||
T * ptr_ = nullptr;
|
||||
};
|
||||
|
||||
/** Root class for all xo GC-collectable objects.
|
||||
*
|
||||
* Design note:
|
||||
*
|
||||
* relying on inheritance means we insist that GC traits
|
||||
* for a type appear directly in that type's vtable, and at specific locations.
|
||||
* This implies one level of indirection when GC traverses an instance.
|
||||
*
|
||||
* Would be feasible to relax the must-inherit-from-Object constraint,
|
||||
* but cost would be an extra layer of indirection
|
||||
**/
|
||||
class Object {
|
||||
public:
|
||||
virtual ~Object() = default;
|
||||
|
||||
/** memory allocator for objects. Likely this will be a GC instance,
|
||||
* but simple arena also supported.
|
||||
**/
|
||||
static gc::IAlloc * mm;
|
||||
|
||||
/** use from GC aux functions **/
|
||||
static gc::GC * _gc() { return reinterpret_cast<gc::GC*>(mm); }
|
||||
|
||||
/** during GC
|
||||
* 1. copy destination object @p *addr to (new) to-space.
|
||||
* 2. overwrite existing object @p *addr with a forwarding pointer to
|
||||
* copy made in step 1.
|
||||
* 3. return the location of the copy make in step 1.
|
||||
*
|
||||
* @p src. source object to be forwarded
|
||||
* @p gc. garbage collector
|
||||
*/
|
||||
static Object * _forward(Object * src, gc::GC * gc);
|
||||
|
||||
template <typename T>
|
||||
static void _forward_inplace(T ** src_addr) {
|
||||
Object * fwd = _forward(*src_addr, _gc());
|
||||
|
||||
*src_addr = reinterpret_cast<T *>(fwd);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void _forward_inplace(gp<T> & src) {
|
||||
_forward_inplace<T>(src.ptr_address());
|
||||
}
|
||||
|
||||
/** primary workhorse for garbage collection.
|
||||
*
|
||||
* we assign each object one of three colors: black|gray|white.
|
||||
*
|
||||
* color | location | children | action |
|
||||
* ------+------------+------------+-------------------------+
|
||||
* black | from-space | any | move to to-space |
|
||||
* gray | to-space | any | move remaining children |
|
||||
* white | to-space | white/gray | done |
|
||||
*
|
||||
* initially all reachable objects are black.
|
||||
* GC is complete when all reachable objects are white.
|
||||
* GC needs a variable amount of temporary storage to keep track of all gray objects
|
||||
**/
|
||||
static Object * _deep_move(Object * src, gc::GC * gc, gc::ObjectStatistics * stats);
|
||||
|
||||
/** copy @p src to to-space, and replace original with forwarding pointer to new location.
|
||||
* return the new location
|
||||
**/
|
||||
static Object * _shallow_move(Object * src, gc::GC * gc);
|
||||
|
||||
// GC support
|
||||
|
||||
/** true iff this object represents a forwarding pointer.
|
||||
* Forwarding pointers are exclusively created by the garbage collector;
|
||||
* forwarding pointers (and only forwarding pointers) return true here.
|
||||
**/
|
||||
virtual bool _is_forwarded() const { return false; }
|
||||
|
||||
/** offset for uncommon situation where pointer address is offset from object
|
||||
* base address
|
||||
**/
|
||||
virtual Object * _offset_destination(Object * src) const { return src; };
|
||||
|
||||
/** replace this object with a forwarding pointer referring to @p dest.
|
||||
**/
|
||||
virtual void _forward_to(Object * dest);
|
||||
|
||||
/** if this object represents a forwarding pointer, return its new location.
|
||||
* forwarding pointers belong to the garbage collector implementation.
|
||||
* (if you have to ask -- no, your class is not a forwarding pointer)
|
||||
* all other objects return nullptr here.
|
||||
**/
|
||||
virtual Object * _destination() { return nullptr; }
|
||||
|
||||
/** return amount of storage (including padding) consumed by this object,
|
||||
* excluding immediate Object-pointer children
|
||||
**/
|
||||
virtual std::size_t _shallow_size() const = 0;
|
||||
|
||||
// TODO: _shallow_move() also overwrite *this with gc-only forwarding object point to C
|
||||
|
||||
/** if subject is allocated by GC:
|
||||
* - create copy C in to-space
|
||||
* - destination C will be nursery|tenured depending on location of this.
|
||||
* else
|
||||
* - return this to disengage from GC
|
||||
*
|
||||
* Require: @ref mm is an instance of @ref gc::GC
|
||||
**/
|
||||
virtual Object * _shallow_copy() const = 0;
|
||||
|
||||
/** update child pointers that refer to forwarding pointers,
|
||||
* replacing them with the correct destination.
|
||||
* See @ref Object::deep_move
|
||||
*
|
||||
* this gray object, located in to-space.
|
||||
* fwd1 forwarding objects.
|
||||
* Located in from-space. Invalid at end of GC cycle.
|
||||
* p1,p2 source pointers.
|
||||
* D1,D2 already-forwarded objects. located in to-space.
|
||||
*
|
||||
* before:
|
||||
* this fwd1
|
||||
* +----+ +-+
|
||||
* | p1 ----->|x|-------> D1
|
||||
* | | +-+
|
||||
* | |
|
||||
* | p2 ----------------> D2
|
||||
* +----+
|
||||
*
|
||||
* after:
|
||||
* this
|
||||
* +----+
|
||||
* | p1 ----------------> D1
|
||||
* | |
|
||||
* | |
|
||||
* | p2 ----------------> D2
|
||||
* +----+
|
||||
*
|
||||
* this is now white
|
||||
*
|
||||
* @return shallow size of *this. Must exactly match the amount of memory in to-space
|
||||
* allocated by @ref _shallow_move
|
||||
*
|
||||
**/
|
||||
virtual std::size_t _forward_children() = 0;
|
||||
};
|
||||
|
||||
/** @class Cpof
|
||||
* @brief argument to operator new used for garbage collector evacuation phase
|
||||
*
|
||||
* Tag overloaded operator new to activate allocation policy based on location
|
||||
* in memory of source object.
|
||||
**/
|
||||
class Cpof {
|
||||
public:
|
||||
explicit Cpof(const Object * src) : src_{src} {}
|
||||
|
||||
const void * src_ = nullptr;
|
||||
};
|
||||
} /*namespace xo*/
|
||||
|
||||
void * operator new (std::size_t z, const xo::Cpof & copy);
|
||||
|
||||
/* end Object.hpp */
|
||||
49
xo-alloc/include/xo/alloc/Stack.hpp
Normal file
49
xo-alloc/include/xo/alloc/Stack.hpp
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/* Stack.hpp
|
||||
*
|
||||
* author: Roland Conybeare, jul 2025
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
/** Simple stack implementation
|
||||
**/
|
||||
template <typename T>
|
||||
class Stack {
|
||||
public:
|
||||
explicit Stack(std::size_t capacity) {
|
||||
this->contents_.reserve(capacity);
|
||||
}
|
||||
|
||||
bool is_empty() const { return contents_.empty(); }
|
||||
std::size_t available() const { return contents_.capacity() - contents_.size(); }
|
||||
void drop() { contents_.resize(contents_.size() - 1); }
|
||||
void push(const T & x) { contents_.push_back(x); }
|
||||
T pop() {
|
||||
T retval = contents_[contents_.size() - 1];
|
||||
this->drop();
|
||||
return retval;
|
||||
}
|
||||
const T & top() const {
|
||||
return this->lookup(0);
|
||||
}
|
||||
const T & lookup(std::size_t i) const {
|
||||
return contents_.at(contents_.size() - 1 - i);
|
||||
}
|
||||
void clear() { contents_.clear(); }
|
||||
void reset_to(std::size_t z) { contents_.resize(z); }
|
||||
|
||||
std::size_t n_elements() const { return contents_.size(); }
|
||||
std::size_t capacity() const { return contents_.capacity(); }
|
||||
|
||||
private:
|
||||
std::vector<T> contents_;
|
||||
};
|
||||
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Stack.hpp */
|
||||
13
xo-alloc/src/alloc/AllocPolicy.cpp
Normal file
13
xo-alloc/src/alloc/AllocPolicy.cpp
Normal 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 */
|
||||
|
|
@ -1,29 +1,33 @@
|
|||
/* file LinearAlloc.cpp
|
||||
/* file ArenaAlloc.cpp
|
||||
*
|
||||
* author: Roland Conybeare
|
||||
*/
|
||||
|
||||
#include "LinearAlloc.hpp"
|
||||
#include "ArenaAlloc.hpp"
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include "xo/indentlog/print/tag.hpp"
|
||||
#include <cassert>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
LinearAlloc::LinearAlloc(std::size_t rz, std::size_t z)
|
||||
ArenaAlloc::ArenaAlloc(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag)
|
||||
{
|
||||
this->lo_ = (new std::uint8_t [rz + z]);
|
||||
this->name_ = name;
|
||||
this->lo_ = (new std::byte [rz + z]);
|
||||
this->checkpoint_ = lo_;
|
||||
this->free_ptr_ = lo_;
|
||||
this->limit_ = lo_ + z;
|
||||
this->redline_z_ = rz;
|
||||
this->hi_ = limit_ + rz;
|
||||
this->debug_flag_ = debug_flag;
|
||||
|
||||
if (!lo_) {
|
||||
throw std::runtime_error(tostr("LinearAlloc: allocation failed",
|
||||
throw std::runtime_error(tostr("ArenaAlloc: allocation failed",
|
||||
xtag("size", rz + z)));
|
||||
}
|
||||
}
|
||||
|
||||
LinearAlloc::~LinearAlloc()
|
||||
ArenaAlloc::~ArenaAlloc()
|
||||
{
|
||||
delete [] this->lo_;
|
||||
|
||||
|
|
@ -33,17 +37,19 @@ namespace xo {
|
|||
this->checkpoint_ = nullptr;
|
||||
this->free_ptr_ = nullptr;
|
||||
this->limit_ = nullptr;
|
||||
this->redline_z_ = 0;
|
||||
this->hi_ = nullptr;
|
||||
this->debug_flag_ = false;
|
||||
}
|
||||
|
||||
up<LinearAlloc>
|
||||
LinearAlloc::make(std::size_t rz, std::size_t z)
|
||||
up<ArenaAlloc>
|
||||
ArenaAlloc::make(const std::string & name, std::size_t rz, std::size_t z, bool debug_flag)
|
||||
{
|
||||
return up<LinearAlloc>(new LinearAlloc(rz, z));
|
||||
return up<ArenaAlloc>(new ArenaAlloc(name, rz, z, debug_flag));
|
||||
}
|
||||
|
||||
void
|
||||
LinearAlloc::set_free_ptr(std::uint8_t * x)
|
||||
ArenaAlloc::set_free_ptr(std::byte * x)
|
||||
{
|
||||
assert(lo_ <= x);
|
||||
assert(x < limit_);
|
||||
|
|
@ -57,70 +63,79 @@ namespace xo {
|
|||
}
|
||||
|
||||
std::size_t
|
||||
LinearAlloc::size() const {
|
||||
ArenaAlloc::size() const {
|
||||
return limit_ - lo_;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LinearAlloc::available() const {
|
||||
ArenaAlloc::available() const {
|
||||
return limit_ - free_ptr_;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LinearAlloc::allocated() const {
|
||||
ArenaAlloc::allocated() const {
|
||||
return free_ptr_ - lo_;
|
||||
}
|
||||
|
||||
bool
|
||||
LinearAlloc::is_before_checkpoint(const std::uint8_t * x) const {
|
||||
ArenaAlloc::contains(const void * x) const {
|
||||
return (lo_ <= x) && (x < hi_);
|
||||
}
|
||||
|
||||
bool
|
||||
ArenaAlloc::is_before_checkpoint(const void * x) const {
|
||||
return (lo_ <= x) && (x < checkpoint_);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LinearAlloc::before_checkpoint() const
|
||||
ArenaAlloc::before_checkpoint() const
|
||||
{
|
||||
return checkpoint_ - lo_;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LinearAlloc::after_checkpoint() const
|
||||
ArenaAlloc::after_checkpoint() const
|
||||
{
|
||||
return free_ptr_ - checkpoint_;
|
||||
}
|
||||
|
||||
void
|
||||
LinearAlloc::clear()
|
||||
ArenaAlloc::clear()
|
||||
{
|
||||
this->checkpoint_ = lo_;
|
||||
this->free_ptr_ = lo_;
|
||||
this->limit_ = lo_;
|
||||
this->limit_ = hi_ - redline_z_;
|
||||
}
|
||||
|
||||
void
|
||||
LinearAlloc::checkpoint()
|
||||
ArenaAlloc::checkpoint()
|
||||
{
|
||||
this->checkpoint_ = this->free_ptr_;
|
||||
}
|
||||
|
||||
std::uint8_t *
|
||||
LinearAlloc::alloc(std::size_t z)
|
||||
std::byte *
|
||||
ArenaAlloc::alloc(std::size_t z0)
|
||||
{
|
||||
scope log(XO_DEBUG(debug_flag_));
|
||||
|
||||
/* word size for alignment */
|
||||
constexpr uint32_t c_bpw = sizeof(void*);
|
||||
constexpr uint32_t c_bpw = sizeof(std::uintptr_t);
|
||||
|
||||
std::uintptr_t free_u64 = reinterpret_cast<std::uintptr_t>(free_ptr_);
|
||||
|
||||
assert(free_u64 % c_bpw == 0ul);
|
||||
|
||||
/* round up to multiple of c_bpw */
|
||||
std::uint32_t dz = (c_bpw - (z % c_bpw));
|
||||
z += dz;
|
||||
std::uint32_t dz = alloc_padding(z0);
|
||||
|
||||
assert(z % c_bpw == 0ul);
|
||||
std::size_t z1 = z0 + dz;
|
||||
|
||||
std::uint8_t * retval = this->free_ptr_;
|
||||
assert(z1 % c_bpw == 0ul);
|
||||
|
||||
this->free_ptr_ += z;
|
||||
std::byte * retval = this->free_ptr_;
|
||||
|
||||
this->free_ptr_ += z1;
|
||||
|
||||
log && log(xtag("self", name_), xtag("z0", z0), xtag("+pad", dz), xtag("z1", z1));
|
||||
|
||||
if (free_ptr_ > limit_) {
|
||||
return nullptr;
|
||||
|
|
@ -128,8 +143,14 @@ namespace xo {
|
|||
|
||||
return retval;
|
||||
}
|
||||
|
||||
void
|
||||
ArenaAlloc::release_redline_memory() {
|
||||
this->limit_ = this->hi_;
|
||||
}
|
||||
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
|
||||
/* end LinearAlloc.cpp */
|
||||
/* end ArenaAlloc.cpp */
|
||||
|
|
@ -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})
|
||||
|
|
|
|||
45
xo-alloc/src/alloc/Forwarding1.cpp
Normal file
45
xo-alloc/src/alloc/Forwarding1.cpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/* file Forwarding1.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Forwarding1.hpp"
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
Forwarding1::Forwarding1(gp<Object> dest)
|
||||
: dest_{dest}
|
||||
{}
|
||||
|
||||
Object *
|
||||
Forwarding1::_offset_destination(Object * src) const
|
||||
{
|
||||
intptr_t offset = src - static_cast<const Object *>(this);
|
||||
|
||||
return dest_.ptr() + offset;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Forwarding1::_shallow_size() const {
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Object *
|
||||
Forwarding1::_shallow_copy() const {
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Forwarding1::_forward_children() {
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Forwarding1.cpp */
|
||||
492
xo-alloc/src/alloc/GC.cpp
Normal file
492
xo-alloc/src/alloc/GC.cpp
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
/* GC.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include "GC.hpp"
|
||||
#include "Object.hpp"
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
void
|
||||
PerGenerationStatistics::include_gc(std::size_t alloc_z,
|
||||
std::size_t before_z,
|
||||
std::size_t after_z,
|
||||
std::size_t promote_z)
|
||||
{
|
||||
this->update_snapshot(after_z);
|
||||
|
||||
new_alloc_z_ += alloc_z;
|
||||
scanned_z_ += before_z;
|
||||
survive_z_ += after_z;
|
||||
promote_z_ += promote_z;
|
||||
}
|
||||
|
||||
void
|
||||
PerGenerationStatistics::update_snapshot(std::size_t after_z)
|
||||
{
|
||||
used_z_ = after_z;
|
||||
}
|
||||
|
||||
void
|
||||
PerGenerationStatistics::display(std::ostream & os) const
|
||||
{
|
||||
os << "<PerGenerationStatistics"
|
||||
<< xtag("used", used_z_)
|
||||
<< xtag("n_gc", n_gc_)
|
||||
<< xtag("new_alloc_z", new_alloc_z_)
|
||||
<< xtag("scanned_z", scanned_z_)
|
||||
<< xtag("survive_z", survive_z_)
|
||||
<< xtag("promote_z", promote_z_)
|
||||
<< ">";
|
||||
}
|
||||
|
||||
void
|
||||
GcStatistics::include_gc(generation upto,
|
||||
std::size_t alloc_z,
|
||||
std::size_t before_z,
|
||||
std::size_t after_z,
|
||||
std::size_t promote_z)
|
||||
{
|
||||
gen_v_[static_cast<std::size_t>(upto)].include_gc(alloc_z, before_z, after_z, promote_z);
|
||||
}
|
||||
|
||||
void
|
||||
GcStatistics::update_snapshot(generation upto,
|
||||
std::size_t after_z)
|
||||
{
|
||||
gen_v_[static_cast<std::size_t>(upto)].update_snapshot(after_z);
|
||||
}
|
||||
|
||||
void
|
||||
GcStatistics::display(std::ostream & os) const
|
||||
{
|
||||
os << "<GcStatistics"
|
||||
<< xtag("gen_v", gen_v_)
|
||||
<< xtag("total_allocated", total_allocated_)
|
||||
// << xtag("per_type_stats", per_type_stats_)
|
||||
<< ">";
|
||||
}
|
||||
|
||||
GC::GC(const Config & config)
|
||||
: config_{config}
|
||||
{
|
||||
enum { NurseryFrom, NurseryTo, TenuredFrom, TenuredTo };
|
||||
|
||||
std::size_t nursery_size = config.initial_nursery_z_;
|
||||
std::size_t tenured_size = config.initial_tenured_z_;
|
||||
|
||||
nursery_[role2int(role::from_space)]
|
||||
= ListAlloc::make("NA", nursery_size, 2 * nursery_size, config.debug_flag_);
|
||||
nursery_[role2int(role::to_space) ]
|
||||
= ListAlloc::make("NB", nursery_size, 2 * nursery_size, config.debug_flag_);
|
||||
|
||||
tenured_[role2int(role::from_space)]
|
||||
= ListAlloc::make("TA", tenured_size, 2 * tenured_size, config.debug_flag_);
|
||||
tenured_[role2int(role::to_space) ]
|
||||
= ListAlloc::make("TB", tenured_size, 2 * tenured_size, config.debug_flag_);
|
||||
|
||||
this->checkpoint();
|
||||
}
|
||||
|
||||
up<GC>
|
||||
GC::make(const Config & config)
|
||||
{
|
||||
GC * gc = new GC(config);
|
||||
|
||||
return up<GC>{gc};
|
||||
}
|
||||
|
||||
std::size_t
|
||||
GC::size() const
|
||||
{
|
||||
return nursery_[role2int(role::to_space)]->size() + tenured_[role2int(role::to_space)]->size();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
GC::allocated() const
|
||||
{
|
||||
return (nursery_[role2int(role::to_space)]->allocated()
|
||||
+ tenured_[role2int(role::to_space)]->allocated());
|
||||
}
|
||||
|
||||
std::size_t
|
||||
GC::available() const
|
||||
{
|
||||
return nursery_[role2int(role::to_space)]->available();
|
||||
}
|
||||
|
||||
bool
|
||||
GC::fromspace_contains(const void * x) const
|
||||
{
|
||||
return (nursery_[role2int(role::from_space)]->contains(x)
|
||||
|| tenured_[role2int(role::from_space)]->contains(x));
|
||||
}
|
||||
|
||||
bool
|
||||
GC::contains(const void * x) const
|
||||
{
|
||||
return (nursery_[role2int(role::to_space)]->contains(x)
|
||||
|| tenured_[role2int(role::to_space)]->contains(x));
|
||||
}
|
||||
|
||||
bool
|
||||
GC::is_before_checkpoint(const void * x) const
|
||||
{
|
||||
return nursery_[role2int(role::to_space)]->is_before_checkpoint(x);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
GC::before_checkpoint() const
|
||||
{
|
||||
return nursery_[role2int(role::to_space)]->before_checkpoint();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
GC::after_checkpoint() const
|
||||
{
|
||||
return nursery_[role2int(role::to_space)]->after_checkpoint();
|
||||
}
|
||||
|
||||
generation
|
||||
GC::fromspace_generation_of(const void * x) const
|
||||
{
|
||||
if (tenured_[role2int(role::from_space)]->contains(x))
|
||||
return generation::tenured;
|
||||
|
||||
return generation::nursery;
|
||||
}
|
||||
|
||||
generation
|
||||
GC::generation_of(const void * x) const
|
||||
{
|
||||
if (tenured_[role2int(role::to_space)]->contains(x))
|
||||
return generation::tenured;
|
||||
|
||||
return generation::nursery;
|
||||
}
|
||||
|
||||
std::byte *
|
||||
GC::free_ptr(generation gen)
|
||||
{
|
||||
switch(gen) {
|
||||
case generation::nursery:
|
||||
return nursery_[role2int(role::to_space)]->free_ptr();
|
||||
case generation::tenured:
|
||||
return tenured_[role2int(role::to_space)]->free_ptr();
|
||||
case generation::N:
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
GC::clear()
|
||||
{
|
||||
nursery_[role2int(role::from_space)]->clear();
|
||||
nursery_[role2int(role::to_space) ]->clear();
|
||||
|
||||
tenured_[role2int(role::from_space)]->clear();
|
||||
tenured_[role2int(role::to_space) ]->clear();
|
||||
}
|
||||
|
||||
void
|
||||
GC::add_gc_root(Object ** addr)
|
||||
{
|
||||
gc_root_v_.push_back(addr);
|
||||
}
|
||||
|
||||
void
|
||||
GC::checkpoint()
|
||||
{
|
||||
nursery_[role2int(role::to_space) ]->checkpoint();
|
||||
}
|
||||
|
||||
std::byte *
|
||||
GC::alloc(std::size_t z)
|
||||
{
|
||||
std::byte * x = nursery_[role2int(role::to_space)]->alloc(z);
|
||||
|
||||
if (!x) {
|
||||
this->request_gc(generation::nursery);
|
||||
|
||||
if (incr_gc_pending_ || full_gc_pending_)
|
||||
nursery_[role2int(role::to_space)]->release_redline_memory();
|
||||
|
||||
/* try (just once) more, maybe request fits in redline space */
|
||||
x = nursery_[role2int(role::to_space)]->alloc(z);
|
||||
|
||||
assert(x);
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
std::byte *
|
||||
GC::alloc_gc_copy(std::size_t z, const void * src)
|
||||
{
|
||||
scope log(XO_DEBUG(config_.debug_flag_), xtag("z", z), xtag("+pad", IAlloc::alloc_padding(z)));
|
||||
|
||||
generation g = this->fromspace_generation_of(src);
|
||||
|
||||
std::byte * retval = nullptr;
|
||||
|
||||
if (g == generation::tenured)
|
||||
{
|
||||
log && log("tenured");
|
||||
|
||||
retval = tenured_[role2int(role::to_space)]->alloc(z);
|
||||
} else if (nursery_[role2int(role::from_space)]->is_before_checkpoint(src))
|
||||
{
|
||||
log && log("promote");
|
||||
|
||||
/* nursery object has survived 2nd collection cycle
|
||||
* -> promote into tenured generation
|
||||
*/
|
||||
retval = tenured_[role2int(role::to_space)]->alloc(z);
|
||||
|
||||
this->gc_statistics_.total_promoted_ += IAlloc::with_padding(z);
|
||||
} else {
|
||||
log && log("nursery");
|
||||
|
||||
retval = nursery_[role2int(role::to_space)]->alloc(z);
|
||||
|
||||
if (!retval) {
|
||||
/* nursery space exhausted */
|
||||
|
||||
this->request_gc(generation::nursery);
|
||||
|
||||
nursery_[role2int(role::to_space)]->release_redline_memory();
|
||||
|
||||
retval = nursery_[role2int(role::to_space)]->alloc(z);
|
||||
}
|
||||
}
|
||||
|
||||
assert(retval);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
void
|
||||
GC::release_redline_memory()
|
||||
{
|
||||
// not supported feature for GC
|
||||
}
|
||||
|
||||
void
|
||||
GC::swap_nursery()
|
||||
{
|
||||
up<ListAlloc> tmp = std::move(nursery_[role2int(role::to_space)]);
|
||||
nursery_[role2int(role::to_space)] = std::move(nursery_[role2int(role::from_space)]);
|
||||
nursery_[role2int(role::from_space)] = std::move(tmp);
|
||||
}
|
||||
|
||||
void
|
||||
GC::swap_tenured()
|
||||
{
|
||||
up<ListAlloc> tmp = std::move(tenured_[role2int(role::to_space)]);
|
||||
tenured_[role2int(role::to_space)] = std::move(tenured_[role2int(role::from_space)]);
|
||||
tenured_[role2int(role::from_space)] = std::move(tmp);
|
||||
}
|
||||
|
||||
void
|
||||
GC::swap_spaces(generation target)
|
||||
{
|
||||
// will be copying into storage currently labelled FromSpace
|
||||
|
||||
/* gc will copy some to-be-determined amount in [0..promote_z]
|
||||
from nursery->tenured generation.
|
||||
*/
|
||||
std::size_t promote_z = nursery_[role2int(role::to_space)]->before_checkpoint();
|
||||
if (target == generation::tenured) {
|
||||
/* gc on tenured generation may need this much space */
|
||||
std::size_t tenured_z = (tenured_[role2int(role::to_space)]->allocated()
|
||||
+ promote_z
|
||||
+ full_gc_threshold_);
|
||||
|
||||
tenured_[role2int(role::from_space)]->reset(tenured_z);
|
||||
|
||||
this->swap_tenured();
|
||||
} else {
|
||||
if (tenured_[role2int(role::to_space)]->available() < promote_z) {
|
||||
tenured_[role2int(role::to_space)]->expand(promote_z);
|
||||
}
|
||||
}
|
||||
|
||||
nursery_[role2int(role::from_space)]->reset(nursery_[role2int(role::to_space)]->allocated()
|
||||
- promote_z
|
||||
+ incr_gc_threshold_);
|
||||
this->swap_nursery();
|
||||
} /*swap_spaces*/
|
||||
|
||||
void
|
||||
GC::copy_object(Object ** pp_object, generation upto, ObjectStatistics * object_stats)
|
||||
{
|
||||
void * object_address = *pp_object;
|
||||
|
||||
if (nursery_[role2int(role::to_space)]->contains(object_address)
|
||||
|| ((upto == generation::tenured)
|
||||
&& tenured_[role2int(role::to_space)]->contains(object_address)))
|
||||
{
|
||||
/* global is already in to-space */
|
||||
;
|
||||
} else if((upto == generation::nursery) && tenured_[role2int(role::to_space)]->contains(object_address))
|
||||
{
|
||||
/* skip tenured objects when incremental collection */
|
||||
;
|
||||
} else {
|
||||
*pp_object = Object::_deep_move(*pp_object, this, object_stats);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GC::copy_globals(generation upto)
|
||||
{
|
||||
for (Object ** pp_root : gc_root_v_) {
|
||||
this->copy_object(pp_root, upto, &gc_statistics_.per_type_stats_);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GC::cleanup_phase(generation upto)
|
||||
{
|
||||
scope log(XO_DEBUG(config_.debug_flag_));
|
||||
|
||||
std::size_t N_allocated = nursery_[role2int(role::from_space)]->after_checkpoint();
|
||||
std::size_t T_allocated = tenured_[role2int(role::from_space)]->after_checkpoint();
|
||||
|
||||
std::size_t N_before_gc = nursery_[role2int(role::from_space)]->allocated();
|
||||
std::size_t T_before_gc = tenured_[role2int(role::from_space)]->allocated();
|
||||
|
||||
std::size_t N_after_gc = nursery_[role2int(role::to_space)]->allocated();
|
||||
std::size_t T_after_gc = tenured_[role2int(role::to_space)]->allocated();
|
||||
//std::byte * N_free_ptr = nursery_[role2int(role::to_space)]->free_ptr();
|
||||
|
||||
std::size_t promote_z = gc_statistics_.total_promoted_ - gc_statistics_.total_promoted_sab_;
|
||||
|
||||
this->nursery_[role2int(role::from_space)]->reset(0);
|
||||
this->tenured_[role2int(role::from_space)]->reset(0);
|
||||
|
||||
/* objects currenty in to-space nursery have survived one collection */
|
||||
this->nursery_[role2int(role::to_space)]->checkpoint();
|
||||
|
||||
// nursery_[role2int(role::to_space)]->set_redline(nursery_[role2int(role::to_space)]->allocated() + incr_gc_threshold_)
|
||||
|
||||
if (upto == generation::tenured)
|
||||
this->tenured_[role2int(role::to_space)]->checkpoint();
|
||||
|
||||
if (log) {
|
||||
log(xtag("N_allocated", N_allocated));
|
||||
log(xtag("N_before_gc", N_before_gc));
|
||||
log(xtag("N_after_gc", N_after_gc));
|
||||
log(xtag("T_allocated", T_allocated));
|
||||
log(xtag("T_before_gc", T_before_gc));
|
||||
log(xtag("T_after_gc", T_after_gc));
|
||||
}
|
||||
|
||||
this->incr_gc_pending_ = false;
|
||||
this->gc_statistics_.include_gc(generation::nursery, N_allocated, N_before_gc, N_after_gc, promote_z);
|
||||
|
||||
if (upto == generation::tenured) {
|
||||
this->full_gc_pending_ = false;
|
||||
this->gc_statistics_.include_gc(generation::tenured, T_allocated, T_before_gc, T_after_gc, 0);
|
||||
} else {
|
||||
// still want to update tenured stats for current alloc size
|
||||
this->gc_statistics_.update_snapshot(generation::tenured, T_after_gc);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GC::execute_gc(generation target)
|
||||
{
|
||||
scope log(XO_DEBUG(config_.debug_flag_));
|
||||
|
||||
bool full_move = (target == generation::tenured);
|
||||
|
||||
// TODO: RAII version in case of exceptions
|
||||
this->runstate_ = GCRunstate(true /*in_progress*/, full_move);
|
||||
|
||||
log && log("step 0: snapshot alloc stats");
|
||||
|
||||
/* new allocation since last GC */
|
||||
std::size_t new_alloc = this->after_checkpoint();
|
||||
|
||||
++(gc_statistics_.gen_v_[static_cast<std::size_t>(target)].n_gc_);
|
||||
gc_statistics_.total_allocated_ += new_alloc;
|
||||
gc_statistics_.total_promoted_sab_ = gc_statistics_.total_promoted_;
|
||||
|
||||
log && log(xtag("new_alloc", new_alloc));
|
||||
|
||||
log && log("step 1: swap to/from roles");
|
||||
|
||||
this->swap_spaces(target);
|
||||
|
||||
log && log("step 2a: copy globals");
|
||||
|
||||
this->copy_globals(target);
|
||||
|
||||
log && log("step 2b: TODO: copy pinned");
|
||||
|
||||
log && log("step 3: TODO: forward mutation log");
|
||||
|
||||
log && log("step 4: TODO: notify destructor log");
|
||||
|
||||
log && log("step 5: TODO: keep reachable weak pointers");
|
||||
|
||||
log && log("step 6: cleanup");
|
||||
|
||||
this->cleanup_phase(target);
|
||||
|
||||
this->runstate_ = GCRunstate();
|
||||
|
||||
log && log("statistics:");
|
||||
log && log(gc_statistics_);
|
||||
}
|
||||
|
||||
void
|
||||
GC::request_gc(generation target)
|
||||
{
|
||||
if (!runstate_.in_progress() && (gc_enabled_ == 0)) {
|
||||
if (!config_.allow_incremental_gc_)
|
||||
target = generation::tenured;
|
||||
|
||||
if ((target == generation::nursery)
|
||||
&& (tenured_[role2int(role::to_space)]->after_checkpoint() > full_gc_threshold_))
|
||||
{
|
||||
/** full collection when >= @ref full_gc_threshold_ bytes added to tenured
|
||||
* generation, since last full collection
|
||||
**/
|
||||
target = generation::tenured;
|
||||
}
|
||||
|
||||
this->execute_gc(target);
|
||||
} else {
|
||||
this->incr_gc_pending_ = true;
|
||||
if (target == generation::tenured)
|
||||
this->full_gc_pending_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GC::disable_gc() {
|
||||
--gc_enabled_;
|
||||
}
|
||||
|
||||
void
|
||||
GC::enable_gc() {
|
||||
++gc_enabled_;
|
||||
|
||||
if (gc_enabled_ == 0) {
|
||||
/* unblock gc */
|
||||
if (incr_gc_pending_)
|
||||
this->request_gc(full_gc_pending_ ? generation::tenured : generation::nursery);
|
||||
}
|
||||
}
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end GC.cpp */
|
||||
54
xo-alloc/src/alloc/IAlloc.cpp
Normal file
54
xo-alloc/src/alloc/IAlloc.cpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* @file IAlloc.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "IAlloc.hpp"
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
|
||||
std::uint32_t
|
||||
IAlloc::alloc_padding(std::size_t z)
|
||||
{
|
||||
/* word size for alignment */
|
||||
constexpr uint32_t c_bpw = sizeof(std::uintptr_t);
|
||||
|
||||
/* round up to multiple of c_bpw, but map 0 -> 0
|
||||
* (table assuming c_bpw==8)
|
||||
*
|
||||
* z%c_bpw dz
|
||||
* ------------
|
||||
* 0 0
|
||||
* 1 7
|
||||
* 2 6
|
||||
* .. ..
|
||||
* 7 1
|
||||
*/
|
||||
std::uint32_t dz = (c_bpw - (z % c_bpw)) % c_bpw;
|
||||
z += dz;
|
||||
|
||||
assert(z % c_bpw == 0ul);
|
||||
|
||||
return dz;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
IAlloc::with_padding(std::size_t z)
|
||||
{
|
||||
return z + alloc_padding(z);
|
||||
}
|
||||
|
||||
std::byte *
|
||||
IAlloc::alloc_gc_copy(std::size_t /*z*/, const void * /*src*/)
|
||||
{
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end IAlloc.cpp */
|
||||
318
xo-alloc/src/alloc/ListAlloc.cpp
Normal file
318
xo-alloc/src/alloc/ListAlloc.cpp
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
/* file ListAlloc.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include "ListAlloc.hpp"
|
||||
#include "ArenaAlloc.hpp"
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace gc {
|
||||
ListAlloc::ListAlloc(std::unique_ptr<ArenaAlloc> hd,
|
||||
ArenaAlloc * marked,
|
||||
std::size_t cz, std::size_t nz, std::size_t tz,
|
||||
bool use_redline,
|
||||
bool debug_flag)
|
||||
: start_z_{cz},
|
||||
hd_{std::move(hd)},
|
||||
marked_{marked},
|
||||
full_l_{},
|
||||
current_z_{cz},
|
||||
next_z_{nz},
|
||||
total_z_{tz},
|
||||
use_redline_{use_redline},
|
||||
debug_flag_{debug_flag}
|
||||
{}
|
||||
|
||||
ListAlloc::~ListAlloc()
|
||||
{
|
||||
this->clear();
|
||||
}
|
||||
|
||||
up<ListAlloc>
|
||||
ListAlloc::make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag)
|
||||
{
|
||||
std::unique_ptr<ArenaAlloc> hd{ArenaAlloc::make(name, 0, cz, debug_flag)};
|
||||
|
||||
if (!hd)
|
||||
return nullptr;
|
||||
|
||||
ArenaAlloc * marked = nullptr;
|
||||
|
||||
up<ListAlloc> retval{new ListAlloc(std::move(hd),
|
||||
marked,
|
||||
cz, nz, cz,
|
||||
false /*!use_redline*/,
|
||||
debug_flag)};
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ListAlloc::size() const {
|
||||
return total_z_;
|
||||
}
|
||||
|
||||
std::byte *
|
||||
ListAlloc::free_ptr() const {
|
||||
return hd_->free_ptr();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ListAlloc::available() const {
|
||||
if (hd_) {
|
||||
/* can only allocate from @ref hd_,
|
||||
* so even if there were available space in @ref full_l_,
|
||||
* it's not accessible to ListAlloc.
|
||||
*/
|
||||
|
||||
return hd_->available();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ListAlloc::allocated() const {
|
||||
std::size_t total = 0;
|
||||
|
||||
if (hd_)
|
||||
total += hd_->allocated();
|
||||
|
||||
for (const auto & alloc : full_l_)
|
||||
total += alloc->allocated();
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
bool
|
||||
ListAlloc::contains(const void * x) const {
|
||||
if (hd_ && hd_->contains(x))
|
||||
return true;
|
||||
|
||||
for (const auto & alloc : full_l_) {
|
||||
if (alloc->contains(x))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
ListAlloc::is_before_checkpoint(const void * x) const {
|
||||
if (!marked_)
|
||||
return false;
|
||||
|
||||
if ((marked_ == hd_.get()) && hd_->contains(x))
|
||||
return hd_->is_before_checkpoint(x);
|
||||
|
||||
/*
|
||||
* 1. allocs in full_l_ appear in youngest-to-oldest order
|
||||
* 2. allocators that appear before marked_ in full_l_ count as 'after checkpoint'
|
||||
* 3. allocators that appear after marked_ in full_l_ count as 'before checkpoint'
|
||||
*/
|
||||
|
||||
bool younger_than_marked = true;
|
||||
|
||||
for (const auto & alloc : full_l_) {
|
||||
if (younger_than_marked) {
|
||||
if (alloc.get() == marked_) {
|
||||
/* nothing else to test on this iteration,
|
||||
* already checked .marked_ specifically
|
||||
*/
|
||||
younger_than_marked = false;
|
||||
} else {
|
||||
/* after checkpoint */
|
||||
if (alloc->contains(x))
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (alloc->contains(x))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ListAlloc::before_checkpoint() const
|
||||
{
|
||||
if (marked_) {
|
||||
if (full_l_.empty()) {
|
||||
assert(marked_ == hd_.get());
|
||||
|
||||
return marked_->before_checkpoint();
|
||||
}
|
||||
} else {
|
||||
/* count everything allocated */
|
||||
return this->allocated();
|
||||
}
|
||||
|
||||
std::size_t z = 0;
|
||||
|
||||
/* control here: .marked & .full_l non-empty. */
|
||||
if (hd_.get() == marked_) {
|
||||
z += hd_->before_checkpoint();
|
||||
|
||||
/* anything in .full_l older than marked .hd */
|
||||
for (const auto & alloc : full_l_) {
|
||||
z += alloc->allocated();
|
||||
}
|
||||
|
||||
return z;
|
||||
} else {
|
||||
/* messiest case: .marked is true,
|
||||
* and not the youngest arena
|
||||
*/
|
||||
bool younger_than_marked = true;
|
||||
|
||||
for (const auto & alloc : full_l_) {
|
||||
if (younger_than_marked) {
|
||||
if (alloc.get() == marked_) {
|
||||
younger_than_marked = false;
|
||||
z += marked_->before_checkpoint();
|
||||
} else {
|
||||
;
|
||||
}
|
||||
} else {
|
||||
z += alloc->allocated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return z;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ListAlloc::after_checkpoint() const
|
||||
{
|
||||
if (!marked_)
|
||||
return 0;
|
||||
|
||||
if (full_l_.empty()) {
|
||||
assert(marked_ == hd_.get());
|
||||
|
||||
return marked_->after_checkpoint();
|
||||
}
|
||||
|
||||
bool younger_than_marked = true;
|
||||
|
||||
std::size_t z = 0;
|
||||
|
||||
for (const auto & alloc : full_l_) {
|
||||
if (younger_than_marked) {
|
||||
if (alloc.get() == marked_) {
|
||||
younger_than_marked = false;
|
||||
z += marked_->after_checkpoint();
|
||||
break;
|
||||
} else {
|
||||
z += alloc->allocated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return z;
|
||||
}
|
||||
|
||||
void
|
||||
ListAlloc::clear() {
|
||||
// general hygiene
|
||||
start_z_ = 0;
|
||||
hd_.reset();
|
||||
marked_ = nullptr;
|
||||
full_l_.clear();
|
||||
current_z_ = 0;
|
||||
next_z_ = 0;
|
||||
total_z_ = 0;
|
||||
use_redline_ = false;
|
||||
}
|
||||
|
||||
bool
|
||||
ListAlloc::reset(std::size_t z)
|
||||
{
|
||||
// warning: hd_->size() does not include redline memory
|
||||
hd_->release_redline_memory();
|
||||
|
||||
bool recycle_head_bucket = hd_ && (z <= hd_->size());
|
||||
|
||||
this->full_l_.clear();
|
||||
this->marked_ = nullptr;
|
||||
this->redlined_flag_ = false;
|
||||
|
||||
if (recycle_head_bucket) {
|
||||
this->hd_->clear();
|
||||
this->total_z_ = hd_->size();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
this->hd_.reset(nullptr);
|
||||
this->total_z_ = 0;
|
||||
|
||||
return this->expand(z);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ListAlloc::expand(std::size_t z)
|
||||
{
|
||||
std::size_t cz = current_z_;
|
||||
std::size_t nz = next_z_;
|
||||
std::size_t tz;
|
||||
|
||||
do {
|
||||
tz = cz + nz;
|
||||
cz = nz;
|
||||
nz = tz;
|
||||
} while (cz < z);
|
||||
|
||||
std::string name = hd_->name() + "+exp";
|
||||
|
||||
std::unique_ptr<ArenaAlloc> new_alloc = ArenaAlloc::make(name, 0, cz, debug_flag_);
|
||||
|
||||
if (!new_alloc)
|
||||
return false;
|
||||
|
||||
this->current_z_ = cz;
|
||||
this->next_z_ = nz;
|
||||
this->total_z_ += cz;
|
||||
|
||||
this->hd_ = std::move(new_alloc);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
ListAlloc::checkpoint() {
|
||||
hd_->checkpoint();
|
||||
|
||||
this->marked_ = hd_.get();
|
||||
}
|
||||
|
||||
std::byte *
|
||||
ListAlloc::alloc(std::size_t z) {
|
||||
std::byte * retval = hd_->alloc(z);
|
||||
|
||||
if (retval)
|
||||
return retval;
|
||||
|
||||
if (this->expand(z))
|
||||
return hd_->alloc(z);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
ListAlloc::release_redline_memory()
|
||||
{
|
||||
if (use_redline_)
|
||||
redlined_flag_ = true;
|
||||
|
||||
this->hd_->release_redline_memory();
|
||||
}
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end ListAlloc.cpp */
|
||||
196
xo-alloc/src/alloc/Object.cpp
Normal file
196
xo-alloc/src/alloc/Object.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/* Object.cpp
|
||||
*
|
||||
* author: Roalnd Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include "Object.hpp"
|
||||
#include "GC.hpp"
|
||||
#include "Forwarding1.hpp"
|
||||
|
||||
using xo::obj::Forwarding1;
|
||||
|
||||
void *
|
||||
operator new (std::size_t z, const xo::Cpof & cpof)
|
||||
{
|
||||
using xo::gc::GC;
|
||||
|
||||
GC * gc = reinterpret_cast<GC *>(xo::Object::mm);
|
||||
|
||||
return gc->alloc_gc_copy(z, cpof.src_);
|
||||
}
|
||||
|
||||
namespace xo {
|
||||
gc::IAlloc *
|
||||
Object::mm = nullptr;
|
||||
|
||||
Object *
|
||||
Object::_forward(Object * src, gc::GC * gc)
|
||||
{
|
||||
if (!src)
|
||||
return src;
|
||||
|
||||
if (src->_is_forwarded())
|
||||
return src->_offset_destination(src);
|
||||
|
||||
bool full_move = gc->runstate().full_move();
|
||||
|
||||
if (!full_move && (gc->generation_of(src) == gc::generation::tenured)) {
|
||||
/* don't move tenured objects during incremental collection */
|
||||
return src;
|
||||
}
|
||||
|
||||
Object::_shallow_move(src, gc);
|
||||
|
||||
/* *src is now a forwarding pointer to copy in to-space */
|
||||
|
||||
return src->_offset_destination(src);
|
||||
}
|
||||
|
||||
Object *
|
||||
Object::_deep_move(Object * from_src, gc::GC * gc, gc::ObjectStatistics * /*stats*/)
|
||||
{
|
||||
using gc::generation;
|
||||
|
||||
if (!from_src)
|
||||
return nullptr;
|
||||
|
||||
Object * retval = from_src->_destination();
|
||||
|
||||
if (retval)
|
||||
return retval;
|
||||
|
||||
bool full_move = gc->runstate().full_move();
|
||||
|
||||
if (!full_move && gc->generation_of(from_src) == generation::tenured) {
|
||||
/** incremental collection does not move already-tenured objects **/
|
||||
return from_src;
|
||||
}
|
||||
|
||||
/**
|
||||
* To-space:
|
||||
*
|
||||
* to_lo = start of to-space
|
||||
* w,W = white objects. An object x is white if x + all immediate children of x are in to-space
|
||||
* (also implies this GC cycle put it there)
|
||||
* g,G = grey objects. An object x is gray if it's in to-space,
|
||||
* but possibly has >0 black children
|
||||
* _ = free to-space memory
|
||||
* N = nursery space
|
||||
* T = tenured space
|
||||
*
|
||||
* wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________...
|
||||
* ^ ^ ^
|
||||
* to_lo grey_lo(N) free_ptr(N)
|
||||
*
|
||||
* After moving children of one object, advancing {nursery_grey_lo, nursery_free_ptr}
|
||||
*
|
||||
* wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______...
|
||||
* ^ ^ ^
|
||||
* to_lo grey_lo(N) free_ptr(N)
|
||||
*
|
||||
* Invariant:
|
||||
*
|
||||
* objects in [to_lo, gray_lo) are white.
|
||||
* all gray objects are in [gray_lo, free_ptr)
|
||||
* memory starting at free_ptr is free.
|
||||
*
|
||||
* deep_move terminates when gray_lo catches up to free_ptr
|
||||
*
|
||||
* Above is simplified. Complication is that GC (including incremental) may
|
||||
* promote objects from nursery (N) to tenured (T)
|
||||
*
|
||||
* So more accurate before/after picture
|
||||
*
|
||||
* N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________...
|
||||
* ^ ^ ^
|
||||
* to_lo(N) grey_lo(N) free_ptr(N)
|
||||
*
|
||||
* T wwwwwwwwwwwwwwgggggggggggg_______________________________...
|
||||
* ^ ^ ^
|
||||
* to_lo(T) grey_lo(T) free_ptr(N)
|
||||
*
|
||||
* After moving children of one object, advancing {nursery_grey_lo, nursery_free_ptr}
|
||||
*
|
||||
* N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____...
|
||||
* ^ ^ ^
|
||||
* to_lo(N) grey_lo(N) free_ptr(N)
|
||||
*
|
||||
* T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________...
|
||||
* ^ ^ ^
|
||||
* to_lo(T) grey_lo(T) free_ptr(T)
|
||||
*
|
||||
* deep_move terminates when both:
|
||||
* - gray_lo(N) catches up with free_ptr(N)
|
||||
* - gray_lo(T) catches up with free_ptr(T)
|
||||
*
|
||||
**/
|
||||
|
||||
std::array<std::byte *, gen2int(generation::N)> gray_lo_v
|
||||
= { gc->free_ptr(generation::nursery), gc->free_ptr(generation::tenured) };
|
||||
|
||||
Object * to_src = Object::_shallow_move(from_src, gc);
|
||||
|
||||
std::size_t fixup_work = 0;
|
||||
do {
|
||||
fixup_work = 0;
|
||||
|
||||
auto fixup_generation = [gc, &gray_lo_v](generation gen) {
|
||||
std::size_t work = 0;
|
||||
while(gray_lo_v[gen2int(gen)] < gc->free_ptr(gen)) {
|
||||
Object * x = reinterpret_cast<Object *>(gray_lo_v[gen2int(gen)]);
|
||||
|
||||
// update per-class stats here
|
||||
|
||||
std::size_t xz = x->_forward_children();
|
||||
|
||||
// must pad xz to multiple of word size,
|
||||
// to match behavior of LinearAlloc::alloc()
|
||||
//
|
||||
xz += gc::IAlloc::alloc_padding(xz);
|
||||
|
||||
gray_lo_v[gen2int(gen)] += xz;
|
||||
++work;
|
||||
}
|
||||
|
||||
return work;
|
||||
};
|
||||
|
||||
fixup_work += fixup_generation(generation::nursery);
|
||||
fixup_work += fixup_generation(generation::tenured);
|
||||
} while (fixup_work > 0);
|
||||
|
||||
return to_src;
|
||||
} /*deep_move*/
|
||||
|
||||
Object *
|
||||
Object::_shallow_move(Object * src, gc::GC * gc)
|
||||
{
|
||||
/* filter for source objects that are owned by GC.
|
||||
* Care required though -- during GC from/to spaces have been swapped already
|
||||
*/
|
||||
if (gc->fromspace_contains(src))
|
||||
{
|
||||
Object * dest = src->_shallow_copy();
|
||||
|
||||
if (dest != src)
|
||||
src->_forward_to(dest);
|
||||
|
||||
return dest;
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Object::_forward_to(Object * dest)
|
||||
{
|
||||
char * mem = reinterpret_cast<char *>(this);
|
||||
|
||||
Forwarding1 * fwd = new (mem) Forwarding1(dest);
|
||||
|
||||
(void)fwd;
|
||||
}
|
||||
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Object.cpp*/
|
||||
87
xo-alloc/utest/ArenaAlloc.test.cpp
Normal file
87
xo-alloc/utest/ArenaAlloc.test.cpp
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* @file ArenaAlloc.test.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/ArenaAlloc.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
namespace xo {
|
||||
using xo::gc::ArenaAlloc;
|
||||
|
||||
namespace ut {
|
||||
|
||||
namespace {
|
||||
struct testcase_alloc {
|
||||
testcase_alloc(std::size_t rz, std::size_t z)
|
||||
: redline_z_{rz}, arena_z_{z} {}
|
||||
|
||||
std::size_t redline_z_;
|
||||
std::size_t arena_z_;
|
||||
|
||||
};
|
||||
|
||||
std::vector<testcase_alloc>
|
||||
s_testcase_v = {
|
||||
testcase_alloc(0, 4096)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("linearalloc", "[alloc]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const testcase_alloc & tc = s_testcase_v[i_tc];
|
||||
|
||||
constexpr bool c_debug_flag = false;
|
||||
|
||||
auto alloc = ArenaAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag);
|
||||
|
||||
REQUIRE(alloc.get());
|
||||
REQUIRE(alloc->name() == "linearalloc");
|
||||
REQUIRE(alloc->size() == tc.arena_z_);
|
||||
REQUIRE(alloc->available() == tc.arena_z_);
|
||||
REQUIRE(alloc->allocated() == 0);
|
||||
REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == 0);
|
||||
|
||||
auto free0 = alloc->free_ptr();
|
||||
|
||||
auto mem = alloc->alloc(tc.arena_z_);
|
||||
|
||||
REQUIRE(mem != nullptr);
|
||||
|
||||
REQUIRE(mem == free0);
|
||||
|
||||
REQUIRE(alloc->size() == tc.arena_z_);
|
||||
REQUIRE(alloc->available() == 0);
|
||||
REQUIRE(alloc->allocated() == tc.arena_z_);
|
||||
REQUIRE(alloc->is_before_checkpoint(mem) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == tc.arena_z_);
|
||||
|
||||
alloc->clear();
|
||||
|
||||
REQUIRE(alloc->free_ptr() == free0);
|
||||
REQUIRE(alloc->available() == tc.arena_z_);
|
||||
REQUIRE(alloc->allocated() == 0);
|
||||
REQUIRE(alloc->is_before_checkpoint(free0) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == 0);
|
||||
|
||||
mem = alloc->alloc(1);
|
||||
|
||||
auto used = sizeof(void*);
|
||||
REQUIRE(alloc->size() == tc.arena_z_);
|
||||
REQUIRE(alloc->available() == tc.arena_z_ - used);
|
||||
REQUIRE(alloc->allocated() == used);
|
||||
REQUIRE(alloc->is_before_checkpoint(free0) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == used);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} /*namespace ut */
|
||||
} /*namespace xo*/
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
69
xo-alloc/utest/GC.test.cpp
Normal file
69
xo-alloc/utest/GC.test.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/* @file GC.test.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/GC.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
namespace xo {
|
||||
using xo::gc::GC;
|
||||
using xo::gc::generation;
|
||||
using xo::gc::Config;
|
||||
|
||||
namespace ut {
|
||||
|
||||
namespace {
|
||||
struct testcase_gc {
|
||||
testcase_gc(std::size_t nz, std::size_t tz) : nursery_z_{nz}, tenured_z_{tz} {}
|
||||
|
||||
std::size_t nursery_z_;
|
||||
std::size_t tenured_z_;
|
||||
};
|
||||
|
||||
std::vector<testcase_gc>
|
||||
s_testcase_v = {
|
||||
testcase_gc(1024, 4096)
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("gc", "[alloc][gc]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const testcase_gc & tc = s_testcase_v[i_tc];
|
||||
|
||||
up<GC> gc = GC::make(
|
||||
{.initial_nursery_z_ = tc.nursery_z_,
|
||||
.initial_tenured_z_ = tc.tenured_z_});
|
||||
|
||||
REQUIRE(gc.get());
|
||||
REQUIRE(gc->size() == tc.nursery_z_ + tc.tenured_z_);
|
||||
REQUIRE(gc->allocated() == 0);
|
||||
REQUIRE(gc->available() == tc.nursery_z_);
|
||||
REQUIRE(gc->before_checkpoint() == 0);
|
||||
// ListAlloc model is that nothing is before checkpoint
|
||||
// until it's first established
|
||||
REQUIRE(gc->after_checkpoint() == 0);
|
||||
|
||||
REQUIRE(gc->gc_in_progress() == false);
|
||||
REQUIRE(gc->is_gc_enabled() == true);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 0);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
|
||||
|
||||
/* gc with empty state */
|
||||
gc->request_gc(generation::nursery);
|
||||
|
||||
REQUIRE(gc->gc_in_progress() == false);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
|
||||
|
||||
/* still empty state */
|
||||
gc->request_gc(generation::tenured);
|
||||
|
||||
REQUIRE(gc->gc_in_progress() == false);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
|
||||
}
|
||||
}
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
* author: Roland Conybeare, Jul 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/LinearAlloc.hpp"
|
||||
#include "xo/alloc/ArenaAlloc.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
namespace xo {
|
||||
|
|
@ -33,15 +33,53 @@ namespace xo {
|
|||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const testcase_alloc & tc = s_testcase_v[i_tc];
|
||||
|
||||
auto alloc = LinearAlloc::make(tc.redline_z_, tc.arena_z_);
|
||||
constexpr bool c_debug_flag = false;
|
||||
|
||||
auto alloc = LinearAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag);
|
||||
|
||||
REQUIRE(alloc.get());
|
||||
REQUIRE(alloc->name() == "linearalloc");
|
||||
REQUIRE(alloc->size() == tc.arena_z_);
|
||||
REQUIRE(alloc->available() == tc.arena_z_);
|
||||
REQUIRE(alloc->allocated() == 0);
|
||||
REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == 0);
|
||||
|
||||
auto free0 = alloc->free_ptr();
|
||||
|
||||
auto mem = alloc->alloc(tc.arena_z_);
|
||||
|
||||
REQUIRE(mem != nullptr);
|
||||
|
||||
REQUIRE(mem == free0);
|
||||
|
||||
REQUIRE(alloc->size() == tc.arena_z_);
|
||||
REQUIRE(alloc->available() == 0);
|
||||
REQUIRE(alloc->allocated() == tc.arena_z_);
|
||||
REQUIRE(alloc->is_before_checkpoint(mem) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == tc.arena_z_);
|
||||
|
||||
alloc->clear();
|
||||
|
||||
REQUIRE(alloc->free_ptr() == free0);
|
||||
REQUIRE(alloc->available() == tc.arena_z_);
|
||||
REQUIRE(alloc->allocated() == 0);
|
||||
REQUIRE(alloc->is_before_checkpoint(free0) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == 0);
|
||||
|
||||
mem = alloc->alloc(1);
|
||||
|
||||
auto used = sizeof(void*);
|
||||
REQUIRE(alloc->size() == tc.arena_z_);
|
||||
REQUIRE(alloc->available() == tc.arena_z_ - used);
|
||||
REQUIRE(alloc->allocated() == used);
|
||||
REQUIRE(alloc->is_before_checkpoint(free0) == false);
|
||||
REQUIRE(alloc->before_checkpoint() == 0);
|
||||
REQUIRE(alloc->after_checkpoint() == used);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
xo-cmake
|
||||
xo-indentlog
|
||||
xo-alloc
|
||||
xo-object
|
||||
xo-refcnt
|
||||
xo-subsys
|
||||
xo-randomgen
|
||||
|
|
|
|||
26
xo-object/CMakeLists.txt
Normal file
26
xo-object/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# object/CMakeLists.txt
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(xo_object VERSION 0.1)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(cmake/xo-bootstrap-macros.cmake)
|
||||
|
||||
xo_cxx_toplevel_options3()
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# c++ settings
|
||||
|
||||
set(PROJECT_CXX_FLAGS "")
|
||||
add_definitions(${PROJECT_CXX_FLAGS})
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
add_subdirectory(src/object)
|
||||
|
||||
xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
add_subdirectory(utest)
|
||||
35
xo-object/cmake/xo-bootstrap-macros.cmake
Normal file
35
xo-object/cmake/xo-bootstrap-macros.cmake
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# ----------------------------------------------------------------
|
||||
# for example:
|
||||
# $ PREFIX=/usr/local # for example
|
||||
# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build
|
||||
#
|
||||
# will get
|
||||
# CMAKE_MODULE_PATH
|
||||
# from xo-cmake-config --cmake-module-path
|
||||
#
|
||||
# and expect .cmake macros in
|
||||
# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED)
|
||||
|
||||
if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND")
|
||||
message(FATAL "could not find xo-cmake-config executable")
|
||||
endif()
|
||||
|
||||
message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}")
|
||||
|
||||
if (NOT XO_SUBMODULE_BUILD)
|
||||
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
|
||||
# default to typical install location for xo-project-macros
|
||||
execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH)
|
||||
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
|
||||
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
|
||||
#
|
||||
include(xo_macros/xo_cxx)
|
||||
|
||||
xo_cxx_bootstrap_message()
|
||||
8
xo-object/cmake/xo_objectConfig.cmake.in
Normal file
8
xo-object/cmake/xo_objectConfig.cmake.in
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
# reminder: deps here must also appear in xo-object/src/object/CMakeLists.txt
|
||||
find_dependency(xo_alloc)
|
||||
#find_dependency(xo_flatstring)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
|
||||
check_required_components("@PROJECT_NAME@")
|
||||
37
xo-object/include/xo/object/Boolean.hpp
Normal file
37
xo-object/include/xo/object/Boolean.hpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* @file Boolean.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
/** @class Boolean
|
||||
* @brief Boxed wrapper for a boolean value
|
||||
**/
|
||||
class Boolean : public Object {
|
||||
public:
|
||||
/** @return instance representing boolean with truth-value @p x **/
|
||||
static gp<Boolean> boolean_obj(bool x);
|
||||
static gp<Boolean> true_obj();
|
||||
static gp<Boolean> false_obj();
|
||||
|
||||
bool value() const { return value_; }
|
||||
|
||||
// inherited from Object..
|
||||
|
||||
virtual std::size_t _shallow_size() const override;
|
||||
virtual Object * _shallow_copy() const override;
|
||||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
explicit Boolean(bool x) : value_{x} {}
|
||||
|
||||
private:
|
||||
bool value_;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* end Boolean.hpp */
|
||||
37
xo-object/include/xo/object/BooleanObj.hpp
Normal file
37
xo-object/include/xo/object/BooleanObj.hpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* @file BooleanObj.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
/** @class BooleanObj
|
||||
* @brief Boxed wrapper for a boolean value
|
||||
**/
|
||||
class BooleanObj : public Object {
|
||||
public:
|
||||
/** @return instance representing boolean with truth-value @p x **/
|
||||
static gp<BooleanObj> boolean_obj(bool x);
|
||||
static gp<BooleanObj> true_obj();
|
||||
static gp<BooleanObj> false_obj();
|
||||
|
||||
bool value() const { return value_; }
|
||||
|
||||
// inherited from Object..
|
||||
|
||||
virtual std::size_t _shallow_size() const override;
|
||||
virtual Object * _shallow_copy() const override;
|
||||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
explicit BooleanObj(bool x) : value_{x} {}
|
||||
|
||||
private:
|
||||
bool value_;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* end BooleanObj.hpp */
|
||||
21
xo-object/include/xo/object/Collection.hpp
Normal file
21
xo-object/include/xo/object/Collection.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* @file Collection.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Collection : public Object {
|
||||
// inherited from Object..
|
||||
|
||||
//virtual std::size_t _shallow_size() const override;
|
||||
//virtual Object * _shallow_copy() const override;
|
||||
//virtual std::size_t _forward_children() override;
|
||||
};
|
||||
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Collection.hpp */
|
||||
31
xo-object/include/xo/object/Integer.hpp
Normal file
31
xo-object/include/xo/object/Integer.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* @file Integer.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Number.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Integer : public Number {
|
||||
public:
|
||||
using int_type = long long;
|
||||
|
||||
public:
|
||||
Integer() = default;
|
||||
explicit Integer(int_type x);
|
||||
|
||||
static gp<Integer> make(int_type x);
|
||||
|
||||
// inherited from Object..
|
||||
virtual std::size_t _shallow_size() const override;
|
||||
virtual Object * _shallow_copy() const override;
|
||||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
int_type value_ = 0;
|
||||
};
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Integer.hpp */
|
||||
43
xo-object/include/xo/object/List.hpp
Normal file
43
xo-object/include/xo/object/List.hpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* @file List.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Sequence.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
/** @class List
|
||||
* @brief A list element -- aka cons cell
|
||||
**/
|
||||
class List : public Sequence {
|
||||
public:
|
||||
/** the empty list. unique sentinel object **/
|
||||
static gp<List> nil;
|
||||
|
||||
/** @return list with first element @p car, and tail @p cdr **/
|
||||
static gp<List> cons(gp<Object> car, gp<List> cdr);
|
||||
|
||||
bool is_nil() const { return this == nil.ptr(); }
|
||||
|
||||
gp<Object> head() const { return head_; }
|
||||
gp<List> tail() const { return tail_; }
|
||||
|
||||
std::size_t size() const;
|
||||
gp<Object> list_ref(std::size_t i) const;
|
||||
|
||||
// inherited from Object..
|
||||
|
||||
virtual std::size_t _shallow_size() const override;
|
||||
virtual Object * _shallow_copy() const override;
|
||||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
List(gp<Object> head, gp<List> tail);
|
||||
|
||||
private:
|
||||
gp<Object> head_;
|
||||
gp<List> tail_;
|
||||
};
|
||||
}
|
||||
} /*namespace xo*/
|
||||
20
xo-object/include/xo/object/Number.hpp
Normal file
20
xo-object/include/xo/object/Number.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* @file Number.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Scalar.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Number : public Scalar {
|
||||
// inherited from Object..
|
||||
|
||||
//virtual std::size_t _shallow_size() const override;
|
||||
//virtual Object * _shallow_copy() override;
|
||||
//virtual std::size_t _forward_children() override;
|
||||
};
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Number.hpp */
|
||||
20
xo-object/include/xo/object/Numeric.hpp
Normal file
20
xo-object/include/xo/object/Numeric.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* @file Numeric.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Numeric : public Object {
|
||||
// inherited from Object..
|
||||
|
||||
//virtual std::size_t _shallow_size() const override;
|
||||
//virtual Object * _shallow_copy() override;
|
||||
//virtual std::size_t _forward_children() override;
|
||||
};
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Numeric.hpp */
|
||||
20
xo-object/include/xo/object/Scalar.hpp
Normal file
20
xo-object/include/xo/object/Scalar.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* @file Scalar.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Numeric.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Scalar : public Numeric {
|
||||
// inherited from Object..
|
||||
|
||||
//virtual std::size_t _shallow_size() const override;
|
||||
//virtual Object * _shallow_copy() override;
|
||||
//virtual std::size_t _forward_children() override;
|
||||
};
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Scalar.hpp */
|
||||
20
xo-object/include/xo/object/Sequence.hpp
Normal file
20
xo-object/include/xo/object/Sequence.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* @file Sequence.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Collection.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class Sequence : public Collection {
|
||||
// inherited from Object..
|
||||
|
||||
//virtual std::size_t _shallow_size() const override;
|
||||
//virtual Object * _shallow_copy() const override;
|
||||
//virtual std::size_t _fixup_forwarded_children() override;
|
||||
};
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Sequence.hpp */
|
||||
54
xo-object/include/xo/object/String.hpp
Normal file
54
xo-object/include/xo/object/String.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* @file String.hpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/alloc/IAlloc.hpp"
|
||||
#include "xo/alloc/Object.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
class String : public Object {
|
||||
public:
|
||||
enum Owner { unique, shared };
|
||||
|
||||
/** donwcase from @p x iff x is actually a String. Otherwise nullptr **/
|
||||
static gp<String> from(gp<Object> x);
|
||||
|
||||
/** create copy of string @p s, using allocator @ref Object::mm **/
|
||||
static gp<String> copy(const char * s);
|
||||
/** create copy of string @p s, using allocator @p mm **/
|
||||
static gp<String> copy(gc::IAlloc * mm, const char * s);
|
||||
|
||||
/** create empty string with @p z bytes of string space **/
|
||||
static gp<String> allocate(std::size_t z);
|
||||
/** create string containing contents of @p s1 follwed by contents of @p s2 **/
|
||||
static gp<String> append(gp<String> s1, gp<String> s2);
|
||||
|
||||
const char * c_str() const { return chars_; }
|
||||
std::size_t length() const;
|
||||
|
||||
// inherited from Object..
|
||||
|
||||
virtual std::size_t _shallow_size() const override;
|
||||
virtual Object * _shallow_copy() const override;
|
||||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
String(Owner owner, std::size_t z, char * s);
|
||||
/** create instance, copying string contents (when @p copy_flag is true) using allocator @p mm **/
|
||||
String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy);
|
||||
|
||||
private:
|
||||
/** true iff storage in @ref chars_ is owned by this String.
|
||||
**/
|
||||
Owner owner_ = Owner::shared;
|
||||
/** length of @ref chars_ in bytes (storage allocated, not necessarily string length) **/
|
||||
std::size_t z_chars_ = 0;
|
||||
/** string contents. always null-terminated **/
|
||||
char * chars_ = nullptr;
|
||||
};
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end String.hpp */
|
||||
52
xo-object/src/object/Boolean.cpp
Normal file
52
xo-object/src/object/Boolean.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/* @file Boolean.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Boolean.hpp"
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
gp<Boolean>
|
||||
Boolean::boolean_obj(bool x)
|
||||
{
|
||||
static std::array<gp<Boolean>, 2> s_boolean_v
|
||||
= {{ new Boolean{false}, new Boolean{true} }};
|
||||
|
||||
return s_boolean_v[static_cast<std::size_t>(x)];
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Boolean::_shallow_size() const
|
||||
{
|
||||
return sizeof(Boolean);
|
||||
}
|
||||
|
||||
Object *
|
||||
Boolean::_shallow_copy() const
|
||||
{
|
||||
/* Boolean instances not created in GC-owned space,
|
||||
* so GC will not traverse them.
|
||||
*
|
||||
* If we wanted booleans in GC-owned space, would need
|
||||
* to pad Boolean::value_ with enough space to hold a forwarding
|
||||
* pointer
|
||||
*/
|
||||
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Boolean::_forward_children()
|
||||
{
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Boolean.cpp */
|
||||
11
xo-object/src/object/CMakeLists.txt
Normal file
11
xo-object/src/object/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# object/CMakeLists.txt
|
||||
|
||||
set(SELF_LIB xo_object)
|
||||
set(SELF_SRCS
|
||||
Boolean.cpp
|
||||
String.cpp
|
||||
List.cpp
|
||||
Integer.cpp)
|
||||
|
||||
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
|
||||
xo_dependency(${SELF_LIB} xo_alloc)
|
||||
38
xo-object/src/object/Integer.cpp
Normal file
38
xo-object/src/object/Integer.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* @file Integer.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "Integer.hpp"
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
Integer::Integer(int_type x) : value_{x} {}
|
||||
|
||||
gp<Integer>
|
||||
Integer::make(int_type x) {
|
||||
return new (MMPtr(mm)) Integer(x);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Integer::_shallow_size() const {
|
||||
return sizeof(Integer);
|
||||
}
|
||||
|
||||
Object *
|
||||
Integer::_shallow_copy() const {
|
||||
Cpof cpof(this);
|
||||
|
||||
return new (cpof) Integer(*this);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Integer::_forward_children() {
|
||||
return Integer::_shallow_size();
|
||||
}
|
||||
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end Integer.cpp */
|
||||
74
xo-object/src/object/List.cpp
Normal file
74
xo-object/src/object/List.cpp
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/** @file List.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
**/
|
||||
|
||||
#include "List.hpp"
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
List::List(gp<Object> head, gp<List> tail)
|
||||
: head_{head}, tail_{tail} {}
|
||||
|
||||
gp<List>
|
||||
List::nil = new List(nullptr, nullptr);
|
||||
|
||||
gp<List>
|
||||
List::cons(gp<Object> car, gp<List> cdr) {
|
||||
return new (MMPtr(mm)) List(car, cdr);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
List::size() const {
|
||||
std::size_t retval = 0;
|
||||
|
||||
gp<const List> l(this);
|
||||
while (!l->is_nil()) {
|
||||
++retval;
|
||||
l = l->tail();
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
gp<Object>
|
||||
List::list_ref(std::size_t i) const {
|
||||
gp<const List> rem(this);
|
||||
|
||||
while (i > 0) {
|
||||
assert(!(rem->is_nil()));
|
||||
|
||||
rem = rem->tail();
|
||||
--i;
|
||||
}
|
||||
|
||||
return rem->head();
|
||||
|
||||
}
|
||||
|
||||
std::size_t
|
||||
List::_shallow_size() const {
|
||||
return sizeof(List);
|
||||
}
|
||||
|
||||
Object *
|
||||
List::_shallow_copy() const {
|
||||
assert(!(this->is_nil()));
|
||||
|
||||
Cpof cpof(this);
|
||||
|
||||
return new (cpof) List(*this);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
List::_forward_children() {
|
||||
Object::_forward_inplace(head_);
|
||||
Object::_forward_inplace(tail_);
|
||||
return List::_shallow_size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* end List.cpp */
|
||||
133
xo-object/src/object/String.cpp
Normal file
133
xo-object/src/object/String.cpp
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/* @file String.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "String.hpp"
|
||||
#include "GC.hpp"
|
||||
#include <bsd/string.h>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
String::String(Owner owner, std::size_t z, char * s)
|
||||
: owner_{owner}, z_chars_{z}, chars_{s}
|
||||
{}
|
||||
|
||||
String::String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy)
|
||||
: owner_{owner}, z_chars_{z}
|
||||
{
|
||||
if (copy) {
|
||||
chars_ = reinterpret_cast<char *>(mm->alloc(z));
|
||||
|
||||
assert(chars_);
|
||||
|
||||
strlcpy(chars_, s, z);
|
||||
} else {
|
||||
chars_ = s;
|
||||
}
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::from(gp<Object> x)
|
||||
{
|
||||
return dynamic_cast<String*>(x.ptr());
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::copy(const char * s)
|
||||
{
|
||||
return copy(Object::mm, s);
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::copy(gc::IAlloc * mm, const char * s)
|
||||
{
|
||||
std::size_t z = 1 + (s ? ::strlen(s) : 0);
|
||||
const char * chars = s ? s : "";
|
||||
|
||||
// const-cast ok since chars copied with Owner::unique
|
||||
return new (MMPtr(mm)) String(mm, Owner::unique, z, const_cast<char *>(chars), true /*copy*/);
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::allocate(std::size_t z)
|
||||
{
|
||||
return new (MMPtr(Object::mm)) String(mm, Owner::unique, z, const_cast<char *>(""), true /*copy*/);
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::append(gp<String> s1, gp<String> s2)
|
||||
{
|
||||
std::size_t z1 = s1->length();
|
||||
std::size_t z2 = s2->length();
|
||||
std::size_t z = z1 + z2;
|
||||
|
||||
gp<String> retval = allocate(z);
|
||||
|
||||
strlcpy(retval->chars_, s1->chars_, z1);
|
||||
strlcpy(retval->chars_ + z1, s2->chars_, z2);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
String::length() const
|
||||
{
|
||||
return ::strlen(chars_);
|
||||
}
|
||||
|
||||
// ----- GC support -----
|
||||
|
||||
std::size_t
|
||||
String::_shallow_size() const
|
||||
{
|
||||
/* no child Object* pointers to fixup,
|
||||
* but must count for amount of storage used by _shallow_move()
|
||||
*/
|
||||
std::size_t retval = gc::IAlloc::with_padding(sizeof(String));
|
||||
|
||||
if (owner_ == Owner::unique)
|
||||
retval += gc::IAlloc::with_padding(z_chars_);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
Object *
|
||||
String::_shallow_copy() const
|
||||
{
|
||||
// Reminder: String must come before secondary allocation,
|
||||
|
||||
Cpof cpof(this);
|
||||
|
||||
// might expect to write:
|
||||
// gp<String> copy = new (gcm) String(Object::mm, owner_, z_chars_, chars_);
|
||||
// but this would always put string contents in nursery to-space.
|
||||
//
|
||||
// We need to choose nursery/tenured based on location of this,
|
||||
// achieved by calling GC::alloc_copy() instead of GC::alloc()
|
||||
//
|
||||
gp<String> copy = new (cpof) String(owner_, z_chars_, chars_);
|
||||
|
||||
if (owner_ == Owner::unique) {
|
||||
std::byte * mem = reinterpret_cast<std::byte *>(chars_);
|
||||
|
||||
copy->chars_ = reinterpret_cast<char *>(Object::mm->alloc_gc_copy(z_chars_, mem));
|
||||
strlcpy(copy->chars_, chars_, z_chars_);
|
||||
}
|
||||
|
||||
return copy.ptr();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
String::_forward_children()
|
||||
{
|
||||
return this->_shallow_size();
|
||||
}
|
||||
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end String.cpp */
|
||||
11
xo-object/utest/CMakeLists.txt
Normal file
11
xo-object/utest/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# build unittest object/utest
|
||||
|
||||
set(SELF_EXE utest.object)
|
||||
set(SELF_SRCS
|
||||
object_utest_main.cpp
|
||||
String.test.cpp
|
||||
List.test.cpp)
|
||||
|
||||
xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS})
|
||||
xo_self_dependency(${SELF_EXE} xo_object)
|
||||
xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2)
|
||||
184
xo-object/utest/List.test.cpp
Normal file
184
xo-object/utest/List.test.cpp
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* @file List.test.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/object/List.hpp"
|
||||
#include "xo/object/String.hpp"
|
||||
#include "xo/alloc/GC.hpp"
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace xo {
|
||||
namespace ut {
|
||||
using xo::obj::List;
|
||||
using xo::obj::String;
|
||||
using xo::gc::GC;
|
||||
using xo::gc::generation;
|
||||
|
||||
namespace {
|
||||
struct Testcase_List {
|
||||
Testcase_List(std::size_t nz, std::size_t tz,
|
||||
const std::vector<std::vector<std::string>> & v)
|
||||
: nursery_z_{nz}, tenured_z_{tz}, v_{v}
|
||||
{}
|
||||
|
||||
std::size_t nursery_z_;
|
||||
std::size_t tenured_z_;
|
||||
|
||||
std::vector<std::vector<std::string>> v_;
|
||||
};
|
||||
|
||||
std::vector<Testcase_List>
|
||||
s_testcase_v = {
|
||||
Testcase_List( 512, 1024, {{}}),
|
||||
Testcase_List( 512, 1024, {{"hello", ", ", " world!"}}),
|
||||
Testcase_List(1024, 2048, {{"the", " quick", " brown", "fox", "jumps"},
|
||||
{"over", " the", " lazy", " dog!"}})
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("List", "[List][gc]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const Testcase_List & tc = s_testcase_v[i_tc];
|
||||
|
||||
constexpr bool c_debug_flag = false;
|
||||
|
||||
up<GC> gc = GC::make(
|
||||
{.initial_nursery_z_ = tc.nursery_z_,
|
||||
.initial_tenured_z_ = tc.tenured_z_,
|
||||
.debug_flag_ = c_debug_flag});
|
||||
|
||||
REQUIRE(gc.get());
|
||||
|
||||
/* use gc for all Object allocs */
|
||||
Object::mm = gc.get();
|
||||
|
||||
{
|
||||
scope log(XO_DEBUG(c_debug_flag));
|
||||
log && log(xtag("i_tc", i_tc), xtag("tc.v_.size", tc.v_.size()));
|
||||
|
||||
std::vector<gp<List>> root_v(tc.v_.size());
|
||||
std::size_t i = 0;
|
||||
|
||||
std::size_t expected_alloc_z = 0;
|
||||
|
||||
/* construct example Lists from testcase info */
|
||||
for (const std::vector<std::string> & v : tc.v_)
|
||||
{
|
||||
/* building l1 in reverse order */
|
||||
gp<List> l1 = List::nil;
|
||||
for (std::size_t ip1 = v.size(); ip1 > 0; --ip1) {
|
||||
const std::string & si = v.at(ip1 - 1);
|
||||
log && log(xtag("i", ip1-1), xtag("si", si));
|
||||
gp<String> sobj = String::copy(si.c_str());
|
||||
l1 = List::cons(sobj, l1);
|
||||
log && log(xtag("l1.size", l1->size()));
|
||||
|
||||
std::size_t alloc_z = l1->_shallow_size() + l1->head()->_shallow_size();
|
||||
expected_alloc_z += alloc_z;
|
||||
}
|
||||
|
||||
REQUIRE(l1->is_nil() == (v.size() == 0));
|
||||
REQUIRE(l1->size() == v.size());
|
||||
|
||||
root_v[i] = l1;
|
||||
gc->add_gc_root(reinterpret_cast<Object **>(root_v[i].ptr_address()));
|
||||
|
||||
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
/* gc responsible for a bunch of list objects;
|
||||
* all are roots and should be preserved
|
||||
*/
|
||||
gc->request_gc(generation::nursery);
|
||||
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
/* verify GC preserved list structure and contents */
|
||||
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
|
||||
std::size_t nj = tc.v_.at(i).size();
|
||||
|
||||
REQUIRE(root_v.at(i)->size() == nj);
|
||||
if (!(root_v.at(i)->is_nil()))
|
||||
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
|
||||
|
||||
for (std::size_t j = 0; j < nj; ++j) {
|
||||
gp<String> s = String::from(root_v.at(i)->list_ref(j));
|
||||
REQUIRE(s.ptr());
|
||||
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
|
||||
|
||||
REQUIRE(gc->generation_of(reinterpret_cast<std::byte*>(s.ptr()))
|
||||
== generation::nursery);
|
||||
}
|
||||
}
|
||||
|
||||
/* every has survived one GC cycle. collect again should promote */
|
||||
gc->request_gc(generation::nursery);
|
||||
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 2);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
/* verify GC preserved list structure and contents */
|
||||
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
|
||||
std::size_t nj = tc.v_.at(i).size();
|
||||
|
||||
REQUIRE(root_v.at(i)->size() == nj);
|
||||
if (!(root_v.at(i)->is_nil()))
|
||||
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
|
||||
|
||||
for (std::size_t j = 0; j < nj; ++j) {
|
||||
gp<String> s = String::from(root_v.at(i)->list_ref(j));
|
||||
REQUIRE(s.ptr());
|
||||
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
|
||||
|
||||
REQUIRE(gc->generation_of(reinterpret_cast<std::byte*>(s.ptr()))
|
||||
== generation::tenured);
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(gc->gc_statistics().total_promoted_ == gc->allocated());
|
||||
|
||||
gc->request_gc(generation::tenured);
|
||||
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 2);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
/* verify GC preserved list structure and contents */
|
||||
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
|
||||
std::size_t nj = tc.v_.at(i).size();
|
||||
|
||||
REQUIRE(root_v.at(i)->size() == nj);
|
||||
if (!(root_v.at(i)->is_nil()))
|
||||
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
|
||||
|
||||
for (std::size_t j = 0; j < nj; ++j) {
|
||||
gp<String> s = String::from(root_v.at(i)->list_ref(j));
|
||||
REQUIRE(s.ptr());
|
||||
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
|
||||
|
||||
REQUIRE(gc->generation_of(reinterpret_cast<std::byte*>(s.ptr()))
|
||||
== generation::tenured);
|
||||
}
|
||||
}
|
||||
|
||||
log && log("stats", gc->gc_statistics());
|
||||
}
|
||||
}
|
||||
} /*TEST_CASE(List, ..)*/
|
||||
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end List.test.cpp */
|
||||
157
xo-object/utest/String.test.cpp
Normal file
157
xo-object/utest/String.test.cpp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* @file String.test.cpp
|
||||
*
|
||||
* author: Roland Conybeare, Aug 2025
|
||||
*/
|
||||
|
||||
#include "xo/object/String.hpp"
|
||||
#include "xo/alloc/GC.hpp"
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include "xo/indentlog/print/quoted.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
using xo::gc::IAlloc;
|
||||
using xo::gc::GC;
|
||||
using xo::gc::generation;
|
||||
using xo::obj::String;
|
||||
|
||||
namespace ut {
|
||||
|
||||
namespace {
|
||||
struct Testcase_String {
|
||||
Testcase_String(std::size_t nz, std::size_t tz,
|
||||
const std::vector<std::string> & v)
|
||||
: nursery_z_{nz}, tenured_z_{tz}, v_{v} {}
|
||||
|
||||
std::size_t nursery_z_;
|
||||
std::size_t tenured_z_;
|
||||
|
||||
std::vector<std::string> v_;
|
||||
};
|
||||
|
||||
std::vector<Testcase_String>
|
||||
s_testcase_v = {
|
||||
Testcase_String(1024, 4096, {"hello"}),
|
||||
Testcase_String(1024, 4096, {"hello", ", world!"})
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("String", "[String][gc]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const Testcase_String & tc = s_testcase_v[i_tc];
|
||||
|
||||
up<GC> gc = GC::make(
|
||||
{.initial_nursery_z_ = tc.nursery_z_,
|
||||
.initial_tenured_z_ = tc.tenured_z_,
|
||||
.debug_flag_ = false});
|
||||
|
||||
REQUIRE(gc.get());
|
||||
|
||||
/* use gc for all Object allocs */
|
||||
Object::mm = gc.get();
|
||||
|
||||
{
|
||||
std::size_t n_string = 0;
|
||||
std::size_t expected_alloc_z = 0;
|
||||
|
||||
for (const std::string & s_str : tc.v_)
|
||||
{
|
||||
gp<String> s1 = String::copy(s_str.c_str());
|
||||
|
||||
++n_string;
|
||||
/* 1+ for mandatory null terminator */
|
||||
expected_alloc_z += (IAlloc::with_padding(sizeof(String))
|
||||
+ IAlloc::with_padding(1 + s_str.length()));
|
||||
|
||||
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
REQUIRE(s1->length() == s_str.length());
|
||||
REQUIRE(strcmp(s1->c_str(), s_str.c_str()) == 0);
|
||||
}
|
||||
|
||||
/* gc has n_string objects. Nothing refers to them, so gc going to kill all */
|
||||
gc->request_gc(generation::nursery);
|
||||
|
||||
REQUIRE(gc->gc_in_progress() == false);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
|
||||
REQUIRE(gc->allocated() == 0);
|
||||
REQUIRE(gc->gc_statistics().total_allocated_ == expected_alloc_z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("String2", "[String][gc]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const Testcase_String & tc = s_testcase_v[i_tc];
|
||||
|
||||
up<GC> gc = GC::make(
|
||||
{.initial_nursery_z_ = tc.nursery_z_,
|
||||
.initial_tenured_z_ = tc.tenured_z_,
|
||||
.debug_flag_ = false});
|
||||
|
||||
REQUIRE(gc.get());
|
||||
|
||||
/* use gc for all Object allocs */
|
||||
Object::mm = gc.get();
|
||||
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
std::size_t n_string = 0;
|
||||
std::size_t expected_alloc_z = 0;
|
||||
|
||||
std::vector<gp<String>> sv(tc.v_.size());
|
||||
|
||||
std::size_t i = 0;
|
||||
|
||||
for (const std::string & s_str : tc.v_)
|
||||
{
|
||||
sv[i] = String::copy(s_str.c_str());
|
||||
|
||||
++n_string;
|
||||
/* 1+ for mandatory null terminator */
|
||||
std::size_t alloc_z = (IAlloc::with_padding(sizeof(String))
|
||||
+ IAlloc::with_padding(1 + s_str.length()));
|
||||
expected_alloc_z += alloc_z;
|
||||
|
||||
log && log(xtag("s_str", xo::print::unq(s_str)),
|
||||
xtag("s_str.length", s_str.length()),
|
||||
xtag("alloc_z", alloc_z));
|
||||
log && log(xtag("expected_alloc_z", expected_alloc_z));
|
||||
|
||||
gc->add_gc_root(reinterpret_cast<Object **>(sv[i].ptr_address()));
|
||||
|
||||
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
REQUIRE(sv[i]->length() == s_str.length());
|
||||
REQUIRE(strcmp(sv[i]->c_str(), s_str.c_str()) == 0);
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
/* gc has a bunch of string objects; all are roots + should be preserved */
|
||||
gc->request_gc(generation::nursery);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
|
||||
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
|
||||
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
for (std::size_t i = 0, n = sv.size(); i < n; ++i) {
|
||||
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(sv.at(i).ptr())));
|
||||
REQUIRE(strcmp(sv.at(i)->c_str(), tc.v_[i].c_str()) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end String.test.cpp */
|
||||
6
xo-object/utest/object_utest_main.cpp
Normal file
6
xo-object/utest/object_utest_main.cpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/* file object_utest_main.cpp */
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
/* end object_utest_main.cpp */
|
||||
Loading…
Add table
Add a link
Reference in a new issue