Did some code cleanup,

added some extra thingies.
switched to spans. Let google gemini do whatever it wanted..
This commit is contained in:
Linus Björnstam 2026-04-16 11:51:38 +02:00
parent 978d0873dc
commit 7bea233edc
11 changed files with 944 additions and 248 deletions

View file

@ -13,6 +13,9 @@ namespace PersistentMap
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
where TStrategy : IKeyStrategy<K>
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
Node<K> current = root;
while (true)
{
@ -20,7 +23,7 @@ namespace PersistentMap
{
var leaf = current.AsLeaf<V>();
// Leaf uses standard FindIndex (Lower Bound) to find exact match
int index = FindIndex(leaf, key, strategy);
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
value = leaf.Values[index];
@ -33,169 +36,169 @@ namespace PersistentMap
{
// FIX: Internal uses FindRoutingIndex (Upper Bound)
var internalNode = current.AsInternal();
int index = FindRoutingIndex(internalNode, key, strategy);
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
current = internalNode.Children[index]!;
}
}
}
// Public API
public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool countChanged)
{
root = root.EnsureEditable(owner);
var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged);
if (splitResult != null)
{
var newRoot = new InternalNode<K>(owner);
newRoot.Children[0] = root;
newRoot.Keys[0] = splitResult.Separator;
newRoot.Children[1] = splitResult.NewNode;
newRoot.SetCount(1);
if (strategy.UsesPrefixes)
newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator);
return newRoot;
}
return root;
}
// Recursive Helper
private static SplitResult<K>? InsertRecursive<K, V>(Node<K> node, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool added)
{
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
int index = FindIndex(leaf, key, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool countChanged)
{
leaf.Values[index] = value;
added = false; // Key existed, value updated. Count does not change.
return null;
}
root = root.EnsureEditable(owner);
added = true; // New key. Count +1.
if (leaf.Header.Count < LeafNode<K, V>.Capacity)
{
InsertIntoLeaf(leaf, index, key, value, strategy);
return null;
}
else
{
return SplitLeaf(leaf, index, key, value, strategy, owner);
}
}
else
{
var internalNode = node.AsInternal();
int index = FindRoutingIndex(internalNode, key, strategy);
var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged);
var child = internalNode.Children[index]!.EnsureEditable(owner);
internalNode.Children[index] = child;
var split = InsertRecursive(child, key, value, strategy, owner, out added);
if (split != null)
{
if (internalNode.Header.Count < InternalNode<K>.Capacity - 1)
if (splitResult != null)
{
InsertIntoInternal(internalNode, index, split.Separator, split.NewNode, strategy);
return null;
var newRoot = new InternalNode<K>(owner);
newRoot.Children[0] = root;
newRoot.Keys[0] = splitResult.Separator;
newRoot.Children[1] = splitResult.NewNode;
newRoot.SetCount(1);
if (strategy.UsesPrefixes)
newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator);
return newRoot;
}
return root;
}
// Recursive Helper
private static SplitResult<K>? InsertRecursive<K, V>(Node<K> node, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool added)
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
leaf.Values[index] = value;
added = false; // Key existed, value updated. Count does not change.
return null;
}
added = true; // New key. Count +1.
if (leaf.Header.Count < LeafNode<K, V>.Capacity)
{
InsertIntoLeaf(leaf, index, key, value, strategy);
return null;
}
else
{
return SplitLeaf(leaf, index, key, value, strategy, owner);
}
}
else
{
return SplitInternal(internalNode, index, split.Separator, split.NewNode, strategy, owner);
var internalNode = node.AsInternal();
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
var child = internalNode.Children[index]!.EnsureEditable(owner);
internalNode.Children[index] = child;
var split = InsertRecursive(child, key, value, strategy, owner, out added);
if (split != null)
{
if (internalNode.Header.Count < InternalNode<K>.Capacity - 1)
{
InsertIntoInternal(internalNode, index, split.Separator, split.NewNode, strategy);
return null;
}
else
{
return SplitInternal(internalNode, index, split.Separator, split.NewNode, strategy, owner);
}
}
return null;
}
}
return null;
}
}
// Public API
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged)
where TStrategy : IKeyStrategy<K>
{
root = root.EnsureEditable(owner);
bool rebalanceNeeded = RemoveRecursive<K, V, TStrategy>(root, key, strategy, owner, out countChanged);
if (rebalanceNeeded)
{
if (!root.IsLeaf)
// Public API
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged)
where TStrategy : IKeyStrategy<K>
{
var internalRoot = root.AsInternal();
if (internalRoot.Header.Count == 0)
root = root.EnsureEditable(owner);
bool rebalanceNeeded = RemoveRecursive<K, V, TStrategy>(root, key, strategy, owner, out countChanged);
if (rebalanceNeeded)
{
return internalRoot.Children[0]!;
if (!root.IsLeaf)
{
var internalRoot = root.AsInternal();
if (internalRoot.Header.Count == 0)
{
return internalRoot.Children[0]!;
}
}
}
return root;
}
// Recursive Helper
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed)
where TStrategy : IKeyStrategy<K>
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
RemoveFromLeaf(leaf, index, strategy);
removed = true; // Item removed. Count -1.
return leaf.Header.Count <LeafNode<K, V>.MergeThreshold;
}
removed = false; // Item not found.
return false;
}
else
{
var internalNode = node.AsInternal();
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
var child = internalNode.Children[index]!.EnsureEditable(owner);
internalNode.Children[index] = child;
bool childUnderflow = RemoveRecursive<K, V, TStrategy>(child, key, strategy, owner, out removed);
if (removed && childUnderflow)
{
return HandleUnderflow<K, V, TStrategy>(internalNode, index, strategy, owner);
}
return false;
}
}
}
return root;
}
// Recursive Helper
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed)
where TStrategy : IKeyStrategy<K>
{
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
int index = FindIndex(leaf, key, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
RemoveFromLeaf(leaf, index, strategy);
removed = true; // Item removed. Count -1.
return leaf.Header.Count < LeafNode<K, V>.MergeThreshold;
}
removed = false; // Item not found.
return false;
}
else
{
var internalNode = node.AsInternal();
int index = FindRoutingIndex(internalNode, key, strategy);
var child = internalNode.Children[index]!.EnsureEditable(owner);
internalNode.Children[index] = child;
bool childUnderflow = RemoveRecursive<K, V, TStrategy>(child, key, strategy, owner, out removed);
if (removed && childUnderflow)
{
return HandleUnderflow<K, V, TStrategy>(internalNode, index, strategy, owner);
}
return false;
}
}
// ---------------------------------------------------------
// Internal Helpers: Search
// ---------------------------------------------------------
// Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound)
// 2. Propagate to Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, TStrategy strategy)
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
if (strategy.UsesPrefixes)
{
long keyPrefix = strategy.GetPrefix(key);
// Use the pre-calculated prefix here!
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
// Pass strategy to Refine
return RefineSearch(index, node.GetKeys(), key, strategy);
}
Span<K> keys = node.GetKeys();
return LinearSearchKeys(node.GetKeys(), key, strategy);
}
@ -227,23 +230,18 @@ where TStrategy : IKeyStrategy<K>
// Used by Internal Nodes: Finds the child index to descend into.
// If Key == Separator, we must go RIGHT (index + 1), so we need (Upper Bound).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, TStrategy strategy)
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
if (!strategy.UsesPrefixes)
{
// C. Fallback
return LinearSearchRouting(node.GetKeys(), key, strategy);
}
long keyPrefix = strategy.GetPrefix(key);
// SIMD still finds >=.
// Use the pre-calculated prefix here!
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
// Refine using Upper Bound logic (<= 0 instead of < 0)
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy);
}
@ -261,7 +259,7 @@ where TStrategy : IKeyStrategy<K>
return i;
}
// Overload for primitive types (avoids IComparer call overhead in fallback)
// Overload for primitive types (avoids IComparer call overhead in fallback)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<T>(Span<T> keys, T key) where T : struct, IComparable<T>
{
@ -298,15 +296,17 @@ where TStrategy : IKeyStrategy<K>
private static SplitResult<K>? InsertRecur2sive<K, V, TStrategy>(Node<K> node, K key, V value, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<K>
{
// DELETE the single FindIndex call at the top.
// int index = FindIndex(node, key, strategy); <-- REMOVE THIS
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
// --- LEAF CASE ---
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
// Leaf uses FindIndex
int index = FindIndex(leaf, key, strategy);
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
@ -329,7 +329,7 @@ where TStrategy : IKeyStrategy<K>
var internalNode = node.AsInternal();
// FIX: Internal uses FindRoutingIndex
int childIndex = FindRoutingIndex(internalNode, key, strategy);
int childIndex = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
var child = internalNode.Children[childIndex]!.EnsureEditable(owner);
internalNode.Children[childIndex] = child;
@ -358,16 +358,15 @@ where TStrategy : IKeyStrategy<K>
int count = leaf.Header.Count;
if (index < count)
{
// Arrays allow access up to .Length (64), ignoring 'Count'
Array.Copy(leaf.Keys, index, leaf.Keys, index + 1, count - index);
// This fails if leaf.Values is a Span<V> of length 'count'
Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index);
int moveCount = count - index;
// Fast Span memory moves
leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1));
leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1));
if (strategy.UsesPrefixes)
{
leaf.AllPrefixes.Slice(index, count-index)
.CopyTo(leaf.AllPrefixes.Slice(index+1));
leaf.AllPrefixes.Slice(index, count - index)
.CopyTo(leaf.AllPrefixes.Slice(index + 1));
}
}
@ -396,11 +395,12 @@ where TStrategy : IKeyStrategy<K>
int moveCount = totalCount - splitPoint;
if (moveCount > 0)
{
Array.Copy(left.Keys, splitPoint, right.Keys, 0, moveCount);
Array.Copy(left.Values, splitPoint, right.Values, 0, moveCount);
// Fast Span memory moves
left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0));
left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0));
// Manually copy prefixes if needed or re-calculate
if (strategy.UsesPrefixes)
for(int i=0; i<moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint+i];
for (int i = 0; i < moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + i];
}
// Update Counts
@ -417,9 +417,6 @@ where TStrategy : IKeyStrategy<K>
InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy);
}
// Linked List Maintenance
right.Next = left.Next;
left.Next = right;
// In B+ Tree, the separator is the first key of the right node
return new SplitResult<K>(right, right.Keys[0]);
@ -433,13 +430,17 @@ where TStrategy : IKeyStrategy<K>
// Shift Keys and Prefixes
if (index < count)
{
Array.Copy(node.Keys, index, node.Keys, index + 1, count - index);
int moveCount = count - index;
// Fast Span memory moves
node.Keys.AsSpan(index, moveCount).CopyTo(node.Keys.AsSpan(index + 1));
// FIX: Shift raw prefix array
if (strategy.UsesPrefixes)
{
node.AllPrefixes.Slice(index, count-index)
.CopyTo(node.AllPrefixes.Slice(index +1));
node.AllPrefixes.Slice(index, count - index)
.CopyTo(node.AllPrefixes.Slice(index + 1));
}
}
@ -476,11 +477,16 @@ where TStrategy : IKeyStrategy<K>
// Move Keys/Prefixes to Right
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
Array.Copy(left.Keys, splitPoint + 1, right.Keys, 0, moveCount);
if (strategy.UsesPrefixes)
for(int i=0; i<moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + 1 + i];
// Fast Span memory moves
if (moveCount > 0)
{
left.Keys.AsSpan(splitPoint + 1, moveCount).CopyTo(right.Keys.AsSpan(0));
// Move Children to Right
if (strategy.UsesPrefixes)
{
left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0));
}
}
// Left has children 0..splitPoint. Right has children splitPoint+1..End
for (int i = 0; i <= moveCount; i++)
{
@ -526,16 +532,22 @@ where TStrategy : IKeyStrategy<K>
private static void RemoveFromLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
where TStrategy : IKeyStrategy<K>
{
int count = leaf.Header.Count;
Array.Copy(leaf.Keys, index + 1, leaf.Keys, index, count - index - 1);
Array.Copy(leaf.Values, index + 1, leaf.Values, index, count - index - 1);
int moveCount = count - index - 1;
if (strategy.UsesPrefixes)
if (moveCount > 0)
{
var p = leaf.AllPrefixes;
for (int i = index; i < count - 1; i++) p[i] = p[i + 1];
// Fast Span memory moves
leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index));
leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index));
if (strategy.UsesPrefixes)
{
// Replaced manual 'for' loop with native slice copy
leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index));
}
}
leaf.SetCount(count - 1);
@ -604,9 +616,8 @@ where TStrategy : IKeyStrategy<K>
int lCount = leftLeaf.Header.Count;
int rCount = rightLeaf.Header.Count;
Array.Copy(rightLeaf.Keys, 0, leftLeaf.Keys, lCount, rCount);
Array.Copy(rightLeaf.Values, 0, leftLeaf.Values, lCount, rCount);
rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount));
rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount));
if (strategy.UsesPrefixes)
{
rightLeaf.AllPrefixes.Slice(0, rCount)
@ -614,7 +625,6 @@ where TStrategy : IKeyStrategy<K>
}
leftLeaf.SetCount(lCount + rCount);
leftLeaf.Next = rightLeaf.Next;
}
// Case B: Merging Internal Nodes
else
@ -631,7 +641,7 @@ where TStrategy : IKeyStrategy<K>
leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator);
int rCount = rightInternal.Header.Count;
Array.Copy(rightInternal.Keys, 0, leftInternal.Keys, lCount + 1, rCount);
rightInternal.Keys.AsSpan(0, rCount).CopyTo(leftInternal.Keys.AsSpan(lCount + 1));
if (strategy.UsesPrefixes)
{
rightInternal.AllPrefixes.Slice(0, rCount)
@ -648,14 +658,20 @@ where TStrategy : IKeyStrategy<K>
// Remove Separator and Right Child from Parent
int pCount = parent.Header.Count;
Array.Copy(parent.Keys, separatorIndex + 1, parent.Keys, separatorIndex, pCount - separatorIndex - 1);
if (strategy.UsesPrefixes)
int moveCount = pCount - separatorIndex - 1;
if (moveCount > 0)
{
var pp = parent.AllPrefixes;
for (int i = separatorIndex; i < pCount - 1; i++) pp[i] = pp[i + 1];
parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex));
if (strategy.UsesPrefixes)
{
// Replaced manual 'for' loop with native slice copy
parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex));
}
}
for(int i = separatorIndex + 2; i <= pCount; i++)
for (int i = separatorIndex + 2; i <= pCount; i++)
{
parent.Children[i - 1] = parent.Children[i];
}
@ -702,12 +718,17 @@ where TStrategy : IKeyStrategy<K>
int rCount = rightInternal.Header.Count;
// Shift children
for(int i=0; i<rCount; i++) rightInternal.Children[i] = rightInternal.Children[i+1];
for (int i = 0; i < rCount; i++) rightInternal.Children[i] = rightInternal.Children[i + 1];
if (rCount > 1)
{
// Fast Span memory moves (Replaces Array.Copy & manual loop)
rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0));
// Shift keys
Array.Copy(rightInternal.Keys, 1, rightInternal.Keys, 0, rCount - 1);
var rp = rightInternal.AllPrefixes;
for(int i=0; i<rCount-1; i++) rp[i] = rp[i+1];
if (strategy.UsesPrefixes)
{
rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0));
}
}
rightInternal.SetCount(rCount - 1);
}
@ -750,5 +771,169 @@ where TStrategy : IKeyStrategy<K>
leftInternal.SetCount(last);
}
}
public static bool TryGetMin<K, V>(Node<K> root, out K key, out V value)
{
var current = root;
while (!current.IsLeaf)
{
current = current.AsInternal().Children[0]!;
}
var leaf = current.AsLeaf<V>();
if (leaf.Header.Count == 0)
{
key = default!;
value = default!;
return false;
}
key = leaf.Keys![0];
value = leaf.Values[0];
return true;
}
public static bool TryGetMax<K, V>(Node<K> root, out K key, out V value)
{
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
current = internalNode.Children[internalNode.Header.Count]!;
}
var leaf = current.AsLeaf<V>();
if (leaf.Header.Count == 0)
{
key = default!;
value = default!;
return false;
}
int last = leaf.Header.Count - 1;
key = leaf.Keys![last];
value = leaf.Values[last];
return true;
}
public static bool TryGetSuccessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K nextKey, out V nextValue)
where TStrategy : IKeyStrategy<K>
{
InternalNode<K>[] path = new InternalNode<K>[32];
int[] indices = new int[32];
int depth = 0;
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
path[depth] = internalNode;
indices[depth] = idx;
depth++;
current = internalNode.Children[idx]!;
}
var leaf = current.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) index++;
// 1. Successor is in the same leaf
if (index < leaf.Header.Count)
{
nextKey = leaf.Keys![index];
nextValue = leaf.Values[index];
return true;
}
// 2. Successor is in the next leaf (We must backtrack up the tree!)
for (int i = depth - 1; i >= 0; i--)
{
// If we haven't reached the right-most child of this parent
if (indices[i] < path[i].Header.Count)
{
// Take one step right, then go absolute left all the way down
current = path[i].Children[indices[i] + 1]!;
while (!current.IsLeaf)
{
current = current.AsInternal().Children[0]!;
}
var targetLeaf = current.AsLeaf<V>();
nextKey = targetLeaf.Keys![0];
nextValue = targetLeaf.Values[0];
return true;
}
}
nextKey = default!;
nextValue = default!;
return false;
}
public static bool TryGetPredecessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K prevKey, out V prevValue)
where TStrategy : IKeyStrategy<K>
{
// Max depth of a B-Tree is small, preallocate a small array to track the descent path.
InternalNode<K>[] path = new InternalNode<K>[32];
int[] indices = new int[32];
int depth = 0;
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
path[depth] = internalNode;
indices[depth] = idx;
depth++;
current = internalNode.Children[idx]!;
}
var leaf = current.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
// Easy case: Predecessor is in the same leaf
if (index > 0)
{
prevKey = leaf.Keys![index - 1];
prevValue = leaf.Values[index - 1];
return true;
}
// Hard case: We need to backtrack to find the first left branch we ignored
for (int i = depth - 1; i >= 0; i--)
{
if (indices[i] > 0)
{
// Jump to the left sibling branch, then take the absolute right-most path down
current = path[i].Children[indices[i] - 1]!;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
current = internalNode.Children[internalNode.Header.Count]!;
}
var targetLeaf = current.AsLeaf<V>();
int last = targetLeaf.Header.Count - 1;
prevKey = targetLeaf.Keys![last];
prevValue = targetLeaf.Values[last];
return true;
}
}
prevKey = default!;
prevValue = default!;
return false;
}
}
}

