From 3a6833cc4816ce6f6aec2211afec5030a1ac59c4 Mon Sep 17 00:00:00 2001 From: Schrodinger ZHU Yifan Date: Wed, 25 Feb 2026 14:46:55 -0500 Subject: [PATCH 1/9] prepare to introduce additional tree variant --- docs/AddressSpace.md | 2 +- src/snmalloc/backend_helpers/buddy.h | 4 +- .../backend_helpers/largebuddyrange.h | 10 +- .../backend_helpers/smallbuddyrange.h | 12 +- src/snmalloc/ds_core/ds_core.h | 2 +- src/snmalloc/ds_core/rankbalancetree.h | 78 ++++++++++ src/snmalloc/ds_core/redblacktree.h | 133 +++++------------- src/test/func/redblack/redblack.cc | 8 +- 8 files changed, 130 insertions(+), 119 deletions(-) create mode 100644 src/snmalloc/ds_core/rankbalancetree.h diff --git a/docs/AddressSpace.md b/docs/AddressSpace.md index 1e28491ee..860b23da2 100644 --- a/docs/AddressSpace.md +++ b/docs/AddressSpace.md @@ -135,7 +135,7 @@ The following cases apply: The `MetaEntry`... * has `REMOTE_BACKEND_MARKER` asserted in `remote_and_sizeclass`. * has "small" sizeclass 0, which has size 0. - * the remainder of its `MetaEntry` structure will be a Large Buddy Allocator rbtree node. + * the remainder of its `MetaEntry` structure will be a Large Buddy Allocator RedBlackTree node. * has no associated metadata structure. 3. The address is part of a free chunk inside a backend's Small Buddy Allocator: diff --git a/src/snmalloc/backend_helpers/buddy.h b/src/snmalloc/backend_helpers/buddy.h index 58cafacb1..6280137a6 100644 --- a/src/snmalloc/backend_helpers/buddy.h +++ b/src/snmalloc/backend_helpers/buddy.h @@ -20,11 +20,11 @@ namespace snmalloc struct Entry { typename Rep::Contents cache[3]; - RBTree tree{}; + RedBlackTree tree{}; }; stl::Array entries{}; - // All RBtrees at or above this index should be empty. + // All RedBlackTrees at or above this index should be empty. size_t empty_at_or_above{0}; size_t to_index(size_t size) diff --git a/src/snmalloc/backend_helpers/largebuddyrange.h b/src/snmalloc/backend_helpers/largebuddyrange.h index 15324753f..4ac75f28d 100644 --- a/src/snmalloc/backend_helpers/largebuddyrange.h +++ b/src/snmalloc/backend_helpers/largebuddyrange.h @@ -16,7 +16,7 @@ namespace snmalloc { public: /* - * The values we store in our rbtree are the addresses of (combined spans + * The values we store in our RedBlackTree are the addresses of (combined spans * of) chunks of the address space; as such, bits in (MIN_CHUNK_SIZE - 1) * are unused and so the RED_BIT is packed therein. However, in practice, * these are not "just any" uintptr_t-s, but specifically the uintptr_t-s @@ -87,19 +87,19 @@ namespace snmalloc return entry.get_backend_word(Pagemap::Entry::Word::Two); } - static bool is_red(Contents k) + static bool tree_tag(Contents k) { return (ref(true, k).get() & RED_BIT) == RED_BIT; } - static void set_red(Contents k, bool new_is_red) + static void set_tree_tag(Contents k, bool new_tree_tag) { - if (new_is_red != is_red(k)) + if (new_tree_tag != tree_tag(k)) { auto v = ref(true, k); v = v.get() ^ RED_BIT; } - SNMALLOC_ASSERT(is_red(k) == new_is_red); + SNMALLOC_ASSERT(tree_tag(k) == new_tree_tag); } static Contents offset(Contents k, size_t size) diff --git a/src/snmalloc/backend_helpers/smallbuddyrange.h b/src/snmalloc/backend_helpers/smallbuddyrange.h index 6f8400e83..ba69e2685 100644 --- a/src/snmalloc/backend_helpers/smallbuddyrange.h +++ b/src/snmalloc/backend_helpers/smallbuddyrange.h @@ -18,7 +18,7 @@ namespace snmalloc }; /** - * Class for using the allocations own space to store in the RBTree. + * Class for using the allocations own space to store in the RedBlackTree. */ template class BuddyInplaceRep @@ -57,21 +57,21 @@ namespace snmalloc return &r->right; } - static bool is_red(Contents k) + static bool tree_tag(Contents k) { if (k == nullptr) return false; return (address_cast(*ref(false, k)) & MASK) == MASK; } - static void set_red(Contents k, bool new_is_red) + static void set_tree_tag(Contents k, bool new_tree_tag) { - if (new_is_red != is_red(k)) + if (new_tree_tag != tree_tag(k)) { auto r = ref(false, k); auto old_addr = pointer_align_down<2, FreeChunk>(r->as_void()); - if (new_is_red) + if (new_tree_tag) { if (old_addr == nullptr) *r = CapPtr, bounds>::unsafe_from( @@ -84,7 +84,7 @@ namespace snmalloc { *r = old_addr; } - SNMALLOC_ASSERT(is_red(k) == new_is_red); + SNMALLOC_ASSERT(tree_tag(k) == new_tree_tag); } } diff --git a/src/snmalloc/ds_core/ds_core.h b/src/snmalloc/ds_core/ds_core.h index cc395127b..112d160d9 100644 --- a/src/snmalloc/ds_core/ds_core.h +++ b/src/snmalloc/ds_core/ds_core.h @@ -14,5 +14,5 @@ #include "helpers.h" #include "mitigations.h" #include "ptrwrap.h" -#include "redblacktree.h" +#include "rankbalancetree.h" #include "tid.h" \ No newline at end of file diff --git a/src/snmalloc/ds_core/rankbalancetree.h b/src/snmalloc/ds_core/rankbalancetree.h new file mode 100644 index 000000000..cbf03e04d --- /dev/null +++ b/src/snmalloc/ds_core/rankbalancetree.h @@ -0,0 +1,78 @@ +#pragma once + +#include "snmalloc/stl/array.h" + +#include +#include + +namespace snmalloc +{ +#ifdef __cpp_concepts + /** + * The representation must define two types. `Contents` defines some + * identifier that can be mapped to a node as a value type. `Handle` defines + * a reference to the storage, which can be used to update it. + * + * Conceptually, `Contents` is a node ID and `Handle` is a pointer to a node + * ID. + */ + template + concept RBRepTypes = requires() { + typename Rep::Handle; + typename Rep::Contents; + }; + + /** + * The representation must define operations on the holder and contents + * types. It must be able to 'dereference' a holder with `get`, assign to it + * with `set`, set and query the red/black colour of a node with `set_tree_tag` and + * `tree_tag`. + * + * The `ref` method provides uniform access to the children of a node, + * returning a holder pointing to either the left or right child, depending on + * the direction parameter. + * + * The backend must also provide two constant values. + * `Rep::null` defines a value that, if returned from `get`, indicates a null + * value. `Rep::root` defines a value that, if constructed directly, indicates + * a null value and can therefore be used as the initial raw bit pattern of + * the root node. + */ + template + concept RBRepMethods = + requires(typename Rep::Handle hp, typename Rep::Contents k, bool b) { + { + Rep::get(hp) + } -> ConceptSame; + { + Rep::set(hp, k) + } -> ConceptSame; + { + Rep::tree_tag(k) + } -> ConceptSame; + { + Rep::set_tree_tag(k, b) + } -> ConceptSame; + { + Rep::ref(b, k) + } -> ConceptSame; + { + Rep::null + } -> ConceptSameModRef; + { + typename Rep::Handle{const_cast< + stl::remove_const_t>*>( + &Rep::root)} + } -> ConceptSame; + }; + + template + concept RBRep = // + RBRepTypes // + && RBRepMethods // + && + ConceptSame>; +#endif +} // namespace snmalloc + +#include "redblacktree.h" diff --git a/src/snmalloc/ds_core/redblacktree.h b/src/snmalloc/ds_core/redblacktree.h index ec212fa23..f61693308 100644 --- a/src/snmalloc/ds_core/redblacktree.h +++ b/src/snmalloc/ds_core/redblacktree.h @@ -7,73 +7,6 @@ namespace snmalloc { -#ifdef __cpp_concepts - /** - * The representation must define two types. `Contents` defines some - * identifier that can be mapped to a node as a value type. `Handle` defines - * a reference to the storage, which can be used to update it. - * - * Conceptually, `Contents` is a node ID and `Handle` is a pointer to a node - * ID. - */ - template - concept RBRepTypes = requires() { - typename Rep::Handle; - typename Rep::Contents; - }; - - /** - * The representation must define operations on the holder and contents - * types. It must be able to 'dereference' a holder with `get`, assign to it - * with `set`, set and query the red/black colour of a node with `set_red` and - * `is_red`. - * - * The `ref` method provides uniform access to the children of a node, - * returning a holder pointing to either the left or right child, depending on - * the direction parameter. - * - * The backend must also provide two constant values. - * `Rep::null` defines a value that, if returned from `get`, indicates a null - * value. `Rep::root` defines a value that, if constructed directly, indicates - * a null value and can therefore be used as the initial raw bit pattern of - * the root node. - */ - template - concept RBRepMethods = - requires(typename Rep::Handle hp, typename Rep::Contents k, bool b) { - { - Rep::get(hp) - } -> ConceptSame; - { - Rep::set(hp, k) - } -> ConceptSame; - { - Rep::is_red(k) - } -> ConceptSame; - { - Rep::set_red(k, b) - } -> ConceptSame; - { - Rep::ref(b, k) - } -> ConceptSame; - { - Rep::null - } -> ConceptSameModRef; - { - typename Rep::Handle{const_cast< - stl::remove_const_t>*>( - &Rep::root)} - } -> ConceptSame; - }; - - template - concept RBRep = // - RBRepTypes // - && RBRepMethods // - && - ConceptSame>; -#endif - /** * Contains a self balancing binary tree. * @@ -89,7 +22,7 @@ namespace snmalloc SNMALLOC_CONCEPT(RBRep) Rep, bool run_checks = Debug, bool TRACE = false> - class RBTree + class RedBlackTree { using H = typename Rep::Handle; using K = typename Rep::Contents; @@ -203,9 +136,9 @@ namespace snmalloc } if ( - Rep::is_red(curr) && - (Rep::is_red(get_dir(true, curr)) || - Rep::is_red(get_dir(false, curr)))) + Rep::tree_tag(curr) && + (Rep::tree_tag(get_dir(true, curr)) || + Rep::tree_tag(get_dir(false, curr)))) { report_fatal_error( "Invariant failed: {} is red and has red child", @@ -222,7 +155,7 @@ namespace snmalloc Rep::printable(curr)); } - if (Rep::is_red(curr)) + if (Rep::tree_tag(curr)) return left_inv; return left_inv + 1; @@ -258,7 +191,7 @@ namespace snmalloc // externally. class RBPath { - friend class RBTree; + friend class RedBlackTree; stl::Array path; size_t length = 0; @@ -405,7 +338,7 @@ namespace snmalloc } public: - constexpr RBTree() = default; + constexpr RedBlackTree() = default; void print() { @@ -423,10 +356,10 @@ namespace snmalloc } #ifdef _MSC_VER - auto colour = Rep::is_red(curr) ? "R-" : "B-"; + auto colour = Rep::tree_tag(curr) ? "R-" : "B-"; auto reset = ""; #else - auto colour = Rep::is_red(curr) ? "\e[1;31m" : "\e[1;34m"; + auto colour = Rep::tree_tag(curr) ? "\e[1;31m" : "\e[1;34m"; auto reset = "\e[0m"; #endif @@ -508,13 +441,13 @@ namespace snmalloc path.curr() = child; } - bool leaf_red = Rep::is_red(curr); + bool leaf_red = Rep::tree_tag(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)); + Rep::set_tree_tag(curr, Rep::tree_tag(splice)); get_dir(true, curr) = K{get_dir(true, splice)}; get_dir(false, curr) = K{get_dir(false, splice)}; splice = curr; @@ -555,14 +488,14 @@ namespace snmalloc * * By invariant we know that p, n and m are all initially black. */ - if (Rep::is_red(sibling)) + if (Rep::tree_tag(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); + Rep::set_tree_tag(parent, true); + Rep::set_tree_tag(sibling, false); path.parent() = sibling; // Manually fix path. Using path.fixup would alter the complexity // class. @@ -581,7 +514,7 @@ namespace snmalloc * / \ / \ * on rn c on */ - if (Rep::is_red(get_dir(!cur_dir, sibling))) + if (Rep::tree_tag(get_dir(!cur_dir, sibling))) { debug_log("Red nibling 1", path, path.parent()); K r_nibling = get_dir(!cur_dir, sibling); @@ -589,9 +522,9 @@ namespace snmalloc 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); + Rep::set_tree_tag(r_nibling, false); + Rep::set_tree_tag(sibling, Rep::tree_tag(parent)); + Rep::set_tree_tag(parent, false); debug_log("Red nibling 1 - done", path, path.parent()); break; } @@ -605,7 +538,7 @@ namespace snmalloc * / \ * rno rns */ - if (Rep::is_red(get_dir(cur_dir, sibling))) + if (Rep::tree_tag(get_dir(cur_dir, sibling))) { debug_log("Red nibling 2", path, path.parent()); K r_nibling = get_dir(cur_dir, sibling); @@ -616,18 +549,18 @@ namespace snmalloc 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); + Rep::set_tree_tag(r_nibling, Rep::tree_tag(parent)); + Rep::set_tree_tag(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)) + if (Rep::tree_tag(parent)) { debug_log("Black sibling and red parent case", path, path.parent()); - Rep::set_red(parent, false); - Rep::set_red(sibling, true); + Rep::set_tree_tag(parent, false); + Rep::set_tree_tag(sibling, true); debug_log( "Black sibling and red parent case - done", path, path.parent()); break; @@ -635,7 +568,7 @@ namespace snmalloc // 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); + Rep::set_tree_tag(sibling, true); path.pop(); invariant(path.curr()); debug_log( @@ -653,17 +586,17 @@ namespace snmalloc path.curr() = value; get_dir(true, path.curr()) = Rep::null; get_dir(false, path.curr()) = Rep::null; - Rep::set_red(value, true); + Rep::set_tree_tag(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 + // https://www.cs.cmu.edu/~fp/courses/15122-f10/lectures/17-RedBlackTrees.pdf while (path.curr() != get_root()) { - SNMALLOC_ASSERT(Rep::is_red(path.curr())); - if (!Rep::is_red(path.parent())) + SNMALLOC_ASSERT(Rep::tree_tag(path.curr())); + if (!Rep::tree_tag(path.parent())) { invariant(); return; @@ -672,7 +605,7 @@ namespace snmalloc K curr = path.curr(); K parent = path.parent(); K grand_parent = path.grand_parent(); - SNMALLOC_ASSERT(!Rep::is_red(grand_parent)); + SNMALLOC_ASSERT(!Rep::tree_tag(grand_parent)); if (path.parent_dir() == curr_dir) { debug_log("Insert - double red case 1", path, path.grand_parent()); @@ -689,7 +622,7 @@ namespace snmalloc * S C A S */ K sibling = get_dir(!curr_dir, parent); - Rep::set_red(curr, false); + Rep::set_tree_tag(curr, false); get_dir(curr_dir, grand_parent) = sibling; get_dir(!curr_dir, parent) = grand_parent; path.grand_parent() = parent; @@ -716,7 +649,7 @@ namespace snmalloc K child_g = get_dir(curr_dir, curr); K child_p = get_dir(!curr_dir, curr); - Rep::set_red(parent, false); + Rep::set_tree_tag(parent, false); path.grand_parent() = curr; get_dir(curr_dir, curr) = grand_parent; get_dir(!curr_dir, curr) = parent; @@ -731,7 +664,7 @@ namespace snmalloc path.pop(); invariant(path.curr()); } - Rep::set_red(get_root(), false); + Rep::set_tree_tag(get_root(), false); invariant(); } diff --git a/src/test/func/redblack/redblack.cc b/src/test/func/redblack/redblack.cc index 164a5978f..bdfcfbfeb 100644 --- a/src/test/func/redblack/redblack.cc +++ b/src/test/func/redblack/redblack.cc @@ -98,14 +98,14 @@ class Rep return {&array[k].right}; } - static bool is_red(key k) + static bool tree_tag(key k) { return (array[k].left & 1) == 1; } - static void set_red(key k, bool new_is_red) + static void set_tree_tag(key k, bool new_tree_tag) { - if (new_is_red != is_red(k)) + if (new_tree_tag != tree_tag(k)) array[k].left ^= 1; } @@ -142,7 +142,7 @@ void test(size_t size, unsigned int seed) /// additions and removals from the tree. xoroshiro::p64r32 rand(seed); - snmalloc::RBTree tree; + snmalloc::RedBlackTree tree; std::vector entries; bool first = true; From c624c5982b28256fb3c2690879d8102e90ab4977 Mon Sep 17 00:00:00 2001 From: Schrodinger ZHU Yifan Date: Wed, 25 Feb 2026 15:09:41 -0500 Subject: [PATCH 2/9] add weak avl --- docs/AddressSpace.md | 4 +- src/snmalloc/backend_helpers/buddy.h | 4 +- .../backend_helpers/largebuddyrange.h | 20 +- .../backend_helpers/smallbuddyrange.h | 2 +- src/snmalloc/ds_core/rankbalancetree.h | 7 + src/snmalloc/ds_core/weakavltree.h | 701 ++++++++++++++++++ src/test/func/weakavl/weakavl.cc | 238 ++++++ 7 files changed, 961 insertions(+), 15 deletions(-) create mode 100644 src/snmalloc/ds_core/weakavltree.h create mode 100644 src/test/func/weakavl/weakavl.cc diff --git a/docs/AddressSpace.md b/docs/AddressSpace.md index 860b23da2..5bdddef17 100644 --- a/docs/AddressSpace.md +++ b/docs/AddressSpace.md @@ -115,7 +115,7 @@ Its contents can be decoded as follows: This trick of pointing at the child's chunk rather than at the child `MetaEntry` is particularly useful on CHERI: it allows us to capture the authority to the chunk without needing another pointer and costs just a shift and add.) -3. The `meta` field's `LargeBuddyRep::RED_BIT` is used to carry the red/black color of this node. +3. The `meta` field's `LargeBuddyRep::TREE_TAG_BIT` is used to carry the red/black color of this node. See `src/backend/largebuddyrange.h`. @@ -135,7 +135,7 @@ The following cases apply: The `MetaEntry`... * has `REMOTE_BACKEND_MARKER` asserted in `remote_and_sizeclass`. * has "small" sizeclass 0, which has size 0. - * the remainder of its `MetaEntry` structure will be a Large Buddy Allocator RedBlackTree node. + * the remainder of its `MetaEntry` structure will be a Large Buddy Allocator DefaultRBTree node. * has no associated metadata structure. 3. The address is part of a free chunk inside a backend's Small Buddy Allocator: diff --git a/src/snmalloc/backend_helpers/buddy.h b/src/snmalloc/backend_helpers/buddy.h index 6280137a6..43beb4331 100644 --- a/src/snmalloc/backend_helpers/buddy.h +++ b/src/snmalloc/backend_helpers/buddy.h @@ -20,11 +20,11 @@ namespace snmalloc struct Entry { typename Rep::Contents cache[3]; - RedBlackTree tree{}; + DefaultRBTree tree{}; }; stl::Array entries{}; - // All RedBlackTrees at or above this index should be empty. + // All DefaultRBTrees at or above this index should be empty. size_t empty_at_or_above{0}; size_t to_index(size_t size) diff --git a/src/snmalloc/backend_helpers/largebuddyrange.h b/src/snmalloc/backend_helpers/largebuddyrange.h index 4ac75f28d..0e9ff9f29 100644 --- a/src/snmalloc/backend_helpers/largebuddyrange.h +++ b/src/snmalloc/backend_helpers/largebuddyrange.h @@ -16,9 +16,9 @@ namespace snmalloc { public: /* - * The values we store in our RedBlackTree are the addresses of (combined spans + * The values we store in our DefaultRBTree are the addresses of (combined spans * of) chunks of the address space; as such, bits in (MIN_CHUNK_SIZE - 1) - * are unused and so the RED_BIT is packed therein. However, in practice, + * are unused and so the TREE_TAG_BIT is packed therein. However, in practice, * these are not "just any" uintptr_t-s, but specifically the uintptr_t-s * inside the Pagemap's BackendAllocator::Entry structures. * @@ -37,13 +37,13 @@ namespace snmalloc * a bit that is a valid part of the address of a chunk. * @{ */ - static constexpr address_t RED_BIT = 1 << 8; + static constexpr address_t TREE_TAG_BIT = 1 << 8; - static_assert(RED_BIT < MIN_CHUNK_SIZE); + static_assert(TREE_TAG_BIT < MIN_CHUNK_SIZE); static_assert(MetaEntryBase::is_backend_allowed_value( - MetaEntryBase::Word::One, RED_BIT)); + MetaEntryBase::Word::One, TREE_TAG_BIT)); static_assert(MetaEntryBase::is_backend_allowed_value( - MetaEntryBase::Word::Two, RED_BIT)); + MetaEntryBase::Word::Two, TREE_TAG_BIT)); ///@} /// The value of a null node, as returned by `get` @@ -56,7 +56,7 @@ namespace snmalloc */ static void set(Handle ptr, Contents r) { - ptr = r | (static_cast(ptr.get()) & RED_BIT); + ptr = r | (static_cast(ptr.get()) & TREE_TAG_BIT); } /** @@ -64,7 +64,7 @@ namespace snmalloc */ static Contents get(const Handle ptr) { - return ptr.get() & ~RED_BIT; + return ptr.get() & ~TREE_TAG_BIT; } /** @@ -89,7 +89,7 @@ namespace snmalloc static bool tree_tag(Contents k) { - return (ref(true, k).get() & RED_BIT) == RED_BIT; + return (ref(true, k).get() & TREE_TAG_BIT) == TREE_TAG_BIT; } static void set_tree_tag(Contents k, bool new_tree_tag) @@ -97,7 +97,7 @@ namespace snmalloc if (new_tree_tag != tree_tag(k)) { auto v = ref(true, k); - v = v.get() ^ RED_BIT; + v = v.get() ^ TREE_TAG_BIT; } SNMALLOC_ASSERT(tree_tag(k) == new_tree_tag); } diff --git a/src/snmalloc/backend_helpers/smallbuddyrange.h b/src/snmalloc/backend_helpers/smallbuddyrange.h index ba69e2685..71122d4fb 100644 --- a/src/snmalloc/backend_helpers/smallbuddyrange.h +++ b/src/snmalloc/backend_helpers/smallbuddyrange.h @@ -18,7 +18,7 @@ namespace snmalloc }; /** - * Class for using the allocations own space to store in the RedBlackTree. + * Class for using the allocations own space to store in the DefaultRBTree. */ template class BuddyInplaceRep diff --git a/src/snmalloc/ds_core/rankbalancetree.h b/src/snmalloc/ds_core/rankbalancetree.h index cbf03e04d..ae4985327 100644 --- a/src/snmalloc/ds_core/rankbalancetree.h +++ b/src/snmalloc/ds_core/rankbalancetree.h @@ -76,3 +76,10 @@ namespace snmalloc } // namespace snmalloc #include "redblacktree.h" +#include "weakavltree.h" + +namespace snmalloc +{ + template + using DefaultRBTree = WeakAVLTree; +} \ No newline at end of file diff --git a/src/snmalloc/ds_core/weakavltree.h b/src/snmalloc/ds_core/weakavltree.h new file mode 100644 index 000000000..cd8564ce5 --- /dev/null +++ b/src/snmalloc/ds_core/weakavltree.h @@ -0,0 +1,701 @@ +#pragma once + +#include "snmalloc/stl/array.h" + +#include +#include + +namespace snmalloc +{ + /** + * Weak AVL tree implementation using 1-bit rank parity per node. + * + * The representation must provide: + * - types `Handle`, `Contents` + * - `get(Handle)`, `set(Handle, Contents)` + * - `ref(bool is_left, Contents)` + * - `tree_tag(Contents)`, `set_tree_tag(Contents, uint8_t)` (bit0 = parity) + * - `compare(Contents, Contents)`, `equal(Contents, Contents)` + * - constants `null` and `root` + */ + template + class WeakAVLTree + { + using H = typename Rep::Handle; + using K = typename Rep::Contents; + using RootStorage = + stl::remove_const_t>; + + RootStorage root{Rep::root}; + + static constexpr bool Left = false; + static constexpr bool Right = true; + static constexpr uint8_t ParityMask = 0b1; + static constexpr bool use_checks = run_checks; + + class RBPath + { + friend class WeakAVLTree; + + K parent{Rep::null}; + K curr{Rep::null}; + bool dir{Left}; + }; + + H root_ref() + { + return H{&root}; + } + + K get_root() const + { + return Rep::get(H{const_cast(&root)}); + } + + void set_root(K n) + { + set(root_ref(), n); + } + + static K get(H p) + { + return Rep::get(p); + } + + static void set(H p, K v) + { + Rep::set(p, v); + } + + static H child_ref(K n, bool dir) + { + // Rep uses true for left, false for right. + return Rep::ref(!dir, n); + } + + static K child(K n, bool dir) + { + return get(child_ref(n, dir)); + } + + static void set_child(K n, bool dir, K v) + { + set(child_ref(n, dir), v); + } + + K parent(K n) const + { + if (is_null(n)) + return Rep::null; + + K p = Rep::null; + K cur = get_root(); + while (!is_null(cur) && !Rep::equal(cur, n)) + { + p = cur; + bool dir = Rep::compare(cur, n) ? Left : Right; + cur = child(cur, dir); + } + return is_null(cur) ? Rep::null : p; + } + + static void set_parent(K n, K p) + { + UNUSED(n, p); + } + + static bool is_null(K n) + { + return Rep::equal(n, Rep::null); + } + + static bool parity(K n) + { + if (is_null(n)) + return true; + return (Rep::tree_tag(n) & ParityMask) != 0; + } + + static void set_parity(K n, bool p) + { + if (!is_null(n)) + Rep::set_tree_tag(n, p ? 1 : 0); + } + + static void toggle_parity(K n) + { + if (!is_null(n)) + Rep::set_tree_tag(n, static_cast(Rep::tree_tag(n) ^ 1)); + } + + static void promote(K n) + { + toggle_parity(n); + } + + static void demote(K n) + { + toggle_parity(n); + } + + static void double_promote(K n) + { + UNUSED(n); + } + + static void double_demote(K n) + { + UNUSED(n); + } + + static bool is_leaf(K n) + { + return !is_null(n) && is_null(child(n, Left)) && is_null(child(n, Right)); + } + + static bool is_right_child(K p, K n) + { + return Rep::equal(child(p, Right), n); + } + + static bool is_2_child(K n, K p) + { + return parity(n) == parity(p); + } + + K sibling(K n) const + { + K p = parent(n); + if (is_null(p)) + return Rep::null; + return Rep::equal(child(p, Left), n) ? child(p, Right) : child(p, Left); + } + + void rotate_right_at(K x) + { + K z = parent(x); + K y = child(x, Right); + K p_z = parent(z); + + set_parent(x, p_z); + if (!is_null(p_z)) + { + if (Rep::equal(child(p_z, Left), z)) + set_child(p_z, Left, x); + else + set_child(p_z, Right, x); + } + else + { + set_root(x); + } + + set_child(x, Right, z); + set_parent(z, x); + + set_child(z, Left, y); + if (!is_null(y)) + set_parent(y, z); + } + + void rotate_left_at(K x) + { + K z = parent(x); + K y = child(x, Left); + K p_z = parent(z); + + set_parent(x, p_z); + if (!is_null(p_z)) + { + if (Rep::equal(child(p_z, Left), z)) + set_child(p_z, Left, x); + else + set_child(p_z, Right, x); + } + else + { + set_root(x); + } + + set_child(x, Left, z); + set_parent(z, x); + + set_child(z, Right, y); + if (!is_null(y)) + set_parent(y, z); + } + + void double_rotate_right_at(K y) + { + K x = parent(y); + K z = parent(x); + K p_z = parent(z); + + set_parent(y, p_z); + if (!is_null(p_z)) + { + if (Rep::equal(child(p_z, Left), z)) + set_child(p_z, Left, y); + else + set_child(p_z, Right, y); + } + else + { + set_root(y); + } + + set_child(x, Right, child(y, Left)); + if (!is_null(child(y, Left))) + set_parent(child(y, Left), x); + + set_child(y, Left, x); + set_parent(x, y); + + set_child(z, Left, child(y, Right)); + if (!is_null(child(y, Right))) + set_parent(child(y, Right), z); + + set_child(y, Right, z); + set_parent(z, y); + } + + void double_rotate_left_at(K y) + { + K x = parent(y); + K z = parent(x); + K p_z = parent(z); + + set_parent(y, p_z); + if (!is_null(p_z)) + { + if (Rep::equal(child(p_z, Left), z)) + set_child(p_z, Left, y); + else + set_child(p_z, Right, y); + } + else + { + set_root(y); + } + + set_child(z, Right, child(y, Left)); + if (!is_null(child(y, Left))) + set_parent(child(y, Left), z); + + set_child(y, Left, z); + set_parent(z, y); + + set_child(x, Left, child(y, Right)); + if (!is_null(child(y, Right))) + set_parent(child(y, Right), x); + + set_child(y, Right, x); + set_parent(x, y); + } + + void insert_rebalance(K at) + { + K x = at; + K p_x = parent(x); + bool par_x = false; + bool par_p_x = false; + bool par_s_x = false; + + do + { + promote(p_x); + x = p_x; + p_x = parent(x); + + if (is_null(p_x)) + return; + + par_x = parity(x); + par_p_x = parity(p_x); + par_s_x = parity(sibling(x)); + } while ( + (!par_x && !par_p_x && par_s_x) || (par_x && par_p_x && !par_s_x)); + + if (!((par_x && par_p_x && par_s_x) || (!par_x && !par_p_x && !par_s_x))) + return; + + K z = parent(x); + if (Rep::equal(x, child(p_x, Left))) + { + K y = child(x, Right); + if (is_null(y) || parity(y) == par_x) + { + rotate_right_at(x); + if (!is_null(z)) + demote(z); + } + else + { + double_rotate_right_at(y); + promote(y); + demote(x); + if (!is_null(z)) + demote(z); + } + } + else + { + K y = child(x, Left); + if (is_null(y) || parity(y) == par_x) + { + rotate_left_at(x); + if (!is_null(z)) + demote(z); + } + else + { + double_rotate_left_at(y); + promote(y); + demote(x); + if (!is_null(z)) + demote(z); + } + } + } + + static K minimum_at(K n) + { + K cur = n; + while (!is_null(child(cur, Left))) + cur = child(cur, Left); + return cur; + } + + void swap_in_node_at(K old_node, K new_node) + { + K l = child(old_node, Left); + K r = child(old_node, Right); + K p = parent(old_node); + + set_parent(new_node, p); + if (!is_null(p)) + { + if (Rep::equal(child(p, Left), old_node)) + set_child(p, Left, new_node); + else + set_child(p, Right, new_node); + } + else + { + set_root(new_node); + } + + set_child(new_node, Right, r); + if (!is_null(r)) + set_parent(r, new_node); + + set_child(new_node, Left, l); + if (!is_null(l)) + set_parent(l, new_node); + + set_parity(new_node, parity(old_node)); + + set_child(old_node, Left, Rep::null); + set_child(old_node, Right, Rep::null); + set_parent(old_node, Rep::null); + } + + void delete_rebalance_3_child(K n, K p_n) + { + K x = n; + K p_x = p_n; + if (is_null(p_x)) + return; + K y = Rep::null; + bool creates_3_node = false; + bool done = true; + + do + { + K p_p_x = parent(p_x); + y = Rep::equal(child(p_x, Left), x) ? child(p_x, Right) : child(p_x, Left); + + creates_3_node = !is_null(p_p_x) && (parity(p_x) == parity(p_p_x)); + + if (is_2_child(y, p_x)) + { + demote(p_x); + } + else + { + bool y_parity = parity(y); + if ( + y_parity == parity(child(y, Left)) && + y_parity == parity(child(y, Right))) + { + demote(p_x); + demote(y); + } + else + { + done = false; + break; + } + } + + x = p_x; + p_x = p_p_x; + } while (!is_null(p_x) && creates_3_node); + + if (done) + return; + + K z = p_x; + if (Rep::equal(x, child(p_x, Left))) + { + K w = child(y, Right); + if (parity(w) != parity(y)) + { + rotate_left_at(y); + promote(y); + demote(z); + if (is_leaf(z)) + demote(z); + } + else + { + K v = child(y, Left); + if constexpr (use_checks) + SNMALLOC_ASSERT(parity(y) != parity(v)); + double_rotate_left_at(v); + double_promote(v); + demote(y); + double_demote(z); + } + } + else + { + K w = child(y, Left); + if (parity(w) != parity(y)) + { + rotate_right_at(y); + promote(y); + demote(z); + if (is_leaf(z)) + demote(z); + } + else + { + K v = child(y, Right); + if constexpr (use_checks) + SNMALLOC_ASSERT(parity(y) != parity(v)); + double_rotate_right_at(v); + double_promote(v); + demote(y); + double_demote(z); + } + } + } + + void delete_rebalance_2_2_leaf(K leaf) + { + K x = leaf; + K p = parent(x); + if (is_null(p)) + { + demote(x); + return; + } + if (parity(p) == parity(x)) + { + demote(x); + delete_rebalance_3_child(x, p); + } + else + { + demote(x); + } + } + + void erase_node(K node) + { + K y = Rep::null; + K x = Rep::null; + K p_y = Rep::null; + bool was_2_child = false; + + if (is_null(child(node, Left)) || is_null(child(node, Right))) + { + y = node; + } + else + { + y = minimum_at(child(node, Right)); + } + + x = is_null(child(y, Left)) ? child(y, Right) : child(y, Left); + + if (!is_null(x)) + set_parent(x, parent(y)); + + p_y = parent(y); + if (is_null(p_y)) + { + set_root(x); + } + else + { + was_2_child = is_2_child(y, p_y); + if (Rep::equal(y, child(p_y, Left))) + set_child(p_y, Left, x); + else + set_child(p_y, Right, x); + } + + if (!Rep::equal(y, node)) + { + swap_in_node_at(node, y); + if (Rep::equal(node, p_y)) + p_y = y; + } + + if (!is_null(p_y)) + { + if (was_2_child) + { + delete_rebalance_3_child(x, p_y); + } + else if (is_null(x) && Rep::equal(child(p_y, Left), child(p_y, Right))) + { + delete_rebalance_2_2_leaf(p_y); + } + + if constexpr (use_checks) + { + SNMALLOC_ASSERT(!(is_leaf(p_y) && parity(p_y))); + } + } + + set_child(node, Left, Rep::null); + set_child(node, Right, Rep::null); + set_parent(node, Rep::null); + set_parity(node, false); + } + + K find_node(K value) const + { + K next = get_root(); + while (!is_null(next)) + { + if (Rep::equal(next, value)) + return next; + bool dir = Rep::compare(next, value) ? Left : Right; + next = child(next, dir); + } + return Rep::null; + } + + void insert_known_absent(K value, K parent_node, bool dir) + { + if (is_null(parent_node)) + { + set_parent(value, Rep::null); + set_child(value, Left, Rep::null); + set_child(value, Right, Rep::null); + set_parity(value, false); + set_root(value); + return; + } + + bool was_leaf = + is_null(child(parent_node, Left)) && is_null(child(parent_node, Right)); + set_child(value, Left, Rep::null); + set_child(value, Right, Rep::null); + set_parity(value, false); + set_child(parent_node, dir, value); + set_parent(value, parent_node); + + if (was_leaf) + insert_rebalance(value); + } + + public: + constexpr WeakAVLTree() = default; + + bool is_empty() + { + return is_null(get_root()); + } + + bool insert_elem(K value) + { + auto path = get_root_path(); + if (find(path, value)) + return false; + insert_path(path, value); + return true; + } + + bool remove_elem(K value) + { + auto path = get_root_path(); + if (!find(path, value)) + return false; + remove_path(path); + return true; + } + + K remove_min() + { + K r = get_root(); + if (is_null(r)) + return Rep::null; + + K n = minimum_at(r); + erase_node(n); + return n; + } + + bool find(RBPath& path, K value) + { + K parent_node = Rep::null; + K cursor = get_root(); + bool dir = Left; + + while (!is_null(cursor)) + { + if (Rep::equal(cursor, value)) + { + path.parent = parent_node; + path.curr = cursor; + path.dir = dir; + return true; + } + parent_node = cursor; + dir = Rep::compare(cursor, value) ? Left : Right; + cursor = child(cursor, dir); + } + + path.parent = parent_node; + path.curr = Rep::null; + path.dir = dir; + return false; + } + + bool remove_path(RBPath& path) + { + if (is_null(path.curr)) + return false; + erase_node(path.curr); + return true; + } + + void insert_path(RBPath& path, K value) + { + if constexpr (use_checks) + SNMALLOC_ASSERT(is_null(path.curr)); + insert_known_absent(value, path.parent, path.dir); + path.curr = value; + } + + RBPath get_root_path() + { + return RBPath{}; + } + }; +} // namespace snmalloc diff --git a/src/test/func/weakavl/weakavl.cc b/src/test/func/weakavl/weakavl.cc new file mode 100644 index 000000000..8bff5e420 --- /dev/null +++ b/src/test/func/weakavl/weakavl.cc @@ -0,0 +1,238 @@ +#include "test/opt.h" +#include "test/setup.h" +#include "test/usage.h" +#include "test/xoroshiro.h" + +#include +#include +#include + +#ifndef SNMALLOC_TRACING +# define SNMALLOC_TRACING +#endif +// WeakAVL tree needs some libraries with trace enabled. +#include "snmalloc/snmalloc.h" + +struct NodeRef +{ + // The weakavl tree is going to be used inside the pagemap, + // and the weakavl 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* ptr; + + constexpr NodeRef(size_t* p) : ptr(p) {} + + constexpr NodeRef() : ptr(nullptr) {} + + constexpr NodeRef(const NodeRef& other) : ptr(other.ptr) {} + + constexpr NodeRef(NodeRef&& other) : ptr(other.ptr) {} + + bool operator!=(const NodeRef& other) const + { + return ptr != other.ptr; + } + + NodeRef& operator=(const NodeRef& other) + { + ptr = other.ptr; + return *this; + } + + void set(uint16_t val) + { + *ptr = ((size_t(val) + offset) << 1) + (*ptr & 1); + } + + explicit operator uint16_t() + { + return uint16_t((*ptr >> 1) - offset); + } + + explicit operator size_t*() + { + return ptr; + } +}; + +// Simple representation that is like the pagemap. +// Bottom bit of left is used to store the rank parity. +// We shift the fields up to make room for the parity bit. +struct node +{ + size_t left; + size_t right; +}; + +inline static node array[2048]; + +class Rep +{ +public: + using key = uint16_t; + + static constexpr key null = 0; + static constexpr size_t root{NodeRef::offset << 1}; + + using Handle = NodeRef; + using Contents = uint16_t; + + static void set(Handle ptr, Contents r) + { + ptr.set(r); + } + + static Contents get(Handle ptr) + { + return static_cast(ptr); + } + + static Handle ref(bool direction, key k) + { + if (direction) + return {&array[k].left}; + else + return {&array[k].right}; + } + + static bool tree_tag(key k) + { + return (array[k].left & 1) == 1; + } + + static void set_tree_tag(key k, uint8_t new_tree_tag) + { + bool new_tag = (new_tree_tag & 1) != 0; + if (new_tag != tree_tag(k)) + array[k].left ^= 1; + } + + static bool compare(key k1, key k2) + { + return k1 > k2; + } + + static bool equal(key k1, key k2) + { + return k1 == k2; + } + + static size_t printable(key k) + { + return k; + } + + static size_t* printable(NodeRef k) + { + return static_cast(k); + } + + static const char* name() + { + return "TestRep"; + } +}; + +template +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::WeakAVLTree tree; + std::vector 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(Rep::key(index))) + { + entries.push_back(Rep::key(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(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("--seed", 0); + auto size = opt.is("--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(size, seed); + } + + return 0; + } + + if (seed == 0 || size == 0) + { + std::cout << "Set both --seed and --size" << std::endl; + return 1; + } + + // Trace particular example + test(size, seed); + return 0; +} From f1f8bff6da482ea19d39d35e01e4b11300373152 Mon Sep 17 00:00:00 2001 From: Schrodinger ZHU Yifan Date: Wed, 25 Feb 2026 15:22:33 -0500 Subject: [PATCH 3/9] add comments --- src/snmalloc/ds_core/weakavltree.h | 186 ++++++++++++++++++++++++++--- 1 file changed, 171 insertions(+), 15 deletions(-) diff --git a/src/snmalloc/ds_core/weakavltree.h b/src/snmalloc/ds_core/weakavltree.h index cd8564ce5..8b0cd9a28 100644 --- a/src/snmalloc/ds_core/weakavltree.h +++ b/src/snmalloc/ds_core/weakavltree.h @@ -8,7 +8,37 @@ namespace snmalloc { /** - * Weak AVL tree implementation using 1-bit rank parity per node. + * Self-balancing binary search tree using the Weak AVL (WAVL) strategy, + * with rank encoded as a 1-bit parity per node. + * + * WAVL trees belong to the rank-balanced binary search tree framework + * (Ref 4), alongside AVL and Red-Black trees. + * + * Key Properties: + * - Relationship to Red-Black Trees: A WAVL tree can always be colored as + * a Red-Black tree. + * - Relationship to AVL Trees: An AVL tree meets all WAVL requirements. + * Insertion-only WAVL trees maintain the same structure as AVL trees. + * + * Rank-Based Balancing: + * Each node is assigned a rank (conceptually similar to height). The rank + * difference between a parent and its child is strictly 1 or 2. + * - Null nodes have rank -1; external/leaf nodes have rank 0. + * - Insertion may create a 0-difference; fixed by promoting the parent + * and propagating upwards using at most two rotations. + * - Deletion may create a 3-difference; fixed by demoting the parent + * and propagating upwards. + * + * Implementation Detail: + * Rank is implicitly maintained via a 1-bit parity tag per node. + * A node `n` is a 2-child of parent `p` when parity(n) == parity(p). + * promote/demote toggle the parity (increment/decrement rank mod 2). + * + * References: + * 1. https://maskray.me/blog/2025-12-14-weak-avl-tree + * 2. https://reviews.freebsd.org/D25480 + * 3. https://ics.uci.edu/~goodrich/teach/cs165/notes/WeakAVLTrees.pdf + * 4. https://dl.acm.org/doi/10.1145/2689412 (Rank-Balanced Trees) * * The representation must provide: * - types `Handle`, `Contents` @@ -138,16 +168,6 @@ namespace snmalloc toggle_parity(n); } - static void double_promote(K n) - { - UNUSED(n); - } - - static void double_demote(K n) - { - UNUSED(n); - } - static bool is_leaf(K n) { return !is_null(n) && is_null(child(n, Left)) && is_null(child(n, Right)); @@ -171,6 +191,14 @@ namespace snmalloc return Rep::equal(child(p, Left), n) ? child(p, Right) : child(p, Left); } + // Rotate x up over its parent z (x was z's left child). + // + // (z) (x) + // / ╲ / ╲ + // (x) (D) => (A) (z) + // / ╲ / ╲ + // (A) (y) (y) (D) + // void rotate_right_at(K x) { K z = parent(x); @@ -198,6 +226,14 @@ namespace snmalloc set_parent(y, z); } + // Rotate x up over its parent z (x was z's right child). + // + // (z) (x) + // / ╲ / ╲ + // (A) (x) => (z) (D) + // / ╲ / ╲ + // (y) (D) (A) (y) + // void rotate_left_at(K x) { K z = parent(x); @@ -225,6 +261,17 @@ namespace snmalloc set_parent(y, z); } + // Double rotation: y (x's right child, z's left grandchild) rises to top. + // x is the left child of z. + // + // (z) (y) + // / ╲ / ╲ + // (x) (D) => (x) (z) + // / ╲ / ╲ / ╲ + // (A) (y) (A)(yL)(yR)(D) + // / ╲ + // (yL) (yR) + // void double_rotate_right_at(K y) { K x = parent(y); @@ -259,6 +306,17 @@ namespace snmalloc set_parent(z, y); } + // Double rotation: y (x's left child, z's right grandchild) rises to top. + // x is the right child of z. + // + // (z) (y) + // / ╲ / ╲ + // (A) (x) => (z) (x) + // / ╲ / ╲ / ╲ + // (y) (D) (A)(yL)(yR)(D) + // / ╲ + // (yL) (yR) + // void double_rotate_left_at(K y) { K x = parent(y); @@ -301,6 +359,17 @@ namespace snmalloc bool par_p_x = false; bool par_s_x = false; + // Case 1: x and its sibling are both 1-children of p_x (same parity + // difference from p_x). Promote p_x to resolve the 0-difference, then + // continue upwards since p_x may now violate with its own parent. + // + // (GP) (GP) + // | x Promote | x-1 + // | -----> (P) + // 0 | 1 / ╲ 1 + // (N) -- (P) (N) (S) + // ╲ 1 + // (S) do { promote(p_x); @@ -316,13 +385,32 @@ namespace snmalloc } while ( (!par_x && !par_p_x && par_s_x) || (par_x && par_p_x && !par_s_x)); + // Case 2: x is already a 2-child of p_x; no violation remains. + // + // (P) (P) + // / ╲ / ╲ + // 2 1 => 1 1 + // / ╲ / ╲ + // (N) (*) (N) (*) if (!((par_x && par_p_x && par_s_x) || (!par_x && !par_p_x && !par_s_x))) return; + // At this point x is a 1-child of p_x but p_x's sibling is a 2-child; + // a single or double rotation can restore balance. K z = parent(x); if (Rep::equal(x, child(p_x, Left))) { K y = child(x, Right); + // Case 3: x has a 1-child along the same direction (or no inner + // child). A single right rotation at x suffices. + // + // (GP) (GP) + // 0 | X Rotate | + // (N) -- (P) -----> (N) + // 1 / ╲ ╲ 2 1 / ╲ 1 + // (C1) ╲ (S) (C1) (P) + // (C2) 1 / ╲ 1 + // (C2) (S) if (is_null(y) || parity(y) == par_x) { rotate_right_at(x); @@ -331,6 +419,16 @@ namespace snmalloc } else { + // Case 4: x has a 1-child along the opposite direction. A + // double (zig-zag) rotation through y is required. + // + // (GP) (GP) + // 0 | X Zig-Zag | + // (N) -- (P) -----> (y) + // 2 / ╲ 1 ╲ 2 1 / ╲ 1 + // (y) (S) (N) (P) + // L / ╲ R 1/ ╲L R/ ╲1 + // (A) (B) (C2)(A) (B) (S) double_rotate_right_at(y); promote(y); demote(x); @@ -341,6 +439,7 @@ namespace snmalloc else { K y = child(x, Left); + // Case 3 (mirrored): single left rotation. if (is_null(y) || parity(y) == par_x) { rotate_left_at(x); @@ -349,6 +448,7 @@ namespace snmalloc } else { + // Case 4 (mirrored): double (zig-zag) rotation. double_rotate_left_at(y); promote(y); demote(x); @@ -417,6 +517,15 @@ namespace snmalloc creates_3_node = !is_null(p_p_x) && (parity(p_x) == parity(p_p_x)); + // Case 0: sibling y is a 2-child of p_x. Demote p_x to fix the + // 3-difference; this may push a violation upward. + // + // (P) (P) + // | X | (X+1) + // (C) | + // / ╲ => (C) + // 3 / ╲ 2 1 / ╲ 2 + // (*) (D) (*) (D) if (is_2_child(y, p_x)) { demote(p_x); @@ -424,6 +533,17 @@ namespace snmalloc else { bool y_parity = parity(y); + // Case 1/2: sibling y is a 1-child of p_x and is itself a 2-2 node + // (both its children are 2-children). Demote both p_x and y. + // + // (P) (P) + // | X | (X+1) + // (C) | + // 1 / ╲ => (C) + // (S) ╲ 3 1 / ╲ 2 + // 2 / ╲ 2 (D) (S) (D) + // (*) (*) 1 / ╲ 1 + // (*) (*) if ( y_parity == parity(child(y, Left)) && y_parity == parity(child(y, Right))) @@ -433,6 +553,7 @@ namespace snmalloc } else { + // Sibling cannot be demoted; rotation needed. done = false; break; } @@ -449,6 +570,17 @@ namespace snmalloc if (Rep::equal(x, child(p_x, Left))) { K w = child(y, Right); + // Case 3: sibling y has a 1-child w along the same direction as x. + // A single rotation at y restores balance. + // + // (P) (P) + // | X Rotate | X + // (C) -----> (S=y) + // 1 / ╲ 2 / ╲ 1 + // (S=y) ╲ 3 / (C) + // 1 / ╲ Y (D) (w=T) Y / ╲ 2 + // (w=T) ╲ (*) (D) + // (*) if (parity(w) != parity(y)) { rotate_left_at(y); @@ -459,18 +591,29 @@ namespace snmalloc } else { + // Case 4: sibling y has its rank-1 child v on the opposite side. + // A double (zig-zag) rotation through v is required. + // + // (P) (P) + // | X Zig-Zag | X + // (C) -----> (v) + // 1 / ╲ 2 / ╲ 2 + // (S=y) ╲ 3 (y) (C) + // 2 / ╲ 1 (D) 1 /Y ╲ / Z ╲ 1 + // (*) (v) (*) (A) (B) (D) + // Y / ╲ Z + // (A) (B) K v = child(y, Left); if constexpr (use_checks) SNMALLOC_ASSERT(parity(y) != parity(v)); double_rotate_left_at(v); - double_promote(v); demote(y); - double_demote(z); } } else { K w = child(y, Left); + // Case 3 (mirrored): single right rotation. if (parity(w) != parity(y)) { rotate_right_at(y); @@ -481,17 +624,30 @@ namespace snmalloc } else { + // Case 4 (mirrored): double rotation. K v = child(y, Right); if constexpr (use_checks) SNMALLOC_ASSERT(parity(y) != parity(v)); double_rotate_right_at(v); - double_promote(v); demote(y); - double_demote(z); } } } + // Handle the case where deletion has left `leaf` as a 2,2-leaf (both null + // children, but rank > 0). This corresponds to WAVL deletion Case 0. + // + // Case 0: the deleted node was a 1-child of its parent, so the parent's + // rank-difference on that side increments from 1 to 2, which is valid for + // internal nodes. But if the parent is now a leaf with both rank-diffs 2 + // (a 2,2-leaf), weak-AVL requires us to demote it. If that demotion turns + // the leaf into a 3-child of its own parent, we continue with the full + // 3-child rebalance. + // + // (P) (P) + // X / ╲ 1 => X / ╲ + // (*) (leaf) (*) ╲ 2 + // (leaf) void delete_rebalance_2_2_leaf(K leaf) { K x = leaf; From d00052a13175e2dd70bc553e32a32257546c60e0 Mon Sep 17 00:00:00 2001 From: Schrodinger ZHU Yifan Date: Wed, 25 Feb 2026 18:43:02 -0500 Subject: [PATCH 4/9] Fix format checks, add WAVL invariants, and restore large_alloc perf test (#1) --- .../backend_helpers/largebuddyrange.h | 18 ++--- src/snmalloc/ds_core/rankbalancetree.h | 40 ++++----- src/snmalloc/ds_core/redblacktree.h | 6 +- src/snmalloc/ds_core/weakavltree.h | 69 ++++++++++++++-- src/test/perf/large_alloc/large_alloc.cc | 81 +++++++++++++++++++ 5 files changed, 172 insertions(+), 42 deletions(-) create mode 100644 src/test/perf/large_alloc/large_alloc.cc diff --git a/src/snmalloc/backend_helpers/largebuddyrange.h b/src/snmalloc/backend_helpers/largebuddyrange.h index 0e9ff9f29..efb3c8d1d 100644 --- a/src/snmalloc/backend_helpers/largebuddyrange.h +++ b/src/snmalloc/backend_helpers/largebuddyrange.h @@ -16,11 +16,11 @@ namespace snmalloc { public: /* - * The values we store in our DefaultRBTree are the addresses of (combined spans - * of) chunks of the address space; as such, bits in (MIN_CHUNK_SIZE - 1) - * are unused and so the TREE_TAG_BIT is packed therein. However, in practice, - * these are not "just any" uintptr_t-s, but specifically the uintptr_t-s - * inside the Pagemap's BackendAllocator::Entry structures. + * The values we store in our DefaultRBTree are the addresses of (combined + * spans of) chunks of the address space; as such, bits in (MIN_CHUNK_SIZE - + * 1) are unused and so the TREE_TAG_BIT is packed therein. However, in + * practice, these are not "just any" uintptr_t-s, but specifically the + * uintptr_t-s inside the Pagemap's BackendAllocator::Entry structures. * * The BackendAllocator::Entry provides us with helpers that guarantee that * we use only the bits that we are allowed to. @@ -260,8 +260,8 @@ namespace snmalloc { range_to_pow_2_blocks( base, length, [this](capptr::Arena base, size_t align, bool) { - auto overflow = - capptr::Arena::unsafe_from(reinterpret_cast( + auto overflow = capptr::Arena::unsafe_from( + reinterpret_cast( buddy_large.add_block(base.unsafe_uintptr(), align))); dealloc_overflow(overflow); @@ -383,8 +383,8 @@ namespace snmalloc } } - auto overflow = - capptr::Arena::unsafe_from(reinterpret_cast( + auto overflow = capptr::Arena::unsafe_from( + reinterpret_cast( buddy_large.add_block(base.unsafe_uintptr(), size))); dealloc_overflow(overflow); } diff --git a/src/snmalloc/ds_core/rankbalancetree.h b/src/snmalloc/ds_core/rankbalancetree.h index ae4985327..b1d704300 100644 --- a/src/snmalloc/ds_core/rankbalancetree.h +++ b/src/snmalloc/ds_core/rankbalancetree.h @@ -18,15 +18,15 @@ namespace snmalloc */ template concept RBRepTypes = requires() { - typename Rep::Handle; - typename Rep::Contents; - }; + typename Rep::Handle; + typename Rep::Contents; + }; /** * The representation must define operations on the holder and contents * types. It must be able to 'dereference' a holder with `get`, assign to it - * with `set`, set and query the red/black colour of a node with `set_tree_tag` and - * `tree_tag`. + * with `set`, set and query the red/black colour of a node with + * `set_tree_tag` and `tree_tag`. * * The `ref` method provides uniform access to the children of a node, * returning a holder pointing to either the left or right child, depending on @@ -41,29 +41,17 @@ namespace snmalloc template concept RBRepMethods = requires(typename Rep::Handle hp, typename Rep::Contents k, bool b) { - { - Rep::get(hp) - } -> ConceptSame; - { - Rep::set(hp, k) - } -> ConceptSame; - { - Rep::tree_tag(k) - } -> ConceptSame; - { - Rep::set_tree_tag(k, b) - } -> ConceptSame; - { - Rep::ref(b, k) - } -> ConceptSame; - { - Rep::null - } -> ConceptSameModRef; + { Rep::get(hp) } -> ConceptSame; + { Rep::set(hp, k) } -> ConceptSame; + { Rep::tree_tag(k) } -> ConceptSame; + { Rep::set_tree_tag(k, b) } -> ConceptSame; + { Rep::ref(b, k) } -> ConceptSame; + { Rep::null } -> ConceptSameModRef; { typename Rep::Handle{const_cast< stl::remove_const_t>*>( &Rep::root)} - } -> ConceptSame; + } -> ConceptSame; }; template @@ -80,6 +68,6 @@ namespace snmalloc namespace snmalloc { - template - using DefaultRBTree = WeakAVLTree; + template + using DefaultRBTree = WeakAVLTree; } \ No newline at end of file diff --git a/src/snmalloc/ds_core/redblacktree.h b/src/snmalloc/ds_core/redblacktree.h index f61693308..c8fe880af 100644 --- a/src/snmalloc/ds_core/redblacktree.h +++ b/src/snmalloc/ds_core/redblacktree.h @@ -429,7 +429,8 @@ namespace snmalloc */ path.move(true); while (path.move(false)) - {} + { + } K curr = path.curr(); @@ -680,7 +681,8 @@ namespace snmalloc auto path = get_root_path(); while (path.move(true)) - {} + { + } K result = path.curr(); diff --git a/src/snmalloc/ds_core/weakavltree.h b/src/snmalloc/ds_core/weakavltree.h index 8b0cd9a28..e21237e5a 100644 --- a/src/snmalloc/ds_core/weakavltree.h +++ b/src/snmalloc/ds_core/weakavltree.h @@ -72,6 +72,62 @@ namespace snmalloc bool dir{Left}; }; + void invariant() + { + invariant(get_root()); + } + + /* + * Verify structural invariants. Returns the computed rank of `curr`. + */ + int invariant(K curr, K lower = Rep::null, K upper = Rep::null) + { + if constexpr (!use_checks) + { + UNUSED(curr, lower, upper); + return 0; + } + else + { + if (is_null(curr)) + return -1; + + if ( + ((lower != Rep::null) && Rep::compare(lower, curr)) || + ((upper != Rep::null) && Rep::compare(curr, upper))) + { + report_fatal_error( + "Invariant failed: {} is out of bounds {}..{}", + Rep::printable(curr), + Rep::printable(lower), + Rep::printable(upper)); + } + + K left = child(curr, Left); + K right = child(curr, Right); + + int left_rank = invariant(left, lower, curr); + int right_rank = invariant(right, curr, upper); + + int from_left = left_rank + (parity(curr) == parity(left) ? 2 : 1); + int from_right = right_rank + (parity(curr) == parity(right) ? 2 : 1); + if (from_left != from_right) + { + report_fatal_error( + "Invariant failed: {} has inconsistent ranks", + Rep::printable(curr)); + } + + if (is_null(left) && is_null(right) && (from_left != 0 || parity(curr))) + { + report_fatal_error( + "Invariant failed: {} is not a rank-0 leaf", Rep::printable(curr)); + } + + return from_left; + } + } + H root_ref() { return H{&root}; @@ -382,15 +438,15 @@ namespace snmalloc par_x = parity(x); par_p_x = parity(p_x); par_s_x = parity(sibling(x)); - } while ( - (!par_x && !par_p_x && par_s_x) || (par_x && par_p_x && !par_s_x)); + } while ((!par_x && !par_p_x && par_s_x) || + (par_x && par_p_x && !par_s_x)); // Case 2: x is already a 2-child of p_x; no violation remains. // // (P) (P) - // / ╲ / ╲ + // / ╲ / ╲ // 2 1 => 1 1 - // / ╲ / ╲ + // / ╲ / ╲ // (N) (*) (N) (*) if (!((par_x && par_p_x && par_s_x) || (!par_x && !par_p_x && !par_s_x))) return; @@ -513,7 +569,8 @@ namespace snmalloc do { K p_p_x = parent(p_x); - y = Rep::equal(child(p_x, Left), x) ? child(p_x, Right) : child(p_x, Left); + y = Rep::equal(child(p_x, Left), x) ? child(p_x, Right) : + child(p_x, Left); creates_3_node = !is_null(p_p_x) && (parity(p_x) == parity(p_p_x)); @@ -838,6 +895,7 @@ namespace snmalloc if (is_null(path.curr)) return false; erase_node(path.curr); + invariant(); return true; } @@ -847,6 +905,7 @@ namespace snmalloc SNMALLOC_ASSERT(is_null(path.curr)); insert_known_absent(value, path.parent, path.dir); path.curr = value; + invariant(); } RBPath get_root_path() diff --git a/src/test/perf/large_alloc/large_alloc.cc b/src/test/perf/large_alloc/large_alloc.cc new file mode 100644 index 000000000..1bffb1fe5 --- /dev/null +++ b/src/test/perf/large_alloc/large_alloc.cc @@ -0,0 +1,81 @@ +#include +#include +#include + +using namespace snmalloc; + +static constexpr size_t ALLOC_SIZE = 800 * 1024; // 800 KB +static constexpr size_t ITERATIONS = 100000; + +void test_alloc_dealloc_cycle() +{ + { + MeasureTime m; + m << "Alloc/dealloc 800KB x " << ITERATIONS; + + for (size_t i = 0; i < ITERATIONS; i++) + { + void* p = snmalloc::alloc(ALLOC_SIZE); + SNMALLOC_CHECK(p != nullptr); + snmalloc::dealloc(p); + } + } + + snmalloc::debug_check_empty(); +} + +void test_batch_alloc_then_dealloc() +{ + static constexpr size_t BATCH = 128; + + void* ptrs[BATCH]; + + MeasureTime m; + m << "Batch alloc then dealloc 800KB x " << BATCH; + for (size_t i = 0; i < ITERATIONS / BATCH; i++) + { + for (size_t i = 0; i < BATCH; i++) + { + ptrs[i] = snmalloc::alloc(ALLOC_SIZE); + SNMALLOC_CHECK(ptrs[i] != nullptr); + } + + for (size_t i = 0; i < BATCH; i++) + { + snmalloc::dealloc(ptrs[i]); + } + } + + snmalloc::debug_check_empty(); +} + +void test_alloc_dealloc_with_touch() +{ + { + MeasureTime m; + m << "Alloc/touch/dealloc 800KB x " << ITERATIONS; + + for (size_t i = 0; i < ITERATIONS; i++) + { + char* p = static_cast(snmalloc::alloc(ALLOC_SIZE)); + SNMALLOC_CHECK(p != nullptr); + // Touch first and last bytes to ensure pages are faulted in + p[0] = 1; + p[ALLOC_SIZE - 1] = 1; + snmalloc::dealloc(p); + } + } + + snmalloc::debug_check_empty(); +} + +int main(int, char**) +{ + setup(); + + test_alloc_dealloc_cycle(); + test_batch_alloc_then_dealloc(); + test_alloc_dealloc_with_touch(); + + return 0; +} From 1e25f93a12e1323c5ab0e30a032d3674e2964d38 Mon Sep 17 00:00:00 2001 From: Schrodinger ZHU Yifan Date: Wed, 25 Feb 2026 18:49:33 -0500 Subject: [PATCH 5/9] remove large alloc from this PR --- src/test/perf/large_alloc/large_alloc.cc | 81 ------------------------ 1 file changed, 81 deletions(-) delete mode 100644 src/test/perf/large_alloc/large_alloc.cc diff --git a/src/test/perf/large_alloc/large_alloc.cc b/src/test/perf/large_alloc/large_alloc.cc deleted file mode 100644 index 1bffb1fe5..000000000 --- a/src/test/perf/large_alloc/large_alloc.cc +++ /dev/null @@ -1,81 +0,0 @@ -#include -#include -#include - -using namespace snmalloc; - -static constexpr size_t ALLOC_SIZE = 800 * 1024; // 800 KB -static constexpr size_t ITERATIONS = 100000; - -void test_alloc_dealloc_cycle() -{ - { - MeasureTime m; - m << "Alloc/dealloc 800KB x " << ITERATIONS; - - for (size_t i = 0; i < ITERATIONS; i++) - { - void* p = snmalloc::alloc(ALLOC_SIZE); - SNMALLOC_CHECK(p != nullptr); - snmalloc::dealloc(p); - } - } - - snmalloc::debug_check_empty(); -} - -void test_batch_alloc_then_dealloc() -{ - static constexpr size_t BATCH = 128; - - void* ptrs[BATCH]; - - MeasureTime m; - m << "Batch alloc then dealloc 800KB x " << BATCH; - for (size_t i = 0; i < ITERATIONS / BATCH; i++) - { - for (size_t i = 0; i < BATCH; i++) - { - ptrs[i] = snmalloc::alloc(ALLOC_SIZE); - SNMALLOC_CHECK(ptrs[i] != nullptr); - } - - for (size_t i = 0; i < BATCH; i++) - { - snmalloc::dealloc(ptrs[i]); - } - } - - snmalloc::debug_check_empty(); -} - -void test_alloc_dealloc_with_touch() -{ - { - MeasureTime m; - m << "Alloc/touch/dealloc 800KB x " << ITERATIONS; - - for (size_t i = 0; i < ITERATIONS; i++) - { - char* p = static_cast(snmalloc::alloc(ALLOC_SIZE)); - SNMALLOC_CHECK(p != nullptr); - // Touch first and last bytes to ensure pages are faulted in - p[0] = 1; - p[ALLOC_SIZE - 1] = 1; - snmalloc::dealloc(p); - } - } - - snmalloc::debug_check_empty(); -} - -int main(int, char**) -{ - setup(); - - test_alloc_dealloc_cycle(); - test_batch_alloc_then_dealloc(); - test_alloc_dealloc_with_touch(); - - return 0; -} From e238e7a30ebef5d41a6a34d1320f0fde7cd39b20 Mon Sep 17 00:00:00 2001 From: Schrodinger ZHU Yifan Date: Wed, 25 Feb 2026 18:54:10 -0500 Subject: [PATCH 6/9] format --- .../backend_helpers/largebuddyrange.h | 8 ++--- src/snmalloc/ds_core/rankbalancetree.h | 32 +++++++++++++------ src/snmalloc/ds_core/redblacktree.h | 6 ++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/snmalloc/backend_helpers/largebuddyrange.h b/src/snmalloc/backend_helpers/largebuddyrange.h index efb3c8d1d..6e2925780 100644 --- a/src/snmalloc/backend_helpers/largebuddyrange.h +++ b/src/snmalloc/backend_helpers/largebuddyrange.h @@ -260,8 +260,8 @@ namespace snmalloc { range_to_pow_2_blocks( base, length, [this](capptr::Arena base, size_t align, bool) { - auto overflow = capptr::Arena::unsafe_from( - reinterpret_cast( + auto overflow = + capptr::Arena::unsafe_from(reinterpret_cast( buddy_large.add_block(base.unsafe_uintptr(), align))); dealloc_overflow(overflow); @@ -383,8 +383,8 @@ namespace snmalloc } } - auto overflow = capptr::Arena::unsafe_from( - reinterpret_cast( + auto overflow = + capptr::Arena::unsafe_from(reinterpret_cast( buddy_large.add_block(base.unsafe_uintptr(), size))); dealloc_overflow(overflow); } diff --git a/src/snmalloc/ds_core/rankbalancetree.h b/src/snmalloc/ds_core/rankbalancetree.h index b1d704300..98ea3f821 100644 --- a/src/snmalloc/ds_core/rankbalancetree.h +++ b/src/snmalloc/ds_core/rankbalancetree.h @@ -18,9 +18,9 @@ namespace snmalloc */ template concept RBRepTypes = requires() { - typename Rep::Handle; - typename Rep::Contents; - }; + typename Rep::Handle; + typename Rep::Contents; + }; /** * The representation must define operations on the holder and contents @@ -41,17 +41,29 @@ namespace snmalloc template concept RBRepMethods = requires(typename Rep::Handle hp, typename Rep::Contents k, bool b) { - { Rep::get(hp) } -> ConceptSame; - { Rep::set(hp, k) } -> ConceptSame; - { Rep::tree_tag(k) } -> ConceptSame; - { Rep::set_tree_tag(k, b) } -> ConceptSame; - { Rep::ref(b, k) } -> ConceptSame; - { Rep::null } -> ConceptSameModRef; + { + Rep::get(hp) + } -> ConceptSame; + { + Rep::set(hp, k) + } -> ConceptSame; + { + Rep::tree_tag(k) + } -> ConceptSame; + { + Rep::set_tree_tag(k, b) + } -> ConceptSame; + { + Rep::ref(b, k) + } -> ConceptSame; + { + Rep::null + } -> ConceptSameModRef; { typename Rep::Handle{const_cast< stl::remove_const_t>*>( &Rep::root)} - } -> ConceptSame; + } -> ConceptSame; }; template diff --git a/src/snmalloc/ds_core/redblacktree.h b/src/snmalloc/ds_core/redblacktree.h index c8fe880af..f61693308 100644 --- a/src/snmalloc/ds_core/redblacktree.h +++ b/src/snmalloc/ds_core/redblacktree.h @@ -429,8 +429,7 @@ namespace snmalloc */ path.move(true); while (path.move(false)) - { - } + {} K curr = path.curr(); @@ -681,8 +680,7 @@ namespace snmalloc auto path = get_root_path(); while (path.move(true)) - { - } + {} K result = path.curr(); From 68c08919263cb780d98d2653df6941878e4f8350 Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Sun, 22 Feb 2026 15:57:43 +0000 Subject: [PATCH 7/9] Remove unnecessary init from fast path --- src/snmalloc/ds_core/redblacktree.h | 11 +++++++++-- src/snmalloc/mem/metadata.h | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/snmalloc/ds_core/redblacktree.h b/src/snmalloc/ds_core/redblacktree.h index f61693308..0c1595ab7 100644 --- a/src/snmalloc/ds_core/redblacktree.h +++ b/src/snmalloc/ds_core/redblacktree.h @@ -34,7 +34,7 @@ namespace snmalloc H ptr; public: - ChildRef() : ptr(nullptr) {} + constexpr ChildRef() = default; ChildRef(H p) : ptr(p) {} @@ -165,7 +165,14 @@ namespace snmalloc struct RBStep { ChildRef node; - bool dir = false; + bool dir; + + // Default constructor needed for Array. + constexpr RBStep() = default; + + // Remove copy constructors to avoid accidentally copying and mutating the path. + RBStep(const RBStep& other) = delete; + RBStep& operator=(const RBStep& other) = delete; /** * Update the step to point to a new node and direction. diff --git a/src/snmalloc/mem/metadata.h b/src/snmalloc/mem/metadata.h index d034f5f3b..0284e8a5d 100644 --- a/src/snmalloc/mem/metadata.h +++ b/src/snmalloc/mem/metadata.h @@ -282,6 +282,11 @@ namespace snmalloc uintptr_t* val; public: + /** + * Uninitialised constructor. + */ + BackendStateWordRef() = default; + /** * Constructor, wraps a `uintptr_t`. Note that this may be used outside * of the meta entry by code wishing to provide uniform storage to things From 341b72085971ea2c6887ea9391f2acce07d463fb Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Thu, 26 Feb 2026 12:02:58 +0000 Subject: [PATCH 8/9] Clangformat --- src/snmalloc/ds_core/redblacktree.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/snmalloc/ds_core/redblacktree.h b/src/snmalloc/ds_core/redblacktree.h index 0c1595ab7..c3fd789c4 100644 --- a/src/snmalloc/ds_core/redblacktree.h +++ b/src/snmalloc/ds_core/redblacktree.h @@ -170,7 +170,8 @@ namespace snmalloc // Default constructor needed for Array. constexpr RBStep() = default; - // Remove copy constructors to avoid accidentally copying and mutating the path. + // Remove copy constructors to avoid accidentally copying and mutating the + // path. RBStep(const RBStep& other) = delete; RBStep& operator=(const RBStep& other) = delete; From 3b950d49e3d5669d3b698f8dea1677662b79a7ad Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Sun, 22 Feb 2026 10:16:38 +0000 Subject: [PATCH 9/9] Benchmark for repeated large deallocations. --- src/test/perf/large_alloc/large_alloc.cc | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/test/perf/large_alloc/large_alloc.cc diff --git a/src/test/perf/large_alloc/large_alloc.cc b/src/test/perf/large_alloc/large_alloc.cc new file mode 100644 index 000000000..2532b41e3 --- /dev/null +++ b/src/test/perf/large_alloc/large_alloc.cc @@ -0,0 +1,83 @@ +#include +#include +#include + +using namespace snmalloc; + +static constexpr size_t ALLOC_SIZE = 800 * 1024; // 800 KB +static constexpr size_t ITERATIONS = 100000; + +void test_alloc_dealloc_cycle() +{ + { + MeasureTime m; + m << "Alloc/dealloc 800KB x " << ITERATIONS; + + for (size_t i = 0; i < ITERATIONS; i++) + { + void* p = snmalloc::alloc(ALLOC_SIZE); + SNMALLOC_CHECK(p != nullptr); + snmalloc::dealloc(p); + } + } + + snmalloc::debug_check_empty(); +} + +void test_batch_alloc_then_dealloc() +{ + static constexpr size_t BATCH = 128; + + void* ptrs[BATCH]; + + MeasureTime m; + m << "Batch alloc then dealloc 800KB x " << BATCH; + for (size_t j = 0; j < ITERATIONS / BATCH; j++) + { + for (size_t i = 0; i < BATCH; i++) + { + ptrs[i] = snmalloc::alloc(ALLOC_SIZE); + SNMALLOC_CHECK(ptrs[i] != nullptr); + } + + for (size_t i = 0; i < BATCH; i++) + { + snmalloc::dealloc(ptrs[i]); + } + } + + snmalloc::debug_check_empty(); +} + +void test_alloc_dealloc_with_touch() +{ + { + MeasureTime m; + m << "Alloc/touch/dealloc 800KB x " << ITERATIONS; + + for (size_t i = 0; i < ITERATIONS; i++) + { + char* p = static_cast(snmalloc::alloc(ALLOC_SIZE)); + SNMALLOC_CHECK(p != nullptr); + // Touch every 4KiB and last bytes to ensure pages are faulted in + for (size_t offset = 0; offset < ALLOC_SIZE; offset += 4096) + { + p[offset] = 1; + } + snmalloc::dealloc(p); + } + } + + snmalloc::debug_check_empty(); +} + +int main(int, char**) +{ + setup(); + + test_alloc_dealloc_cycle(); + test_batch_alloc_then_dealloc(); + test_alloc_dealloc_with_touch(); + + return 0; +}