Add a red-black tree implementation and testing.

This commit is contained in:
Matthew Parkinson
2022-01-24 13:13:58 +00:00
committed by Matthew Parkinson
parent 3d1b973480
commit 63d3928687
2 changed files with 876 additions and 0 deletions

698
src/ds/redblacktree.h Normal file
View File

@@ -0,0 +1,698 @@
#pragma once
#include "concept.h"
#include "defines.h"
#include <array>
#include <cstddef>
#include <cstdint>
namespace snmalloc
{
#ifdef __cpp_concepts
template<typename Rep>
concept RBRepTypes = requires()
{
typename Rep::Holder;
typename Rep::Contents;
};
template<typename Rep>
concept RBRepMethods =
requires(typename Rep::Holder* hp, typename Rep::Contents k, bool b)
{
{
Rep::get(hp)
}
->ConceptSame<typename Rep::Contents>;
{
Rep::set(hp, k)
}
->ConceptSame<void>;
{
Rep::is_red(k)
}
->ConceptSame<bool>;
{
Rep::set_red(k, b)
}
->ConceptSame<void>;
{
Rep::ref(b, k)
}
->ConceptSame<typename Rep::Holder&>;
};
template<typename Rep>
concept RBRep = //
RBRepTypes<Rep> //
&& RBRepMethods<Rep> //
&& ConceptSame<
decltype(Rep::null),
std::add_const_t<typename Rep::Contents>>;
#endif
/**
* Contains a self balancing binary tree.
*
* The template parameter Rep provides the representation of the nodes as a
* collection of functions and types that are requires. See the associated
* test for an example.
*
* run_checks enables invariant checking on the tree. Enabled in Debug.
* TRACE prints all the sets of the rebalancing operations. Only enabled by
* the test when debugging a specific failure.
*/
template<
SNMALLOC_CONCEPT(RBRep) Rep,
bool run_checks = DEBUG,
bool TRACE = false>
class RBTree
{
using H = typename Rep::Holder;
using K = typename Rep::Contents;
// Container that behaves like a C++ Ref type to enable assignment
// to treat left, right and root uniformly.
class ChildRef
{
H* ptr;
public:
ChildRef() : ptr(nullptr) {}
ChildRef(H& p) : ptr(&p) {}
operator K()
{
return Rep::get(ptr);
}
K operator=(K t)
{
// Use representations assigment, so we update the correct bits
// color and other things way also be stored in the Holder.
Rep::set(ptr, t);
return t;
}
bool operator==(ChildRef& t)
{
return ptr == t.ptr;
}
bool operator!=(ChildRef& t)
{
return ptr != t.ptr;
}
H* addr()
{
return ptr;
}
};
// Root field of the tree
H root{};
static ChildRef get_dir(bool direction, K k)
{
return {Rep::ref(direction, k)};
}
ChildRef get_root()
{
return {root};
}
void invariant()
{
invariant(get_root());
}
/*
* Verify structural invariants. Returns the black depth of the `curr`ent
* node.
*/
int invariant(K curr, K lower = Rep::MinKey, K upper = Rep::MaxKey)
{
if constexpr (!run_checks)
{
UNUSED(curr, lower, upper);
return 0;
}
if (curr == Rep::null)
return 1;
if (curr < lower || curr > upper)
{
if constexpr (TRACE)
{
std::cout << "Invariant failed: " << curr << " is out of bounds "
<< lower << ", " << upper << std::endl;
print();
}
snmalloc::error("Invariant failed");
}
if (
Rep::is_red(curr) &&
(Rep::is_red(get_dir(true, curr)) || Rep::is_red(get_dir(false, curr))))
{
if constexpr (TRACE)
{
std::cout << "Red invariant failed: " << curr
<< " is red and has red children" << std::endl;
print();
}
snmalloc::error("Invariant failed");
}
int left_inv = invariant(get_dir(true, curr), lower, curr);
int right_inv = invariant(get_dir(false, curr), curr, upper);
if (left_inv != right_inv)
{
if constexpr (TRACE)
{
std::cout << "Balance failed: " << curr
<< " has different black depths on left and right"
<< std::endl;
print();
}
snmalloc::error("Invariant failed");
}
if (Rep::is_red(curr))
return left_inv;
else
return left_inv + 1;
}
struct RBStep
{
ChildRef node;
bool dir = false;
};
public:
// Internal representation of a path in the tree.
// Exposed to allow for some composite operations to be defined
// externally.
class RBPath
{
friend class RBTree;
std::array<RBStep, 128> path;
size_t length = 0;
RBPath(typename Rep::Holder& root) : path{}
{
path[0] = {root, false};
length = 1;
}
ChildRef ith(size_t n)
{
SNMALLOC_ASSERT(length >= n);
return path[length - n - 1].node;
}
bool ith_dir(size_t n)
{
SNMALLOC_ASSERT(length >= n);
return path[length - n - 1].dir;
}
ChildRef curr()
{
return ith(0);
}
bool curr_dir()
{
return ith_dir(0);
}
ChildRef parent()
{
return ith(1);
}
bool parent_dir()
{
return ith_dir(1);
}
ChildRef grand_parent()
{
return ith(2);
}
// Extend path in `direction`.
// If `direction` contains `Rep::null`, do not extend the path.
// Returns false if path is not extended.
bool move(bool direction)
{
auto next = get_dir(direction, curr());
if (next == Rep::null)
return false;
path[length] = {next, direction};
length++;
return true;
}
// Extend path in `direction`.
// If `direction` contains zero, do not extend the path.
// Returns false if path is extended with null.
bool move_inc_null(bool direction)
{
auto next = get_dir(direction, curr());
path[length] = {next, direction};
length++;
return next != Rep::null;
}
// Remove top element from the path.
void pop()
{
SNMALLOC_ASSERT(length > 0);
length--;
}
// If a path is changed in place, then some references can be stale.
// This rewalks the updated path, and corrects any internal references.
// `expected` is used to run the update, or if `false` used to check
// that no update is required.
void fixup(bool expected = true)
{
if (!run_checks && !expected)
return;
// During a splice in remove the path can be invalidated,
// this refreshs the path so that the it refers to the spliced
// nodes fields.
// TODO optimise usage to avoid traversing whole path.
for (size_t i = 1; i < length; i++)
{
auto parent = path[i - 1].node;
auto& curr = path[i].node;
auto dir = path[i].dir;
auto actual = get_dir(dir, parent);
if (actual != curr)
{
if (!expected)
{
snmalloc::error("Performed an unexpected fixup.");
}
curr = actual;
}
}
}
void print()
{
if constexpr (TRACE)
{
for (size_t i = 0; i < length; i++)
{
std::cout << "->" << K(path[i].node) << "@" << path[i].node.addr()
<< " (" << path[i].dir << ") ";
}
std::cout << std::endl;
}
}
};
private:
void debug_log(const char* msg, RBPath& path)
{
debug_log(msg, path, get_root());
}
void debug_log(const char* msg, RBPath& path, ChildRef base)
{
if constexpr (TRACE)
{
std::cout << "-------" << std::endl;
std::cout << msg << std::endl;
path.print();
print(base);
}
else
{
UNUSED(msg, path, base);
}
}
public:
RBTree() {}
void print()
{
print(get_root());
}
void print(ChildRef curr, const char* indent = "", size_t depth = 0)
{
if constexpr (TRACE)
{
std::cout << indent << "\\_";
if (curr == Rep::null)
{
std::cout << "null" << std::endl;
return;
}
#ifdef _MSC_VER
auto colour = Rep::is_red(curr) ? "R-" : "B-";
auto reset = "";
#else
auto colour = Rep::is_red(curr) ? "\e[1;31m" : "\e[1;34m";
auto reset = "\e[0m";
#endif
std::cout << colour << curr << reset << curr.addr() << " (" << depth
<< ")" << std::endl;
if ((get_dir(true, curr) != 0) || (get_dir(false, curr) != 0))
{
auto s_indent = std::string(indent);
print(get_dir(true, curr), (s_indent + "|").c_str(), depth + 1);
print(get_dir(false, curr), (s_indent + " ").c_str(), depth + 1);
}
}
}
bool find(RBPath& path, K value)
{
bool dir;
if (path.curr() == Rep::null)
return false;
do
{
if (path.curr() == value)
return true;
dir = path.curr() > value;
} while (path.move_inc_null(dir));
return false;
}
bool remove_path(RBPath& path)
{
ChildRef splice = path.curr();
SNMALLOC_ASSERT(splice != Rep::null);
debug_log("Removing", path);
/*
* Find immediately smaller leaf element (rightmost descendant of left
* child) to serve as the replacement for this node. We may not have a
* left subtree, so this may not move the path at all.
*/
path.move(true);
while (path.move(false))
{
}
K curr = path.curr();
{
// Locally extract right-child-less replacement, replacing it with its
// left child, if any
K child = get_dir(true, path.curr());
// Unlink target replacing with possible child.
path.curr() = child;
}
bool leaf_red = Rep::is_red(curr);
if (path.curr() != splice)
{
// If we had a left child, replace ourselves with the extracted value
// from above
Rep::set_red(curr, Rep::is_red(splice));
get_dir(true, curr) = K(get_dir(true, splice));
get_dir(false, curr) = K(get_dir(false, splice));
splice = curr;
path.fixup();
}
debug_log("Splice done", path);
// Red leaf removal requires no rebalancing.
if (leaf_red)
return true;
// Now in the double black case.
// End of path is considered double black, that is, one black element
// shorter than satisfies the invariant. The following algorithm moves up
// the path until it finds a close red element or the root. If we convert
// the tree to one, in which the root is double black, then the algorithm
// is complete, as there is nothing to be out of balance with. Otherwise,
// we are searching for nearby red elements so we can rotate the tree to
// rebalance. The following slides nicely cover the case analysis below
// https://www.cs.purdue.edu/homes/ayg/CS251/slides/chap13c.pdf
while (path.curr() != ChildRef(root))
{
K parent = path.parent();
bool cur_dir = path.curr_dir();
K sibling = get_dir(!cur_dir, parent);
/* Handle red sibling case.
* This performs a rotation to give a black sibling.
*
* p s(b)
* / \ / \
* c s(r) --> p(r) m
* / \ / \
* n m c n
*
* By invariant we know that p, n and m are all initially black.
*/
if (Rep::is_red(sibling))
{
debug_log("Red sibling", path, path.parent());
K nibling = get_dir(cur_dir, sibling);
get_dir(!cur_dir, parent) = nibling;
get_dir(cur_dir, sibling) = parent;
Rep::set_red(parent, true);
Rep::set_red(sibling, false);
path.parent() = sibling;
// Manually fix path. Using path.fixup would alter the complexity
// class.
path.pop();
path.move(cur_dir);
path.move_inc_null(cur_dir);
path.fixup(false);
debug_log("Red sibling - done", path, path.parent());
continue;
}
/* Handle red nibling case 1.
* <p> <s>
* / \ / \
* c s --> p rn
* / \ / \
* on rn c on
*/
if (Rep::is_red(get_dir(!cur_dir, sibling)))
{
debug_log("Red nibling 1", path, path.parent());
K r_nibling = get_dir(!cur_dir, sibling);
K o_nibling = get_dir(cur_dir, sibling);
get_dir(cur_dir, sibling) = parent;
get_dir(!cur_dir, parent) = o_nibling;
path.parent() = sibling;
Rep::set_red(r_nibling, false);
Rep::set_red(sibling, Rep::is_red(parent));
Rep::set_red(parent, false);
debug_log("Red nibling 1 - done", path, path.parent());
break;
}
/* Handle red nibling case 2.
* <p> <rn>
* / \ / \
* c s --> p s
* / \ / \ / \
* rn on c rno rns on
* / \
* rno rns
*/
if (Rep::is_red(get_dir(cur_dir, sibling)))
{
debug_log("Red nibling 2", path, path.parent());
K r_nibling = get_dir(cur_dir, sibling);
K r_nibling_same = get_dir(cur_dir, r_nibling);
K r_nibling_opp = get_dir(!cur_dir, r_nibling);
get_dir(!cur_dir, parent) = r_nibling_same;
get_dir(cur_dir, sibling) = r_nibling_opp;
get_dir(cur_dir, r_nibling) = parent;
get_dir(!cur_dir, r_nibling) = sibling;
path.parent() = r_nibling;
Rep::set_red(r_nibling, Rep::is_red(parent));
Rep::set_red(parent, false);
debug_log("Red nibling 2 - done", path, path.parent());
break;
}
// Handle black sibling and niblings, and red parent.
if (Rep::is_red(parent))
{
// std::cout << "Black sibling and red parent case" << std::endl;
Rep::set_red(parent, false);
Rep::set_red(sibling, true);
break;
}
// Handle black sibling and niblings and black parent.
debug_log(
"Black sibling, niblings and black parent case", path, path.parent());
Rep::set_red(sibling, true);
path.pop();
invariant(path.curr());
debug_log(
"Black sibling, niblings and black parent case - done",
path,
path.curr());
}
return true;
}
// Insert an element at the given path.
void insert_path(RBPath path, K value)
{
SNMALLOC_ASSERT(path.curr() == Rep::null);
path.curr() = value;
get_dir(true, path.curr()) = Rep::null;
get_dir(false, path.curr()) = Rep::null;
Rep::set_red(value, true);
debug_log("Insert ", path);
// Propogate double red up to rebalance.
// These notes were particularly clear for explaining insert
// https://www.cs.cmu.edu/~fp/courses/15122-f10/lectures/17-rbtrees.pdf
while (path.curr() != get_root())
{
SNMALLOC_ASSERT(Rep::is_red(path.curr()));
if (!Rep::is_red(path.parent()))
{
invariant();
return;
}
bool curr_dir = path.curr_dir();
K curr = path.curr();
K parent = path.parent();
K grand_parent = path.grand_parent();
SNMALLOC_ASSERT(!Rep::is_red(grand_parent));
if (path.parent_dir() == curr_dir)
{
debug_log("Insert - double red case 1", path, path.grand_parent());
/* Same direction case
* G - grand parent
* P - parent
* C - current
* S - sibling
*
* G P
* / \ / \
* A P --> G C
* / \ / \
* S C A S
*/
K sibling = get_dir(!curr_dir, parent);
Rep::set_red(curr, false);
get_dir(curr_dir, grand_parent) = sibling;
get_dir(!curr_dir, parent) = grand_parent;
path.grand_parent() = parent;
debug_log(
"Insert - double red case 1 - done", path, path.grand_parent());
}
else
{
debug_log("Insert - double red case 2", path, path.grand_parent());
/* G - grand parent
* P - parent
* C - current
* Cg - Current child for grand parent
* Cp - Current child for parent
*
* G C
* / \ / \
* A P G P
* / \ --> / \ / \
* C B A Cg Cp B
* / \
* Cg Cp
*/
K child_g = get_dir(curr_dir, curr);
K child_p = get_dir(!curr_dir, curr);
Rep::set_red(parent, false);
path.grand_parent() = curr;
get_dir(curr_dir, curr) = grand_parent;
get_dir(!curr_dir, curr) = parent;
get_dir(curr_dir, parent) = child_p;
get_dir(!curr_dir, grand_parent) = child_g;
debug_log(
"Insert - double red case 2 - done", path, path.grand_parent());
}
// Move to what replaced grand parent.
path.pop();
path.pop();
invariant(path.curr());
}
Rep::set_red(get_root(), false);
invariant();
}
K remove_min()
{
if (get_root() == Rep::null)
return Rep::null;
auto path = get_root_path();
while (path.move(true))
{
}
K result = path.curr();
remove_path(path);
return result;
}
bool remove_elem(K value)
{
if (get_root() == Rep::null)
return false;
auto path = get_root_path();
if (!find(path, value))
return false;
remove_path(path);
return true;
}
bool insert_elem(K value)
{
auto path = get_root_path();
if (find(path, value))
return false;
insert_path(path, value);
return true;
}
RBPath get_root_path()
{
return RBPath(root);
}
};
}

