xo-ordinaltree: add _gc_assign_member feature to gc-aware allocs

This commit is contained in:
Roland Conybeare 2025-12-04 17:33:40 -05:00
commit c56dd72292
9 changed files with 488 additions and 145 deletions

View file

@ -21,6 +21,24 @@ namespace xo {
public: public:
/** impl inheriting this class must provide gc hooks **/ /** impl inheriting this class must provide gc hooks **/
static constexpr bool _requires_gc_hooks = true; static constexpr bool _requires_gc_hooks = true;
/** impl inheriting this class must use write barriers
* (so that GC allocator can remember cross-generational pointers)
**/
static constexpr bool _requires_write_barrier = true;
/** GC write barrier:
* assign value @p rhs to member @p *lhs of @p parent.
* Identifiy and remember cross-generational pointers.
**/
template <typename T, typename Allocator>
void _gc_assign_member(T ** lhs,
T * rhs,
Allocator & alloc)
{
static_assert(std::is_convertible_v<decltype(*lhs), IObject*>);
alloc.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.
* Forwarding pointers are exclusively created by the garbage collector; * Forwarding pointers are exclusively created by the garbage collector;

View file

@ -7,6 +7,7 @@
#include <type_traits> #include <type_traits>
#include <memory> #include <memory>
#include <cassert>
namespace xo { namespace xo {
namespace gc { namespace gc {
@ -15,7 +16,18 @@ namespace xo {
* Introduces additional i/face methods * Introduces additional i/face methods
* for garbage-collector-enabled allocators * for garbage-collector-enabled allocators
* *
* allocator A can identify itself as a copying collector: * Use Cases:
* 1. drop-in replacement for std::allocator_traits<Allocator>
* with non-gc-aware allocators.
* 2. allows a gc-aware template class to activate
* gc support when used with a collecting allocator
* (i.e. xo::gc::allocator<xo::gc::GC>)
* 3. allows a gc-aware template class T to fallback
* to ordinary allocator-aware behavior for non-gc
* allocators, such as std::allocator<T>,
* but also pool allocators etc.
*
* An allocator A can identify itself as a copying collector:
* *
* 1. provide A::object_interface * 1. provide A::object_interface
* per-object header interface: tells garbage collector * per-object header interface: tells garbage collector
@ -33,7 +45,24 @@ namespace xo {
* - xo::gc::GC has a collection API and also provides * - xo::gc::GC has a collection API and also provides
* garbage collection * garbage collection
* *
* GC-allocated objects must: * GC object model
* 2a. A GC-allocated object is an object that GC manages
* atomically. All memory associated with a GC-allocated
* object has the same lifetime.
* 2b. A GC-allocation is 1:1 with a GC-allocated object
* 2c. A GC-allocated object may have internal pointers.
* These are pointer interior to the same original
* allocation. It's the responsibility of the object to update these
* (if/when GC moves said object) via GC hooks.
* 2d. A GC-allocated object may have external pointers
* to other GC-allocated objects. Managing these is split
* between GC and object itself. GC takes responsibility
* for moving the destination objects.
* Object is responsible for telling GC about such pointers
* and changes to their values
* (e.g. IObject::_forward_children())
*
* GC object implementation: gc objects must:
* 2a. inherit A::object_interface * 2a. inherit A::object_interface
* 2b. implement A::object_interface::_shallow_size() * 2b. implement A::object_interface::_shallow_size()
* 2c. implement A::object_interface::_shallow_copy(alloc) * 2c. implement A::object_interface::_shallow_copy(alloc)
@ -41,6 +70,22 @@ namespace xo {
* in multiple inheritance scenarios * in multiple inheritance scenarios
* 2e. implement A::object_interface::_offset_destination(src) * 2e. implement A::object_interface::_offset_destination(src)
* *
* 3. write barrier support:
* A generational GC needs to track changes that create or modify
* inter-generational pointers.
*
* GC-aware classes could write:
* MyClass::update_pointer_state(IObject *new_value, gc::IAlloc *gc) {
* if constexpr (GcObjectInterface::_requires_write_barrier) {
* gc->assign_member(this, &some_member_, new_value);
* } else {
* this->some_member_ = new_value;
* }
* }
*
* but simpler:
* GcObjectInterface::_gc_assign_member(this, &some_member_, new_value, alloc_);
*
* Design Notes: * Design Notes:
* - virtual-method choice requires vtable pointer per object; * - virtual-method choice requires vtable pointer per object;
* but zero *marginal* space cost for types that would have * but zero *marginal* space cost for types that would have
@ -68,7 +113,7 @@ namespace xo {
// opt-in: A provides nested type 'has_incremental_collector_interface': // opt-in: A provides nested type 'has_incremental_collector_interface':
// struct A { // struct A {
// using is_incremental_collector = std::true_type; // using has_incremental_collector = std::true_type;
// }; // };
template <typename A> template <typename A>
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>> :
@ -110,6 +155,8 @@ namespace xo {
*lhs = rhs; *lhs = rhs;
} }
virtual bool _is_forwarded() const { return false; }
virtual std::size_t _shallow_size() const { assert(false); return 0; }
}; };
// specialization when A provides gc_object_interface // specialization when A provides gc_object_interface

View file

@ -66,6 +66,7 @@ namespace xo {
using key_type = Key; using key_type = Key;
using mapped_type = Value; using mapped_type = Value;
using value_type = std::pair<Key const, Value>; using value_type = std::pair<Key const, Value>;
// using key_compare = Compare // not yet // using key_compare = Compare // not yet
using allocator_type = Allocator; using allocator_type = Allocator;
using allocator_traits = xo::gc::gc_allocator_traits<Allocator>; using allocator_traits = xo::gc::gc_allocator_traits<Allocator>;
@ -81,6 +82,10 @@ namespace xo {
using node_allocator_type = allocator_traits::template rebind_alloc<node_type>; using node_allocator_type = allocator_traits::template rebind_alloc<node_type>;
using node_allocator_traits = xo::gc::gc_allocator_traits<node_allocator_type>; using node_allocator_traits = xo::gc::gc_allocator_traits<node_allocator_type>;
/* for make() factory method */
using tree_allocator_type = allocator_traits::template rebind_alloc<RedBlackTree>;
using tree_allocator_traits = xo::gc::gc_allocator_traits<tree_allocator_type>;
using Direction = detail::Direction; using Direction = detail::Direction;
using size_type = std::size_t; using size_type = std::size_t;
using difference_type = std::ptrdiff_t; using difference_type = std::ptrdiff_t;
@ -118,16 +123,19 @@ namespace xo {
{} {}
#endif #endif
static RedBlackTree * make(const allocator_type & alloc = allocator_type{}, static RedBlackTree * make(allocator_type kvpair_alloc = allocator_type{},
bool debug_flag = false) bool debug_flag = false)
{ {
RedBlackTree * tree = allocator_traits::allocate(alloc, 1); tree_allocator_type tree_alloc(kvpair_alloc);
RedBlackTree * tree = tree_allocator_traits::allocate(tree_alloc, 1);
try { try {
// placement new // placement new
allocator_traits::construct(alloc, tree, alloc, debug_flag); tree_allocator_traits::construct(tree_alloc, tree, kvpair_alloc, debug_flag);
return tree; return tree;
} catch(...) { } catch(...) {
allocator_traits::deallocate(alloc, tree, 1); tree_allocator_traits::deallocate(tree_alloc, tree, 1);
} }
return nullptr; return nullptr;
@ -314,6 +322,8 @@ namespace xo {
*/ */
RbTreeConstLhs operator[](Key const & k) const RbTreeConstLhs operator[](Key const & k) const
{ {
//scope log(XO_DEBUG(true), xtag("variant", "readonly"), xtag("key", k));
RbNode const * node = RbUtil::find(this->root_, k); RbNode const * node = RbUtil::find(this->root_, k);
return RbTreeConstLhs(this, node); return RbTreeConstLhs(this, node);
@ -347,11 +357,14 @@ namespace xo {
* // v.node contents may have been copied and v.node deleted * // v.node contents may have been copied and v.node deleted
*/ */
RbTreeLhs operator[](Key const & k) { RbTreeLhs operator[](Key const & k) {
//scope log(XO_DEBUG(true), tag("variant", "autoinsert"), tag("key", k));
std::pair<bool, RbNode *> insert_result std::pair<bool, RbNode *> insert_result
= RbUtil::template insert_aux<node_allocator_type>(this->node_alloc_, = RbUtil::template insert_aux<node_allocator_type>(this->node_alloc_,
value_type(k, Value() /*used iff creating new node*/), value_type(k, Value() /*used iff creating new node*/),
false /*allow_replace_flag*/, false /*allow_replace_flag*/,
this->reduce_fn_, this->reduce_fn_,
this->debug_flag_,
&(this->root_)); &(this->root_));
return RbTreeLhs(this, insert_result.second, k); return RbTreeLhs(this, insert_result.second, k);
@ -609,7 +622,7 @@ namespace xo {
virtual void display(std::ostream & os) const { virtual void display(std::ostream & os) const {
os << "<RedBlackTree>"; os << "<RedBlackTree>";
} }
virtual std::size_t _shallow_size() const { return sizeof(*this); } virtual std::size_t _shallow_size() const final override { return sizeof(*this); }
virtual IObject * _shallow_copy(gc::IAlloc * gc) const { virtual IObject * _shallow_copy(gc::IAlloc * gc) const {
if constexpr (GcObjectInterface::_requires_gc_hooks) { if constexpr (GcObjectInterface::_requires_gc_hooks) {
xo::Cpof cpof(gc, this); xo::Cpof cpof(gc, this);

View file

@ -321,7 +321,7 @@ namespace xo {
os << "<Node>"; os << "<Node>";
} }
virtual std::size_t _shallow_size() const { return sizeof(*this); } virtual std::size_t _shallow_size() const final override { return sizeof(*this); }
/* note: only relevant when GcObjectInterface is xo::IObject */ /* note: only relevant when GcObjectInterface is xo::IObject */
virtual IObject * _shallow_copy(gc::IAlloc * gc) const { virtual IObject * _shallow_copy(gc::IAlloc * gc) const {
if constexpr (GcObjectInterface::_requires_gc_hooks) { if constexpr (GcObjectInterface::_requires_gc_hooks) {

View file

@ -37,7 +37,7 @@ namespace xo {
if (!this->node_) { if (!this->node_) {
throw std::runtime_error throw std::runtime_error
(tostr("rbtree: attempt to use empty lhs object as rvalue")); (tostr("RedBlackTreeLhsBase: attempt to use empty lhs object as rvalue"));
} }
return this->node_->contents().second; return this->node_->contents().second;

View file

@ -786,12 +786,12 @@ namespace xo {
value_type const & kv_pair, value_type const & kv_pair,
bool allow_replace_flag, bool allow_replace_flag,
Reduce const & reduce_fn, Reduce const & reduce_fn,
bool debug_flag,
RbNode ** pp_root) RbNode ** pp_root)
{ {
using xo::xtag; using xo::xtag;
constexpr bool c_debug_flag = false; scope log(XO_DEBUG(debug_flag));
//XO_SCOPE2(log, true /*debug_flag*/);
RbNode * N = *pp_root; RbNode * N = *pp_root;
@ -809,7 +809,7 @@ namespace xo {
/* after modifying a node n, must recalculate reductions /* after modifying a node n, must recalculate reductions
* along path [root .. n] * along path [root .. n]
*/ */
RbTreeUtil::fixup_ancestor_size(reduce_fn, N, c_debug_flag); RbTreeUtil::fixup_ancestor_size(reduce_fn, N, debug_flag);
//log && log(xtag("path", (char const *)"A")); //log && log(xtag("path", (char const *)"A"));
@ -843,7 +843,7 @@ namespace xo {
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] */
RbTreeUtil::fixup_ancestor_size(reduce_fn, N, c_debug_flag); RbTreeUtil::fixup_ancestor_size(reduce_fn, N, debug_flag);
/* after adding a node, must rebalance to restore RB-shape */ /* after adding a node, must rebalance to restore RB-shape */
RbTreeUtil::fixup_red_shape(d, N, reduce_fn, pp_root); RbTreeUtil::fixup_red_shape(d, N, reduce_fn, pp_root);
@ -1700,19 +1700,40 @@ namespace xo {
&black_height] (RbNode const *x, &black_height] (RbNode const *x,
uint32_t bd) uint32_t bd)
{ {
XO_EXPECT(x->_is_forwarded() == false,
tostr(c_self, (": stray forwarding pointer where node expected"),
xtag("i", i_node), xtag("node[i]", x)
));
/* 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()) {
XO_EXPECT(x->left_child()->_is_forwarded() == false,
tostr(c_self, (": forwarding pointer where left child expected"),
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"),
xtag("i", i_node), xtag("node[i]", x), xtag("i", i_node), xtag("node[i]", x),
xtag("key[i]", x->key()), xtag("key[i]", x->key()),
xtag("child", x->left_child()), xtag("child", x->left_child()),
xtag("child.key", x->left_child()->key()), xtag("child.key", x->left_child()->key()),
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())
));
} }
if (x->right_child()) { if (x->right_child()) {
XO_EXPECT(x->right_child()->_is_forwarded() == false,
tostr(c_self, (": forwarding pointer where right child expected"),
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),
@ -1720,7 +1741,9 @@ namespace xo {
xtag("key[i]", x->key()), xtag("key[i]", x->key()),
xtag("child", x->right_child()), xtag("child", x->right_child()),
xtag("child.key", x->right_child()->key()), xtag("child.key", x->right_child()->key()),
xtag("child.parent", x->right_child()->parent_))); xtag("child.parent", x->right_child()->parent_),
xtag("child.parent._is_forwarded", x->right_child()->parent_->_is_forwarded())
));
} }
/* RB3. all nodes have the same black-height */ /* RB3. all nodes have the same black-height */

View file

@ -21,24 +21,31 @@ namespace xo {
Testcase_RbTree(std::size_t nz, Testcase_RbTree(std::size_t nz,
std::size_t tz, std::size_t tz,
std::size_t ngct, std::size_t ngct,
std::size_t tgct) : nursery_z_{nz}, std::size_t tgct,
bool do_extra_gc) : nursery_z_{nz},
tenured_z_{tz}, tenured_z_{tz},
incr_gc_threshold_{ngct}, incr_gc_threshold_{ngct},
full_gc_threshold_{tgct} {} full_gc_threshold_{tgct},
do_extra_gc_{do_extra_gc} {}
std::size_t nursery_z_; std::size_t nursery_z_;
std::size_t tenured_z_; std::size_t tenured_z_;
std::size_t incr_gc_threshold_; std::size_t incr_gc_threshold_;
std::size_t full_gc_threshold_; std::size_t full_gc_threshold_;
bool do_extra_gc_;
}; };
std::vector<Testcase_RbTree> std::vector<Testcase_RbTree>
s_testcase_v = { s_testcase_v = {
Testcase_RbTree(1024, 4096, 512, 512), //Testcase_RbTree(1024, 4096, 512, 512, false),
Testcase_RbTree(1024, 4096, 512, 512, true),
}; };
} }
/* test that we can allocate RbTree::RbNode instances,
* using gc allocator
*/
TEST_CASE("rbnode-gc-1", "[gc][redblacktree]") TEST_CASE("rbnode-gc-1", "[gc][redblacktree]")
{ {
using RbTree = RedBlackTree<int, using RbTree = RedBlackTree<int,
@ -103,6 +110,10 @@ namespace xo {
} }
} }
/* test RbTree with gc allocator.
*
* do lots of GC-requesting, to create old->new cross-generational pointers
*/
TEST_CASE("rbtree-gc-1", "[gc][redblacktree]") TEST_CASE("rbtree-gc-1", "[gc][redblacktree]")
{ {
using RbTree = RedBlackTree<int, using RbTree = RedBlackTree<int,
@ -118,13 +129,16 @@ 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<1; ++n) { //for (std::uint32_t n=0; n<1024; ++n)
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) {
bool debug_flag = c_debug_flag || (attention == 1); bool debug_flag = c_debug_flag || (attention == 1);
scope log(XO_DEBUG2(debug_flag, "rbtree-gc-1")); scope log(XO_DEBUG2(debug_flag, "rbtree-gc-1"), xtag("i_tc", i_tc), xtag("n", n));
INFO(tostr(xtag("i_tc", i_tc), xtag("n", n)));
ok_flag = true; // unless contradicted below ok_flag = true; // unless contradicted below
@ -137,16 +151,152 @@ namespace xo {
.debug_flag_ = debug_flag .debug_flag_ = debug_flag
} }
); );
REQUIRE(gc.get());
gc->disable_gc();
xo::gc::allocator<RbTree::node_type> allocator(gc.get()); REQUIRE(gc->native_gc_statistics().n_gc() == 0);
RbTree rbtree(allocator, c_debug_flag); xo::gc::allocator<RbTree> allocator(gc.get());
#ifdef NOT_YET gp<RbTree> rbtree = RbTree::make(allocator, c_debug_flag);
/* insert [0..n-1] in random order **/
ok_flag &= TreeUtil<RbTree>::random_inserts(n, debug_flag, &rgen, &rbtree); gc->add_gc_root_dwim(&rbtree);
#endif
REQUIRE(rbtree.get() != nullptr);
REQUIRE(rbtree->verify_ok(debug_flag));
if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery);
REQUIRE(gc->is_gc_pending());
REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false);
}
REQUIRE(rbtree->verify_ok(debug_flag));
{
INFO("insert phase A - random_inserts(0, n, 2, ..)");
/* insert even integers in [0, n), in random order **/
ok_flag &= TreeUtil<RbTree>::random_inserts(0, n, 2,
debug_flag,
&rgen,
rbtree.get());
if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery);
REQUIRE(gc->is_gc_pending());
REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false);
}
}
if (n > 0) {
{
INFO("insert phase B - random_inserts(1, n+1, ..)");
/* insert odd integers in [1, n+1), in random order **/
ok_flag &= TreeUtil<RbTree>::random_inserts(1, n+1, 2,
debug_flag,
&rgen,
rbtree.get());
if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery);
REQUIRE(gc->is_gc_pending());
REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false);
}
}
}
/* check iterator traverses [0..n-1] in both directions */
ok_flag &= TreeUtil<RbTree>::check_ordinal_lookup(0 /*dvalue*/,
debug_flag,
*rbtree);
/* verify end-to-end iteration */
ok_flag &= TreeUtil<RbTree>::check_bidirectional_iterator(0,
debug_flag,
*rbtree);
/* check reduced sums for each cut */
ok_flag &= TreeUtil<RbTree>::check_reduced_sum(0,
debug_flag,
*rbtree);
/* verify read-only variant of operator[] */
ok_flag &= TreeUtil<RbTree>::random_lookups(debug_flag,
*rbtree,
&rgen);
/* re-verify lookup on (better-be-monotonic) reduced sums
* remove doubt that operator[] changed something.
*/
ok_flag &= TreeUtil<RbTree>::check_ordinal_lookup(0 /*dvalue*/,
debug_flag,
*rbtree);
/* re-verify end-to-end iteration, so we can say we did */
ok_flag &= TreeUtil<RbTree>::check_bidirectional_iterator(0,
debug_flag,
*rbtree);
/* make random updates, along with basic consistency checks */
ok_flag &= TreeUtil<RbTree>::random_updates(10000,
debug_flag,
rbtree.get(),
&rgen);
if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery);
REQUIRE(gc->is_gc_pending());
REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false);
}
/* verify that updates changed tree contents in expected way */
ok_flag &= TreeUtil<RbTree>::check_ordinal_lookup(10000 /*dvalue*/,
debug_flag,
*rbtree);
/* verify end-to-end iteration */
ok_flag &= TreeUtil<RbTree>::check_bidirectional_iterator(10000,
debug_flag,
*rbtree);
/* check reduced sums for each cut */
ok_flag &= TreeUtil<RbTree>::check_reduced_sum(10000,
debug_flag,
*rbtree);
/* verify behavior of read/write variant of operator[] */
ok_flag &= TreeUtil<RbTree>::random_removes(debug_flag,
&rgen,
rbtree.get());
if (tc.do_extra_gc_) {
REQUIRE(gc->gc_in_progress() == false);
gc->request_gc(gc::generation::nursery);
REQUIRE(gc->is_gc_pending());
REQUIRE(gc->enable_gc_once());
REQUIRE(gc->gc_in_progress() == false);
}
/* verify iteration one more time --
* remove doubt w.r.t.
*/
REQUIRE(rbtree->verify_ok(debug_flag));
} }
if (n == 0)
n = 1;
else
n = 2*n;
} }
} }

View file

@ -80,11 +80,16 @@ namespace utest {
return ok_flag; return ok_flag;
} /*test_clear*/ } /*test_clear*/
/* do n random inserts (taken from *p_rgen) into *p_rbtreẹ /* do
* inserted keys will be distinct values in [0, .., n-1] * n = (hi - lo) / k
* random inserts (taken from *p_rgen) into *p_rbtreẹ
* inserted keys will comprise the distinct values
* {lo, lo+k, lo+2k, ..., lo+n.k}
*/ */
static bool static bool
random_inserts(std::uint32_t n, random_inserts(std::uint32_t lo,
std::uint32_t hi,
std::uint32_t k,
bool catch_flag, bool catch_flag,
xo::rng::xoshiro256ss * p_rgen, xo::rng::xoshiro256ss * p_rgen,
Tree * p_tree) Tree * p_tree)
@ -93,18 +98,25 @@ namespace utest {
bool ok_flag = true; bool ok_flag = true;
xo::scope log(XO_DEBUG(catch_flag)); xo::scope log(XO_DEBUG(catch_flag), xtag("lo", lo), xtag("hi", hi), xtag("k", k));
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag)); REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag));
if ((hi <= lo) || (k == 0))
return true;
uint32_t n = (hi - lo) / k;
/* n keys 0..n-1 */ /* n keys 0..n-1 */
std::vector<std::uint32_t> u(n); std::vector<std::uint32_t> u(n);
for(std::uint32_t i=0; i<n; ++i) for(std::uint32_t i=0; i<n; ++i)
u[i] = i; u[i] = lo + i*k;
/* shuffle to get unpredictable insert order */ /* shuffle to get unpredictable insert order */
std::shuffle(u.begin(), u.end(), *p_rgen); std::shuffle(u.begin(), u.end(), *p_rgen);
size_t tree_z0 = p_tree->size();
/* insert keys according to permutation u */ /* insert keys according to permutation u */
uint32_t i = 1; uint32_t i = 1;
for(uint32_t x : u) { for(uint32_t x : u) {
@ -126,11 +138,20 @@ namespace utest {
++i; ++i;
} }
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->size() == n); REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->size() == tree_z0 + n);
return ok_flag; return ok_flag;
} /*random_inserts*/ } /*random_inserts*/
static bool
random_inserts(std::uint32_t n,
bool catch_flag,
xo::rng::xoshiro256ss * p_rgen,
Tree * p_tree)
{
return random_inserts(0, n, 1, catch_flag, p_rgen, p_tree);
}
/* do n random removes (taken from *p_rgen) from *p_rbtree; /* do n random removes (taken from *p_rgen) from *p_rbtree;
* assumes *p_rbtree has keys [0 .. n-1] where n=p_rbtreẹsize * assumes *p_rbtree has keys [0 .. n-1] where n=p_rbtreẹsize
*/ */
@ -444,6 +465,102 @@ namespace utest {
return ok_flag; return ok_flag;
} /*check_bidirectional_iterator*/ } /*check_bidirectional_iterator*/
/** Require:
* - tree has keys [0..n-1], where n=treesize()
* - tree valu at key k is dvalue+10*k
*
* @p catch_flag. control behavior at each test assertion.
* true -> log to console + interact with catch2
* false -> verify iteration behavior for return code.
*
**/
static bool
check_reduced_sum(uint32_t dvalue,
bool catch_flag,
Tree const & rbtree)
{
using xo::scope;
using xo::xtag;
scope log(XO_DEBUG(catch_flag));
/* -> false if/when check fails */
bool ok_flag = true;
size_t const n = rbtree.size();
for(size_t i = 0; i < n; ++i) {
/* compute reduction up to key=i */
double reduced_upto
= rbtree.reduce_lub(i /*key*/,
true /*is_closed*/);
double reduced = (i+1) * (5*i + dvalue);
INFO(tostr(xtag("i", i), xtag("n", n),
xtag("tree.reduced_upto", reduced_upto),
xtag("reduced", reduced),
xtag("dvalue", dvalue)));
auto glb_ix = rbtree.cfind_sum_glb(reduced);
REQUIRE_ORFAIL(ok_flag, catch_flag, reduced_upto == reduced);
REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix.is_dereferenceable());
/* glb_ix is truth-y */
REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix);
REQUIRE_ORFAIL(ok_flag, catch_flag, glb_ix->first == i);
}
return ok_flag;
} /*check_reduced_sum*/
/* Require:
* - *p_rbtree has keys [0..n-1], where n=rbtree.size()
* - for each key k, associated value is 10*k
*
* Promise:
* - for each key k, associated value is dvalue + 10*k
*/
static bool
random_updates(uint32_t dvalue,
bool catch_flag,
Tree * p_rbtree,
xo::rng::xoshiro256ss * p_rgen)
{
using xo::scope;
using xo::xtag;
scope log(XO_DEBUG(catch_flag));
/* -> false if/when check fails */
bool ok_flag = true;
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok());
std::size_t n = p_rbtree->size();
std::vector<uint32_t> u
= Util::random_permutation(n, p_rgen);
/* update key/value pairs in permutation order */
uint32_t i = 1;
for (uint32_t x : u) {
REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == x*10);
(*p_rbtree)[x] = dvalue + 10*x;
REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == dvalue + 10*x);
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok());
/* assignment to existing key does not change tree size */
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->size() == n);
++i;
}
REQUIRE(p_rbtree->size() == n);
return ok_flag;
} /*random_updates*/
}; /*TreeUtil*/ }; /*TreeUtil*/
} /*namespace utest*/ } /*namespace utest*/

