xo-ordinaltree: expand unittest + debug logging

This commit is contained in:
Roland Conybeare 2025-12-05 18:38:29 -05:00
commit 3d06980b42
10 changed files with 298 additions and 71 deletions

View file

@ -107,7 +107,7 @@ namespace xo {
bool is_dead() const { return false; } bool is_dead() const { return false; }
MutationLogEntry update_parent_moved(IObject * parent_to) const; MutationLogEntry update_parent_moved(IObject * parent_to) const;
void fixup_parent_child_moved(IObject * child_to) { *lhs_ = child_to; } void fixup_parent_child_moved(IObject * child_to);
private: private:
IObject * parent_ = nullptr; IObject * parent_ = nullptr;
@ -341,7 +341,7 @@ namespace xo {
* is recorded in mutation log, * is recorded in mutation log,
* given an object @p parent that contains object pointer @p lhs * given an object @p parent that contains object pointer @p lhs
**/ **/
virtual bool check_write_barrier(IObject * parent, IObject ** lhs, bool may_throw) const final; virtual bool check_write_barrier(const void * parent, const void * const * lhs, bool may_throw) const final;
virtual std::byte * alloc(std::size_t z) final override; virtual std::byte * alloc(std::size_t z) final override;
virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) final override; virtual std::byte * alloc_gc_copy(std::size_t z, const void * src) final override;

View file

@ -58,6 +58,12 @@ namespace xo {
reinterpret_cast<IObject **>(lhs_to)); reinterpret_cast<IObject **>(lhs_to));
} }
void
MutationLogEntry::fixup_parent_child_moved(IObject * child_to)
{
*(this->lhs_) = child_to;
}
GC::GC(const Config & config) GC::GC(const Config & config)
: config_{config} : config_{config}
{ {
@ -508,12 +514,17 @@ namespace xo {
assert(retval); assert(retval);
log && log(xtag("retval", retval));
return retval; return retval;
} }
void void
GC::assign_member(IObject * parent, IObject ** lhs, IObject * rhs) GC::assign_member(IObject * parent, IObject ** lhs, IObject * rhs)
{ {
scope log(XO_DEBUG(config_.debug_flag_),
xtag("parent", parent), xtag("lhs", lhs), xtag("rhs", rhs));
++gc_statistics_.n_mutation_; ++gc_statistics_.n_mutation_;
*lhs = rhs; *lhs = rhs;
@ -532,23 +543,28 @@ namespace xo {
{ {
case generation_result::tenured: case generation_result::tenured:
/* only need to log mutations that create tenured->nursery pointers */ /* only need to log mutations that create tenured->nursery pointers */
log && log(xtag("act", "any->T no log"));
return; return;
case generation_result::nursery: case generation_result::nursery:
switch (tospace_generation_of(parent)) { switch (tospace_generation_of(parent)) {
case generation_result::nursery: case generation_result::nursery:
if (is_before_checkpoint(parent)) { if (is_before_checkpoint(parent)) {
log && log(xtag("act", "N1->N0 must mlog"));
// N1->N0, so must log // N1->N0, so must log
this->mutation_log_[role2int(role::to_space)]->push_back(MutationLogEntry(parent, lhs)); this->mutation_log_[role2int(role::to_space)]->push_back(MutationLogEntry(parent, lhs));
++(this->gc_statistics_.n_logged_mutation_); ++(this->gc_statistics_.n_logged_mutation_);
++(this->gc_statistics_.n_xckp_mutation_); ++(this->gc_statistics_.n_xckp_mutation_);
} else { } else {
// parent in N0, not an xckp mutation // parent in N0, not an xckp mutation
return; log && log(xtag("act", "N0->any no long"));
} }
break; break;
case generation_result::tenured: case generation_result::tenured:
// T->N, so must log // T->N, so must log
log && log(xtag("act", "T->N must mlog"));
this->mutation_log_[role2int(role::to_space)]->push_back(MutationLogEntry(parent, lhs)); this->mutation_log_[role2int(role::to_space)]->push_back(MutationLogEntry(parent, lhs));
++(this->gc_statistics_.n_logged_mutation_); ++(this->gc_statistics_.n_logged_mutation_);
++(this->gc_statistics_.n_xgen_mutation_); ++(this->gc_statistics_.n_xgen_mutation_);
@ -556,11 +572,13 @@ namespace xo {
case generation_result::not_found: case generation_result::not_found:
// parent is global // parent is global
// This may be ok (provided lhs is a gc root) // This may be ok (provided lhs is a gc root)
log && log(xtag("act", "root->any no log"));
break; break;
} }
break; break;
case generation_result::not_found: case generation_result::not_found:
log && log(xtag("act", "any->root no log"));
// child is global; // child is global;
// logging not required // logging not required
@ -571,6 +589,8 @@ namespace xo {
void void
GC::forward_inplace(IObject ** lhs) GC::forward_inplace(IObject ** lhs)
{ {
scope log(XO_DEBUG(config_.debug_flag_), xtag("lhs", lhs));
Object::_forward_inplace(lhs, this); Object::_forward_inplace(lhs, this);
} }
@ -588,10 +608,13 @@ namespace xo {
} }
bool bool
GC::check_write_barrier(IObject * parent, GC::check_write_barrier(const void * parent,
IObject ** lhs, const void * const * lhs,
bool may_throw_flag) const bool may_throw_flag) const
{ {
scope log(XO_DEBUG(config_.debug_flag_),
xtag("P", parent), xtag("L", lhs));
if (!this->contains(parent)) { if (!this->contains(parent)) {
if (may_throw_flag) { if (may_throw_flag) {
throw std::runtime_error(tostr("GC::check_write_barrier", throw std::runtime_error(tostr("GC::check_write_barrier",
@ -601,10 +624,11 @@ namespace xo {
return false; return false;
} }
#ifdef NOPE // don't want to assume IObject*
std::size_t parent_z = parent->_shallow_size(); std::size_t parent_z = parent->_shallow_size();
std::byte * parent_addr = reinterpret_cast<std::byte *>(parent); const std::byte * parent_addr = reinterpret_cast<const std::byte *>(parent);
std::byte * lhs_addr = reinterpret_cast<std::byte *>(lhs); const std::byte * lhs_addr = reinterpret_cast<const std::byte *>(lhs);
if ((lhs_addr < parent_addr) || (parent_addr + parent_z < lhs_addr)) { if ((lhs_addr < parent_addr) || (parent_addr + parent_z < lhs_addr)) {
if (may_throw_flag) { if (may_throw_flag) {
@ -617,12 +641,21 @@ namespace xo {
} }
return false; return false;
} }
#endif
IObject * rhs = *lhs; const void * rhs = *lhs;
if (!rhs)
return true;
auto parent_gen = tospace_generation_of(parent); auto parent_gen = tospace_generation_of(parent);
auto rhs_gen = tospace_generation_of(rhs); auto rhs_gen = tospace_generation_of(rhs);
log && log(xtag("C", rhs),
xtag("gen(P)", parent_gen), xtag("gen(C)", rhs_gen),
xtag("P.before-ckp", is_before_checkpoint(parent)),
xtag("C.before-ckp", is_before_checkpoint(rhs)));
switch(parent_gen) { switch(parent_gen) {
case generation_result::nursery: case generation_result::nursery:
if (is_before_checkpoint(parent)) { if (is_before_checkpoint(parent)) {
@ -630,21 +663,25 @@ namespace xo {
case generation_result::nursery: case generation_result::nursery:
if (is_before_checkpoint(rhs)) { if (is_before_checkpoint(rhs)) {
/* no mlog entry needed */ /* no mlog entry needed */
log && log(xtag("msg", "N1->N1 - trivial"));
return true; return true;
} else { } else {
/* need to check mlog */ /* need to check mlog */
; log && log(xtag("msg", "N1->N0 - xgen"));
} }
break; break;
case generation_result::tenured: case generation_result::tenured:
/* no mlog entry needed */ /* no mlog entry needed */
log && log(xtag("msg", "N1->T - trivial"));
return true; return true;
case generation_result::not_found: case generation_result::not_found:
/* possible non-gc rhs */ /* possible non-gc rhs */
log && log(xtag("msg", "non-gc rhs - trivial"));
return true; return true;
} }
} else { } else {
/* no mlog entry needed */ /* no mlog entry needed */
log && log(xtag("msg", "N0->any - trivial"));
return true; return true;
} }
break; break;
@ -652,16 +689,21 @@ namespace xo {
switch(rhs_gen) { switch(rhs_gen) {
case generation_result::nursery: case generation_result::nursery:
/* need to check mlog */ /* need to check mlog */
log && log(xtag("msg", "T->N - xgen"));
break; break;
case generation_result::tenured: case generation_result::tenured:
/* no mlog entry needed */ /* no mlog entry needed */
log && log(xtag("msg", "T->T - trivial"));
return true; return true;
case generation_result::not_found: case generation_result::not_found:
/* possible non-gc rhs */ /* possible non-gc rhs */
log && log(xtag("msg", "non-gc rhs - trivial"));
return true; return true;
} }
break;
case generation_result::not_found: case generation_result::not_found:
/* already excluded -> impossible */ /* already excluded -> impossible */
log && log(xtag("msg", "assert"));
assert(false && "already verified parent owned by GC"); assert(false && "already verified parent owned by GC");
} }
@ -669,7 +711,7 @@ namespace xo {
* search mutation log + verify such entry exists * search mutation log + verify such entry exists
*/ */
for (MutationLogEntry & mlog : *(mutation_log_[role2int(role::to_space)])) { for (MutationLogEntry & mlog : *(mutation_log_[role2int(role::to_space)])) {
if ((mlog.parent() == parent) && (mlog.lhs() == lhs)) { if ((mlog.parent() == parent) && ((const void * const *)mlog.lhs() == lhs)) {
return true; return true;
} }
mlog.lhs(); mlog.lhs();
@ -872,7 +914,7 @@ namespace xo {
for (MutationLogEntry & from_entry : *from_mlog) for (MutationLogEntry & from_entry : *from_mlog)
{ {
if (log) { if (log) {
if (i_from % 10000 == 0) if (i_from % 10000 == 0 || true)
log(xtag("i_from", i_from)); log(xtag("i_from", i_from));
} }
@ -905,8 +947,12 @@ namespace xo {
++n_rescue; ++n_rescue;
log && log(xtag("parent", parent), xtag("act", "move child"), xtag("child.from", child_from));
Object::_deep_move(child_from, this, per_type_stats); Object::_deep_move(child_from, this, per_type_stats);
log && log(xtag("child.to", child_from->_destination()));
// C forwards to C', fall thru to parent fixup below // C forwards to C', fall thru to parent fixup below
// (a) T->N1' // (a) T->N1'
// (c) T->T // (c) T->T
@ -924,13 +970,24 @@ namespace xo {
IObject * child_to = child_from->_destination(); IObject * child_to = child_from->_destination();
log && log(xtag("act", "fixup parent"), xtag("parent", parent), xtag("lhs", from_entry.lhs()), xtag("child.from", child_from), xtag("child.to", child_to));
from_entry.fixup_parent_child_moved(child_to); from_entry.fixup_parent_child_moved(child_to);
{
// verify fixup was effective
IObject * child_from2 = from_entry.child();
assert(child_from2 == child_to);
}
// P->C', loc(C') in {N1', T'} // P->C', loc(C') in {N1', T'}
if (tospace_generation_of(child_to) == generation_result::nursery) { if (tospace_generation_of(child_to) == generation_result::nursery) {
// (b) loc(P)=T, loc(C')=N1'; also case (a) // (b) loc(P)=T, loc(C')=N1'; also case (a)
log && log(xtag("act", "still xgen -> keep mlog entry"));
// still have xgen pointer, so need mlog for it // still have xgen pointer, so need mlog for it
to_mlog->push_back(from_entry); to_mlog->push_back(from_entry);
} else { } else {
@ -972,6 +1029,8 @@ namespace xo {
} }
} else { } else {
log && log("defer");
// loc(P) = N1, loc(C) = N0, P may be garbage // loc(P) = N1, loc(C) = N0, P may be garbage
// Includes cases: // Includes cases:
// (e) P->C, C not moved // (e) P->C, C not moved
@ -1347,7 +1406,7 @@ namespace xo {
void void
GC::execute_gc(generation upto) GC::execute_gc(generation upto)
{ {
scope log(XO_DEBUG(config_.stats_flag_)); scope log(XO_DEBUG(config_.stats_flag_ || config_.debug_flag_));
auto t0 = std::chrono::steady_clock::now(); auto t0 = std::chrono::steady_clock::now();

View file

@ -42,18 +42,24 @@ namespace xo {
Object::_forward(IObject * src, Object::_forward(IObject * src,
gc::IAlloc * gc) gc::IAlloc * gc)
{ {
scope log(XO_DEBUG(gc->debug_flag()), xtag("src", src));
if (!src) if (!src)
return src; return src;
if (src->_is_forwarded()) if (src->_is_forwarded()) {
log && log("already forwarded", xtag("dest", src->_offset_destination(src)));
return src->_offset_destination(src); return src->_offset_destination(src);
}
if (gc->check_move(src)) { if (gc->check_move(src)) {
log && log("needs forwarding");
Object::_shallow_move(src, gc); Object::_shallow_move(src, gc);
/* *src is now a forwarding pointer to a copy in to-space */ /* *src is now a forwarding pointer to a copy in to-space */
return src->_offset_destination(src); return src->_offset_destination(src);
} else { } else {
log && log("already tenured + incr collection");
/* don't move tenured objects during incremental collection */ /* don't move tenured objects during incremental collection */
return src; return src;
} }
@ -62,6 +68,8 @@ namespace xo {
IObject * IObject *
Object::_deep_move(IObject * from_src, gc::GC * gc, gc::ObjectStatistics * /*stats*/) Object::_deep_move(IObject * from_src, gc::GC * gc, gc::ObjectStatistics * /*stats*/)
{ {
scope log(XO_DEBUG(gc->config().debug_flag_));
using gc::generation; using gc::generation;
if (!from_src) if (!from_src)
@ -146,13 +154,15 @@ namespace xo {
do { do {
fixup_work = 0; fixup_work = 0;
auto fixup_generation = [gc, &gray_lo_v](generation gen) { auto fixup_generation = [gc, &log, &gray_lo_v](generation gen) {
std::size_t work = 0; std::size_t work = 0;
while(gray_lo_v[gen2int(gen)] < gc->free_ptr(gen)) { while(gray_lo_v[gen2int(gen)] < gc->free_ptr(gen)) {
Object * x = reinterpret_cast<Object *>(gray_lo_v[gen2int(gen)]); Object * x = reinterpret_cast<Object *>(gray_lo_v[gen2int(gen)]);
// update per-class stats here // update per-class stats here
log && log("fwd children", xtag("x", x));
std::size_t xz = x->_forward_children(gc); std::size_t xz = x->_forward_children(gc);
// must pad xz to multiple of word size, // must pad xz to multiple of word size,

View file

@ -202,6 +202,29 @@ namespace xo {
vector_type member2_; vector_type member2_;
bool ctor_ran_ = false; bool ctor_ran_ = false;
}; };
#ifdef NOT_YET
struct MemberType2 {
public:
MemberType2() = default;
/** GC hooks rely on copy constructor. But can't write it without allocator state.
* Therefore: need copy-like constructor that takes allocator argument
**/
template <typename Allocator>
explicit MemberType2(Allocator & alloc, uint64 payload) {
using traits = gc_allocator_traits<Allocator>;
uint64_t * ptr = traits::allocate(alloc, 1);
this->payload_ = payload;
this->ctor_ran_ = true;
}
uint64_t * payload_ = nullptr;
bool ctor_ran_ = false;
}
#endif
} }
TEST_CASE("vector_custom_allocator", "[alloc][vector]") TEST_CASE("vector_custom_allocator", "[alloc][vector]")

View file

@ -143,8 +143,8 @@ namespace xo {
/** check write barrier (if impl has write barrier) /** check write barrier (if impl has write barrier)
* given an object @p parent that contains object pointer @p lhs. * given an object @p parent that contains object pointer @p lhs.
**/ **/
virtual bool check_write_barrier(IObject * /*parent*/, virtual bool check_write_barrier(const void * /*parent*/,
IObject ** /*lhs*/, const void * const * /*lhs*/,
bool /*may_throw*/) const { return true; }; bool /*may_throw*/) const { return true; };
/** write barrier for collector. perform assignment /** write barrier for collector. perform assignment
* @code * @code
@ -169,6 +169,16 @@ namespace xo {
} }
}; };
/** for gc_allocator_traits, want an allocator pointer we can inherit from **/
struct IAllocPtr {
public:
inline bool check_write_barrier(const void * parent, const void * const * lhs, bool may_throw) const {
return mm_->check_write_barrier(parent, lhs, may_throw);
}
IAlloc * mm_ = nullptr;
};
/** allocator wrapper (in the style of std::allocator) /** allocator wrapper (in the style of std::allocator)
**/ **/
template <typename T> template <typename T>
@ -183,10 +193,11 @@ namespace xo {
using difference_type = std::ptrdiff_t; using difference_type = std::ptrdiff_t;
using gc_object_interface = IAlloc::gc_object_interface; using gc_object_interface = IAlloc::gc_object_interface;
using gc_interface = IAllocPtr;
using has_incremental_gc_interface = IAlloc::has_incremental_gc_interface; using has_incremental_gc_interface = IAlloc::has_incremental_gc_interface;
/** rebind is for typed allocators. since IAlloc is untyped, /** rebind is for typed allocators. since IAlloc is untyped,
* we want degenerate version * rebind is almost trivial
**/ **/
template <typename U> template <typename U>
struct rebind { struct rebind {
@ -194,16 +205,16 @@ namespace xo {
}; };
public: public:
explicit allocator(IAlloc * mm) : mm_{mm} {} explicit allocator(IAlloc * mm) : impl_{mm} {}
allocator(const allocator &) = default; allocator(const allocator &) = default;
allocator & operator=(const allocator &) = default; allocator & operator=(const allocator &) = default;
template <typename U> template <typename U>
allocator(const allocator<U> & other) : mm_{other.mm_} {} allocator(const allocator<U> & other) : impl_{other.impl_.mm_} {}
pointer allocate(size_type n) { pointer allocate(size_type n) {
std::byte * raw = mm_->allocate(n * sizeof(T)); std::byte * raw = impl_.mm_->allocate(n * sizeof(T));
return reinterpret_cast<pointer>(raw); return reinterpret_cast<pointer>(raw);
} }
@ -211,7 +222,7 @@ namespace xo {
void deallocate(pointer p, size_type n) { void deallocate(pointer p, size_type n) {
std::byte * raw = reinterpret_cast<std::byte *>(p); std::byte * raw = reinterpret_cast<std::byte *>(p);
mm_->deallocate(raw, n * sizeof(T)); impl_.mm_->deallocate(raw, n * sizeof(T));
} }
// optional construct, destroy (but allocator_traits provides defaults) // optional construct, destroy (but allocator_traits provides defaults)
@ -221,11 +232,14 @@ namespace xo {
**/ **/
template <typename U> template <typename U>
bool operator==(const allocator<U> & other) const noexcept { bool operator==(const allocator<U> & other) const noexcept {
return mm_ == other.mm_; return impl_.mm_ == other.impl_.mm_;
} }
/** gc_interface=IAlloc **/
operator gc_interface () const { return impl_; }
public: public:
IAlloc * mm_ = nullptr; IAllocPtr impl_;
}; };
} /*namespace gc*/ } /*namespace gc*/

View file

@ -29,6 +29,8 @@ namespace xo {
/** GC write barrier: /** GC write barrier:
* assign value @p rhs to member @p *lhs of @p parent. * assign value @p rhs to member @p *lhs of @p parent.
* Identifiy and remember cross-generational pointers. * Identifiy and remember cross-generational pointers.
*
* Expect Allocator is xo::gc::allocator<T> (see IAlloc.hpp)
**/ **/
template <typename T, typename Allocator> template <typename T, typename Allocator>
void _gc_assign_member(T ** lhs, void _gc_assign_member(T ** lhs,
@ -37,7 +39,7 @@ namespace xo {
{ {
static_assert(std::is_convertible_v<decltype(*lhs), IObject*>); static_assert(std::is_convertible_v<decltype(*lhs), IObject*>);
alloc.mm_->assign_member(this, reinterpret_cast<IObject **>(lhs), rhs); alloc.impl_.mm_->assign_member(this, reinterpret_cast<IObject **>(lhs), rhs);
} }
/** true iff this object represents a forwarding pointer. /** true iff this object represents a forwarding pointer.

View file

@ -40,6 +40,19 @@ namespace xo {
virtual std::size_t _forward_children(gc::IAlloc *) { assert(false); return 0; } virtual std::size_t _forward_children(gc::IAlloc *) { assert(false); return 0; }
}; };
/** dummy GC interface.
* non-empty intersection with IAlloc
**/
template <typename GcObjectInterface>
struct FallbackGcInterface {
template <typename Allocator>
FallbackGcInterface(Allocator & alloc) {}
bool check_write_barrier(const void * parent,
const void * const * lhs,
bool may_throw) { return true; };
};
/** Extended version of /** Extended version of
* std::allocator_traits<Allocator> * std::allocator_traits<Allocator>
* Introduces additional i/face methods * Introduces additional i/face methods
@ -136,6 +149,8 @@ namespace xo {
using super::allocate; using super::allocate;
using super::deallocate; using super::deallocate;
// ----------------------------------------------------------------
// default: allocator A fallback to standard non-gc allocator behavior // default: allocator A fallback to standard non-gc allocator behavior
template <typename A, typename = void> template <typename A, typename = void>
struct has_incremental_gc_interface : std::false_type {}; struct has_incremental_gc_interface : std::false_type {};
@ -148,6 +163,19 @@ namespace xo {
struct has_incremental_gc_interface<A, std::void_t<typename A::has_incremental_gc_interface>> : struct has_incremental_gc_interface<A, std::void_t<typename A::has_incremental_gc_interface>> :
A::has_incremental_gc_interface {}; A::has_incremental_gc_interface {};
/** true iff this allocator advertises itself as an incremental collector.
* Allocator will include:
*
* struct IAlloc {
* using has_incremental_gc_interface = std::true_type;
* };
**/
static inline constexpr
bool
has_incremental_gc_interface_v = has_incremental_gc_interface<Allocator>::value;
// ----------------------------------------------------------------
// default: allocate A fallback to standard non-GC allocator behavior // default: allocate A fallback to standard non-GC allocator behavior
template <typename A, typename = void> template <typename A, typename = void>
struct has_trivial_deallocate : std::false_type {}; struct has_trivial_deallocate : std::false_type {};
@ -160,6 +188,19 @@ namespace xo {
struct has_trivial_deallocate<A, std::void_t<typename A::has_trivial_deallocate>> : struct has_trivial_deallocate<A, std::void_t<typename A::has_trivial_deallocate>> :
A::has_trivial_deallocate {}; A::has_trivial_deallocate {};
/** true iff this allocator advertises trivial deallocate
* Allocate will include:
*
* struct IAlloc {
* using has_trivial_deallocate = std::true_type;
* };
**/
static inline constexpr
bool
has_trivial_deallocate_v = has_trivial_deallocate<Allocator>::value;
// ----------------------------------------------------------------
// default: empty object interface. // default: empty object interface.
// //
// classes that want to conditionally support GC // classes that want to conditionally support GC
@ -187,27 +228,32 @@ namespace xo {
// //
using object_interface_type = object_interface<Allocator>; using object_interface_type = object_interface<Allocator>;
/** true iff this allocator advertises itself as an incremental collector. // ----------------------------------------------------------------
* Allocator will include:
* // default: minimal garbage collector interface.
* struct IAlloc { //
* using has_incremental_gc_interface = std::true_type; // Use in allocator-aware components that need conditionally
* }; // to engage with GC functionality.
**/ // For example in RedBlackTree::verify_ok() want to check
static inline constexpr // cross-generational pointers.
bool //
has_incremental_gc_interface_v = has_incremental_gc_interface<Allocator>::value; // gc_interface gc
// - gc_interface(A & alloc)
// - gc.check_write_barrier(const object_interface_type * p,
// const object_interface_type * const * lhs,
// bool may_throw)
//
template <typename A, typename = void>
struct gc_interface : public FallbackGcInterface<object_interface_type> {};
// allocator opt-in by providing a gc_interface type
template <typename A>
struct gc_interface<A, std::void_t<typename A::gc_interface>> : public A::gc_interface {};
// interface for (narrow) GC interaction.
// Construct from allocator
using gc_interface_type = gc_interface<Allocator>;
/** true iff this allocator advertises trivial deallocate
* Allocate will include:
*
* struct IAlloc {
* using has_trivial_deallocate = std::true_type;
* };
**/
static inline constexpr
bool
has_trivial_deallocate_v = has_trivial_deallocate<Allocator>::value;
}; };
} /*namespace gc*/ } /*namespace gc*/
} /*namespace xo*/ } /*namespace xo*/

View file

@ -580,14 +580,19 @@ namespace xo {
* R is reduced-value for right child * R is reduced-value for right child
* RB8. RedBlackTree.size() equals the #of nodes in tree * RB8. RedBlackTree.size() equals the #of nodes in tree
*/ */
bool verify_ok(bool /*throw_flag_not_implemented*/ = true) const { bool verify_ok(bool /*throw_flag_not_implemented*/ = true) const
{
using xo::scope; using xo::scope;
using xo::tostr; using xo::tostr;
using xo::xtag; using xo::xtag;
constexpr const char *c_self = "RedBlackTree::verify_ok"; constexpr const char *c_self = "RedBlackTree::verify_ok";
scope log(XO_DEBUG(debug_flag_)); scope log(XO_DEBUG(debug_flag_), xtag("size", size_));
if (debug_flag_) {
// look forward to upgrading this to pp
this->display_to_log();
}
/* RB0. */ /* RB0. */
if (root_ == nullptr) { if (root_ == nullptr) {
@ -610,7 +615,8 @@ namespace xo {
int32_t black_height = 0; int32_t black_height = 0;
/* n_node: #of nodes in this->root_ */ /* n_node: #of nodes in this->root_ */
size_t n_node = RbUtil::verify_subtree_ok(this->reduce_fn_, size_t n_node = RbUtil::verify_subtree_ok(this->node_alloc_,
this->reduce_fn_,
this->root_, this->root_,
&black_height); &black_height);

View file

@ -572,7 +572,7 @@ namespace xo {
for (uint32_t iter = 0;; ++iter) { for (uint32_t iter = 0;; ++iter) {
if (c_excessive_verify_enabled) if (c_excessive_verify_enabled)
RbTreeUtil::verify_subtree_ok(reduce_fn, G, nullptr /*&black_height*/); RbTreeUtil::verify_subtree_ok(alloc, reduce_fn, G, nullptr /*&black_height*/);
if (log.enabled()) { if (log.enabled()) {
if (G) { if (G) {
@ -720,7 +720,7 @@ namespace xo {
RbTreeUtil::rotate(alloc, d, P, reduce_fn, debug_flag, pp_root); RbTreeUtil::rotate(alloc, d, P, reduce_fn, debug_flag, pp_root);
if (c_excessive_verify_enabled) if (c_excessive_verify_enabled)
RbTreeUtil::verify_subtree_ok(reduce_fn, S, nullptr /*&black_height*/); RbTreeUtil::verify_subtree_ok(alloc, reduce_fn, S, nullptr /*&black_height*/);
/* (relabel S->P etc. for merged control flow below) */ /* (relabel S->P etc. for merged control flow below) */
R = P; R = P;
@ -755,7 +755,7 @@ namespace xo {
log("verify subtree at GG", xtag("GG", GG), log("verify subtree at GG", xtag("GG", GG),
xtag("GG.key", GG->key())); xtag("GG.key", GG->key()));
RbTreeUtil::verify_subtree_ok(reduce_fn, GG, nullptr /*&black_height*/); RbTreeUtil::verify_subtree_ok(alloc, reduce_fn, GG, nullptr /*&black_height*/);
RbTreeUtil::display_aux(D_Invalid, GG, 0 /*depth*/, &log); RbTreeUtil::display_aux(D_Invalid, GG, 0 /*depth*/, &log);
log("fixup complete"); log("fixup complete");
@ -841,11 +841,28 @@ namespace xo {
RbNode * new_node = RbNode::make_leaf(alloc, RbNode * new_node = RbNode::make_leaf(alloc,
kv_pair, kv_pair,
reduce_fn.leaf(kv_pair.second)); reduce_fn.leaf(kv_pair.second));
log && log(xtag("act", "N gets new leaf"),
xtag("N", N),
xtag("d", d),
xtag("new_node", new_node));
N->assign_child_reparent(alloc, N->assign_child_reparent(alloc,
d, d,
new_node); new_node);
{
using allocator_traits = xo::gc::gc_allocator_traits<NodeAllocator>;
typename allocator_traits::gc_interface_type gc(alloc);
const void * src = N;
const void * const * lhs
= reinterpret_cast<const void * const *>(&(N->child_v_[0]));
XO_EXPECT(gc.check_write_barrier(src, lhs, false),
tostr("RbTreeUtil::insert_aux",
": expect mlog entry for xgen child pointer"));
}
assert(is_red(N->child(d))); assert(is_red(N->child(d)));
/* recalculate Node sizes on path [root .. N] */ /* recalculate Node sizes on path [root .. N] */
@ -1679,13 +1696,16 @@ namespace xo {
* *
* returns the #of nodes in subtree rooted at N. * returns the #of nodes in subtree rooted at N.
*/ */
static size_t verify_subtree_ok(Reduce const & reduce_fn, template <typename NodeAllocator>
static size_t verify_subtree_ok(NodeAllocator & alloc,
Reduce const & reduce_fn,
RbNode const * N, RbNode const * N,
int32_t * p_black_height) int32_t * p_black_height)
{ {
using xo::scope; using xo::scope;
using xo::xtag; using xo::xtag;
using xo::print::ccs; using xo::print::ccs;
using allocator_traits = xo::gc::gc_allocator_traits<NodeAllocator>;
constexpr char const *c_self = "RbTreeUtil::verify_subtree_ok"; constexpr char const *c_self = "RbTreeUtil::verify_subtree_ok";
@ -1699,12 +1719,15 @@ namespace xo {
/* establish on first leaf node encountered */ /* establish on first leaf node encountered */
uint32_t black_height = 0; uint32_t black_height = 0;
typename allocator_traits::gc_interface_type gc(alloc);
auto verify_fn = [c_self, auto verify_fn = [c_self,
&gc,
&reduce_fn, &reduce_fn,
&i_node, &i_node,
&last_key, &last_key,
&i_black_height, &i_black_height,
&black_height] (RbNode const *x, &black_height] (RbNode const * x,
uint32_t bd) uint32_t bd)
{ {
XO_EXPECT(x->_is_forwarded() == false, XO_EXPECT(x->_is_forwarded() == false,
@ -1712,6 +1735,17 @@ namespace xo {
xtag("i", i_node), xtag("node[i]", x) xtag("i", i_node), xtag("node[i]", x)
)); ));
if (x->parent()) {
const void * src = x;
const void * const * lhs = reinterpret_cast<const void * const *>(&(x->parent_));
XO_EXPECT(gc.check_write_barrier(src, lhs, false),
tostr(c_self, (": expect mlog entry for xgen parent pointer"),
xtag("i", i_node), xtag("node[i]", x),
xtag("key[i]", x->key()),
xtag("parent", x->parent())));
}
/* RB2. if c=x->child(d), then c->parent()=x */ /* RB2. if c=x->child(d), then c->parent()=x */
if (x->left_child()) { if (x->left_child()) {
@ -1721,6 +1755,17 @@ namespace xo {
xtag("key[i]", x->key()), xtag("key[i]", x->key()),
xtag("child", x->left_child()) xtag("child", x->left_child())
)); ));
{
const void * parent = x;
const void * const * lhs = reinterpret_cast<const void * const *>(&(x->child_v_[0]));
XO_EXPECT(gc.check_write_barrier(parent, lhs, false),
tostr(c_self, (": expect mlog entry for xgen left child pointer"),
xtag("i", i_node), xtag("node[i]", x),
xtag("key[i]", x->key()),
xtag("child", x->left_child())));
}
XO_EXPECT(x == x->left_child()->parent(), XO_EXPECT(x == x->left_child()->parent(),
tostr(c_self, (": expect symmetric child/parent pointers"), tostr(c_self, (": expect symmetric child/parent pointers"),
@ -1731,6 +1776,7 @@ namespace xo {
xtag("child.parent", x->left_child()->parent_), xtag("child.parent", x->left_child()->parent_),
xtag("child.parent._is_forwarded", x->left_child()->parent_->_is_forwarded()) xtag("child.parent._is_forwarded", x->left_child()->parent_->_is_forwarded())
)); ));
} }
if (x->right_child()) { if (x->right_child()) {
@ -1741,6 +1787,17 @@ namespace xo {
xtag("child", x->right_child()) xtag("child", x->right_child())
)); ));
{
const void * parent = x;
const void * const * lhs = reinterpret_cast<const void * const *>(&(x->child_v_[1]));
XO_EXPECT(gc.check_write_barrier(parent, lhs, false),
tostr(c_self, (": expect mlog entry for xgen right child pointer"),
xtag("i", i_node), xtag("node[i]", x),
xtag("key[i]", x->key()),
xtag("child", x->right_child())));
}
XO_EXPECT(x == x->right_child()->parent(), XO_EXPECT(x == x->right_child()->parent(),
tostr(c_self, ": expect symmetric child/parent pointers", tostr(c_self, ": expect symmetric child/parent pointers",
xtag("i", i_node), xtag("i", i_node),
@ -1897,6 +1954,8 @@ namespace xo {
scope log(XO_DEBUG(true /*debug_flag*/)); scope log(XO_DEBUG(true /*debug_flag*/));
display_aux(D_Invalid, N, d, &log); display_aux(D_Invalid, N, d, &log);
assert(false);
} /*display*/ } /*display*/
}; /*RbTreeUtil*/ }; /*RbTreeUtil*/
} /*namespace detail*/ } /*namespace detail*/

View file

@ -1,4 +1,6 @@
/** @file RedBlackTree-gc.test.cpp /** @file RedBlackTree-gc.test.cpp
*
* @author Roland Conybeare, Dec 2025
**/ **/
#include "random_tree_ops.hpp" #include "random_tree_ops.hpp"
@ -38,7 +40,7 @@ namespace xo {
std::vector<Testcase_RbTree> std::vector<Testcase_RbTree>
s_testcase_v = { s_testcase_v = {
//Testcase_RbTree(1024, 4096, 512, 512, false), Testcase_RbTree(1024, 4096, 512, 512, false),
Testcase_RbTree(1024, 4096, 512, 512, true), Testcase_RbTree(1024, 4096, 512, 512, true),
}; };
} }
@ -129,8 +131,7 @@ namespace xo {
std::uint64_t seed = 8813374093428528487ULL; std::uint64_t seed = 8813374093428528487ULL;
auto rgen = xo::rng::xoshiro256ss(seed); auto rgen = xo::rng::xoshiro256ss(seed);
//for (std::uint32_t n=0; n<1024; ++n) for (std::uint32_t n=0; n<=1024;) {
for (std::uint32_t n=0; n<=128;) {
bool ok_flag = false; bool ok_flag = false;
for (std::uint32_t attention = 0; !ok_flag && (attention < 2); ++attention) { for (std::uint32_t attention = 0; !ok_flag && (attention < 2); ++attention) {
@ -191,26 +192,29 @@ namespace xo {
REQUIRE(gc->enable_gc_once()); REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false); REQUIRE(gc->gc_in_progress() == false);
} }
REQUIRE(rbtree->verify_ok(debug_flag));
} }
if (n > 0) { if (n > 0) {
{ INFO("insert phase B - random_inserts(1, n+1, ..)");
INFO("insert phase B - random_inserts(1, n+1, ..)");
/* insert odd integers in [1, n+1), in random order **/ /* insert odd integers in [1, n+1), in random order **/
ok_flag &= TreeUtil<RbTree>::random_inserts(1, n+1, 2, ok_flag &= TreeUtil<RbTree>::random_inserts(1, n+1, 2,
debug_flag, debug_flag,
&rgen, &rgen,
rbtree.get()); rbtree.get());
if (tc.do_extra_gc_) { if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false); REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery); gc->request_gc(gc::generation::nursery);
REQUIRE(gc->is_gc_pending()); REQUIRE(gc->is_gc_pending());
REQUIRE(gc->enable_gc_once()); REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false); REQUIRE(gc->gc_in_progress() == false);
}
} }
REQUIRE(rbtree->verify_ok(debug_flag));
} }
/* check iterator traverses [0..n-1] in both directions */ /* check iterator traverses [0..n-1] in both directions */
@ -250,7 +254,9 @@ namespace xo {
debug_flag, debug_flag,
rbtree.get(), rbtree.get(),
&rgen); &rgen);
REQUIRE(rbtree->verify_ok(debug_flag));
if (tc.do_extra_gc_) { if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false); REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery); gc->request_gc(gc::generation::nursery);
@ -259,6 +265,8 @@ namespace xo {
REQUIRE(gc->gc_in_progress() == false); REQUIRE(gc->gc_in_progress() == false);
} }
REQUIRE(rbtree->verify_ok(debug_flag));
/* verify that updates changed tree contents in expected way */ /* verify that updates changed tree contents in expected way */
ok_flag &= TreeUtil<RbTree>::check_ordinal_lookup(10000 /*dvalue*/, ok_flag &= TreeUtil<RbTree>::check_ordinal_lookup(10000 /*dvalue*/,
debug_flag, debug_flag,