View File

@@ -0,0 +1,178 @@
#include "test/opt.h"
#include "test/setup.h"
#include "test/usage.h"
#include "test/xoroshiro.h"
#include <algorithm>
#include <array>
#include <iostream>
#include <vector>
// Redblack tree needs some libraries with trace enabled.
#include "ds/redblacktree.h"
#include "snmalloc.h"
struct Wrapper
{
// The redblack tree is going to be used inside the pagemap,
// and the redblack tree cannot use all the bits. Applying an offset
// to the stored value ensures that we have some abstraction over
// the representation.
static constexpr size_t offset = 10000;
size_t value = offset << 1;
};
// Simple representation that is like the pagemap.
// Bottom bit of left is used to store the colour.
// We shift the fields up to make room for the colour.
struct node
{
Wrapper left;
Wrapper right;
};
inline static node array[2048];
class Rep
{
public:
using key = size_t;
static constexpr key null = 0;
static constexpr key MinKey = 0;
static constexpr key MaxKey = ~MinKey;
using Holder = Wrapper;
using Contents = size_t;
static void set(Holder* ptr, Contents r)
{
ptr->value = ((r + Wrapper::offset) << 1) + (ptr->value & 1);
}
static Contents get(Holder* ptr)
{
return (ptr->value >> 1) - Wrapper::offset;
}
static Holder& ref(bool direction, key k)
{
if (direction)
return array[k].left;
else
return array[k].right;
}
static bool is_red(key k)
{
return (array[k].left.value & 1) == 1;
}
static void set_red(key k, bool new_is_red)
{
if (new_is_red != is_red(k))
array[k].left.value ^= 1;
}
};
template<bool TRACE>
void test(size_t size, unsigned int seed)
{
/// Perform a pseudo-random series of
/// additions and removals from the tree.
xoroshiro::p64r32 rand(seed);
snmalloc::RBTree<Rep, true, TRACE> tree;
std::vector<Rep::key> entries;
bool first = true;
std::cout << "size: " << size << " seed: " << seed << std::endl;
for (size_t i = 0; i < 20 * size; i++)
{
auto batch = 1 + rand.next() % (3 + (size / 2));
auto op = rand.next() % 4;
if (op < 2 || first)
{
first = false;
for (auto j = batch; j > 0; j--)
{
auto index = 1 + rand.next() % size;
if (tree.insert_elem(index))
{
entries.push_back(index);
}
}
}
else if (op == 3)
{
for (auto j = batch; j > 0; j--)
{
if (entries.size() == 0)
continue;
auto index = rand.next() % entries.size();
auto elem = entries[index];
if (!tree.remove_elem(elem))
{
std::cout << "Failed to remove element: " << elem << std::endl;
abort();
}
entries.erase(entries.begin() + static_cast<int>(index));
}
}
else
{
for (auto j = batch; j > 0; j--)
{
// print();
auto min = tree.remove_min();
auto s = entries.size();
if (min == 0)
break;
entries.erase(
std::remove(entries.begin(), entries.end(), min), entries.end());
if (s != entries.size() + 1)
{
std::cout << "Failed to remove min: " << min << std::endl;
abort();
}
}
}
if (entries.size() == 0)
{
break;
}
}
}
int main(int argc, char** argv)
{
setup();
opt::Opt opt(argc, argv);
auto seed = opt.is<unsigned int>("--seed", 0);
auto size = opt.is<size_t>("--size", 0);
if (seed == 0 && size == 0)
{
for (size = 1; size <= 300; size = size + 1 + (size >> 3))
for (seed = 1; seed < 5 + (8 * size); seed++)
{
test<false>(size, seed);
}
return 0;
}
if (seed == 0 || size == 0)
{
std::cout << "Set both --seed and --size" << std::endl;
return 1;
}
// Trace particular example
test<true>(size, seed);
return 0;
}