From 764e98e12e10c644b7347dd82d4d4e7f35eb1a0b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 2 Dec 2025 17:07:19 -0500 Subject: [PATCH] xo-alloc: UT for allocator interation + misc improvements --- include/xo/alloc/ArenaAllocT.hpp | 74 ------------- utest/ArenaAllocT.test.cpp | 67 ------------ utest/CMakeLists.txt | 1 - utest/Forwarding1.test.cpp | 10 +- utest/GC.test.cpp | 179 ++++++++++++++++++++++++++++++- utest/IAlloc.test.cpp | 118 ++++++++++++++++++-- 6 files changed, 295 insertions(+), 154 deletions(-) delete mode 100644 include/xo/alloc/ArenaAllocT.hpp delete mode 100644 utest/ArenaAllocT.test.cpp diff --git a/include/xo/alloc/ArenaAllocT.hpp b/include/xo/alloc/ArenaAllocT.hpp deleted file mode 100644 index 6c8a9b41..00000000 --- a/include/xo/alloc/ArenaAllocT.hpp +++ /dev/null @@ -1,74 +0,0 @@ -/** @file Allocator.hpp - * - * @author Roland Conybeare, Nov 2025 - **/ - -#pragma once - -#include "xo/alloc/ArenaAlloc.hpp" - -namespace xo { - namespace gc { - /** @class allocator - * @brief c++ allocator with allocator traits - * - * Can use ArenaAllocT with std::map etc. - **/ - template - class ArenaAllocT { - public: - using value_type = T; - /** copy assignment: leave lhs allocator in place **/ - using propagate_on_container_copy_assignment = std::false_type; - /** move assignment: adopt rhs allocator - * (Forced: cannot mix allocations from different allocators - * within a container) - **/ - using propagate_on_container_move_assignment = std::true_type; - /** swap: also swap allocators - * (Forced: cannot mix allocations from different allocators - * within a containers) - **/ - using propagate_on_container_swap = std::true_type; - /** An ArenaAlloc instance is unique owner of its own memory: - * no other instance can dealloc - **/ - using is_always_equal = std::false_type; - - public: - explicit ArenaAllocT(ArenaAlloc * mm) : mm_{mm} {} - ArenaAllocT(const ArenaAllocT & other) = default; - - /** rebind ctor. Allows container to use supplied allocator - * for multiple types - **/ - template - ArenaAllocT(const ArenaAllocT & other) noexcept : mm_{other.mm_} {} - - T * allocate(size_t n) { - void * mem = mm_->alloc(n * sizeof(T)); - - return reinterpret_cast(mem); - } - - void deallocate(T * p, size_t n) noexcept { - assert(mm_->contains(p)); - assert(n == 0 || mm_->contains(p + n - 1)); - - //arena_->deallocate(p, n * sizeof(T)); - } - - bool operator==(const ArenaAllocT & other) const { - return mm_ == other.mm_; - } - - bool operator!=(const ArenaAllocT & other) const { - return mm_ != other.mm_; - } - - ArenaAlloc * mm_ = nullptr; - }; - } /*namespace gc*/ -} /*namespace xo*/ - -/* end Allocator.hpp */ diff --git a/utest/ArenaAllocT.test.cpp b/utest/ArenaAllocT.test.cpp deleted file mode 100644 index a9257173..00000000 --- a/utest/ArenaAllocT.test.cpp +++ /dev/null @@ -1,67 +0,0 @@ -/** @file ArenaAllocT.test.cpp - * - * @author Roland Conybeare, Nov 2025 - **/ - -#include "xo/alloc/ArenaAllocT.hpp" -#include -#include - -namespace xo { - using xo::gc::ArenaAllocT; - using xo::gc::ArenaAlloc; - - namespace ut { - - namespace { - struct testcase_ArenaAllocT { - std::size_t arena_z_; - std::vector> kv_pairs_; - }; - - std::vector - s_testcase_v = { - { 4096, {} }, - { 4096, {{"a", "apple"}} }, - { 4096, {{"a", "apple"}, {"b", "banana"}, {"c", "carrot"}} }, - { 4096, {{"a", "apple"}, {"b", "banana"}, {"c", "carrot"}, {"e", "eggplant"}} }, - }; - } - - TEST_CASE("ArenaAllocT", "[alloc][traits]") - { - for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { - const testcase_ArenaAllocT & tc = s_testcase_v[i_tc]; - - constexpr bool c_debug_flag = false; - - auto arena = ArenaAlloc::make("arena", tc.arena_z_, c_debug_flag); - auto alloc = ArenaAllocT>(arena.get()); - - using TestMapType = std::map, - ArenaAllocT>>; - - TestMapType test_map(alloc); - - size_t n = 0; - for (const auto & kv_ix : tc.kv_pairs_) { - test_map[kv_ix.first] = kv_ix.second; - ++n; - - REQUIRE(test_map.size() == n); - - for (const auto & map_ix : test_map) { - // verify alloc was used for both Key + Value. - REQUIRE(arena->contains(&map_ix.first)); - REQUIRE(arena->contains(&map_ix.second)); - } - } - - } - } - } -} /*namespace xo*/ - -/* end ArenaAllocT.test.cpp */ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 6c644f51..366cf664 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -7,7 +7,6 @@ set(UTEST_SRCS alloc_utest_main.cpp IAlloc.test.cpp ArenaAlloc.test.cpp - ArenaAllocT.test.cpp ListAlloc.test.cpp GC.test.cpp GcStatistics.test.cpp diff --git a/utest/Forwarding1.test.cpp b/utest/Forwarding1.test.cpp index f0a81c14..64c9f3f8 100644 --- a/utest/Forwarding1.test.cpp +++ b/utest/Forwarding1.test.cpp @@ -7,6 +7,7 @@ #include "ArenaAlloc.hpp" #include "xo/reflect/Reflect.hpp" #include +#include #include namespace xo { @@ -58,7 +59,14 @@ namespace xo { std::stringstream ss; ss << fwd; - REQUIRE(ss.str() == ""); + // forwarding printer looks like + // "" + // + + std::regex pattern(R"()"); + REQUIRE(std::regex_match(ss.str(), pattern)); + + //REQUIRE(ss.str() == ""); tag_config::tag_color_enabled = saved; } diff --git a/utest/GC.test.cpp b/utest/GC.test.cpp index 9e6c17e3..c5245243 100644 --- a/utest/GC.test.cpp +++ b/utest/GC.test.cpp @@ -4,12 +4,16 @@ */ #include "xo/alloc/GC.hpp" +#include "xo/allocutil/gc_allocator_traits.hpp" #include namespace xo { + using xo::gc::IAlloc; using xo::gc::GC; + using xo::gc::gc_allocator_traits; using xo::gc::generation; using xo::gc::Config; + using xo::reflect::TaggedPtr; namespace ut { @@ -80,13 +84,186 @@ namespace xo { REQUIRE(gc->gc_in_progress() == false); REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1); - } catch (std::exception &ex) { + } catch (std::exception & ex) { std::cerr << "caught exception: " << ex.what() << std::endl; REQUIRE(false); } } } + /** gc-enabled allocator **/ + namespace { + /** Setup test with custom allocator + * + **/ + template + struct TestClass : public GcObjectInterface { + TestClass() = default; + explicit TestClass(const Nested & member1) : member1_{member1} {} + + // using allocator_type = Allocator; + // using allocator_traits = xo::gc::gc_allocator_traits; + + /** stage1 - just allocates some memory using allocator **/ + template + static TestClass * make_0(Allocator & alloc) { + TestClass * mem = alloc.allocate(sizeof(TestClass)); + + /* but ctor will not have run, so ub to visit object */ + + return mem; + } + + /** stage2 - use allocator_traits construct **/ + template + static TestClass * make_1(Allocator & alloc) { + using traits = gc_allocator_traits; + + TestClass * mem = traits::allocate(alloc, 1); + + /* ctor will not have run here either */ + return mem; + } + + /** stage3 - invoke construct **/ + template + static TestClass * make_2(Allocator & alloc) { + using traits = gc_allocator_traits; + + TestClass * obj = traits::allocate(alloc, 1); + try { + // placement new + traits::construct(alloc, obj); + + return obj; + } catch(...) { + traits::deallocate(alloc, obj, 1); + throw; + } + } + + /** stage4 - init nested type **/ + template + static TestClass * make_3(Allocator & alloc) { + using traits = gc_allocator_traits; + + TestClass * obj = traits::allocate(alloc, 1); + try { + Nested nested; + + // placemenet new + traits::construct(alloc, obj); + + return obj; + } catch(...) { + traits::deallocate(alloc, obj, 1); + throw; + } + } + + // ----- inherited from Object ----- + + virtual TaggedPtr self_tp() const final override { + assert(false); return TaggedPtr::universal_null(); + } + virtual void display(std::ostream & os) const final override { + os << ""; + } + virtual std::size_t _shallow_size() const final override { + assert(false); return sizeof(*this); + } + virtual IObject * _shallow_copy(IAlloc * gc) const final override { + assert(false); return nullptr; + } + virtual std::size_t _forward_children(IAlloc * gc) final override { + assert(false); return _shallow_size(); + } + + Nested member1_; + }; + + struct MemberType { + MemberType() : ctor_ran_{true} {} + explicit MemberType(const std::vector> & mem2) : member2_{mem2} {} + + std::vector> member2_; + std::size_t ctor_ran_ = false; + }; + } + + TEST_CASE("gc_allocator_traits", "[alloc][gc]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + try { + const testcase_gc & tc = s_testcase_v[i_tc]; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .incr_gc_threshold_ = tc.incr_gc_threshold_, + .full_gc_threshold_ = tc.full_gc_threshold_, + }); + + REQUIRE(gc.get()); + REQUIRE(gc->name() == "GC"); + + using ex_allocator = xo::gc::allocator; + using MyObjectInterface = gc_allocator_traits::template object_interface; + using NestedElementAllocator = xo::gc::allocator>; + using NestedType = MemberType; + using MyType = TestClass; + using MyAllocator = xo::gc::allocator; + + MyAllocator alloc(gc.get()); + + { + /* verify that MyType is constructible */ + MyType obj0; + + REQUIRE(obj0.member1_.ctor_ran_ == true); + } + + { + MyType * mem0 = MyType::make_0(alloc); + + REQUIRE(mem0 != nullptr); + REQUIRE(mem0->member1_.ctor_ran_ == false); + } + + { + MyType * mem1 = MyType::make_1(alloc); + + REQUIRE(mem1 != nullptr); + REQUIRE(mem1->member1_.ctor_ran_ == false); + } + + { + MyType * mem2 = MyType::make_2(alloc); + + REQUIRE(mem2 != nullptr); + REQUIRE(mem2->member1_.ctor_ran_ == true); + } + + { + MyType * mem3 = MyType::make_3(alloc); + + REQUIRE(mem3 != nullptr); + REQUIRE(mem3->member1_.ctor_ran_ == true); + } + + gp ptr; + { + REQUIRE(ptr.is_null()); + //ptr = MyType::make_0(); + } + } catch (std::exception & ex) { + std::cerr << "caught exception: " << ex.what() << std::endl; + REQUIRE(false); + } + } + + } + } /*namespace ut*/ } /*namespace xo*/ diff --git a/utest/IAlloc.test.cpp b/utest/IAlloc.test.cpp index 823791ab..7c1fdc56 100644 --- a/utest/IAlloc.test.cpp +++ b/utest/IAlloc.test.cpp @@ -3,26 +3,124 @@ * author: Roland Conybeare, Aug 2025 */ -#include "xo/allocutil/IAlloc.hpp" +//#include "xo/allocutil/IAlloc.hpp" +#include "xo/alloc/ArenaAlloc.hpp" +#include "xo/indentlog/print/tag.hpp" #include namespace xo { using xo::gc::IAlloc; + using xo::gc::ArenaAlloc; namespace ut { TEST_CASE("ialloc", "[alloc]") { + static_assert((sizeof(std::uintptr_t) == 8) && "possibly fine if this fails, but would want to know"); + REQUIRE(IAlloc::alloc_padding(0) == 0); - REQUIRE(IAlloc::alloc_padding(1) == 7); - REQUIRE(IAlloc::alloc_padding(2) == 6); - REQUIRE(IAlloc::alloc_padding(3) == 5); - REQUIRE(IAlloc::alloc_padding(4) == 4); - REQUIRE(IAlloc::alloc_padding(5) == 3); - REQUIRE(IAlloc::alloc_padding(6) == 2); - REQUIRE(IAlloc::alloc_padding(7) == 1); - REQUIRE(IAlloc::alloc_padding(8) == 0); - REQUIRE(IAlloc::alloc_padding(9) == 7); + + for (std::size_t i = 1; i < sizeof(std::uintptr_t); ++i) { + REQUIRE(IAlloc::alloc_padding(i) + i == IAlloc::c_alloc_alignment); + } + + REQUIRE(IAlloc::alloc_padding(IAlloc::c_alloc_alignment) == 0); + + for (std::size_t i = 1; i < sizeof(std::uintptr_t); ++i) { + REQUIRE(IAlloc::alloc_padding(IAlloc::c_alloc_alignment + i) + i == IAlloc::c_alloc_alignment); + } } + + /* although xo::gc::allocator is intended for + * IAlloc derivatives (so T is ArenaAlloc | GC), + * + * it only relies on allocate() and deallocate() methods + */ + + namespace { + struct TestCase { + explicit TestCase(size_t arena_z, size_t n, size_t n2) : arena_z_{arena_z}, n_{n}, n2_{n2} {} + + size_t arena_z_ = 0; + size_t n_ = 0; + size_t n2_ = 0; + }; + + std::vector s_testcase_v = { TestCase{1024*1024, 9, 13} }; + } + + TEST_CASE("gc.allocator", "[alloc]") + { + using xo::gc::allocator; + + constexpr bool c_debug_flag = false; + + for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + INFO(xtag("i_tc", i_tc)); + + const TestCase & tc = s_testcase_v[i_tc]; + + up mm1 = ArenaAlloc::make("arena1", + tc.arena_z_, + c_debug_flag); + up mm2 = ArenaAlloc::make("arena2", + tc.arena_z_, + c_debug_flag); + + REQUIRE(mm1.get()); + REQUIRE(mm1->allocated() == 0); + + allocator alloc1(mm1.get()); + allocator alloc1a(mm1.get()); + + REQUIRE(mm2.get()); + REQUIRE(mm2->allocated() == 0); + + allocator alloc2(mm2.get()); + + SECTION("IAlloc identity determines allocator equality") { + REQUIRE(alloc1 == alloc1a); + REQUIRE(alloc1 != alloc2); + } + + int * p1 = nullptr; + size_t z1 = 0; + + SECTION("alloc space for ints") { + p1 = alloc1.allocate(tc.n_); + + REQUIRE(p1 != nullptr); + + // note: allowing for alignment + REQUIRE(mm1->allocated() >= sizeof(int32_t) * tc.n_); + REQUIRE(mm1->allocated() < sizeof(int32_t) * tc.n_ + IAlloc::c_alloc_alignment); + z1 = mm1->allocated(); + + // deallocate exists.. + alloc1.deallocate(p1, tc.n_); + + // ..but is a no-op + REQUIRE(mm1->allocated() == z1); + } + + int * p2 = nullptr; + + SECTION("allocator independence") { + REQUIRE(mm2->allocated() == 0); + + p2 = alloc2.allocate(tc.n2_); + + REQUIRE(p2 != nullptr); + REQUIRE(p1 != p2); + + REQUIRE(mm2->allocated() >= sizeof(int32_t) * tc.n2_); + REQUIRE(mm2->allocated() < sizeof(int32_t) * tc.n2_ + IAlloc::c_alloc_alignment); + + // mm1 unaffected by mm2 allocation + REQUIRE(mm1->allocated() == z1); + } + } + } + } /*namespace ut*/ } /*namespace xo*/