View file

@ -80,4 +80,118 @@ public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair
// 4. Until (Start at beginning)
public BTreeEnumerable<K, V, TStrategy> Until(K max)
=> new(_root, _strategy, false, default, true, max);
// ---------------------------------------------------------
// Navigation Operations
// ---------------------------------------------------------
public bool TryGetMin(out K key, out V value) => BTreeFunctions.TryGetMin(_root, out key, out value);
public bool TryGetMax(out K key, out V value) => BTreeFunctions.TryGetMax(_root, out key, out value);
public bool TryGetSuccessor(K key, out K nextKey, out V nextValue) => BTreeFunctions.TryGetSuccessor(_root, key, _strategy, out nextKey, out nextValue);
public bool TryGetPredecessor(K key, out K prevKey, out V prevValue) => BTreeFunctions.TryGetPredecessor(_root, key, _strategy, out prevKey, out prevValue);
// ---------------------------------------------------------
// Set Operations (Linear Merge O(N+M))
// ---------------------------------------------------------
public IEnumerable<KeyValuePair<K, V>> Intersect(BaseOrderedMap<K, V, TStrategy> other)
{
using var enum1 = this.GetEnumerator();
using var enum2 = other.GetEnumerator();
bool has1 = enum1.MoveNext();
bool has2 = enum2.MoveNext();
while (has1 && has2)
{
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
if (cmp == 0)
{
yield return enum1.Current;
has1 = enum1.MoveNext();
has2 = enum2.MoveNext();
}
else if (cmp < 0) has1 = enum1.MoveNext();
else has2 = enum2.MoveNext();
}
}
public IEnumerable<KeyValuePair<K, V>> Except(BaseOrderedMap<K, V, TStrategy> other)
{
using var enum1 = this.GetEnumerator();
using var enum2 = other.GetEnumerator();
bool has1 = enum1.MoveNext();
bool has2 = enum2.MoveNext();
while (has1 && has2)
{
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
if (cmp == 0)
{
has1 = enum1.MoveNext();
has2 = enum2.MoveNext();
}
else if (cmp < 0)
{
yield return enum1.Current;
has1 = enum1.MoveNext();
}
else
{
has2 = enum2.MoveNext();
}
}
while (has1)
{
yield return enum1.Current;
has1 = enum1.MoveNext();
}
}
public IEnumerable<KeyValuePair<K, V>> SymmetricExcept(BaseOrderedMap<K, V, TStrategy> other)
{
using var enum1 = this.GetEnumerator();
using var enum2 = other.GetEnumerator();
bool has1 = enum1.MoveNext();
bool has2 = enum2.MoveNext();
while (has1 && has2)
{
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
if (cmp == 0)
{
has1 = enum1.MoveNext();
has2 = enum2.MoveNext();
}
else if (cmp < 0)
{
yield return enum1.Current;
has1 = enum1.MoveNext();
}
else
{
yield return enum2.Current;
has2 = enum2.MoveNext();
}
}
while (has1)
{
yield return enum1.Current;
has1 = enum1.MoveNext();
}
while (has2)
{
yield return enum2.Current;
has2 = enum2.MoveNext();
}
}
}

