From f115a1a153d9117e46cfb5c3db2e09c2c37fe556 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Tue, 2 Jun 2026 21:41:48 -0700 Subject: [PATCH 01/11] Reimplement ghost_tree iteratively instead of recurisively (Rust) --- src/alloc/ghost_tree.rs | 375 ++++++++++++++++++++++------------------ 1 file changed, 210 insertions(+), 165 deletions(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 87f9eb0..4158d90 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -34,6 +34,20 @@ const NODE_HEIGHT_OFF: u64 = 9; // u8 height (max ~59 for balanced; slightly mor const NODE_LEFT_OFF: u64 = 16; const NODE_RIGHT_OFF: u64 = 24; +/// A node visited during a downward AVL tree traversal. +/// +/// Used by [`avl_insert`](GhostTreeBstackAllocator::avl_insert) and +/// [`avl_find_best_fit_and_remove`](GhostTreeBstackAllocator::avl_find_best_fit_and_remove) +/// to record the path so that balance factors and heights can be updated on +/// the way back up without recursion. +struct PathEntry { + ptr: u64, + size: u64, + left: u64, + right: u64, + went_left: bool, +} + /// A pure-AVL general-purpose allocator built on top of a [`BStack`]. /// /// Free blocks store their AVL node inline at offset 0 within the block — @@ -374,190 +388,202 @@ impl GhostTreeBstackAllocator { } } - /// Recursive insert into subtree at `root`; return new subtree root. - fn avl_insert_rec(&self, root: u64, ptr: u64, size: u64, depth: u32) -> io::Result { - if root == NULL_PTR { - self.write_node(ptr, size, 0, 1, NULL_PTR, NULL_PTR)?; - return Ok(ptr); - } - if depth == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "AVL insert exceeded maximum depth: corrupted tree (possible cycle)", - )); - } - let (root_sz, _, _, left, right) = self.read_node(root)?; - if (size, ptr) < (root_sz, root) { - let new_left = self.avl_insert_rec(left, ptr, size, depth - 1)?; - self.avl_write_and_update(root, root_sz, new_left, right)?; - } else { - let new_right = self.avl_insert_rec(right, ptr, size, depth - 1)?; - self.avl_write_and_update(root, root_sz, left, new_right)?; - } - self.avl_rebalance(root) - } - /// Insert a free block at `ptr` with `size` bytes into the AVL tree. fn avl_insert(&self, ptr: u64, size: u64) -> io::Result<()> { let root = self.read_root()?; - let new_root = self.avl_insert_rec(root, ptr, size, MAX_AVL_DEPTH)?; - self.write_root(new_root) - } - /// Return `(ptr, size)` of the leftmost (minimum-key) node in `subtree`. - fn avl_min(&self, subtree: u64, depth: u32) -> io::Result<(u64, u64)> { - let (size, _, _, left, _) = self.read_node(subtree)?; - if left == NULL_PTR { - Ok((subtree, size)) - } else { - if depth == 0 { + // Down-pass: walk to the insertion position, recording the path. + let mut path: Vec = Vec::new(); + let mut current = root; + while current != NULL_PTR { + if path.len() >= MAX_AVL_DEPTH as usize { return Err(io::Error::new( io::ErrorKind::InvalidData, - "AVL min exceeded maximum depth: corrupted tree (possible cycle)", + "AVL insert exceeded maximum depth: corrupted tree (possible cycle)", )); } - self.avl_min(left, depth - 1) + let (root_sz, _, _, left, right) = self.read_node(current)?; + let went_left = (size, ptr) < (root_sz, current); + path.push(PathEntry { + ptr: current, + size: root_sz, + left, + right, + went_left, + }); + current = if went_left { left } else { right }; } - } - /// Recursive remove of `(size, ptr)` from subtree at `root`; return new root. - fn avl_remove_rec(&self, root: u64, ptr: u64, size: u64, depth: u32) -> io::Result { - if root == NULL_PTR { - return Ok(NULL_PTR); - } - if depth == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "AVL remove exceeded maximum depth: corrupted tree (possible cycle)", - )); + // Write the new leaf. + self.write_node(ptr, size, 0, 1, NULL_PTR, NULL_PTR)?; + + // Up-pass: propagate the new child pointer and rebalance each ancestor. + let mut child = ptr; + for entry in path.iter().rev() { + let (new_left, new_right) = if entry.went_left { + (child, entry.right) + } else { + (entry.left, child) + }; + self.avl_write_and_update(entry.ptr, entry.size, new_left, new_right)?; + child = self.avl_rebalance(entry.ptr)?; } - let (root_sz, _, _, left, right) = self.read_node(root)?; - if (size, ptr) < (root_sz, root) { - let new_left = self.avl_remove_rec(left, ptr, size, depth - 1)?; - self.avl_write_and_update(root, root_sz, new_left, right)?; - return self.avl_rebalance(root); - } - if (size, ptr) > (root_sz, root) { - let new_right = self.avl_remove_rec(right, ptr, size, depth - 1)?; - self.avl_write_and_update(root, root_sz, left, new_right)?; - return self.avl_rebalance(root); - } - // Found the node to remove. - if left == NULL_PTR { - return Ok(right); - } - if right == NULL_PTR { - return Ok(left); - } - // Two children: replace with the in-order successor (leftmost of right - // subtree), then delete the successor from the right subtree. - let (succ, succ_sz) = self.avl_min(right, depth - 1)?; - let new_right = self.avl_remove_rec(right, succ, succ_sz, depth - 1)?; - self.avl_write_and_update(succ, succ_sz, left, new_right)?; - self.avl_rebalance(succ) + self.write_root(child) } - /// Find and remove the best-fit block (smallest block ≥ `min_size`) from - /// the subtree at `root` in a single O(log n) pass. - /// - /// Returns `(new_subtree_root, Option<(found_ptr, found_size)>)`. + /// Remove the minimum-key (leftmost) node from the subtree rooted at `root`, + /// rebalancing the path back up. /// - /// Strategy: when the current node fits, recurse left to try to find a - /// smaller fit. If the left subtree yields a candidate, wire the updated - /// left child back and rebalance — the current node stays in the tree. If - /// not, the current node *is* the best fit and is removed using the standard - /// two-child replacement (in-order successor). - fn avl_find_best_fit_and_remove_rec( - &self, - root: u64, - min_size: u64, - depth: u32, - ) -> io::Result<(u64, Option<(u64, u64)>)> { - if root == NULL_PTR { - return Ok((NULL_PTR, None)); - } - if depth == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "AVL find exceeded maximum depth: corrupted tree (possible cycle)", - )); - } - let (root_sz, _, _, left, right) = self.read_node(root)?; - if root_sz >= min_size { - // This node fits — try left for something smaller. - let (new_left, found) = - self.avl_find_best_fit_and_remove_rec(left, min_size, depth - 1)?; - if let Some(candidate) = found { - // A smaller fit was found; keep root, update its left child. - self.avl_write_and_update(root, root_sz, new_left, right)?; - let new_root = self.avl_rebalance(root)?; - return Ok((new_root, Some(candidate))); + /// Returns `(min_ptr, min_size, new_subtree_root)`. The minimum node always + /// has no left child, so its replacement is its right child (or [`NULL_PTR`]). + fn avl_remove_min(&self, root: u64) -> io::Result<(u64, u64, u64)> { + // Walk left, recording (ptr, size, right_child) for each ancestor. + let mut path: Vec<(u64, u64, u64)> = Vec::new(); + let mut current = root; + loop { + let (size, _, _, left, right) = self.read_node(current)?; + if left == NULL_PTR { + // `current` is the minimum; replace it with its right child. + let mut child = right; + for &(anc_ptr, anc_sz, anc_right) in path.iter().rev() { + self.avl_write_and_update(anc_ptr, anc_sz, child, anc_right)?; + child = self.avl_rebalance(anc_ptr)?; + } + return Ok((current, size, child)); } - // No smaller fit in the left subtree — remove root itself. - // Use `new_left` (not the stale `left`): even though no node was - // removed, the recursive call may have rebalanced the left subtree, - // changing its root pointer. - let new_root = if new_left == NULL_PTR { - right - } else if right == NULL_PTR { - new_left - } else { - let (succ, succ_sz) = self.avl_min(right, depth - 1)?; - let new_right = self.avl_remove_rec(right, succ, succ_sz, depth - 1)?; - self.avl_write_and_update(succ, succ_sz, new_left, new_right)?; - self.avl_rebalance(succ)? - }; - Ok((new_root, Some((root, root_sz)))) - } else { - // Too small — only right subtree can have a fit. - let (new_right, found) = - self.avl_find_best_fit_and_remove_rec(right, min_size, depth - 1)?; - // Only update the tree structure if a node was actually removed. - // Updating unconditionally would corrupt child pointers on a no-fit - // path (the recursive call may have rebalanced without removing). - if found.is_some() { - self.avl_write_and_update(root, root_sz, left, new_right)?; - let new_root = self.avl_rebalance(root)?; - Ok((new_root, found)) - } else { - Ok((root, None)) + if path.len() >= MAX_AVL_DEPTH as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "AVL min exceeded maximum depth: corrupted tree (possible cycle)", + )); } + path.push((current, size, right)); + current = left; } } /// Find and remove the best-fit block (smallest block ≥ `min_size`). /// /// Returns `(ptr, size)`, or `None` if no block fits. + /// + /// Strategy: when the current node fits, go left to try to find a smaller + /// fit. The best fit is the last fitting node encountered before the + /// traversal exhausts the left subtree. Path entries after that index + /// searched the best-fit node's left subtree and found nothing — they + /// require no updates. fn avl_find_best_fit_and_remove(&self, min_size: u64) -> io::Result> { let root = self.read_root()?; - let (new_root, found) = - self.avl_find_best_fit_and_remove_rec(root, min_size, MAX_AVL_DEPTH)?; - self.write_root(new_root)?; - Ok(found) + if root == NULL_PTR { + return Ok(None); + } + + // Down-pass: record the full traversal path and the index of the last + // node that satisfies size >= min_size (the best fit). + let mut path: Vec = Vec::new(); + let mut last_fit_idx: Option = None; + let mut current = root; + while current != NULL_PTR { + if path.len() >= MAX_AVL_DEPTH as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "AVL find exceeded maximum depth: corrupted tree (possible cycle)", + )); + } + let (root_sz, _, _, left, right) = self.read_node(current)?; + if root_sz >= min_size { + last_fit_idx = Some(path.len()); + path.push(PathEntry { + ptr: current, + size: root_sz, + left, + right, + went_left: true, + }); + current = left; + } else { + path.push(PathEntry { + ptr: current, + size: root_sz, + left, + right, + went_left: false, + }); + current = right; + } + } + + let fit_idx = match last_fit_idx { + None => return Ok(None), + Some(i) => i, + }; + + let found_ptr = path[fit_idx].ptr; + let found_size = path[fit_idx].size; + let found_left = path[fit_idx].left; + let found_right = path[fit_idx].right; + + // Remove the best-fit node. The left subtree (path[fit_idx+1..]) was + // searched and yielded nothing, so found_left is returned unchanged. + let replacement = if found_left == NULL_PTR { + found_right + } else if found_right == NULL_PTR { + found_left + } else { + // Two children: replace with in-order successor (min of right subtree). + let (succ, succ_sz, new_right) = self.avl_remove_min(found_right)?; + self.avl_write_and_update(succ, succ_sz, found_left, new_right)?; + self.avl_rebalance(succ)? + }; + + // Up-pass: update path[0..fit_idx] (path[fit_idx] was removed). + let mut child = replacement; + for entry in path[..fit_idx].iter().rev() { + let (new_left, new_right) = if entry.went_left { + (child, entry.right) + } else { + (entry.left, child) + }; + self.avl_write_and_update(entry.ptr, entry.size, new_left, new_right)?; + child = self.avl_rebalance(entry.ptr)?; + } + self.write_root(child)?; + Ok(Some((found_ptr, found_size))) } /// In-order walk of the subtree at `root`, calling `f(ptr, size)` per node. /// Tolerates imbalance — visits every reachable node. Returns `InvalidData` - /// if `depth` reaches zero (cycle guard against corrupted trees). + /// if the traversal stack exceeds [`MAX_AVL_DEPTH`] (cycle guard). fn avl_walk_inorder( &self, root: u64, - depth: u32, f: &mut dyn FnMut(u64, u64) -> io::Result<()>, ) -> io::Result<()> { - if root == NULL_PTR { - return Ok(()); - } - if depth == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "AVL walk exceeded maximum depth: corrupted tree (possible cycle)", - )); + // Each stack entry is `(ptr, right_child, size)` for a node whose left + // subtree is currently being visited. + let mut stack: Vec<(u64, u64, u64)> = Vec::new(); + let mut current = root; + loop { + // Descend left, pushing nodes onto the stack. + while current != NULL_PTR { + if stack.len() >= MAX_AVL_DEPTH as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "AVL walk exceeded maximum depth: corrupted tree (possible cycle)", + )); + } + let (size, _, _, left, right) = self.read_node(current)?; + stack.push((current, right, size)); + current = left; + } + // Pop and visit; then follow the right child. + match stack.pop() { + None => return Ok(()), + Some((ptr, right, size)) => { + f(ptr, size)?; + current = right; + } + } } - let (size, _, _, left, right) = self.read_node(root)?; - self.avl_walk_inorder(left, depth - 1, f)?; - f(root, size)?; - self.avl_walk_inorder(right, depth - 1, f) } /// Collect all free blocks, merge adjacent ones, and rebuild a balanced AVL @@ -571,7 +597,7 @@ impl GhostTreeBstackAllocator { // Step 1: collect all free blocks in key order let root = self.read_root()?; let mut blocks: Vec<(u64, u64)> = Vec::new(); // (ptr, size) - self.avl_walk_inorder(root, MAX_AVL_DEPTH, &mut |ptr, size| { + self.avl_walk_inorder(root, &mut |ptr, size| { blocks.push((ptr, size)); Ok(()) })?; @@ -610,25 +636,44 @@ impl GhostTreeBstackAllocator { // Step 4: rebuild a balanced AVL tree // Coalescing sorted by address; now re-sort by the tree's key (size, ptr) - // so `build` produces a valid BST. Without this, insert/remove would + // so the build produces a valid BST. Without this, insert/remove would // navigate by (size, ptr) into an address-ordered tree and miss nodes. coalesced.sort_by_key(|&(ptr, size)| (size, ptr)); - // Recursive helper: build an optimally balanced BST from a sorted slice, - // writing each node and returning the root ptr. - fn build(this: &GhostTreeBstackAllocator, blocks: &[(u64, u64)]) -> io::Result { - if blocks.is_empty() { - return Ok(NULL_PTR); + // Iterative balanced BST build using an explicit ops stack. + // Enter(lo, hi) — process range [lo, hi): push Combine(mid), then + // Enter(mid+1, hi), then Enter(lo, mid) (reverse order so left + // executes first). + // Combine(i) — pop right_root then left_root from results, write + // coalesced[i] as a node, push its ptr onto results. + enum BuildOp { + Enter(usize, usize), + Combine(usize), + } + let mut ops: Vec = vec![BuildOp::Enter(0, coalesced.len())]; + let mut results: Vec = Vec::new(); + while let Some(op) = ops.pop() { + match op { + BuildOp::Enter(lo, hi) => { + if lo >= hi { + results.push(NULL_PTR); + } else { + let mid = lo + (hi - lo) / 2; + ops.push(BuildOp::Combine(mid)); + ops.push(BuildOp::Enter(mid + 1, hi)); + ops.push(BuildOp::Enter(lo, mid)); + } + } + BuildOp::Combine(i) => { + let right_root = results.pop().unwrap(); + let left_root = results.pop().unwrap(); + let (ptr, size) = coalesced[i]; + self.avl_write_and_update(ptr, size, left_root, right_root)?; + results.push(ptr); + } } - let mid = blocks.len() / 2; - let (ptr, size) = blocks[mid]; - let left = build(this, &blocks[..mid])?; - let right = build(this, &blocks[mid + 1..])?; - this.avl_write_and_update(ptr, size, left, right)?; - Ok(ptr) } - - let new_root = build(self, &coalesced)?; + let new_root = results.pop().unwrap_or(NULL_PTR); self.write_root(new_root) } } From 161a2174f57764be76f8e26e6b344c754912ba99 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Tue, 2 Jun 2026 21:54:58 -0700 Subject: [PATCH 02/11] Reimplement ghost_tree iteratively instead of recurisively (C) --- c/bstack_alloc.c | 453 ++++++++++++++++++++++++++++------------------- 1 file changed, 268 insertions(+), 185 deletions(-) diff --git a/c/bstack_alloc.c b/c/bstack_alloc.c index 7c96893..7aa8330 100644 --- a/c/bstack_alloc.c +++ b/c/bstack_alloc.c @@ -2021,6 +2021,14 @@ bstack_t *first_fit_bstack_allocator_into_stack(first_fit_bstack_allocator_t *al static const uint8_t algt_magic[8] = {'A','L','G','T',0,1,1,0}; static const uint8_t algt_magic_prefix[6] = {'A','L','G','T',0,1}; +typedef struct { + uint64_t ptr; + uint64_t size; + uint64_t left; + uint64_t right; + int went_left; +} algt_path_entry_t; + /* ---- alignment helpers ------------------------------------------------- */ /* Round len up to a multiple of 32, minimum 32. */ @@ -2114,23 +2122,6 @@ static int algt_avl_write_and_update(bstack_t *bs, uint64_t ptr, /* ---- AVL helpers ------------------------------------------------------- */ -/* Return the leftmost (minimum-key) node in subtree. */ -static int algt_avl_min(bstack_t *bs, uint64_t subtree, uint32_t depth, - uint64_t *out_ptr, uint64_t *out_size) -{ - uint64_t size, left, right; - int8_t bf; uint8_t height; - if (depth == 0) { errno = EINVAL; return -1; } - if (algt_read_node(bs, subtree, &size, &bf, &height, &left, &right) != 0) - return -1; - if (left == ALGT_NULL_PTR) { - *out_ptr = subtree; - *out_size = size; - return 0; - } - return algt_avl_min(bs, left, depth - 1, out_ptr, out_size); -} - /* Right-rotate around node; return the new subtree root. */ static int algt_avl_rotate_right(bstack_t *bs, uint64_t node, uint64_t *out_root) { @@ -2199,86 +2190,100 @@ static int algt_avl_rebalance(bstack_t *bs, uint64_t node, uint64_t *out_root) /* ---- AVL insert -------------------------------------------------------- */ -static int algt_avl_insert_rec(bstack_t *bs, uint64_t root, - uint64_t ptr, uint64_t size, uint32_t depth, uint64_t *out_root) +static int algt_avl_insert(bstack_t *bs, uint64_t ptr, uint64_t size) { - if (root == ALGT_NULL_PTR) { + algt_path_entry_t path[ALGT_MAX_AVL_DEPTH]; + size_t path_len = 0; + uint64_t root, current, child; + int i; + + if (algt_read_root(bs, &root) != 0) return -1; + + current = root; + while (current != ALGT_NULL_PTR) { + uint64_t root_sz, left, right; + int8_t bf; uint8_t height; + int went_left; + if (path_len >= ALGT_MAX_AVL_DEPTH) { errno = EINVAL; return -1; } + if (algt_read_node(bs, current, &root_sz, &bf, &height, &left, &right) != 0) + return -1; + went_left = (size < root_sz || (size == root_sz && ptr < current)); + path[path_len].ptr = current; + path[path_len].size = root_sz; + path[path_len].left = left; + path[path_len].right = right; + path[path_len].went_left = went_left; + path_len++; + current = went_left ? left : right; + } + + { uint8_t buf[32]; memset(buf, 0, 32); write_le64(buf, size); - buf[9] = 1; /* height = 1, bf = 0 */ + buf[9] = 1; if (bstack_set(bs, ptr, buf, 32) != 0) return -1; - *out_root = ptr; - return 0; } - if (depth == 0) { errno = EINVAL; return -1; } - { - uint64_t root_sz, left, right; - int8_t bf; uint8_t height; - if (algt_read_node(bs, root, &root_sz, &bf, &height, &left, &right) != 0) - return -1; - if (size < root_sz || (size == root_sz && ptr < root)) { - uint64_t new_left; - if (algt_avl_insert_rec(bs, left, ptr, size, depth - 1, &new_left) != 0) return -1; - if (algt_avl_write_and_update(bs, root, root_sz, new_left, right, NULL) != 0) - return -1; + + child = ptr; + for (i = (int)path_len - 1; i >= 0; i--) { + uint64_t new_left, new_right, new_child; + if (path[i].went_left) { + new_left = child; + new_right = path[i].right; } else { - uint64_t new_right; - if (algt_avl_insert_rec(bs, right, ptr, size, depth - 1, &new_right) != 0) return -1; - if (algt_avl_write_and_update(bs, root, root_sz, left, new_right, NULL) != 0) - return -1; + new_left = path[i].left; + new_right = child; } + if (algt_avl_write_and_update(bs, path[i].ptr, path[i].size, + new_left, new_right, NULL) != 0) return -1; + if (algt_avl_rebalance(bs, path[i].ptr, &new_child) != 0) return -1; + child = new_child; } - return algt_avl_rebalance(bs, root, out_root); + return algt_write_root(bs, child); } -static int algt_avl_insert(bstack_t *bs, uint64_t ptr, uint64_t size) -{ - uint64_t root, new_root; - if (algt_read_root(bs, &root) != 0) return -1; - if (algt_avl_insert_rec(bs, root, ptr, size, ALGT_MAX_AVL_DEPTH, &new_root) != 0) return -1; - return algt_write_root(bs, new_root); -} +/* ---- AVL remove-min ---------------------------------------------------- */ -/* ---- AVL remove -------------------------------------------------------- */ - -static int algt_avl_remove_rec(bstack_t *bs, uint64_t root, - uint64_t ptr, uint64_t size, uint32_t depth, uint64_t *out_root) +/* Remove the minimum (leftmost) node from subtree rooted at root. + * Sets *out_min_ptr and *out_min_size to the removed node; *out_new_root is the + * rebalanced subtree after removal. */ +static int algt_avl_remove_min(bstack_t *bs, uint64_t root, + uint64_t *out_min_ptr, uint64_t *out_min_size, uint64_t *out_new_root) { - uint64_t root_sz, left, right; - int8_t bf; uint8_t height; - int cmp; - - if (root == ALGT_NULL_PTR) { *out_root = ALGT_NULL_PTR; return 0; } - if (depth == 0) { errno = EINVAL; return -1; } - if (algt_read_node(bs, root, &root_sz, &bf, &height, &left, &right) != 0) return -1; - - if (size < root_sz || (size == root_sz && ptr < root)) cmp = -1; - else if (size > root_sz || (size == root_sz && ptr > root)) cmp = 1; - else cmp = 0; - - if (cmp < 0) { - uint64_t new_left; - if (algt_avl_remove_rec(bs, left, ptr, size, depth - 1, &new_left) != 0) return -1; - if (algt_avl_write_and_update(bs, root, root_sz, new_left, right, NULL) != 0) return -1; - return algt_avl_rebalance(bs, root, out_root); - } - if (cmp > 0) { - uint64_t new_right; - if (algt_avl_remove_rec(bs, right, ptr, size, depth - 1, &new_right) != 0) return -1; - if (algt_avl_write_and_update(bs, root, root_sz, left, new_right, NULL) != 0) return -1; - return algt_avl_rebalance(bs, root, out_root); - } - /* Found: remove this node */ - if (left == ALGT_NULL_PTR) { *out_root = right; return 0; } - if (right == ALGT_NULL_PTR) { *out_root = left; return 0; } - /* Two children: replace with in-order successor (leftmost of right subtree) */ - { - uint64_t succ, succ_sz, new_right; - if (algt_avl_min(bs, right, depth - 1, &succ, &succ_sz) != 0) return -1; - if (algt_avl_remove_rec(bs, right, succ, succ_sz, depth - 1, &new_right) != 0) return -1; - if (algt_avl_write_and_update(bs, succ, succ_sz, left, new_right, NULL) != 0) return -1; - return algt_avl_rebalance(bs, succ, out_root); + uint64_t stk_ptr [ALGT_MAX_AVL_DEPTH]; + uint64_t stk_size [ALGT_MAX_AVL_DEPTH]; + uint64_t stk_right[ALGT_MAX_AVL_DEPTH]; + size_t stk_len = 0; + uint64_t current = root; + + for (;;) { + uint64_t size, left, right; + int8_t bf; uint8_t height; + if (algt_read_node(bs, current, &size, &bf, &height, &left, &right) != 0) + return -1; + if (left == ALGT_NULL_PTR) { + uint64_t child = right; + int i; + for (i = (int)stk_len - 1; i >= 0; i--) { + uint64_t new_child; + if (algt_avl_write_and_update(bs, stk_ptr[i], stk_size[i], + child, stk_right[i], NULL) != 0) + return -1; + if (algt_avl_rebalance(bs, stk_ptr[i], &new_child) != 0) return -1; + child = new_child; + } + *out_min_ptr = current; + *out_min_size = size; + *out_new_root = child; + return 0; + } + if (stk_len >= ALGT_MAX_AVL_DEPTH) { errno = EINVAL; return -1; } + stk_ptr [stk_len] = current; + stk_size [stk_len] = size; + stk_right[stk_len] = right; + stk_len++; + current = left; } } @@ -2286,91 +2291,92 @@ static int algt_avl_remove_rec(bstack_t *bs, uint64_t root, /* Find and remove the best-fit (smallest block >= min_size) in one O(log n) * pass. Sets *out_found_ptr = ALGT_NULL_PTR when no block fits. */ -static int algt_avl_find_best_fit_rec(bstack_t *bs, uint64_t root, - uint64_t min_size, uint32_t depth, uint64_t *out_new_root, +static int algt_avl_find_best_fit_and_remove(bstack_t *bs, uint64_t min_size, uint64_t *out_found_ptr, uint64_t *out_found_size) { - uint64_t root_sz, left, right; - int8_t bf; uint8_t height; + algt_path_entry_t path[ALGT_MAX_AVL_DEPTH]; + size_t path_len = 0; + int fit_found = 0; + size_t last_fit_idx = 0; + uint64_t root, current; + uint64_t found_ptr, found_size, found_left, found_right; + uint64_t replacement, child; + int i; + if (algt_read_root(bs, &root) != 0) return -1; if (root == ALGT_NULL_PTR) { - *out_new_root = ALGT_NULL_PTR; - *out_found_ptr = ALGT_NULL_PTR; + *out_found_ptr = ALGT_NULL_PTR; + *out_found_size = 0; return 0; } - if (depth == 0) { errno = EINVAL; return -1; } - if (algt_read_node(bs, root, &root_sz, &bf, &height, &left, &right) != 0) - return -1; - if (root_sz >= min_size) { - /* This node fits — try left for something smaller. */ - uint64_t new_left, found_ptr, found_sz; - if (algt_avl_find_best_fit_rec(bs, left, min_size, depth - 1, - &new_left, &found_ptr, &found_sz) != 0) return -1; - if (found_ptr != ALGT_NULL_PTR) { - /* Smaller fit found; keep root, update its left child. */ - uint64_t new_root; - if (algt_avl_write_and_update(bs, root, root_sz, new_left, right, NULL) != 0) - return -1; - if (algt_avl_rebalance(bs, root, &new_root) != 0) return -1; - *out_new_root = new_root; - *out_found_ptr = found_ptr; - *out_found_size = found_sz; - return 0; - } - /* No smaller fit — remove root itself. Use new_left (may be rebalanced). */ - { - uint64_t new_root; - if (new_left == ALGT_NULL_PTR) { - new_root = right; - } else if (right == ALGT_NULL_PTR) { - new_root = new_left; - } else { - uint64_t succ, succ_sz, new_right; - if (algt_avl_min(bs, right, depth - 1, &succ, &succ_sz) != 0) return -1; - if (algt_avl_remove_rec(bs, right, succ, succ_sz, depth - 1, &new_right) != 0) return -1; - if (algt_avl_write_and_update(bs, succ, succ_sz, new_left, new_right, NULL) != 0) - return -1; - if (algt_avl_rebalance(bs, succ, &new_root) != 0) return -1; - } - *out_new_root = new_root; - *out_found_ptr = root; - *out_found_size = root_sz; + current = root; + while (current != ALGT_NULL_PTR) { + uint64_t root_sz, left, right; + int8_t bf; uint8_t height; + int went_left; + if (path_len >= ALGT_MAX_AVL_DEPTH) { errno = EINVAL; return -1; } + if (algt_read_node(bs, current, &root_sz, &bf, &height, &left, &right) != 0) + return -1; + if (root_sz >= min_size) { + last_fit_idx = path_len; + fit_found = 1; + went_left = 1; + } else { + went_left = 0; } + path[path_len].ptr = current; + path[path_len].size = root_sz; + path[path_len].left = left; + path[path_len].right = right; + path[path_len].went_left = went_left; + path_len++; + current = went_left ? left : right; + } + + if (!fit_found) { + *out_found_ptr = ALGT_NULL_PTR; + *out_found_size = 0; return 0; } - /* Too small — only the right subtree can have a fit. */ - { - uint64_t new_right, found_ptr, found_sz; - if (algt_avl_find_best_fit_rec(bs, right, min_size, depth - 1, - &new_right, &found_ptr, &found_sz) != 0) return -1; - if (found_ptr != ALGT_NULL_PTR) { - uint64_t new_root; - if (algt_avl_write_and_update(bs, root, root_sz, left, new_right, NULL) != 0) - return -1; - if (algt_avl_rebalance(bs, root, &new_root) != 0) return -1; - *out_new_root = new_root; - *out_found_ptr = found_ptr; - *out_found_size = found_sz; + found_ptr = path[last_fit_idx].ptr; + found_size = path[last_fit_idx].size; + found_left = path[last_fit_idx].left; + found_right = path[last_fit_idx].right; + + if (found_left == ALGT_NULL_PTR) { + replacement = found_right; + } else if (found_right == ALGT_NULL_PTR) { + replacement = found_left; + } else { + uint64_t succ, succ_sz, new_right; + if (algt_avl_remove_min(bs, found_right, &succ, &succ_sz, &new_right) != 0) + return -1; + if (algt_avl_write_and_update(bs, succ, succ_sz, found_left, new_right, NULL) != 0) + return -1; + if (algt_avl_rebalance(bs, succ, &replacement) != 0) return -1; + } + + child = replacement; + for (i = (int)last_fit_idx - 1; i >= 0; i--) { + uint64_t new_left, new_right, new_child; + if (path[i].went_left) { + new_left = child; + new_right = path[i].right; } else { - *out_new_root = root; - *out_found_ptr = ALGT_NULL_PTR; + new_left = path[i].left; + new_right = child; } - return 0; + if (algt_avl_write_and_update(bs, path[i].ptr, path[i].size, + new_left, new_right, NULL) != 0) return -1; + if (algt_avl_rebalance(bs, path[i].ptr, &new_child) != 0) return -1; + child = new_child; } -} + if (algt_write_root(bs, child) != 0) return -1; -static int algt_avl_find_best_fit_and_remove(bstack_t *bs, uint64_t min_size, - uint64_t *out_found_ptr, uint64_t *out_found_size) -{ - uint64_t root, new_root, found_ptr, found_sz; - if (algt_read_root(bs, &root) != 0) return -1; - if (algt_avl_find_best_fit_rec(bs, root, min_size, ALGT_MAX_AVL_DEPTH, - &new_root, &found_ptr, &found_sz) != 0) return -1; - if (algt_write_root(bs, new_root) != 0) return -1; *out_found_ptr = found_ptr; - *out_found_size = found_sz; + *out_found_size = found_size; return 0; } @@ -2401,46 +2407,123 @@ struct algt_walk_ctx { int err; }; -static void algt_avl_walk_inorder(bstack_t *bs, uint64_t root, uint32_t depth, +static void algt_avl_walk_inorder(bstack_t *bs, uint64_t root, struct algt_walk_ctx *ctx) { - uint64_t size, left, right; - int8_t bf; uint8_t height; + uint64_t stk_ptr [ALGT_MAX_AVL_DEPTH]; + uint64_t stk_right[ALGT_MAX_AVL_DEPTH]; + uint64_t stk_size [ALGT_MAX_AVL_DEPTH]; + size_t stk_len = 0; + uint64_t current = root; - if (root == ALGT_NULL_PTR || ctx->err) return; - if (depth == 0) { ctx->err = 1; return; } - if (algt_read_node(bs, root, &size, &bf, &height, &left, &right) != 0) { - ctx->err = 1; return; - } - algt_avl_walk_inorder(bs, left, depth - 1, ctx); if (ctx->err) return; - if (ctx->count == ctx->cap) { - size_t nc = ctx->cap ? ctx->cap * 2 : 16; - algt_block_t *t = realloc(ctx->blocks, nc * sizeof *t); - if (!t) { ctx->err = 1; return; } - ctx->blocks = t; - ctx->cap = nc; - } - ctx->blocks[ctx->count].ptr = root; - ctx->blocks[ctx->count].size = size; - ctx->count++; - algt_avl_walk_inorder(bs, right, depth - 1, ctx); + + for (;;) { + while (current != ALGT_NULL_PTR) { + uint64_t size, left, right; + int8_t bf; uint8_t height; + if (stk_len >= ALGT_MAX_AVL_DEPTH) { ctx->err = 1; return; } + if (algt_read_node(bs, current, &size, &bf, &height, &left, &right) != 0) { + ctx->err = 1; return; + } + stk_ptr [stk_len] = current; + stk_right[stk_len] = right; + stk_size [stk_len] = size; + stk_len++; + current = left; + } + if (stk_len == 0) return; + stk_len--; + { + uint64_t ptr = stk_ptr [stk_len]; + uint64_t right = stk_right[stk_len]; + uint64_t size = stk_size [stk_len]; + if (ctx->count == ctx->cap) { + size_t nc = ctx->cap ? ctx->cap * 2 : 16; + algt_block_t *t = realloc(ctx->blocks, nc * sizeof *t); + if (!t) { ctx->err = 1; return; } + ctx->blocks = t; + ctx->cap = nc; + } + ctx->blocks[ctx->count].ptr = ptr; + ctx->blocks[ctx->count].size = size; + ctx->count++; + current = right; + } + } } -/* Build an optimally balanced BST from a sorted (by key) block array. */ +/* Build an optimally balanced BST from a sorted (by key) block array. + * Uses an explicit ops-stack to avoid recursion. */ +typedef struct { int is_combine; size_t lo; size_t hi; } algt_build_op_t; + static int algt_build_tree(bstack_t *bs, const algt_block_t *blocks, size_t n, uint64_t *out_root) { - size_t mid; - uint64_t left, right; - if (n == 0) { *out_root = ALGT_NULL_PTR; return 0; } - mid = n / 2; - if (algt_build_tree(bs, blocks, mid, &left) != 0) return -1; - if (algt_build_tree(bs, blocks + mid + 1, n - mid - 1, &right) != 0) return -1; - if (algt_avl_write_and_update(bs, blocks[mid].ptr, blocks[mid].size, - left, right, NULL) != 0) return -1; - *out_root = blocks[mid].ptr; - return 0; + algt_build_op_t *ops = NULL; + uint64_t *results = NULL; + size_t ops_len = 0, ops_cap = 0; + size_t res_len = 0, res_cap = 0; + int ret = 0; + +#define BUILD_OPS_PUSH(comb, lo_, hi_) do { \ + if (ops_len == ops_cap) { \ + size_t nc_ = ops_cap ? ops_cap * 2 : 32; \ + algt_build_op_t *t_ = realloc(ops, nc_ * sizeof *t_); \ + if (!t_) { ret = -1; goto build_done; } \ + ops = t_; ops_cap = nc_; \ + } \ + ops[ops_len].is_combine = (comb); \ + ops[ops_len].lo = (lo_); \ + ops[ops_len].hi = (hi_); \ + ops_len++; \ +} while (0) + +#define BUILD_RES_PUSH(v) do { \ + if (res_len == res_cap) { \ + size_t nc_ = res_cap ? res_cap * 2 : 32; \ + uint64_t *t_ = realloc(results, nc_ * sizeof *t_); \ + if (!t_) { ret = -1; goto build_done; } \ + results = t_; res_cap = nc_; \ + } \ + results[res_len++] = (v); \ +} while (0) + + BUILD_OPS_PUSH(0, 0, n); + + while (ops_len > 0 && ret == 0) { + algt_build_op_t op = ops[--ops_len]; + if (!op.is_combine) { + size_t lo = op.lo, hi = op.hi; + if (lo >= hi) { + BUILD_RES_PUSH(ALGT_NULL_PTR); + } else { + size_t mid = lo + (hi - lo) / 2; + BUILD_OPS_PUSH(1, mid, 0); + BUILD_OPS_PUSH(0, mid + 1, hi); + BUILD_OPS_PUSH(0, lo, mid); + } + } else { + size_t mid = op.lo; + uint64_t right_root = results[--res_len]; + uint64_t left_root = results[--res_len]; + if (algt_avl_write_and_update(bs, + blocks[mid].ptr, blocks[mid].size, + left_root, right_root, NULL) != 0) { + ret = -1; goto build_done; + } + BUILD_RES_PUSH(blocks[mid].ptr); + } + } + + *out_root = (res_len > 0) ? results[res_len - 1] : ALGT_NULL_PTR; + +build_done: +#undef BUILD_OPS_PUSH +#undef BUILD_RES_PUSH + free(ops); + free(results); + return ret; } /* Collect all free blocks, merge adjacent ones, and rebuild a balanced AVL @@ -2457,7 +2540,7 @@ static int algt_coalesce_and_rebalance(bstack_t *bs) if (algt_read_root(bs, &root) != 0) return -1; ctx.blocks = NULL; ctx.count = 0; ctx.cap = 0; ctx.err = 0; - algt_avl_walk_inorder(bs, root, ALGT_MAX_AVL_DEPTH, &ctx); + algt_avl_walk_inorder(bs, root, &ctx); if (ctx.err) { free(ctx.blocks); return -1; } if (ctx.count == 0) { free(ctx.blocks); return 0; } From 51a33d2d331759ca7cfeaeb80e87a877844c24f5 Mon Sep 17 00:00:00 2001 From: William Wu <204092015+williamwutq@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:34:13 -0700 Subject: [PATCH 03/11] Preallocate vecs to avoid repeated reallocations Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/alloc/ghost_tree.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 4158d90..a870f31 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -393,7 +393,7 @@ impl GhostTreeBstackAllocator { let root = self.read_root()?; // Down-pass: walk to the insertion position, recording the path. - let mut path: Vec = Vec::new(); + let mut path: Vec = Vec::with_capacity(MAX_AVL_DEPTH as usize); let mut current = root; while current != NULL_PTR { if path.len() >= MAX_AVL_DEPTH as usize { @@ -438,7 +438,7 @@ impl GhostTreeBstackAllocator { /// has no left child, so its replacement is its right child (or [`NULL_PTR`]). fn avl_remove_min(&self, root: u64) -> io::Result<(u64, u64, u64)> { // Walk left, recording (ptr, size, right_child) for each ancestor. - let mut path: Vec<(u64, u64, u64)> = Vec::new(); + let mut path: Vec<(u64, u64, u64)> = Vec::with_capacity(MAX_AVL_DEPTH as usize); let mut current = root; loop { let (size, _, _, left, right) = self.read_node(current)?; @@ -479,7 +479,7 @@ impl GhostTreeBstackAllocator { // Down-pass: record the full traversal path and the index of the last // node that satisfies size >= min_size (the best fit). - let mut path: Vec = Vec::new(); + let mut path: Vec = Vec::with_capacity(MAX_AVL_DEPTH as usize); let mut last_fit_idx: Option = None; let mut current = root; while current != NULL_PTR { From 93564426ebfc976d86ece6d0a6fd8dc04768a444 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Tue, 2 Jun 2026 22:39:04 -0700 Subject: [PATCH 04/11] Enforce build invariants in GhostTreeBstackAllocator by adding debug assertions --- src/alloc/ghost_tree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index a870f31..6c1aa0e 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -673,7 +673,13 @@ impl GhostTreeBstackAllocator { } } } - let new_root = results.pop().unwrap_or(NULL_PTR); + let new_root = results + .pop() + .expect("build invariant: results must have exactly one element"); + debug_assert!( + results.is_empty(), + "build invariant: excess elements on results stack" + ); self.write_root(new_root) } } From 944b0508a46dce591ff3dd0e2b86ac96a779cb48 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Wed, 3 Jun 2026 17:44:05 -0700 Subject: [PATCH 05/11] Basic atomic impl for ghost_tree --- src/alloc/ghost_tree.rs | 105 ++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 6c1aa0e..82cfdbb 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -1,9 +1,13 @@ use super::{BStackAllocator, BStackBulkAllocator, BStackSlice}; use crate::BStack; +#[cfg(not(feature = "atomic"))] use std::cell::Cell; use std::fmt; use std::io; +#[cfg(not(feature = "atomic"))] use std::marker::PhantomData; +#[cfg(feature = "atomic")] +use std::sync::Mutex; const ALGT_MAGIC: [u8; 8] = *b"ALGT\x00\x01\x01\x00"; const ALGT_MAGIC_PREFIX: [u8; 6] = *b"ALGT\x00\x01"; @@ -128,23 +132,36 @@ struct PathEntry { /// /// # Thread safety /// -/// `GhostTreeBstackAllocator` is **`Send`** — ownership can be transferred to -/// another thread — but **not `Sync`**. All allocator operations take `&self` -/// and mutate the on-disk AVL tree through `BStack`; concurrent shared access -/// from multiple threads would race on that state. Each instance must be used -/// from at most one thread at a time. +/// `GhostTreeBstackAllocator` is always **`Send`** — ownership can be +/// transferred to another thread. +/// +/// Without the `atomic` feature it is **not `Sync`**: all allocator operations +/// take `&self` and mutate the on-disk AVL tree through `BStack`, so concurrent +/// shared access from multiple threads would race on that state. Each instance +/// must be used from at most one thread at a time. +/// +/// With the `atomic` feature it **is `Sync`**. An internal [`Mutex`] serialises +/// all AVL tree mutations and tail-stack operations that are not already +/// serialised by `BStack`'s own locking. /// /// ``` /// fn assert_send() {} /// assert_send::(); /// ``` /// -/// ```compile_fail +/// Without `atomic` the type is `!Sync` (this fails to compile); with `atomic` +/// the internal `Mutex` makes it `Sync` (this compiles): +/// +#[cfg_attr(not(feature = "atomic"), doc = "```compile_fail")] +#[cfg_attr(feature = "atomic", doc = "```")] /// fn assert_sync() {} /// assert_sync::(); /// ``` pub struct GhostTreeBstackAllocator { stack: BStack, + #[cfg(feature = "atomic")] + lock: Mutex<()>, + #[cfg(not(feature = "atomic"))] _not_sync: PhantomData>, } @@ -181,6 +198,9 @@ impl GhostTreeBstackAllocator { // ROOT_OFFSET is zeroed by extend — null root pointer. return Ok(Self { stack, + #[cfg(feature = "atomic")] + lock: Mutex::new(()), + #[cfg(not(feature = "atomic"))] _not_sync: PhantomData, }); } @@ -214,6 +234,9 @@ impl GhostTreeBstackAllocator { let this = Self { stack, + #[cfg(feature = "atomic")] + lock: Mutex::new(()), + #[cfg(not(feature = "atomic"))] _not_sync: PhantomData, }; this.coalesce_and_rebalance()?; @@ -712,6 +735,8 @@ impl BStackAllocator for GhostTreeBstackAllocator { // SAFETY: zero-length slice at offset 0 is safe return Ok(unsafe { BStackSlice::from_raw_parts(self, 0, 0) }); } + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); let aligned = Self::align_up_len(len); if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(aligned)? { let remainder = block_size - aligned; @@ -788,36 +813,45 @@ impl BStackAllocator for GhostTreeBstackAllocator { return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); } - let is_tail = slice.start() + aligned_old == self.stack.len()?; - - if aligned_new < aligned_old { - // Shrink. - let freed_tail = aligned_old - aligned_new; - let tail_ptr = slice.start() + aligned_new; - if is_tail { - // Zero the gap [new_len..aligned_new] only; then truncate - // the BStack rather than recycling the freed tail. - if new_len < aligned_new { + { + // Hold the lock across the tail check and the action that follows + // (shrink or tail-grow), so the read-modify on the stack tail and the + // AVL tree are atomic w.r.t. other threads. For non-tail grow the + // guard is dropped here; alloc/dealloc below acquire their own locks. + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); + + let is_tail = slice.start() + aligned_old == self.stack.len()?; + + if aligned_new < aligned_old { + // Shrink. + let freed_tail = aligned_old - aligned_new; + let tail_ptr = slice.start() + aligned_new; + if is_tail { + // Zero the gap [new_len..aligned_new] only; then truncate + // the BStack rather than recycling the freed tail. + if new_len < aligned_new { + self.stack + .zero(slice.start() + new_len, aligned_new - new_len)?; + } + self.stack.discard(freed_tail)?; + } else { + // Zero [new_len..aligned_old] in one call (gap + freed tail), + // then insert the freed tail into the AVL tree. self.stack - .zero(slice.start() + new_len, aligned_new - new_len)?; + .zero(slice.start() + new_len, aligned_old - new_len)?; + self.avl_insert(tail_ptr, freed_tail)?; } - self.stack.discard(freed_tail)?; - } else { - // Zero [new_len..aligned_old] in one call (gap + freed tail), - // then insert the freed tail into the AVL tree. - self.stack - .zero(slice.start() + new_len, aligned_old - new_len)?; - self.avl_insert(tail_ptr, freed_tail)?; + // SAFETY: slice shrunk, block size reduced + return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); } - // SAFETY: slice shrunk, block size reduced - return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); - } - if is_tail { - // Grow at the tail: extend the BStack directly, no copy needed. - self.stack.extend(aligned_new - aligned_old)?; - // SAFETY: slice extended at tail - return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); + if is_tail { + // Grow at the tail: extend the BStack directly, no copy needed. + self.stack.extend(aligned_new - aligned_old)?; + // SAFETY: slice extended at tail + return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); + } } // Grow (non-tail): allocate new region, copy old data, free old region. @@ -851,6 +885,9 @@ impl BStackAllocator for GhostTreeBstackAllocator { let ptr = slice.start(); let true_len = Self::align_up_len(slice.len()); + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); + // Tail optimisation: truncate instead of recycling through the AVL tree. if ptr + true_len == self.stack.len()? { return self.stack.discard(true_len); @@ -915,6 +952,8 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { // Allocate one contiguous block. `total` is already a sum of multiples // of MIN_ALLOC so no further rounding is needed. + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); let block_ptr = if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(total)? { let remainder = block_size - total; if remainder >= MIN_ALLOC { @@ -994,6 +1033,8 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { } // Free each merged block: tail-truncate when possible, otherwise zero + insert. + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); for (ptr, size) in merged { if ptr + size == self.stack.len()? { self.stack.discard(size)?; From 3998a597b0824b589a95eb765b3289dadb552dbd Mon Sep 17 00:00:00 2001 From: williamwutq Date: Wed, 3 Jun 2026 17:51:51 -0700 Subject: [PATCH 06/11] Optimizations --- src/alloc/ghost_tree.rs | 136 +++++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 56 deletions(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 82cfdbb..22c4241 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -735,33 +735,34 @@ impl BStackAllocator for GhostTreeBstackAllocator { // SAFETY: zero-length slice at offset 0 is safe return Ok(unsafe { BStackSlice::from_raw_parts(self, 0, 0) }); } - #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); let aligned = Self::align_up_len(len); - if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(aligned)? { - let remainder = block_size - aligned; - if remainder >= MIN_ALLOC { - // Split: the leading `remainder` bytes become a new free block. - // The AVL node is written into those bytes by avl_insert. - // The tail `aligned` bytes are already zeroed by invariant. - self.avl_insert(ptr, remainder)?; - // SAFETY: ptr + remainder is the allocated portion after splitting - Ok(unsafe { BStackSlice::from_raw_parts(self, ptr + remainder, len) }) - } else { - // No split: give the whole block. The stale AVL node in the - // first 32 bytes must be zeroed; the rest is already zeroed. - // Any bytes beyond `len` (up to `block_size`) are internal - // padding and will be recovered on dealloc by re-aligning. - self.stack.zero(ptr, MIN_ALLOC)?; - // SAFETY: ptr from allocated block via avl_find_best_fit_and_remove - Ok(unsafe { BStackSlice::from_raw_parts(self, ptr, len) }) + { + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); + if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(aligned)? { + let remainder = block_size - aligned; + if remainder >= MIN_ALLOC { + // Split: the leading `remainder` bytes become a new free block. + // The AVL node is written into those bytes by avl_insert. + // The tail `aligned` bytes are already zeroed by invariant. + self.avl_insert(ptr, remainder)?; + // SAFETY: ptr + remainder is the allocated portion after splitting + return Ok(unsafe { BStackSlice::from_raw_parts(self, ptr + remainder, len) }); + } else { + // No split: give the whole block. The stale AVL node in the + // first 32 bytes must be zeroed; the rest is already zeroed. + // Any bytes beyond `len` (up to `block_size`) are internal + // padding and will be recovered on dealloc by re-aligning. + self.stack.zero(ptr, MIN_ALLOC)?; + // SAFETY: ptr from allocated block via avl_find_best_fit_and_remove + return Ok(unsafe { BStackSlice::from_raw_parts(self, ptr, len) }); + } } - } else { - // No free block fits: grow the BStack (returns zeroed bytes). - let start = self.stack.extend(aligned)?; - // SAFETY: start from fresh allocation via self.stack.extend - Ok(unsafe { BStackSlice::from_raw_parts(self, start, len) }) } + // No free block fits: lock released; grow the BStack (returns zeroed bytes). + let start = self.stack.extend(aligned)?; + // SAFETY: start from fresh allocation via self.stack.extend + Ok(unsafe { BStackSlice::from_raw_parts(self, start, len) }) } /// Resize `slice` to `new_len` bytes. @@ -813,45 +814,57 @@ impl BStackAllocator for GhostTreeBstackAllocator { return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); } - { - // Hold the lock across the tail check and the action that follows - // (shrink or tail-grow), so the read-modify on the stack tail and the - // AVL tree are atomic w.r.t. other threads. For non-tail grow the - // guard is dropped here; alloc/dealloc below acquire their own locks. - #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); + if aligned_new < aligned_old { + // Shrink. + let freed_tail = aligned_old - aligned_new; + let tail_ptr = slice.start() + aligned_new; - let is_tail = slice.start() + aligned_old == self.stack.len()?; - - if aligned_new < aligned_old { - // Shrink. - let freed_tail = aligned_old - aligned_new; - let tail_ptr = slice.start() + aligned_new; - if is_tail { - // Zero the gap [new_len..aligned_new] only; then truncate - // the BStack rather than recycling the freed tail. - if new_len < aligned_new { - self.stack - .zero(slice.start() + new_len, aligned_new - new_len)?; - } - self.stack.discard(freed_tail)?; - } else { - // Zero [new_len..aligned_old] in one call (gap + freed tail), - // then insert the freed tail into the AVL tree. + // Atomic fast path: discard the tail block without taking the lock. + #[cfg(feature = "atomic")] + if self + .stack + .try_discard(slice.start() + aligned_old, freed_tail)? + { + if new_len < aligned_new { self.stack - .zero(slice.start() + new_len, aligned_old - new_len)?; - self.avl_insert(tail_ptr, freed_tail)?; + .zero(slice.start() + new_len, aligned_new - new_len)?; } - // SAFETY: slice shrunk, block size reduced return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); } - if is_tail { - // Grow at the tail: extend the BStack directly, no copy needed. - self.stack.extend(aligned_new - aligned_old)?; - // SAFETY: slice extended at tail + #[cfg(not(feature = "atomic"))] + if slice.start() + aligned_old == self.stack.len()? { + if new_len < aligned_new { + self.stack + .zero(slice.start() + new_len, aligned_new - new_len)?; + } + self.stack.discard(freed_tail)?; return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); } + + // Not tail: zero gap + freed tail before taking the lock, then insert. + self.stack + .zero(slice.start() + new_len, aligned_old - new_len)?; + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); + self.avl_insert(tail_ptr, freed_tail)?; + return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); + } + + // Grow path. + // Atomic fast path: extend the tail without taking the lock. + #[cfg(feature = "atomic")] + if self + .stack + .try_extend_zeros(slice.start() + aligned_old, aligned_new - aligned_old)? + { + return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); + } + + #[cfg(not(feature = "atomic"))] + if slice.start() + aligned_old == self.stack.len()? { + self.stack.extend(aligned_new - aligned_old)?; + return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) }); } // Grow (non-tail): allocate new region, copy old data, free old region. @@ -885,10 +898,17 @@ impl BStackAllocator for GhostTreeBstackAllocator { let ptr = slice.start(); let true_len = Self::align_up_len(slice.len()); + // Atomic fast path: discard the tail block without taking the lock. + // try_discard succeeds only if the stack size is still ptr + true_len, + // making the check-and-discard atomic w.r.t. other threads' pushes. + // If it fails the block is no longer at the tail; fall through to insert. #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); + if self.stack.try_discard(ptr + true_len, true_len)? { + return Ok(()); + } // Tail optimisation: truncate instead of recycling through the AVL tree. + #[cfg(not(feature = "atomic"))] if ptr + true_len == self.stack.len()? { return self.stack.discard(true_len); } @@ -897,7 +917,11 @@ impl BStackAllocator for GhostTreeBstackAllocator { // headers for live allocations, so reliable double-free detection is not // possible without false-positives on ordinary user data. + // Zero before taking the lock: the block is owned by the caller and no + // other thread will touch it until it appears in the AVL tree. self.stack.zero(ptr, true_len)?; + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); self.avl_insert(ptr, true_len) } } From c3f4793d7ac22152928776b18492d2256f13f9e7 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Wed, 3 Jun 2026 17:58:13 -0700 Subject: [PATCH 07/11] Bulk operation optimizations --- src/alloc/ghost_tree.rs | 77 ++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 22c4241..5e42f7b 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -975,23 +975,31 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { } // Allocate one contiguous block. `total` is already a sum of multiples - // of MIN_ALLOC so no further rounding is needed. - #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); - let block_ptr = if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(total)? { - let remainder = block_size - total; - if remainder >= MIN_ALLOC { - // Split: recycle the leading remainder as a new free block, - // use the trailing `total` bytes for the allocation. - self.avl_insert(ptr, remainder)?; - ptr + remainder + // of MIN_ALLOC so no further rounding is needed. The lock is released + // before extend and before building per-request slices. + let block_ptr = { + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); + if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(total)? { + let remainder = block_size - total; + if remainder >= MIN_ALLOC { + // Split: recycle the leading remainder as a new free block, + // use the trailing `total` bytes for the allocation. + self.avl_insert(ptr, remainder)?; + ptr + remainder + } else { + // No split: zero the stale AVL node header; rest already zeroed. + self.stack.zero(ptr, MIN_ALLOC)?; + ptr + } } else { - // No split: zero the stale AVL node header; rest already zeroed. - self.stack.zero(ptr, MIN_ALLOC)?; - ptr + NULL_PTR // sentinel: no free block found, extend after lock is released } - } else { + }; + let block_ptr = if block_ptr == NULL_PTR { self.stack.extend(total)? + } else { + block_ptr }; // Build per-request slices from the contiguous block. @@ -1056,14 +1064,43 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { } } - // Free each merged block: tail-truncate when possible, otherwise zero + insert. + // Free each merged block. The highest-address block may be at the tail; + // attempt a lock-free discard on it first. All remaining blocks are + // zeroed outside the lock (each is owned by the caller), then inserted + // into the AVL tree under the lock in one pass. + + let last = merged.pop().unwrap(); // highest-address block (merged is sorted) + + // Attempt tail-discard on the highest-address block. + let last_discarded; #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); - for (ptr, size) in merged { - if ptr + size == self.stack.len()? { - self.stack.discard(size)?; + { + last_discarded = self.stack.try_discard(last.0 + last.1, last.1)?; + } + #[cfg(not(feature = "atomic"))] + { + if last.0 + last.1 == self.stack.len()? { + self.stack.discard(last.1)?; + last_discarded = true; } else { - self.stack.zero(ptr, size)?; + last_discarded = false; + } + } + + if !last_discarded { + merged.push(last); + } + + // Zero all blocks to be inserted (outside the lock). + for &(ptr, size) in &merged { + self.stack.zero(ptr, size)?; + } + + // Insert all zeroed blocks under the lock. + if !merged.is_empty() { + #[cfg(feature = "atomic")] + let _guard = self.lock.lock().unwrap(); + for (ptr, size) in merged { self.avl_insert(ptr, size)?; } } From 683b3297cbd924091810697874a4640f7be7ba99 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Thu, 4 Jun 2026 20:59:46 -0700 Subject: [PATCH 08/11] Reduce lock scoping a little bit --- src/alloc/ghost_tree.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 5e42f7b..23b3455 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -738,7 +738,7 @@ impl BStackAllocator for GhostTreeBstackAllocator { let aligned = Self::align_up_len(len); { #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); + let guard = self.lock.lock().unwrap(); if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(aligned)? { let remainder = block_size - aligned; if remainder >= MIN_ALLOC { @@ -749,6 +749,8 @@ impl BStackAllocator for GhostTreeBstackAllocator { // SAFETY: ptr + remainder is the allocated portion after splitting return Ok(unsafe { BStackSlice::from_raw_parts(self, ptr + remainder, len) }); } else { + #[cfg(feature = "atomic")] + drop(guard); // No split: give the whole block. The stale AVL node in the // first 32 bytes must be zeroed; the rest is already zeroed. // Any bytes beyond `len` (up to `block_size`) are internal @@ -979,7 +981,7 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { // before extend and before building per-request slices. let block_ptr = { #[cfg(feature = "atomic")] - let _guard = self.lock.lock().unwrap(); + let guard = self.lock.lock().unwrap(); if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(total)? { let remainder = block_size - total; if remainder >= MIN_ALLOC { @@ -988,6 +990,8 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { self.avl_insert(ptr, remainder)?; ptr + remainder } else { + #[cfg(feature = "atomic")] + drop(guard); // release lock before zeroing // No split: zero the stale AVL node header; rest already zeroed. self.stack.zero(ptr, MIN_ALLOC)?; ptr From 42da1727e6286e1f3ff00af4a762b8c528a9e39d Mon Sep 17 00:00:00 2001 From: williamwutq Date: Thu, 4 Jun 2026 22:47:46 -0700 Subject: [PATCH 09/11] Add test to validation thread-safety of ghost tree under atomic --- src/alloc/ghost_tree.rs | 207 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 23b3455..0789e02 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -1111,3 +1111,210 @@ impl BStackBulkAllocator for GhostTreeBstackAllocator { Ok(()) } } + +#[cfg(all(test, feature = "set"))] +mod tests { + use super::GhostTreeBstackAllocator; + use crate::BStack; + use crate::alloc::{BStackAllocator, BStackBulkAllocator}; + use std::sync::atomic::{AtomicU64, Ordering}; + + struct Guard(std::path::PathBuf); + impl Drop for Guard { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } + } + + fn temp_path() -> std::path::PathBuf { + static COUNTER: AtomicU64 = AtomicU64::new(0); + let id = COUNTER.fetch_add(1, Ordering::Relaxed); + let pid = std::process::id(); + std::env::temp_dir().join(format!("bstack_ghost_{pid}_{id}.bin")) + } + + fn empty_alloc() -> (GhostTreeBstackAllocator, std::path::PathBuf) { + let path = temp_path(); + ( + GhostTreeBstackAllocator::new(BStack::open(&path).unwrap()).unwrap(), + path, + ) + } + + // ── concurrent (feature = "atomic") ─────────────────────────────────────── + + #[cfg(feature = "atomic")] + #[test] + fn concurrent_alloc_dealloc_no_live_duplicates() { + use std::collections::HashSet; + use std::sync::{Arc, Mutex}; + use std::thread; + + // Verify that concurrent alloc/dealloc never hands the same block to + // two callers simultaneously. Each thread claims a block, inserts its + // offset into a shared live-set (asserting uniqueness), writes and reads + // back its thread id, then removes the offset and deallocates. A bug + // in the AVL mutex would produce a duplicate entry in the set. + const THREADS: usize = 8; + const ROUNDS: usize = 200; + + let (alloc, path) = empty_alloc(); + let _g = Guard(path); + let alloc = Arc::new(alloc); + let live: Arc>> = Arc::new(Mutex::new(HashSet::new())); + + let handles: Vec<_> = (0..THREADS) + .map(|tid| { + let alloc = Arc::clone(&alloc); + let live = Arc::clone(&live); + thread::spawn(move || { + let a: &GhostTreeBstackAllocator = &alloc; + for _ in 0..ROUNDS { + let slice = a.alloc(32).unwrap(); + let off = slice.start(); + { + let mut set = live.lock().unwrap(); + assert!(set.insert(off), "duplicate live offset {off}"); + } + slice.write(&[tid as u8; 32]).unwrap(); + let data = slice.read().unwrap(); + assert_eq!(data, vec![tid as u8; 32]); + { + let mut set = live.lock().unwrap(); + set.remove(&off); + } + a.dealloc(slice).unwrap(); + } + }) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } + } + + #[cfg(feature = "atomic")] + #[test] + fn concurrent_realloc_hammers_tail_paths() { + use std::sync::Arc; + use std::thread; + + // T threads each own one allocation and repeatedly grow then shrink it. + // Whichever allocation sits at the tail exercises try_extend_zeros / + // try_discard; the others hit the non-tail copy-grow / AVL-insert paths. + // Both branches are exercised on every round because threads race for + // the tail. Verify each thread's data survives every round intact. + // + // All sizes are multiples of 32 (GhostTree's MIN_ALLOC): + // SMALL = 32 → 32-byte aligned block + // LARGE = 96 → 96-byte aligned block (3 × 32) + const THREADS: usize = 6; + const ROUNDS: usize = 150; + const SMALL: u64 = 32; + const LARGE: u64 = 96; + + let (alloc, path) = empty_alloc(); + let _g = Guard(path); + let alloc = Arc::new(alloc); + + let handles: Vec<_> = (0..THREADS) + .map(|tid| { + let alloc = Arc::clone(&alloc); + thread::spawn(move || { + let a: &GhostTreeBstackAllocator = &alloc; + let mut slice = a.alloc(SMALL).unwrap(); + slice.write(&[tid as u8; SMALL as usize]).unwrap(); + + for _ in 0..ROUNDS { + // Grow: tail → try_extend_zeros; non-tail → copy to new region. + slice = a.realloc(slice, LARGE).unwrap(); + let data = slice.read().unwrap(); + assert_eq!( + &data[..SMALL as usize], + &[tid as u8; SMALL as usize], + "data corrupted after grow (tid {tid})", + ); + + // Shrink: tail → try_discard; non-tail → AVL insert of freed tail. + slice = a.realloc(slice, SMALL).unwrap(); + let data = slice.read().unwrap(); + assert_eq!( + data, + vec![tid as u8; SMALL as usize], + "data corrupted after shrink (tid {tid})", + ); + } + + a.dealloc(slice).unwrap(); + }) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } + } + + #[cfg(feature = "atomic")] + #[test] + fn concurrent_alloc_bulk_dealloc_bulk_no_live_duplicates() { + use std::collections::HashSet; + use std::sync::{Arc, Mutex}; + use std::thread; + + // Verify that concurrent alloc_bulk / dealloc_bulk never hand the same + // block to two callers at once. Each thread requests three slices per + // round, inserts all offsets into a shared live-set (asserting + // uniqueness), writes and reads back a pattern, then bulk-deallocates. + // A bug in the AVL mutex or bulk-allocation path would produce a + // duplicate offset in the set. + const THREADS: usize = 6; + const ROUNDS: usize = 100; + const SIZES: [u64; 3] = [32, 64, 32]; // all 32-byte aligned; 128 bytes total + + let (alloc, path) = empty_alloc(); + let _g = Guard(path); + let alloc = Arc::new(alloc); + let live: Arc>> = Arc::new(Mutex::new(HashSet::new())); + + let handles: Vec<_> = (0..THREADS) + .map(|tid| { + let alloc = Arc::clone(&alloc); + let live = Arc::clone(&live); + thread::spawn(move || { + let a: &GhostTreeBstackAllocator = &alloc; + for _ in 0..ROUNDS { + let slices = a.alloc_bulk(SIZES).unwrap(); + { + let mut set = live.lock().unwrap(); + for s in &slices { + assert!( + set.insert(s.start()), + "duplicate live offset {}", + s.start() + ); + } + } + for (s, &sz) in slices.iter().zip(SIZES.iter()) { + s.write(&vec![tid as u8; sz as usize]).unwrap(); + let data = s.read().unwrap(); + assert_eq!(data, vec![tid as u8; sz as usize]); + } + { + let mut set = live.lock().unwrap(); + for s in &slices { + set.remove(&s.start()); + } + } + a.dealloc_bulk(slices).unwrap(); + } + }) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } + } +} From 4ab79fee1e0e3cbf29d73c62864d62b5ad1b9516 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Thu, 4 Jun 2026 22:52:38 -0700 Subject: [PATCH 10/11] Documentation --- CHANGELOG.md | 1 + README.md | 17 +++++++++++++---- src/alloc/mod.rs | 5 +++-- src/lib.rs | 4 +++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 319026d..453c803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`CheckedSlabBStackAllocator` version bumped to 0.1.1** (`alloc` + `set` features): Magic number updated from `ALCK\x00\x01\x00\x00` to `ALCK\x00\x01\x01\x00`. Reflects the addition of `atomic` / `Sync` support. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open). - **`SlabBStackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Without `atomic`, free-list mutations read then write `free_head` as separate `BStack` calls — a TOCTOU race that can hand the same block to two callers. With `atomic`, an internal mutex serialises free-list pop/push; tail operations use `BStack::try_discard` / `BStack::try_extend_zeros` (atomic check-and-act under `BStack`'s own write lock, no allocator lock needed). Non-tail paths lock only around `push_free_blocks`. - **`CheckedSlabBStackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Same mutex model. Free-list pop in `alloc` is lock-scoped; tail extend runs lock-free. `dealloc` uses `try_discard` for the tail path without the lock; free-list push is locked. In `realloc`, the grow path uses `try_extend_zeros` lock-free; the shrink path holds the lock across tail-check + overhead-write + discard (overhead must be committed before truncation for crash safety). `recover` holds the lock for its full duration. +- **`GhostTreeBstackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Without `atomic`, all allocator operations take `&self` and mutate the on-disk AVL tree — concurrent shared access from multiple threads would race on that state. With `atomic`, an internal `Mutex` serialises all AVL tree mutations (`avl_insert`, `avl_find_best_fit_and_remove`, `write_root`); tail operations use `BStack::try_discard` / `BStack::try_extend_zeros` (check-and-act atomically under `BStack`'s own write lock, no allocator lock needed). The `PhantomData>` field that previously opted out of `Sync` is replaced by the `Mutex`, which confers `Sync` without an `unsafe impl`. Documentation updated across type-level docs, module overview, crate overview, and README. --- diff --git a/README.md b/README.md index b2d4a0f..fd8b2db 100644 --- a/README.md +++ b/README.md @@ -865,10 +865,19 @@ imbalanced — corrected on the next `GhostTreeBstackAllocator::new`. #### Thread safety -`GhostTreeBstackAllocator` is **`Send`** but **not `Sync`**. Ownership can be -transferred to another thread, but concurrent `&self` access from multiple -threads would race on the on-disk AVL tree without any allocator-level lock. -Each instance must be used from at most one thread at a time. +`GhostTreeBstackAllocator` is always **`Send`** — ownership can be transferred +to another thread. + +Without the `atomic` feature it is **not `Sync`**: all allocator operations +take `&self` and mutate the on-disk AVL tree, so concurrent shared access from +multiple threads would race on that state. Each instance must be used from at +most one thread at a time. + +With the `atomic` feature it is **`Send + Sync`**. An internal `Mutex` +serialises all AVL tree mutations; tail operations use +`BStack::try_discard` / `BStack::try_extend_zeros`, which check-and-act +atomically under `BStack`'s own write lock without holding the allocator +mutex. #### Example diff --git a/src/alloc/mod.rs b/src/alloc/mod.rs index 4734908..af52d8b 100644 --- a/src/alloc/mod.rs +++ b/src/alloc/mod.rs @@ -43,8 +43,9 @@ //! offset 0 within the block — live allocations carry **zero** overhead //! (no headers, no footers). The tree is keyed on `(size, address)` for a //! strict total order. All memory is kept zeroed: the BStack zeroes on -//! extension, and the allocator zeroes on free. `Send` but not `Sync` — -//! each instance must be used from at most one thread at a time. +//! extension, and the allocator zeroes on free. `Send` in all +//! configurations; `Send + Sync` with the `atomic` feature, where an +//! internal `Mutex` serialises AVL tree mutations. //! //! * [`SlabBStackAllocator`] — a fixed-block slab allocator (requires both //! `alloc` **and** `set` features). All blocks are exactly `block_size` diff --git a/src/lib.rs b/src/lib.rs index d79d8bb..65b888a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -381,7 +381,9 @@ //! zero-overhead live allocations. Free blocks store their AVL node inline, //! and the tree is keyed on `(size, address)` for best-fit allocation. //! Provides O(log n) allocation and deallocation with crash recovery through -//! tree rebalancing on mount. `Send` but not `Sync`. +//! tree rebalancing on mount. `Send` in all configurations; `Send + Sync` +//! with the `atomic` feature, where an internal `Mutex` serialises AVL tree +//! mutations. //! //! * [`SlabBStackAllocator`] — **Experimental.** Fixed-block slab allocator. All blocks are //! exactly `block_size` bytes with no per-block header or footer; freed From aed71b769029d60253457d5413871076219406b3 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Thu, 4 Jun 2026 22:55:39 -0700 Subject: [PATCH 11/11] Bump version of allocator --- CHANGELOG.md | 1 + README.md | 2 +- src/alloc/ghost_tree.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 453c803..fbe7a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **`GhostTreeBstackAllocator` version bumped to 0.1.2** (`alloc` + `set` features): Magic number updated from `ALGT\x00\x01\x01\x00` to `ALGT\x00\x01\x02\x00`. Reflects the addition of `atomic` / `Sync` support. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open). - **`SlabBStackAllocator` version bumped to 0.1.1** (`alloc` + `set` features): Magic number updated from `ALSL\x00\x01\x00\x00` to `ALSL\x00\x01\x01\x00`. Reflects the addition of `atomic` / `Sync` support. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open). - **`CheckedSlabBStackAllocator` version bumped to 0.1.1** (`alloc` + `set` features): Magic number updated from `ALCK\x00\x01\x00\x00` to `ALCK\x00\x01\x01\x00`. Reflects the addition of `atomic` / `Sync` support. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open). - **`SlabBStackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Without `atomic`, free-list mutations read then write `free_head` as separate `BStack` calls — a TOCTOU race that can hand the same block to two callers. With `atomic`, an internal mutex serialises free-list pop/push; tail operations use `BStack::try_discard` / `BStack::try_extend_zeros` (atomic check-and-act under `BStack`'s own write lock, no allocator lock needed). Non-tail paths lock only around `push_free_blocks`. diff --git a/README.md b/README.md index fd8b2db..248e88e 100644 --- a/README.md +++ b/README.md @@ -838,7 +838,7 @@ bstack = { version = "0.2", features = ["alloc"] } ┌─────────────────────────────┐ payload offset 0 │ User-reserved (32 bytes) │ ├─────────────────────────────┤ offset 32 -│ Magic number (8 bytes) │ "ALGT\x00\x01\x00\x00" +│ Magic number (8 bytes) │ "ALGT\x00\x01\x02\x00" ├─────────────────────────────┤ offset 40 │ AVL root pointer (8 B) │ absolute payload offset of the root node ├─────────────────────────────┤ offset 48 ← arena start (32-byte aligned) diff --git a/src/alloc/ghost_tree.rs b/src/alloc/ghost_tree.rs index 0789e02..c84dce1 100644 --- a/src/alloc/ghost_tree.rs +++ b/src/alloc/ghost_tree.rs @@ -9,7 +9,7 @@ use std::marker::PhantomData; #[cfg(feature = "atomic")] use std::sync::Mutex; -const ALGT_MAGIC: [u8; 8] = *b"ALGT\x00\x01\x01\x00"; +const ALGT_MAGIC: [u8; 8] = *b"ALGT\x00\x01\x02\x00"; const ALGT_MAGIC_PREFIX: [u8; 6] = *b"ALGT\x00\x01"; /// Payload offset of the magic number. @@ -82,7 +82,7 @@ struct PathEntry { /// ┌─────────────────────────────┐ payload offset 0 /// │ User-reserved (32 bytes) │ /// ├─────────────────────────────┤ offset 32 -/// │ Magic number (8 bytes) │ "ALGT\x00\x01\x01\x00" +/// │ Magic number (8 bytes) │ "ALGT\x00\x01\x02\x00" /// ├─────────────────────────────┤ offset 40 /// │ AVL root pointer (8 B) │ absolute payload offset of the root node /// ├─────────────────────────────┤ offset 48 ← arena start (32-byte aligned)