View file

@ -44,120 +44,93 @@ namespace {
} /*check_ordinal_lookup*/ } /*check_ordinal_lookup*/
#endif #endif
/* check that RedBlackTree<>::find_sum_glb() works as advertised. /* check that RedBlackTree<>::find_sum_glb() works as advertised.
* *
* partial sums of v[j] for j<=i will be: * partial sums of v[j] for j<=i will be:
* *
* (i+1) . i * (i+1) . i
* 10 . --------- + ((i+1) . dvalue) * 10 . --------- + ((i+1) . dvalue)
* 2 * 2
* *
* = (i+1).(5.i + dvalue) * = (i+1).(5.i + dvalue)
* *
* Require: * Require:
* - rbtree has keys [0..n-1], where n=rbtree.size() * - rbtree has keys [0..n-1], where n=rbtree.size()
* - rbtree value at key k is dvalue+10*k * - rbtree value at key k is dvalue+10*k
*/ */
void void
check_reduced_sum(uint32_t dvalue, check_reduced_sum(uint32_t dvalue,
RbTree const & rbtree) RbTree const & rbtree)
{ {
size_t const n = rbtree.size(); size_t const n = rbtree.size();
for(size_t i = 0; i < n; ++i) { for(size_t i = 0; i < n; ++i) {
/* compute reduction up to key=i */ /* compute reduction up to key=i */
double reduced_upto double reduced_upto
= rbtree.reduce_lub(i /*key*/, = rbtree.reduce_lub(i /*key*/,
true /*is_closed*/); true /*is_closed*/);
double reduced = (i+1) * (5*i + dvalue); double reduced = (i+1) * (5*i + dvalue);
INFO(tostr(xtag("i", i), xtag("n", n), INFO(tostr(xtag("i", i), xtag("n", n),
xtag("tree.reduced_upto", reduced_upto), xtag("tree.reduced_upto", reduced_upto),
xtag("reduced", reduced), xtag("reduced", reduced),
xtag("dvalue", dvalue))); xtag("dvalue", dvalue)));
auto glb_ix = rbtree.cfind_sum_glb(reduced); auto glb_ix = rbtree.cfind_sum_glb(reduced);
REQUIRE(reduced_upto == reduced); REQUIRE(reduced_upto == reduced);
REQUIRE(glb_ix.is_dereferenceable()); REQUIRE(glb_ix.is_dereferenceable());
/* glb_ix is truth-y */ /* glb_ix is truth-y */
REQUIRE(glb_ix); REQUIRE(glb_ix);
REQUIRE(glb_ix->first == i); REQUIRE(glb_ix->first == i);
} }
} /*check_reduced_sum*/ } /*check_reduced_sum*/
#ifdef OBSOLETE /* Require:
/* Require: * - *p_rbtree has keys [0..n-1], where n=rbtree.size()
* - *p_rbtree has keys [0..n-1], where n=rbtree.size() * - for each key k, associated value is 10*k
* - for each key k, associated value is 10*k *
*/ * Promise:
void * - for each key k, associated value is dvalue + 10*k
random_lookups(RbTree const & rbtree, */
xoshiro256ss * p_rgen) void
{ random_updates(uint32_t dvalue,
REQUIRE(rbtree.verify_ok()); RbTree * p_rbtree,
xoshiro256ss * p_rgen)
{
REQUIRE(p_rbtree->verify_ok());
size_t n = rbtree.size(); std::size_t n = p_rbtree->size();
std::vector<uint32_t> u std::vector<uint32_t> u
= Util::random_permutation(n, p_rgen); = Util::random_permutation(n, p_rgen);
/* lookup keys in permutation order */ /* update key/value pairs in permutation order */
uint32_t i = 1; uint32_t i = 1;
for (uint32_t x : u) { for (uint32_t x : u) {
INFO(tostr(xtag("i", i), xtag("n", n), xtag("x", x))); REQUIRE((*p_rbtree)[x] == x*10);
REQUIRE(rbtree[x] == x*10); (*p_rbtree)[x] = dvalue + 10*x;
REQUIRE(rbtree.verify_ok());
REQUIRE(rbtree.size() == n);
++i;
}
REQUIRE(rbtree.size() == n); REQUIRE((*p_rbtree)[x] == dvalue + 10*x);
} /*random_lookups*/ REQUIRE(p_rbtree->verify_ok());
#endif /* assignment to existing key does not change tree size */
REQUIRE(p_rbtree->size() == n);
++i;
}
/* Require: REQUIRE(p_rbtree->size() == n);
* - *p_rbtree has keys [0..n-1], where n=rbtree.size() } /*random_updates*/
* - for each key k, associated value is 10*k
*
* Promise:
* - for each key k, associated value is dvalue + 10*k
*/
void
random_updates(uint32_t dvalue,
RbTree * p_rbtree,
xoshiro256ss * p_rgen)
{
REQUIRE(p_rbtree->verify_ok());
std::size_t n = p_rbtree->size(); TEST_CASE("rbtree", "[redblacktree]")
std::vector<uint32_t> u {
= Util::random_permutation(n, p_rgen);
/* update key/value pairs in permutation order */
uint32_t i = 1;
for (uint32_t x : u) {
REQUIRE((*p_rbtree)[x] == x*10);
(*p_rbtree)[x] = dvalue + 10*x;
REQUIRE((*p_rbtree)[x] == dvalue + 10*x);
REQUIRE(p_rbtree->verify_ok());
/* assignment to existing key does not change tree size */
REQUIRE(p_rbtree->size() == n);
++i;
}
REQUIRE(p_rbtree->size() == n);
} /*random_updates_1*/
TEST_CASE("rbtree", "[redblacktree]") {
constexpr bool c_debug_flag = false; constexpr bool c_debug_flag = false;
RbTree rbtree{RbTree::allocator_type{}, c_debug_flag}; RbTree rbtree{RbTree::allocator_type{}, c_debug_flag};
REQUIRE(rbtree.size() == 0);
std::uint64_t seed = 14950349842636922572UL; std::uint64_t seed = 14950349842636922572UL;
/* can reseed from /dev/urandom with: */ /* can reseed from /dev/urandom with: */
//arc4random_buf(&seed, sizeof(seed)); //arc4random_buf(&seed, sizeof(seed));
@ -216,6 +189,7 @@ namespace {
debug_flag, debug_flag,
rbtree); rbtree);
/* similarly reverify end-to-end iteration */
ok_flag &= TreeUtil<RbTree>::check_bidirectional_iterator(0, ok_flag &= TreeUtil<RbTree>::check_bidirectional_iterator(0,
debug_flag, debug_flag,
rbtree); rbtree);
@ -235,6 +209,7 @@ namespace {
check_reduced_sum(10000, rbtree); check_reduced_sum(10000, rbtree);
/* verify behavior of read/write variant of operator[] */ /* verify behavior of read/write variant of operator[] */
ok_flag &= TreeUtil<RbTree>::random_removes(debug_flag, &rgen, &rbtree); ok_flag &= TreeUtil<RbTree>::random_removes(debug_flag, &rgen, &rbtree);
} }
log.end_scope(); log.end_scope();