From 7a2dc433bc0cae510c6aa395eff3a4aee78d6b20 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 1 Dec 2025 01:20:49 -0500 Subject: [PATCH] xo-alloc + xo-allocutil: refactor to shrink dep surface area --- CMakeLists.txt | 43 ++++++ cmake/xo-bootstrap-macros.cmake | 14 ++ cmake/xo_allocutilConfig.cmake.in | 6 + include/xo/allocutil/IAlloc.hpp | 154 +++++++++++++++++++ include/xo/allocutil/IObject.hpp | 97 ++++++++++++ include/xo/allocutil/gc_allocator_traits.hpp | 75 +++++++++ include/xo/allocutil/gc_ptr.hpp | 77 ++++++++++ 7 files changed, 466 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 cmake/xo-bootstrap-macros.cmake create mode 100644 cmake/xo_allocutilConfig.cmake.in create mode 100644 include/xo/allocutil/IAlloc.hpp create mode 100644 include/xo/allocutil/IObject.hpp create mode 100644 include/xo/allocutil/gc_allocator_traits.hpp create mode 100644 include/xo/allocutil/gc_ptr.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..83f0a59 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +# xo-allocutil/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_allocutil VERSION 0.1) +enable_language(CXX) + +# common XO macros (see github:Rconybea/xo-cmake) +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +set(PROJECT_CXX_FLAGS "") +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- +# output targets + +#add_subdirectory(utest) + +# ---------------------------------------------------------------- +# header-only library + +set(SELF_LIB xo_allocutil) +xo_add_headeronly_library(${SELF_LIB}) + +# ---------------------------------------------------------------- +# +xo_install_library4(${SELF_LIB} ${PROJECT_NAME}Targets) +# (note: ..Targets from xo_install_library2()) +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# input dependencies + +# NOTE: dependency set here must be kept consistent with allocutil/cmake/xo_allocutilConfig.cmake.in + +# xo-allocutil is also header-only +#xo_headeronly_dependency(${SELF_LIB} randomgen) diff --git a/cmake/xo-bootstrap-macros.cmake b/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 0000000..694d9b5 --- /dev/null +++ b/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,14 @@ +if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL "prefix")) + # default to typical install location for xo-project-macros + set(CMAKE_MODULE_PATH ${CMAKE_INSTALL_PREFIX}/share/cmake) +endif() + +if (NOT XO_SUBMODULE_BUILD) + message("-- CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}") + message("-- CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") +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) diff --git a/cmake/xo_allocutilConfig.cmake.in b/cmake/xo_allocutilConfig.cmake.in new file mode 100644 index 0000000..00b2de9 --- /dev/null +++ b/cmake/xo_allocutilConfig.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +#find_dependency(randomgen) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/xo/allocutil/IAlloc.hpp b/include/xo/allocutil/IAlloc.hpp new file mode 100644 index 0000000..f2e6de3 --- /dev/null +++ b/include/xo/allocutil/IAlloc.hpp @@ -0,0 +1,154 @@ +/** @file IAlloc.hpp + * + * @author: Roland Conybeare, Jul 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + template + using up = std::unique_ptr; + + class IObject; + + namespace gc { + /** @class IAllocator + * @brief Abstract API for allocation interface + * + * Garbage collector support methods: + * - checkpoint() + * - assign_member() + * - alloc_gc_copy() + * + * See class GC for copying incremental collector. + * See class ArenaAlloc for arena allocator + **/ + class IAlloc { + public: + virtual ~IAlloc() {} + + static inline std::uint32_t 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; + + return dz; + } + + static inline std::size_t with_padding(std::size_t z) { + return z + alloc_padding(z); + } + + /** optional name for this allocator; labelling for diagnostics **/ + virtual const std::string & name() const = 0; + /** allocator size in bytes (up to reserved limit) + * Includes unallocated mmeory + **/ + virtual std::size_t size() const = 0; + /** committed size in bytes **/ + virtual std::size_t committed() const = 0; + /** number of unallocated bytes available (up to soft limit) + * from this allocator + **/ + 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 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 **/ + virtual std::size_t after_checkpoint() const = 0; + /** @return true iff debug logging enabled **/ + virtual bool debug_flag() const = 0; + + /** remember allocator state. All currently-allocated addresses xo + * will satisfy is_before_checkpoint(x). Subsequent allocations x + * will fail is_before_checkpoint(x), until checkpoint superseded + * by @ref clear or another call to @ref checkpoint + **/ + virtual void checkpoint() = 0; + + /** allocate @p z bytes of memory. returns pointer to first address **/ + virtual std::byte * alloc(std::size_t z) = 0; + /** reset allocator to empty state. **/ + virtual void clear() = 0; + + // ----- GC-specific methods ----- + + /** true iff this allocator owns object at address @p src. + * Use to assist Object::_shallow_move + **/ + virtual bool check_owned(IObject * /*src*/) const { return false; } + /** true iff object at address @p src must move as part of + * in-progress collection phase + **/ + virtual bool check_move(IObject * /*src*/) const { return false; } + /** write barrier for collector. perform assignment + * @code + * *lhs = rhs + * @endcode + * plus additional book keeping if needed (e.g. in @ref GC) + * Default implementation just does the assignment. + **/ + virtual void assign_member(IObject * /*parent*/, + IObject ** lhs, + IObject * rhs) { *lhs = rhs; } + /** 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*/) { + // LCOV_EXCL_START + //assert(false); + return nullptr; + // LCOV_EXCL_STOP + } + }; + } /*namespace gc*/ + + class MMPtr { + public: + explicit MMPtr(gc::IAlloc * mm) : mm_{mm} {} + + gc::IAlloc * mm_ = nullptr; + }; +} /*namespace xo*/ + +inline void * operator new (std::size_t z, const xo::MMPtr & mmp) { + return mmp.mm_->alloc(z); +} + +//inline void operator delete (void * p, const MMPtr & mmp) { +// mmp.mm_->free(reinterpret_cast(p)); +//} + +inline void * operator new[] (std::size_t z, const xo::MMPtr & mmp) { + return mmp.mm_->alloc(z); +} + +//inline void operator delete[] (void * p, const MMPtr & mmp) { +// mmp.mm_->free(reinterpret_cast(p)); +//} + + +/* end IAlloc.hpp */ diff --git a/include/xo/allocutil/IObject.hpp b/include/xo/allocutil/IObject.hpp new file mode 100644 index 0000000..c4b1580 --- /dev/null +++ b/include/xo/allocutil/IObject.hpp @@ -0,0 +1,97 @@ +/** @file IObject.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace gc { class IAlloc; } + + /** @class IObject + * @brief Base interface for GC interaction + * + * Classes that can be collected inherit this api + * indirectly via xo::Object (see xo/alloc/Object.hpp) + **/ + class IObject { + public: + /** 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 IObject * _offset_destination(IObject * src) const { return src; }; + + /** replace this object with a forwarding pointer referring to @p dest. + **/ + virtual void _forward_to(IObject * dest) = 0; + + /** 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 IObject * _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; + + /** 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 IObject * _shallow_copy(gc::IAlloc * gc) 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(gc::IAlloc * gc) = 0; + }; +} + +/* end IObject.hpp */ diff --git a/include/xo/allocutil/gc_allocator_traits.hpp b/include/xo/allocutil/gc_allocator_traits.hpp new file mode 100644 index 0000000..b2fc416 --- /dev/null +++ b/include/xo/allocutil/gc_allocator_traits.hpp @@ -0,0 +1,75 @@ +/** @file gc_allocator_traits.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace gc { + /** Extended version of + * std::allocator_traits + * Introduces additional i/face methods + * for garbage-collector-enabled allocators + * + * allocator A can identify itself as a copying collector: + * 1. provide A::object_interface + * A::object_interface = xo::Object + * 2. provide A::is_incremental_collector + * A::is_incremental_collector = std::true_type + * Collectible objects must: + * 2a. inherit A::object_interface + * 2b. implement A::object_interface::_shallow_size() + * 2c. implement A::object_interface::_shallow_copy(alloc) + * 2d. implement A::object_interface::_forward_children(alloc) + **/ + template + struct gc_allocator_traits : std::allocator_traits { + using super = std::allocator_traits; + using pointer = typename super::pointer; + using value_type = typename super::value_type; + + // default: allocator A fallback to standard non-gc allocator behavior + template + struct is_incremental_collector : std::false_type {}; + + // opt-in: A provides nested type 'is_incremental_collector': + // struct A { + // using is_incremental_collector = std::true_type; + // }; + template + struct is_incremental_collector> : + A::is_incremental_collector {}; + + // default: empty object interface. + // classes that want to conditionally support GC + // (e.g. see xo::tree::RedBlackTree, xo::tree::Node + // in xo-ordinal-tree) + // can inherit + // gc_allocator_traits::template object_interface + // + template + struct object_interface {}; + + // specialization when A provides gc_object_interface + template + struct object_interface> + : A::gc_object_interface {}; + + /** true iff this allocator advertises itself as an incremental collector + * allocator will include: + * + * struct GC { + * using is_incremental_collector = std::true_type; + * }; + **/ + static inline constexpr bool is_incremental_collector_v = is_incremental_collector::value; + + }; + } /*namespace gc*/ +} /*namespace xo*/ + +/* end gc_allocator_traits.hpp */ diff --git a/include/xo/allocutil/gc_ptr.hpp b/include/xo/allocutil/gc_ptr.hpp new file mode 100644 index 0000000..bb4225a --- /dev/null +++ b/include/xo/allocutil/gc_ptr.hpp @@ -0,0 +1,77 @@ +/** @file gc_ptr.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include + +namespace xo { + template + class gc_ptr; + + template + using gp = gc_ptr; + + /** wrapper for a pointer to garbage-collector-eligible T. + * Application code will usually use the alias template gp + **/ + template + class gc_ptr { + public: + using element_type = T; + + public: + gc_ptr() = default; + gc_ptr(T * p) : ptr_{p} {} + gc_ptr(const gc_ptr & x) : ptr_{x.ptr_} {} + + /** create from gc_ptr to some related type @tparam S **/ + template + gc_ptr(const gc_ptr & x) : ptr_{x.ptr()} {} + + /** runtime downcast. shorthand for dynamic_cast **/ + template + static gc_ptr from(const gc_ptr & x) { return gc_ptr{dynamic_cast(x.ptr())}; } + + /** convenience for static asserts **/ + static constexpr bool is_gc_ptr = true; + /** see also: xo/refcnt/Refcounted.hpp **/ + static constexpr bool is_rc_ptr = false; + + static bool is_eq(gc_ptr x1, gc_ptr x2) { + std::uintptr_t u1 = reinterpret_cast(x1.ptr()); + std::uintptr_t u2 = reinterpret_cast(x2.ptr()); + + // multiple inheritance shenanigans. + // (allow interface pointers separated by one pointer) + + if (u1 >= u2) + return (u1 <= u2 + sizeof(std::uintptr_t)); + else + return (u2 <= u1 + sizeof(std::uintptr_t)); + } + + /** (for consistency's sake) **/ + T * get() const { return ptr_; } + + 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_; } + T & operator*() const { return *ptr_; } + + private: + T * ptr_ = nullptr; + }; +} /*namespace xo*/ + +/* end gc_ptr.hpp */