Did some code cleanup,
added some extra thingies. switched to spans. Let google gemini do whatever it wanted..
This commit is contained in:
parent
978d0873dc
commit
7bea233edc
11 changed files with 944 additions and 248 deletions
|
|
@ -13,6 +13,9 @@ namespace PersistentMap
|
||||||
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
|
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
|
||||||
where TStrategy : IKeyStrategy<K>
|
where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
|
// 1. Calculate ONCE
|
||||||
|
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||||
|
|
||||||
Node<K> current = root;
|
Node<K> current = root;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +23,7 @@ namespace PersistentMap
|
||||||
{
|
{
|
||||||
var leaf = current.AsLeaf<V>();
|
var leaf = current.AsLeaf<V>();
|
||||||
// Leaf uses standard FindIndex (Lower Bound) to find exact match
|
// 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)
|
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||||
{
|
{
|
||||||
value = leaf.Values[index];
|
value = leaf.Values[index];
|
||||||
|
|
@ -33,172 +36,172 @@ namespace PersistentMap
|
||||||
{
|
{
|
||||||
// FIX: Internal uses FindRoutingIndex (Upper Bound)
|
// FIX: Internal uses FindRoutingIndex (Upper Bound)
|
||||||
var internalNode = current.AsInternal();
|
var internalNode = current.AsInternal();
|
||||||
int index = FindRoutingIndex(internalNode, key, strategy);
|
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||||
current = internalNode.Children[index]!;
|
current = internalNode.Children[index]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool countChanged)
|
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)
|
|
||||||
{
|
{
|
||||||
leaf.Values[index] = value;
|
root = root.EnsureEditable(owner);
|
||||||
added = false; // Key existed, value updated. Count does not change.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
added = true; // New key. Count +1.
|
var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged);
|
||||||
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 child = internalNode.Children[index]!.EnsureEditable(owner);
|
|
||||||
internalNode.Children[index] = child;
|
|
||||||
|
|
||||||
var split = InsertRecursive(child, key, value, strategy, owner, out added);
|
if (splitResult != null)
|
||||||
|
|
||||||
if (split != null)
|
|
||||||
{
|
|
||||||
if (internalNode.Header.Count < InternalNode<K>.Capacity - 1)
|
|
||||||
{
|
{
|
||||||
InsertIntoInternal(internalNode, index, split.Separator, split.NewNode, strategy);
|
var newRoot = new InternalNode<K>(owner);
|
||||||
return null;
|
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
|
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 API
|
||||||
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged)
|
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged)
|
||||||
where TStrategy : IKeyStrategy<K>
|
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)
|
|
||||||
{
|
{
|
||||||
var internalRoot = root.AsInternal();
|
root = root.EnsureEditable(owner);
|
||||||
if (internalRoot.Header.Count == 0)
|
|
||||||
|
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
|
// Internal Helpers: Search
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
// Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound)
|
// Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound)
|
||||||
// 2. Propagate to Helpers
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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>
|
where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
{
|
{
|
||||||
long keyPrefix = strategy.GetPrefix(key);
|
// Use the pre-calculated prefix here!
|
||||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
||||||
|
|
||||||
// Pass strategy to Refine
|
|
||||||
return RefineSearch(index, node.GetKeys(), key, strategy);
|
return RefineSearch(index, node.GetKeys(), key, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<K> keys = node.GetKeys();
|
|
||||||
|
|
||||||
|
|
||||||
return LinearSearchKeys(node.GetKeys(), key, strategy);
|
return LinearSearchKeys(node.GetKeys(), key, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static int LinearSearchKeys<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
|
private static int LinearSearchKeys<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
|
||||||
where TStrategy : IKeyStrategy<K>
|
where TStrategy : IKeyStrategy<K>
|
||||||
|
|
@ -227,26 +230,21 @@ where TStrategy : IKeyStrategy<K>
|
||||||
|
|
||||||
// Used by Internal Nodes: Finds the child index to descend into.
|
// 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).
|
// If Key == Separator, we must go RIGHT (index + 1), so we need (Upper Bound).
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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>
|
where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
if (!strategy.UsesPrefixes)
|
if (!strategy.UsesPrefixes)
|
||||||
{
|
{
|
||||||
// C. Fallback
|
|
||||||
return LinearSearchRouting(node.GetKeys(), key, strategy);
|
return LinearSearchRouting(node.GetKeys(), key, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the pre-calculated prefix here!
|
||||||
long keyPrefix = strategy.GetPrefix(key);
|
|
||||||
|
|
||||||
// SIMD still finds >=.
|
|
||||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
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);
|
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static int LinearSearchRouting<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
|
private static int LinearSearchRouting<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
|
||||||
where TStrategy : IKeyStrategy<K>
|
where TStrategy : IKeyStrategy<K>
|
||||||
|
|
@ -261,7 +259,7 @@ where TStrategy : IKeyStrategy<K>
|
||||||
return i;
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static int LinearSearchRouting<T>(Span<T> keys, T key) where T : struct, IComparable<T>
|
private static int LinearSearchRouting<T>(Span<T> keys, T key) where T : struct, IComparable<T>
|
||||||
{
|
{
|
||||||
|
|
@ -298,16 +296,18 @@ where TStrategy : IKeyStrategy<K>
|
||||||
private static SplitResult<K>? InsertRecur2sive<K, V, TStrategy>(Node<K> node, K key, V value, TStrategy strategy, OwnerId owner)
|
private static SplitResult<K>? InsertRecur2sive<K, V, TStrategy>(Node<K> node, K key, V value, TStrategy strategy, OwnerId owner)
|
||||||
where TStrategy : IKeyStrategy<K>
|
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 ---
|
// --- LEAF CASE ---
|
||||||
if (node.IsLeaf)
|
if (node.IsLeaf)
|
||||||
{
|
{
|
||||||
var leaf = node.AsLeaf<V>();
|
var leaf = node.AsLeaf<V>();
|
||||||
// Leaf uses FindIndex
|
// 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)
|
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||||
{
|
{
|
||||||
leaf.Values[index] = value;
|
leaf.Values[index] = value;
|
||||||
|
|
@ -327,12 +327,12 @@ where TStrategy : IKeyStrategy<K>
|
||||||
|
|
||||||
// --- INTERNAL CASE ---
|
// --- INTERNAL CASE ---
|
||||||
var internalNode = node.AsInternal();
|
var internalNode = node.AsInternal();
|
||||||
|
|
||||||
// FIX: Internal uses FindRoutingIndex
|
// FIX: Internal uses FindRoutingIndex
|
||||||
int childIndex = FindRoutingIndex(internalNode, key, strategy);
|
int childIndex = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||||
|
|
||||||
var child = internalNode.Children[childIndex]!.EnsureEditable(owner);
|
var child = internalNode.Children[childIndex]!.EnsureEditable(owner);
|
||||||
internalNode.Children[childIndex] = child;
|
internalNode.Children[childIndex] = child;
|
||||||
|
|
||||||
var split = InsertRecursive(child, key, value, strategy, owner, out _);
|
var split = InsertRecursive(child, key, value, strategy, owner, out _);
|
||||||
|
|
||||||
|
|
@ -358,26 +358,25 @@ where TStrategy : IKeyStrategy<K>
|
||||||
int count = leaf.Header.Count;
|
int count = leaf.Header.Count;
|
||||||
if (index < count)
|
if (index < count)
|
||||||
{
|
{
|
||||||
// Arrays allow access up to .Length (64), ignoring 'Count'
|
int moveCount = count - index;
|
||||||
Array.Copy(leaf.Keys, index, leaf.Keys, index + 1, count - index);
|
// Fast Span memory moves
|
||||||
|
leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1));
|
||||||
// This fails if leaf.Values is a Span<V> of length 'count'
|
leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1));
|
||||||
Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index);
|
|
||||||
|
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
{
|
{
|
||||||
leaf.AllPrefixes.Slice(index, count-index)
|
leaf.AllPrefixes.Slice(index, count - index)
|
||||||
.CopyTo(leaf.AllPrefixes.Slice(index+1));
|
.CopyTo(leaf.AllPrefixes.Slice(index + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf.Keys[index] = key;
|
leaf.Keys[index] = key;
|
||||||
|
|
||||||
// This fails if leaf.Values is a Span<V> of length 'count'
|
// This fails if leaf.Values is a Span<V> of length 'count'
|
||||||
leaf.Values[index] = value;
|
leaf.Values[index] = value;
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
leaf.AllPrefixes![index] = strategy.GetPrefix(key);
|
leaf.AllPrefixes![index] = strategy.GetPrefix(key);
|
||||||
|
|
||||||
leaf.SetCount(count + 1);
|
leaf.SetCount(count + 1);
|
||||||
}
|
}
|
||||||
private static SplitResult<K> SplitLeaf<K, V, TStrategy>(LeafNode<K, V> left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner)
|
private static SplitResult<K> SplitLeaf<K, V, TStrategy>(LeafNode<K, V> left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner)
|
||||||
|
|
@ -385,7 +384,7 @@ where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
var right = new LeafNode<K, V>(owner);
|
var right = new LeafNode<K, V>(owner);
|
||||||
int totalCount = left.Header.Count;
|
int totalCount = left.Header.Count;
|
||||||
|
|
||||||
// Heuristics
|
// Heuristics
|
||||||
int splitPoint;
|
int splitPoint;
|
||||||
if (insertIndex == totalCount) splitPoint = totalCount; // Append: Keep all in Left (90/10 logic effectively)
|
if (insertIndex == totalCount) splitPoint = totalCount; // Append: Keep all in Left (90/10 logic effectively)
|
||||||
|
|
@ -396,11 +395,12 @@ where TStrategy : IKeyStrategy<K>
|
||||||
int moveCount = totalCount - splitPoint;
|
int moveCount = totalCount - splitPoint;
|
||||||
if (moveCount > 0)
|
if (moveCount > 0)
|
||||||
{
|
{
|
||||||
Array.Copy(left.Keys, splitPoint, right.Keys, 0, moveCount);
|
// Fast Span memory moves
|
||||||
Array.Copy(left.Values, splitPoint, right.Values, 0, moveCount);
|
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
|
// Manually copy prefixes if needed or re-calculate
|
||||||
if (strategy.UsesPrefixes)
|
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
|
// Update Counts
|
||||||
|
|
@ -417,9 +417,6 @@ where TStrategy : IKeyStrategy<K>
|
||||||
InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy);
|
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
|
// In B+ Tree, the separator is the first key of the right node
|
||||||
return new SplitResult<K>(right, right.Keys[0]);
|
return new SplitResult<K>(right, right.Keys[0]);
|
||||||
|
|
@ -429,20 +426,24 @@ where TStrategy : IKeyStrategy<K>
|
||||||
where TStrategy : IKeyStrategy<K>
|
where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
int count = node.Header.Count;
|
int count = node.Header.Count;
|
||||||
|
|
||||||
// Shift Keys and Prefixes
|
// Shift Keys and Prefixes
|
||||||
if (index < count)
|
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
|
// FIX: Shift raw prefix array
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
{
|
{
|
||||||
node.AllPrefixes.Slice(index, count-index)
|
node.AllPrefixes.Slice(index, count - index)
|
||||||
.CopyTo(node.AllPrefixes.Slice(index +1));
|
.CopyTo(node.AllPrefixes.Slice(index + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shift Children
|
// Shift Children
|
||||||
// Children buffer is indexable like an array but requires manual loop or Unsafe copy
|
// Children buffer is indexable like an array but requires manual loop or Unsafe copy
|
||||||
// if we don't want to use unsafe pointers.
|
// if we don't want to use unsafe pointers.
|
||||||
|
|
@ -453,11 +454,11 @@ where TStrategy : IKeyStrategy<K>
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Keys[index] = separator;
|
node.Keys[index] = separator;
|
||||||
|
|
||||||
// FIX: Write to raw array
|
// FIX: Write to raw array
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
node.AllPrefixes![index] = strategy.GetPrefix(separator);
|
node.AllPrefixes![index] = strategy.GetPrefix(separator);
|
||||||
|
|
||||||
node.Children[index + 1] = newChild;
|
node.Children[index + 1] = newChild;
|
||||||
node.SetCount(count + 1);
|
node.SetCount(count + 1);
|
||||||
}
|
}
|
||||||
|
|
@ -471,16 +472,21 @@ where TStrategy : IKeyStrategy<K>
|
||||||
|
|
||||||
// The key at splitPoint moves UP to become the separator.
|
// The key at splitPoint moves UP to become the separator.
|
||||||
// Keys > splitPoint move to Right.
|
// Keys > splitPoint move to Right.
|
||||||
|
|
||||||
K upKey = left.Keys[splitPoint];
|
K upKey = left.Keys[splitPoint];
|
||||||
|
|
||||||
// Move Keys/Prefixes to Right
|
// Move Keys/Prefixes to Right
|
||||||
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
|
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
|
||||||
Array.Copy(left.Keys, splitPoint + 1, right.Keys, 0, moveCount);
|
// Fast Span memory moves
|
||||||
if (strategy.UsesPrefixes)
|
if (moveCount > 0)
|
||||||
for(int i=0; i<moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + 1 + i];
|
{
|
||||||
|
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
|
// Left has children 0..splitPoint. Right has children splitPoint+1..End
|
||||||
for (int i = 0; i <= moveCount; i++)
|
for (int i = 0; i <= moveCount; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -494,7 +500,7 @@ where TStrategy : IKeyStrategy<K>
|
||||||
// Note: We extracted 'upKey' from the original array.
|
// Note: We extracted 'upKey' from the original array.
|
||||||
// We now have to compare the *incoming* separator with 'upKey'
|
// We now have to compare the *incoming* separator with 'upKey'
|
||||||
// to see if it goes Left or Right.
|
// to see if it goes Left or Right.
|
||||||
|
|
||||||
if (insertIndex == splitPoint)
|
if (insertIndex == splitPoint)
|
||||||
{
|
{
|
||||||
// Special case: The new key is exactly the one pushing up?
|
// Special case: The new key is exactly the one pushing up?
|
||||||
|
|
@ -526,16 +532,22 @@ where TStrategy : IKeyStrategy<K>
|
||||||
|
|
||||||
|
|
||||||
private static void RemoveFromLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, TStrategy strategy)
|
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;
|
int count = leaf.Header.Count;
|
||||||
Array.Copy(leaf.Keys, index + 1, leaf.Keys, index, count - index - 1);
|
int moveCount = count - index - 1;
|
||||||
Array.Copy(leaf.Values, index + 1, leaf.Values, index, count - index - 1);
|
|
||||||
|
|
||||||
if (strategy.UsesPrefixes)
|
if (moveCount > 0)
|
||||||
{
|
{
|
||||||
var p = leaf.AllPrefixes;
|
// Fast Span memory moves
|
||||||
for (int i = index; i < count - 1; i++) p[i] = p[i + 1];
|
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);
|
leaf.SetCount(count - 1);
|
||||||
|
|
@ -582,8 +594,8 @@ where TStrategy : IKeyStrategy<K>
|
||||||
return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
|
return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CanBorrow<K>(Node<K> node)
|
private static bool CanBorrow<K>(Node<K> node)
|
||||||
|
|
@ -601,65 +613,69 @@ where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
var leftLeaf = left.AsLeaf<V>();
|
var leftLeaf = left.AsLeaf<V>();
|
||||||
var rightLeaf = right.AsLeaf<V>();
|
var rightLeaf = right.AsLeaf<V>();
|
||||||
|
|
||||||
int lCount = leftLeaf.Header.Count;
|
int lCount = leftLeaf.Header.Count;
|
||||||
int rCount = rightLeaf.Header.Count;
|
int rCount = rightLeaf.Header.Count;
|
||||||
|
rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount));
|
||||||
Array.Copy(rightLeaf.Keys, 0, leftLeaf.Keys, lCount, rCount);
|
rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount));
|
||||||
Array.Copy(rightLeaf.Values, 0, leftLeaf.Values, lCount, rCount);
|
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
{
|
{
|
||||||
rightLeaf.AllPrefixes.Slice(0, rCount)
|
rightLeaf.AllPrefixes.Slice(0, rCount)
|
||||||
.CopyTo(leftLeaf.AllPrefixes.Slice(lCount));
|
.CopyTo(leftLeaf.AllPrefixes.Slice(lCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
leftLeaf.SetCount(lCount + rCount);
|
leftLeaf.SetCount(lCount + rCount);
|
||||||
leftLeaf.Next = rightLeaf.Next;
|
|
||||||
}
|
}
|
||||||
// Case B: Merging Internal Nodes
|
// Case B: Merging Internal Nodes
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var leftInternal = left.AsInternal();
|
var leftInternal = left.AsInternal();
|
||||||
var rightInternal = right.AsInternal();
|
var rightInternal = right.AsInternal();
|
||||||
|
|
||||||
// Pull separator from parent
|
// Pull separator from parent
|
||||||
K separator = parent.Keys[separatorIndex];
|
K separator = parent.Keys[separatorIndex];
|
||||||
|
|
||||||
int lCount = leftInternal.Header.Count;
|
int lCount = leftInternal.Header.Count;
|
||||||
leftInternal.Keys[lCount] = separator;
|
leftInternal.Keys[lCount] = separator;
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator);
|
leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator);
|
||||||
|
|
||||||
int rCount = rightInternal.Header.Count;
|
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)
|
if (strategy.UsesPrefixes)
|
||||||
{
|
{
|
||||||
rightInternal.AllPrefixes.Slice(0, rCount)
|
rightInternal.AllPrefixes.Slice(0, rCount)
|
||||||
.CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1));
|
.CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i <= rCount; i++)
|
for (int i = 0; i <= rCount; i++)
|
||||||
{
|
{
|
||||||
leftInternal.Children[lCount + 1 + i] = rightInternal.Children[i];
|
leftInternal.Children[lCount + 1 + i] = rightInternal.Children[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
leftInternal.SetCount(lCount + 1 + rCount);
|
leftInternal.SetCount(lCount + 1 + rCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove Separator and Right Child from Parent
|
// Remove Separator and Right Child from Parent
|
||||||
int pCount = parent.Header.Count;
|
int pCount = parent.Header.Count;
|
||||||
Array.Copy(parent.Keys, separatorIndex + 1, parent.Keys, separatorIndex, pCount - separatorIndex - 1);
|
int moveCount = pCount - separatorIndex - 1;
|
||||||
if (strategy.UsesPrefixes)
|
|
||||||
|
if (moveCount > 0)
|
||||||
{
|
{
|
||||||
var pp = parent.AllPrefixes;
|
parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex));
|
||||||
for (int i = separatorIndex; i < pCount - 1; i++) pp[i] = pp[i + 1];
|
|
||||||
|
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];
|
parent.Children[i - 1] = parent.Children[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.SetCount(pCount - 1);
|
parent.SetCount(pCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -671,11 +687,11 @@ where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
var leftLeaf = left.AsLeaf<V>();
|
var leftLeaf = left.AsLeaf<V>();
|
||||||
var rightLeaf = right.AsLeaf<V>();
|
var rightLeaf = right.AsLeaf<V>();
|
||||||
|
|
||||||
// Move first of right to end of left
|
// Move first of right to end of left
|
||||||
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
|
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
|
||||||
RemoveFromLeaf(rightLeaf, 0, strategy);
|
RemoveFromLeaf(rightLeaf, 0, strategy);
|
||||||
|
|
||||||
// Update Parent Separator
|
// Update Parent Separator
|
||||||
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
|
|
@ -685,30 +701,35 @@ where TStrategy : IKeyStrategy<K>
|
||||||
{
|
{
|
||||||
var leftInternal = left.AsInternal();
|
var leftInternal = left.AsInternal();
|
||||||
var rightInternal = right.AsInternal();
|
var rightInternal = right.AsInternal();
|
||||||
|
|
||||||
// 1. Move Parent Separator to Left End
|
// 1. Move Parent Separator to Left End
|
||||||
K sep = parent.Keys[separatorIndex];
|
K sep = parent.Keys[separatorIndex];
|
||||||
InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy);
|
InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy);
|
||||||
|
|
||||||
// 2. Move Right[0] Key to Parent
|
// 2. Move Right[0] Key to Parent
|
||||||
parent.Keys[separatorIndex] = rightInternal.Keys[0];
|
parent.Keys[separatorIndex] = rightInternal.Keys[0];
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]);
|
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]);
|
||||||
|
|
||||||
// 3. Fix Right (Remove key 0 and shift child 0 out)
|
// 3. Fix Right (Remove key 0 and shift child 0 out)
|
||||||
// We basically remove key at 0. Child 0 was moved to left. Child 1 becomes Child 0.
|
// We basically remove key at 0. Child 0 was moved to left. Child 1 becomes Child 0.
|
||||||
// Re-using Remove logic implies shifts.
|
// Re-using Remove logic implies shifts.
|
||||||
// Manual shift for performance:
|
// Manual shift for performance:
|
||||||
int rCount = rightInternal.Header.Count;
|
int rCount = rightInternal.Header.Count;
|
||||||
|
|
||||||
// Shift children
|
// 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)
|
||||||
// Shift keys
|
{
|
||||||
Array.Copy(rightInternal.Keys, 1, rightInternal.Keys, 0, rCount - 1);
|
// Fast Span memory moves (Replaces Array.Copy & manual loop)
|
||||||
var rp = rightInternal.AllPrefixes;
|
rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0));
|
||||||
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);
|
rightInternal.SetCount(rCount - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -722,10 +743,10 @@ where TStrategy : IKeyStrategy<K>
|
||||||
var leftLeaf = left.AsLeaf<V>();
|
var leftLeaf = left.AsLeaf<V>();
|
||||||
var rightLeaf = right.AsLeaf<V>();
|
var rightLeaf = right.AsLeaf<V>();
|
||||||
int last = leftLeaf.Header.Count - 1;
|
int last = leftLeaf.Header.Count - 1;
|
||||||
|
|
||||||
InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy);
|
InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy);
|
||||||
RemoveFromLeaf(leftLeaf, last, strategy);
|
RemoveFromLeaf(leftLeaf, last, strategy);
|
||||||
|
|
||||||
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
|
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
|
||||||
|
|
@ -735,20 +756,184 @@ where TStrategy : IKeyStrategy<K>
|
||||||
var leftInternal = (InternalNode<K>)left;
|
var leftInternal = (InternalNode<K>)left;
|
||||||
var rightInternal = (InternalNode<K>)right;
|
var rightInternal = (InternalNode<K>)right;
|
||||||
int last = leftInternal.Header.Count - 1;
|
int last = leftInternal.Header.Count - 1;
|
||||||
|
|
||||||
// 1. Move Parent Separator to Right Start
|
// 1. Move Parent Separator to Right Start
|
||||||
K sep = parent.Keys[separatorIndex];
|
K sep = parent.Keys[separatorIndex];
|
||||||
// The child moving to right is the *last* child of left (index count)
|
// The child moving to right is the *last* child of left (index count)
|
||||||
InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy);
|
InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy);
|
||||||
|
|
||||||
// 2. Move Left[last] Key to Parent
|
// 2. Move Left[last] Key to Parent
|
||||||
parent.Keys[separatorIndex] = leftInternal.Keys[last];
|
parent.Keys[separatorIndex] = leftInternal.Keys[last];
|
||||||
if (strategy.UsesPrefixes)
|
if (strategy.UsesPrefixes)
|
||||||
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
|
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
|
||||||
|
|
||||||
// 3. Truncate Left
|
// 3. Truncate Left
|
||||||
leftInternal.SetCount(last);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,4 +80,118 @@ public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair
|
||||||
// 4. Until (Start at beginning)
|
// 4. Until (Start at beginning)
|
||||||
public BTreeEnumerable<K, V, TStrategy> Until(K max)
|
public BTreeEnumerable<K, V, TStrategy> Until(K max)
|
||||||
=> new(_root, _strategy, false, default, true, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,14 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
||||||
{
|
{
|
||||||
Node<K> node = _root;
|
Node<K> node = _root;
|
||||||
_depth = 0;
|
_depth = 0;
|
||||||
|
long keyPrefix = _strategy.UsesPrefixes ? _strategy.GetPrefix(key) : 0;
|
||||||
|
|
||||||
|
|
||||||
// Dive using Routing
|
// Dive using Routing
|
||||||
while (!node.IsLeaf)
|
while (!node.IsLeaf)
|
||||||
{
|
{
|
||||||
var internalNode = node.AsInternal();
|
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;
|
_nodeStack[_depth] = internalNode;
|
||||||
_indexStack[_depth] = idx;
|
_indexStack[_depth] = idx;
|
||||||
|
|
@ -136,7 +138,7 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
||||||
|
|
||||||
// Find index in Leaf
|
// Find index in Leaf
|
||||||
_currentLeaf = node.AsLeaf<V>();
|
_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'
|
// Set position to (index - 1) so that the first MoveNext() lands on 'index'
|
||||||
_currentLeafIndex = index - 1;
|
_currentLeafIndex = index - 1;
|
||||||
|
|
@ -242,4 +244,4 @@ public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,10 @@ public static class PrefixScanner
|
||||||
// If target is MinValue, any value in prefixes is >= target.
|
// If target is MinValue, any value in prefixes is >= target.
|
||||||
// So the first element (index 0) is the match.
|
// So the first element (index 0) is the match.
|
||||||
// TODO: evaluate if this is needed.
|
// TODO: evaluate if this is needed.
|
||||||
if (targetPrefix == long.MinValue)
|
//if (targetPrefix == long.MinValue)
|
||||||
{
|
//{
|
||||||
return 0;
|
// return 0;
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Fallback for short arrays or unsupported hardware
|
// Fallback for short arrays or unsupported hardware
|
||||||
if (!Avx2.IsSupported || prefixes.Length < 4)
|
if (!Avx2.IsSupported || prefixes.Length < 4)
|
||||||
|
|
@ -207,4 +207,4 @@ public static class PrefixScanner
|
||||||
|
|
||||||
return LinearScan(prefixes.Slice(i), target) + i;
|
return LinearScan(prefixes.Slice(i), target) + i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
PersistentMap/PersistentMap.csproj
Normal file
11
PersistentMap/PersistentMap.csproj
Normal 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
113
PersistentMap/Readme.org
Normal 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.
|
||||||
|
|
@ -2,7 +2,7 @@ using System.Collections;
|
||||||
|
|
||||||
namespace PersistentMap;
|
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.
|
// This is mutable, but we treat it as readonly for the ID generation logic usually.
|
||||||
private OwnerId _transactionId;
|
private OwnerId _transactionId;
|
||||||
|
|
@ -27,17 +27,12 @@ public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrate
|
||||||
|
|
||||||
public PersistentMap<K, V, TStrategy> ToPersistent()
|
public PersistentMap<K, V, TStrategy> ToPersistent()
|
||||||
{
|
{
|
||||||
// 1. Create the snapshot.
|
// 1. Create the snapshot by copying all relevant information
|
||||||
// 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)
|
|
||||||
|
|
||||||
var snapshot = new PersistentMap<K, V, TStrategy>(_root, _strategy, Count);
|
var snapshot = new PersistentMap<K, V, TStrategy>(_root, _strategy, Count);
|
||||||
|
|
||||||
// 2. Protect the snapshot from THIS TransientMap.
|
// 2. Protect the snapshot from THIS TransientMap by getting a new ownerId
|
||||||
// If we Set() again on this map, we have the same _transactionId.
|
// so that future edits will be done by CoW
|
||||||
// We would mutate the nodes we just gave to the snapshot.
|
|
||||||
// FIX: "Seal" the current transaction by rolling to a new ID.
|
|
||||||
_transactionId = OwnerId.Next();
|
_transactionId = OwnerId.Next();
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class BTreeFuzzTests
|
||||||
// CONFIGURATION
|
// CONFIGURATION
|
||||||
const int Iterations = 100_000; // High enough to trigger all splits/merges
|
const int Iterations = 100_000; // High enough to trigger all splits/merges
|
||||||
const int KeyRange = 5000; // Small enough to cause frequent collisions
|
const int KeyRange = 5000; // Small enough to cause frequent collisions
|
||||||
const bool showOps = true;
|
const bool showOps = false;
|
||||||
int Seed = 2135974; // Environment.TickCount;
|
int Seed = 2135974; // Environment.TickCount;
|
||||||
|
|
||||||
// ORACLES
|
// ORACLES
|
||||||
|
|
@ -167,4 +167,4 @@ public class BTreeFuzzTests
|
||||||
throw new Exception("Enumerator has extra items!");
|
throw new Exception("Enumerator has extra items!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
181
TestProject1/OrderedQueriesTest.cs
Normal file
181
TestProject1/OrderedQueriesTest.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
TestProject1/TestProject1.csproj
Normal file
25
TestProject1/TestProject1.csproj
Normal 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
70
TestProject1/UnitTest1.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue