xo-alloc: UT for allocator interation + misc improvements

This commit is contained in:
Roland Conybeare 2025-12-02 17:07:19 -05:00
commit 764e98e12e
6 changed files with 295 additions and 154 deletions

View file

@ -1,67 +0,0 @@
/** @file ArenaAllocT.test.cpp
*
* @author Roland Conybeare, Nov 2025
**/
#include "xo/alloc/ArenaAllocT.hpp"
#include <catch2/catch.hpp>
#include <map>
namespace xo {
using xo::gc::ArenaAllocT;
using xo::gc::ArenaAlloc;
namespace ut {
namespace {
struct testcase_ArenaAllocT {
std::size_t arena_z_;
std::vector<std::pair<std::string, std::string>> kv_pairs_;
};
std::vector<testcase_ArenaAllocT>
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<std::pair<const std::string, std::string>>(arena.get());
using TestMapType = std::map<std::string,
std::string,
std::less<std::string>,
ArenaAllocT<std::pair<const std::string, std::string>>>;
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 */

View file

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

View file

@ -7,6 +7,7 @@
#include "ArenaAlloc.hpp"
#include "xo/reflect/Reflect.hpp"
#include <catch2/catch.hpp>
#include <regex>
#include <cstring>
namespace xo {
@ -58,7 +59,14 @@ namespace xo {
std::stringstream ss;
ss << fwd;
REQUIRE(ss.str() == "<fwd :dest-td DummyObject>");
// forwarding printer looks like
// "<fwd :dest 0x1ef49c20>"
//
std::regex pattern(R"(<fwd :dest 0x[0-9a-f]+>)");
REQUIRE(std::regex_match(ss.str(), pattern));
//REQUIRE(ss.str() == "<fwd :dest DummyObject>");
tag_config::tag_color_enabled = saved;
}

View file

@ -4,12 +4,16 @@
*/
#include "xo/alloc/GC.hpp"
#include "xo/allocutil/gc_allocator_traits.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::IAlloc;
using xo::gc::GC;
using xo::gc::gc_allocator_traits;
using xo::gc::generation;
using xo::gc::Config;
using xo::reflect::TaggedPtr;
namespace ut {
@ -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 <typename Nested, typename GcObjectInterface>
struct TestClass : public GcObjectInterface {
TestClass() = default;
explicit TestClass(const Nested & member1) : member1_{member1} {}
// using allocator_type = Allocator;
// using allocator_traits = xo::gc::gc_allocator_traits<Allocator>;
/** stage1 - just allocates some memory using allocator **/
template <typename Allocator>
static TestClass * make_0(Allocator & alloc) {
TestClass * mem = alloc.allocate(sizeof(TestClass));
/* but ctor will not have run, so ub to visit object */
return mem;
}
/** stage2 - use allocator_traits construct **/
template <typename Allocator>
static TestClass * make_1(Allocator & alloc) {
using traits = gc_allocator_traits<Allocator>;
TestClass * mem = traits::allocate(alloc, 1);
/* ctor will not have run here either */
return mem;
}
/** stage3 - invoke construct **/
template <typename Allocator>
static TestClass * make_2(Allocator & alloc) {
using traits = gc_allocator_traits<Allocator>;
TestClass * obj = traits::allocate(alloc, 1);
try {
// placement new
traits::construct(alloc, obj);
return obj;
} catch(...) {
traits::deallocate(alloc, obj, 1);
throw;
}
}
/** stage4 - init nested type **/
template <typename Allocator>
static TestClass * make_3(Allocator & alloc) {
using traits = gc_allocator_traits<Allocator>;
TestClass * obj = traits::allocate(alloc, 1);
try {
Nested nested;
// placemenet new
traits::construct(alloc, obj);
return obj;
} catch(...) {
traits::deallocate(alloc, obj, 1);
throw;
}
}
// ----- inherited from Object -----
virtual TaggedPtr self_tp() const final override {
assert(false); return TaggedPtr::universal_null();
}
virtual void display(std::ostream & os) const final override {
os << "<TestClass>";
}
virtual std::size_t _shallow_size() const final override {
assert(false); return sizeof(*this);
}
virtual IObject * _shallow_copy(IAlloc * gc) const final override {
assert(false); return nullptr;
}
virtual std::size_t _forward_children(IAlloc * gc) final override {
assert(false); return _shallow_size();
}
Nested member1_;
};
struct MemberType {
MemberType() : ctor_ran_{true} {}
explicit MemberType(const std::vector<gp<Object>> & mem2) : member2_{mem2} {}
std::vector<gp<Object>> 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 = GC::make(
{.initial_nursery_z_ = tc.nursery_z_,
.initial_tenured_z_ = tc.tenured_z_,
.incr_gc_threshold_ = tc.incr_gc_threshold_,
.full_gc_threshold_ = tc.full_gc_threshold_,
});
REQUIRE(gc.get());
REQUIRE(gc->name() == "GC");
using ex_allocator = xo::gc::allocator<int>;
using MyObjectInterface = gc_allocator_traits<ex_allocator>::template object_interface<ex_allocator>;
using NestedElementAllocator = xo::gc::allocator<gp<Object>>;
using NestedType = MemberType;
using MyType = TestClass<NestedType, MyObjectInterface>;
using MyAllocator = xo::gc::allocator<MyType>;
MyAllocator alloc(gc.get());
{
/* verify that MyType is constructible */
MyType obj0;
REQUIRE(obj0.member1_.ctor_ran_ == true);
}
{
MyType * mem0 = MyType::make_0(alloc);
REQUIRE(mem0 != nullptr);
REQUIRE(mem0->member1_.ctor_ran_ == false);
}
{
MyType * mem1 = MyType::make_1(alloc);
REQUIRE(mem1 != nullptr);
REQUIRE(mem1->member1_.ctor_ran_ == false);
}
{
MyType * mem2 = MyType::make_2(alloc);
REQUIRE(mem2 != nullptr);
REQUIRE(mem2->member1_.ctor_ran_ == true);
}
{
MyType * mem3 = MyType::make_3(alloc);
REQUIRE(mem3 != nullptr);
REQUIRE(mem3->member1_.ctor_ran_ == true);
}
gp<MyType> ptr;
{
REQUIRE(ptr.is_null());
//ptr = MyType::make_0();
}
} catch (std::exception & ex) {
std::cerr << "caught exception: " << ex.what() << std::endl;
REQUIRE(false);
}
}
}
} /*namespace ut*/
} /*namespace xo*/

View file

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