View file

@ -120,12 +120,14 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
{
Node<K> node = _root;
_depth = 0;
long keyPrefix = _strategy.UsesPrefixes ? _strategy.GetPrefix(key) : 0;
// Dive using Routing
while (!node.IsLeaf)
{
var internalNode = node.AsInternal();
int idx = BTreeFunctions.FindRoutingIndex<K, TStrategy>(internalNode, key, _strategy);
int idx = BTreeFunctions.FindRoutingIndex<K, TStrategy>(internalNode, key, keyPrefix, _strategy);
_nodeStack[_depth] = internalNode;
_indexStack[_depth] = idx;
@ -136,7 +138,7 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
// Find index in Leaf
_currentLeaf = node.AsLeaf<V>();
int index = BTreeFunctions.FindIndex<K, TStrategy>(_currentLeaf, key, _strategy);
int index = BTreeFunctions.FindIndex<K, TStrategy>(_currentLeaf, key, keyPrefix, _strategy);
// Set position to (index - 1) so that the first MoveNext() lands on 'index'
_currentLeafIndex = index - 1;

View file

@ -117,10 +117,10 @@ public static class PrefixScanner
// If target is MinValue, any value in prefixes is >= target.
// So the first element (index 0) is the match.
// TODO: evaluate if this is needed.
if (targetPrefix == long.MinValue)
{
return 0;
}
//if (targetPrefix == long.MinValue)
//{
// return 0;
//}
// Fallback for short arrays or unsupported hardware
if (!Avx2.IsSupported || prefixes.Length < 4)

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

113
PersistentMap/Readme.org Normal file
View file

@ -0,0 +1,113 @@
* NiceBtree (PersistentMap)
A high-performance, persistent (Copy-on-Write) B+ Tree implemented in C#.
It is designed for zero-overhead reads, SIMD-accelerated key routing, and allocation-free range queries. It supports both fully immutable usage and "Transient" mode for high-throughput bulk mutations.
** Features
- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tree yields a new version while sharing unmodified nodes.
- *Transient Mode*: Perform bulk mutations in-place with standard mutable performance, then freeze it into a =PersistentMap= in $O(1)$ time.
- *SIMD Prefix Scanning*: Uses AVX2/AVX512 to vectorize B+ tree routing and binary searches via =long= key-prefixes.
- *Linear Time Set Operations*: Sort-merge based =Intersect=, =Except=, and =SymmetricExcept= execute in $O(N+M)$ time using lazy evaluation.
** When should I use this?
Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does not have any problems key collisions, which will slow this map down by a lot.
** Quick Start
*** 1. Basic Immutable Usage
By default, the map is immutable. Every write operation returns a new, updated version of the map.
#+begin_src csharp
// Create a map with a specific key strategy (e.g., Int, Unicode, Double)
var map1 = BaseOrderedMap<int, string, IntStrategy>.Create(new IntStrategy());
// Set returns a new tree instance. map1 remains empty.
var map2 = map1.Set(1, "Apple")
.Set(2, "Banana")
.Set(3, "Cherry");
if (map2.TryGetValue(2, out var value))
{
Console.WriteLine(value); // "Banana"
}
#+end_src
*** 2. Transient Mode (Bulk Mutations)
If you need to insert thousands of elements, creating a new persistent tree on every insert is too slow. Use a =TransientMap= to mutate the tree in-place, then lock it into a persistent snapshot.
#+begin_src csharp
var transientMap = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
// Mutates in-place. No allocations for unchanged tree paths.
for (int i = 0; i < 10_000; i++)
{
transientMap.Set(i, $"Value_{i}");
}
// O(1) freeze. Returns a thread-safe immutable PersistentMap.
var persistentSnapshot = transientMap.ToPersistent();
#+end_src
*** 3. Range Queries and Iteration
Because it is a B+ tree, leaf nodes are linked. Range queries require zero allocations and simply walk the leaves.
#+begin_src csharp
var map = GetPopulatedMap();
// Iterate exact bounds
foreach (var kvp in map.Range(min: 10, max: 50))
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Open-ended queries
var greaterThan100 = map.From(100);
var lessThan50 = map.Until(50);
var allElements = map.AsEnumerable();
#+end_src
*** 4. Tree Navigation
Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound.
#+begin_src csharp
// Get extremes
map.TryGetMin(out int minKey, out string minVal);
map.TryGetMax(out int maxKey, out string maxVal);
// Get the immediate next/previous element (works even if '42' doesn't exist)
if (map.TryGetSuccessor(42, out int nextKey, out string nextVal))
{
Console.WriteLine($"The key immediately after 42 is {nextKey}");
}
if (map.TryGetPredecessor(42, out int prevKey, out string prevVal))
{
Console.WriteLine($"The key immediately before 42 is {prevKey}");
}
#+end_src
*** 5. Set Operations
Set operations take advantage of the tree's underlying sorted linked-list structure to merge trees in linear $O(N+M)$ time.
#+begin_src csharp
var mapA = CreateMap(1, 2, 3, 4);
var mapB = CreateMap(3, 4, 5, 6);
// Returns { 3, 4 }
var common = mapA.Intersect(mapB);
// Returns { 1, 2 }
var onlyInA = mapA.Except(mapB);
// Returns { 1, 2, 5, 6 }
var symmetricDiff = mapA.SymmetricExcept(mapB);
#+end_src
** Architecture Notes: Key Strategies
NiceBtree uses =IKeyStrategy<K>= to map generic keys (like =string= or =double=) into sortable =long= prefixes. This achieves two things:
1. Enables AVX512/AVX2 vector instructions to search internal nodes simultaneously.
2. Avoids expensive =IComparable<T>= interface calls or =string.Compare= during the initial descent of the tree, only falling back to exact comparisons when refining the search within a leaf.
This means that it will be fast for integers and anything you can pack in 8 bytes. If you use stings with high prefix entropy, this will be /very/ performant. If you don't, it is just another b+tree.

View file

@ -2,7 +2,7 @@ using System.Collections;
namespace PersistentMap;
public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy>, IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy> where TStrategy : IKeyStrategy<K>
{
// This is mutable, but we treat it as readonly for the ID generation logic usually.
private OwnerId _transactionId;
@ -27,17 +27,12 @@ public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrate
public PersistentMap<K, V, TStrategy> ToPersistent()
{
// 1. Create the snapshot.
// The nodes currently have _transactionId.
// The PersistentMap will read them fine (it reads anything).
// BUT: If we write to PersistentMap, it uses OwnerId.None, so it COPIES. (Safe)
// 1. Create the snapshot by copying all relevant information
var snapshot = new PersistentMap<K, V, TStrategy>(_root, _strategy, Count);
// 2. Protect the snapshot from THIS TransientMap.
// If we Set() again on this map, we have the same _transactionId.
// We would mutate the nodes we just gave to the snapshot.
// FIX: "Seal" the current transaction by rolling to a new ID.
// 2. Protect the snapshot from THIS TransientMap by getting a new ownerId
// so that future edits will be done by CoW
_transactionId = OwnerId.Next();
return snapshot;

View file

@ -21,7 +21,7 @@ public class BTreeFuzzTests
// CONFIGURATION
const int Iterations = 100_000; // High enough to trigger all splits/merges
const int KeyRange = 5000; // Small enough to cause frequent collisions
const bool showOps = true;
const bool showOps = false;
int Seed = 2135974; // Environment.TickCount;
// ORACLES

View file

@ -0,0 +1,181 @@
using System.Linq;
using Xunit;
using PersistentMap;
namespace PersistentMap.Tests
{
public class BTreeExtendedOperationsTests
{
// Helper to quickly spin up a populated map
private TransientMap<int, string, IntStrategy> CreateMap(params int[] keys)
{
var map = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
foreach (var key in keys)
{
map.Set(key, $"val_{key}");
}
return map;
}
[Fact]
public void MinMax_OnEmptyTree_ReturnsFalse()
{
var map = CreateMap();
bool hasMin = map.TryGetMin(out int minKey, out string minVal);
bool hasMax = map.TryGetMax(out int maxKey, out string maxVal);
Assert.False(hasMin);
Assert.False(hasMax);
Assert.Equal(default, minKey);
Assert.Equal(default, maxKey);
}
[Fact]
public void MinMax_OnPopulatedTree_ReturnsCorrectExtremes()
{
var map = CreateMap(50, 10, 40, 20, 30); // Insert out of order
bool hasMin = map.TryGetMin(out int minKey, out string minVal);
bool hasMax = map.TryGetMax(out int maxKey, out string maxVal);
Assert.True(hasMin);
Assert.Equal(10, minKey);
Assert.Equal("val_10", minVal);
Assert.True(hasMax);
Assert.Equal(50, maxKey);
Assert.Equal("val_50", maxVal);
}
[Theory]
[InlineData(20, true, 30)] // Exact match, get next
[InlineData(25, true, 30)] // Missing key, gets first greater
[InlineData(50, false, 0)] // Max element has no successor
[InlineData(60, false, 0)] // Out of bounds high
public void Successor_ReturnsCorrectNextKey(int searchKey, bool expectedSuccess, int expectedNextKey)
{
var map = CreateMap(10, 20, 30, 40, 50);
bool success = map.TryGetSuccessor(searchKey, out int nextKey, out string nextVal);
Assert.Equal(expectedSuccess, success);
if (expectedSuccess)
{
Assert.Equal(expectedNextKey, nextKey);
Assert.Equal($"val_{expectedNextKey}", nextVal);
}
}
[Theory]
[InlineData(40, true, 30)] // Exact match, get previous
[InlineData(35, true, 30)] // Missing key, gets largest smaller
[InlineData(10, false, 0)] // Min element has no predecessor
[InlineData(5, false, 0)] // Out of bounds low
public void Predecessor_ReturnsCorrectPreviousKey(int searchKey, bool expectedSuccess, int expectedPrevKey)
{
var map = CreateMap(10, 20, 30, 40, 50);
bool success = map.TryGetPredecessor(searchKey, out int prevKey, out string prevVal);
Assert.Equal(expectedSuccess, success);
if (expectedSuccess)
{
Assert.Equal(expectedPrevKey, prevKey);
Assert.Equal($"val_{expectedPrevKey}", prevVal);
}
}
[Fact]
public void SuccessorPredecessor_CrossNodeBoundaries_WorksCorrectly()
{
// Insert 200 elements to guarantee leaf node splits (Capacity is 64)
// and internal node creation.
var keys = Enumerable.Range(1, 200).ToArray();
var map = CreateMap(keys);
// Test boundaries between multiple leaves
for (int i = 1; i < 200; i++)
{
// Successor
bool sFound = map.TryGetSuccessor(i, out int next, out _);
Assert.True(sFound);
Assert.Equal(i + 1, next);
// Predecessor
bool pFound = map.TryGetPredecessor(i + 1, out int prev, out _);
Assert.True(pFound);
Assert.Equal(i, prev);
}
}
[Fact]
public void SetOperations_Intersect_ReturnsCommonElements()
{
var mapA = CreateMap(1, 2, 3, 4, 5);
var mapB = CreateMap(4, 5, 6, 7, 8);
var intersect = mapA.Intersect(mapB).Select(kvp => kvp.Key).ToArray();
Assert.Equal(new[] { 4, 5 }, intersect);
}
[Fact]
public void SetOperations_Except_ReturnsElementsOnlyInFirstMap()
{
var mapA = CreateMap(1, 2, 3, 4, 5);
var mapB = CreateMap(4, 5, 6, 7, 8);
var aExceptB = mapA.Except(mapB).Select(kvp => kvp.Key).ToArray();
var bExceptA = mapB.Except(mapA).Select(kvp => kvp.Key).ToArray();
Assert.Equal(new[] { 1, 2, 3 }, aExceptB);
Assert.Equal(new[] { 6, 7, 8 }, bExceptA);
}
[Fact]
public void SetOperations_SymmetricExcept_ReturnsNonOverlappingElements()
{
var mapA = CreateMap(1, 2, 3, 4, 5);
var mapB = CreateMap(4, 5, 6, 7, 8);
var symmetric = mapA.SymmetricExcept(mapB).Select(kvp => kvp.Key).ToArray();
// Should return elements exclusively in A or B, but not both.
// Expected sorted naturally: 1, 2, 3, 6, 7, 8
Assert.Equal(new[] { 1, 2, 3, 6, 7, 8 }, symmetric);
}
[Fact]
public void SetOperations_WithEmptyMaps_HandleGracefully()
{
var populatedMap = CreateMap(1, 2, 3);
var emptyMap = CreateMap();
// Intersect with empty is empty
Assert.Empty(populatedMap.Intersect(emptyMap));
Assert.Empty(emptyMap.Intersect(populatedMap));
// Populated Except empty is Populated
Assert.Equal(new[] { 1, 2, 3 }, populatedMap.Except(emptyMap).Select(k => k.Key));
// Empty Except Populated is empty
Assert.Empty(emptyMap.Except(populatedMap));
// Symmetric Except with empty is just the populated map
Assert.Equal(new[] { 1, 2, 3 }, populatedMap.SymmetricExcept(emptyMap).Select(k => k.Key));
Assert.Equal(new[] { 1, 2, 3 }, emptyMap.SymmetricExcept(populatedMap).Select(k => k.Key));
}
[Fact]
public void SetOperations_CompleteOverlap_HandlesCorrectly()
{
var mapA = CreateMap(1, 2, 3);
var mapB = CreateMap(1, 2, 3);
Assert.Equal(new[] { 1, 2, 3 }, mapA.Intersect(mapB).Select(k => k.Key));
Assert.Empty(mapA.Except(mapB));
Assert.Empty(mapA.SymmetricExcept(mapB));
}
}
}

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PersistentMap\PersistentMap.csproj" />
</ItemGroup>
</Project>

70
TestProject1/UnitTest1.cs Normal file
View file

@ -0,0 +1,70 @@
namespace TestProject1;
using Xunit;
using PersistentMap;
public class BasicTests
{
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();
[Fact]
public void Transient_InsertAndGet_Works()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("Apple", 1);
map.Set("Banana", 2);
map.Set("Cherry", 3);
Assert.True(map.TryGetValue("Apple", out int v1));
Assert.Equal(1, v1);
Assert.True(map.TryGetValue("Banana", out int v2));
Assert.Equal(2, v2);
Assert.False(map.TryGetValue("Date", out _));
}
[Fact]
public void Transient_Update_Works()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("Key", 100);
map.Set("Key", 200); // Overwrite
map.TryGetValue("Key", out int val);
Assert.Equal(200, val);
}
[Fact]
public void Transient_Remove_Works()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("A", 1);
map.Set("B", 2);
map.Set("C", 3);
map.Remove("B");
Assert.True(map.ContainsKey("A"));
Assert.False(map.ContainsKey("B"));
Assert.True(map.ContainsKey("C"));
}
[Fact]
public void Transient_PrefixCollision_HandlesCollision()
{
// UnicodeStrategy only packs the first 4 chars.
// "Test1" and "Test2" have the same prefix "Test".
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_strategy);
map.Set("Test1", 1);
map.Set("Test2", 2);
Assert.True(map.TryGetValue("Test1", out var v1));
Assert.Equal(1, v1);
Assert.True(map.TryGetValue("Test2", out var v2));
Assert.Equal(2, v2);
}
}