Rename because it is ordered
This commit is contained in:
parent
b5b363ae9f
commit
e3cec3423b
28 changed files with 104 additions and 104 deletions
864
PersistentOrderedMap/BTreeFunctions.cs
Normal file
864
PersistentOrderedMap/BTreeFunctions.cs
Normal file
|
|
@ -0,0 +1,864 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PersistentOrderedMap
|
||||
{
|
||||
public static class BTreeFunctions
|
||||
{
|
||||
// ---------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// <summary>TryGetValue tries to get the value at mapping key. If it finds the key it sets th
|
||||
/// out var to value and returns true. </summary>
|
||||
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
// We always get a strategy to avoid branching already here
|
||||
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
|
||||
|
||||
Node<K> current = root;
|
||||
while (true)
|
||||
{
|
||||
if (current.IsLeaf)
|
||||
{
|
||||
var leaf = current.AsLeaf<V>();
|
||||
int index = FindIndex(leaf, key, keyPrefix, strategy);
|
||||
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
|
||||
{
|
||||
value = leaf.Values[index];
|
||||
return true;
|
||||
}
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var internalNode = current.AsInternal();
|
||||
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
|
||||
current = internalNode.Children[index]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Todo, this should really be made a tuple return value to not stress the GC
|
||||
var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged);
|
||||
|
||||
if (splitResult != null)
|
||||
{
|
||||
var newRoot = strategy.UsesPrefixes
|
||||
? new PrefixInternalNode<K>(owner)
|
||||
: new InternalNode<K>(owner);
|
||||
|
||||
newRoot.Keys[0] = splitResult.Separator;
|
||||
newRoot.Children[0] = root;
|
||||
newRoot.Children[1] = splitResult.NewNode;
|
||||
newRoot.SetCount(1);
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator);
|
||||
}
|
||||
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static SplitResult<K>? InsertRecursive<K, V>(Node<K> node, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool added)
|
||||
{
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
added = true;
|
||||
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, 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;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var internalRoot = root.AsInternal();
|
||||
if (internalRoot.Header.Count == 0)
|
||||
{
|
||||
return internalRoot.Children[0]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
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;
|
||||
return leaf.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||
}
|
||||
|
||||
removed = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Internal Helpers: Search
|
||||
// ---------------------------------------------------------
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, long keyPrefix, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (typeof(K) == typeof(int))
|
||||
{
|
||||
Span<K> keys = node.GetKeys();
|
||||
ref K firstKeyRef = ref MemoryMarshal.GetReference(keys);
|
||||
ref int firstIntRef = ref Unsafe.As<K, int>(ref firstKeyRef);
|
||||
ReadOnlySpan<int> intKeys = MemoryMarshal.CreateReadOnlySpan(ref firstIntRef, keys.Length);
|
||||
int intKey = Unsafe.As<K, int>(ref key);
|
||||
return IntScanner.FindFirstGreaterOrEqual(intKeys, intKey);
|
||||
}
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
||||
return RefineSearch(index, node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
return FallbackSearchKeys(node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, long keyPrefix, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
|
||||
if (typeof(K) == typeof(int))
|
||||
{
|
||||
Span<K> keys = node.GetKeys();
|
||||
ref K firstKeyRef = ref MemoryMarshal.GetReference(keys);
|
||||
ref int firstIntRef = ref Unsafe.As<K, int>(ref firstKeyRef);
|
||||
ReadOnlySpan<int> intKeys = MemoryMarshal.CreateReadOnlySpan(ref firstIntRef, keys.Length);
|
||||
int intKey = Unsafe.As<K, int>(ref key);
|
||||
return IntScanner.FindFirstGreater(intKeys, intKey);
|
||||
}
|
||||
if (!strategy.UsesPrefixes)
|
||||
{
|
||||
return FallbackRoutingKeys(node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
|
||||
return RefineRouting(index, node.GetKeys(), key, strategy);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int RefineSearch<K, TStrategy>(int startIndex, ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int i = startIndex;
|
||||
while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int RefineRouting<K, TStrategy>(int startIndex, ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int i = startIndex;
|
||||
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int FallbackSearchKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
return strategy.UseBinarySearch
|
||||
? BinarySearchKeys(keys, key, strategy)
|
||||
: LinearSearchKeys(keys, key, strategy);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int FallbackRoutingKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
return strategy.UseBinarySearch
|
||||
? BinaryRoutingKeys(keys, key, strategy)
|
||||
: LinearRoutingKeys(keys, key, strategy);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int LinearSearchKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int i = 0;
|
||||
while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int LinearRoutingKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int i = 0;
|
||||
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int BinarySearchKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int low = 0;
|
||||
int high = keys.Length - 1;
|
||||
ref K keysRef = ref MemoryMarshal.GetReference(keys);
|
||||
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = low + ((high - low) >> 1);
|
||||
K midKey = Unsafe.Add(ref keysRef, mid);
|
||||
int cmp = strategy.Compare(midKey, key);
|
||||
|
||||
if (cmp == 0) return mid;
|
||||
if (cmp < 0) low = mid + 1;
|
||||
else high = mid - 1;
|
||||
}
|
||||
return low;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int BinaryRoutingKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int low = 0;
|
||||
int high = keys.Length - 1;
|
||||
ref K keysRef = ref MemoryMarshal.GetReference(keys);
|
||||
|
||||
while (low <= high)
|
||||
{
|
||||
int mid = low + ((high - low) >> 1);
|
||||
K midKey = Unsafe.Add(ref keysRef, mid);
|
||||
int cmp = strategy.Compare(midKey, key);
|
||||
|
||||
if (cmp <= 0) low = mid + 1;
|
||||
else high = mid - 1;
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Insertion Logic
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private class SplitResult<K>
|
||||
{
|
||||
public Node<K> NewNode;
|
||||
public K Separator;
|
||||
public SplitResult(Node<K> newNode, K separator)
|
||||
{
|
||||
NewNode = newNode;
|
||||
Separator = separator;
|
||||
}
|
||||
}
|
||||
|
||||
private static void InsertIntoLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, K key, V value, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int count = leaf.Header.Count;
|
||||
if (index < count)
|
||||
{
|
||||
int moveCount = count - index;
|
||||
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, moveCount).CopyTo(leaf.AllPrefixes.Slice(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
leaf.Keys[index] = key;
|
||||
leaf.Values[index] = value;
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
leaf.AllPrefixes[index] = strategy.GetPrefix(key);
|
||||
}
|
||||
|
||||
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)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
var right = new LeafNode<K, V>(owner, strategy.UsesPrefixes);
|
||||
int totalCount = left.Header.Count;
|
||||
|
||||
int splitPoint = (insertIndex == totalCount) ? totalCount : (insertIndex == 0 ? 0 : totalCount / 2);
|
||||
int moveCount = totalCount - splitPoint;
|
||||
|
||||
if (moveCount > 0)
|
||||
{
|
||||
left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0));
|
||||
left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
left.AllPrefixes.Slice(splitPoint, moveCount).CopyTo(right.AllPrefixes);
|
||||
}
|
||||
}
|
||||
|
||||
left.SetCount(splitPoint);
|
||||
right.SetCount(moveCount);
|
||||
|
||||
if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0))
|
||||
{
|
||||
InsertIntoLeaf(left, insertIndex, key, value, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy);
|
||||
}
|
||||
|
||||
return new SplitResult<K>(right, right.Keys[0]);
|
||||
}
|
||||
|
||||
private static void InsertIntoInternal<K, TStrategy>(InternalNode<K> node, int index, K separator, Node<K> newChild, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int count = node.Header.Count;
|
||||
|
||||
if (index < count)
|
||||
{
|
||||
int moveCount = count - index;
|
||||
Span<K> keysSpan = node.Keys;
|
||||
keysSpan.Slice(index, moveCount).CopyTo(keysSpan.Slice(index + 1));
|
||||
|
||||
Span<Node<K>> childrenSpan = node.Children;
|
||||
childrenSpan.Slice(index + 1, moveCount).CopyTo(childrenSpan.Slice(index + 2));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
node.AllPrefixes.Slice(index, moveCount).CopyTo(node.AllPrefixes.Slice(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
node.Keys[index] = separator;
|
||||
node.Children[index + 1] = newChild;
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
node.AllPrefixes[index] = strategy.GetPrefix(separator);
|
||||
}
|
||||
|
||||
node.SetCount(count + 1);
|
||||
}
|
||||
|
||||
private static SplitResult<K> SplitInternal<K, TStrategy>(InternalNode<K> left, int insertIndex, K separator, Node<K> newChild, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
var right = strategy.UsesPrefixes
|
||||
? new PrefixInternalNode<K>(owner)
|
||||
: new InternalNode<K>(owner);
|
||||
|
||||
int count = left.Header.Count;
|
||||
int splitPoint = count / 2;
|
||||
K upKey = left.Keys[splitPoint];
|
||||
int moveCount = count - splitPoint - 1;
|
||||
|
||||
if (moveCount > 0)
|
||||
{
|
||||
Span<K> leftKeys = left.Keys;
|
||||
Span<K> rightKeys = right.Keys;
|
||||
leftKeys.Slice(splitPoint + 1, moveCount).CopyTo(rightKeys);
|
||||
|
||||
Span<Node<K>> leftChildren = left.Children;
|
||||
Span<Node<K>> rightChildren = right.Children;
|
||||
leftChildren.Slice(splitPoint + 1, moveCount + 1).CopyTo(rightChildren);
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes);
|
||||
}
|
||||
}
|
||||
|
||||
left.SetCount(splitPoint);
|
||||
right.SetCount(moveCount);
|
||||
|
||||
if (insertIndex <= splitPoint)
|
||||
{
|
||||
InsertIntoInternal(left, insertIndex, separator, newChild, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
InsertIntoInternal(right, insertIndex - (splitPoint + 1), separator, newChild, strategy);
|
||||
}
|
||||
|
||||
return new SplitResult<K>(right, upKey);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Removal Logic
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private static void RemoveFromLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
int count = leaf.Header.Count;
|
||||
int moveCount = count - index - 1;
|
||||
|
||||
if (moveCount > 0)
|
||||
{
|
||||
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)
|
||||
{
|
||||
leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index));
|
||||
}
|
||||
}
|
||||
|
||||
leaf.SetCount(count - 1);
|
||||
}
|
||||
|
||||
private static bool HandleUnderflow<K, V, TStrategy>(InternalNode<K> parent, int childIndex, TStrategy strategy, OwnerId owner)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (childIndex < parent.Header.Count)
|
||||
{
|
||||
var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner);
|
||||
parent.Children[childIndex + 1] = rightSibling;
|
||||
var leftChild = parent.Children[childIndex]!;
|
||||
|
||||
if (CanBorrow(rightSibling))
|
||||
{
|
||||
RotateLeft<K, V, TStrategy>(parent, childIndex, leftChild, rightSibling, strategy);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Merge<K, V, TStrategy>(parent, childIndex, leftChild, rightSibling, strategy);
|
||||
return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||
}
|
||||
}
|
||||
else if (childIndex > 0)
|
||||
{
|
||||
var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner);
|
||||
parent.Children[childIndex - 1] = leftSibling;
|
||||
var rightChild = parent.Children[childIndex]!;
|
||||
|
||||
if (CanBorrow(leftSibling))
|
||||
{
|
||||
RotateRight<K, V, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Merge<K, V, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
|
||||
return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanBorrow<K>(Node<K> node)
|
||||
{
|
||||
return node.Header.Count > 8 + 1;
|
||||
}
|
||||
|
||||
private static void Merge<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (left.IsLeaf)
|
||||
{
|
||||
var leftLeaf = left.AsLeaf<V>();
|
||||
var rightLeaf = right.AsLeaf<V>();
|
||||
|
||||
int lCount = leftLeaf.Header.Count;
|
||||
int rCount = rightLeaf.Header.Count;
|
||||
|
||||
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).CopyTo(leftLeaf.AllPrefixes.Slice(lCount));
|
||||
}
|
||||
|
||||
leftLeaf.SetCount(lCount + rCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
var leftInternal = left.AsInternal();
|
||||
var rightInternal = right.AsInternal();
|
||||
|
||||
K separator = parent.Keys[separatorIndex];
|
||||
|
||||
int lCount = leftInternal.Header.Count;
|
||||
leftInternal.Keys[lCount] = separator;
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator);
|
||||
}
|
||||
|
||||
int rCount = rightInternal.Header.Count;
|
||||
Span<K> rightKeys = rightInternal.Keys;
|
||||
Span<K> leftKeys = leftInternal.Keys;
|
||||
rightKeys.Slice(0, rCount).CopyTo(leftKeys.Slice(lCount + 1));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
rightInternal.AllPrefixes.Slice(0, rCount).CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1));
|
||||
}
|
||||
|
||||
Span<Node<K>> rightChildren = rightInternal.Children;
|
||||
Span<Node<K>> leftChildren = leftInternal.Children;
|
||||
rightChildren.Slice(0, rCount + 1).CopyTo(leftChildren.Slice(lCount + 1));
|
||||
|
||||
leftInternal.SetCount(lCount + 1 + rCount);
|
||||
}
|
||||
|
||||
int pCount = parent.Header.Count;
|
||||
int moveCount = pCount - separatorIndex - 1;
|
||||
|
||||
if (moveCount > 0)
|
||||
{
|
||||
Span<K> parentKeys = parent.Keys;
|
||||
parentKeys.Slice(separatorIndex + 1, moveCount).CopyTo(parentKeys.Slice(separatorIndex));
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex));
|
||||
}
|
||||
|
||||
Span<Node<K>> parentChildren = parent.Children;
|
||||
parentChildren.Slice(separatorIndex + 2, moveCount).CopyTo(parentChildren.Slice(separatorIndex + 1));
|
||||
}
|
||||
|
||||
parent.SetCount(pCount - 1);
|
||||
}
|
||||
|
||||
private static void RotateLeft<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (left.IsLeaf)
|
||||
{
|
||||
var leftLeaf = left.AsLeaf<V>();
|
||||
var rightLeaf = right.AsLeaf<V>();
|
||||
|
||||
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
|
||||
RemoveFromLeaf(rightLeaf, 0, strategy);
|
||||
|
||||
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var leftInternal = left.AsInternal();
|
||||
var rightInternal = right.AsInternal();
|
||||
|
||||
K sep = parent.Keys[separatorIndex];
|
||||
InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy);
|
||||
|
||||
parent.Keys[separatorIndex] = rightInternal.Keys[0];
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]);
|
||||
}
|
||||
|
||||
int rCount = rightInternal.Header.Count;
|
||||
|
||||
Span<Node<K>> rightChildren = rightInternal.Children;
|
||||
rightChildren.Slice(1, rCount).CopyTo(rightChildren);
|
||||
|
||||
if (rCount > 1)
|
||||
{
|
||||
Span<K> rightKeys = rightInternal.Keys;
|
||||
rightKeys.Slice(1, rCount - 1).CopyTo(rightKeys);
|
||||
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes);
|
||||
}
|
||||
}
|
||||
|
||||
rightInternal.SetCount(rCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RotateRight<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
if (left.IsLeaf)
|
||||
{
|
||||
var leftLeaf = left.AsLeaf<V>();
|
||||
var rightLeaf = right.AsLeaf<V>();
|
||||
int last = leftLeaf.Header.Count - 1;
|
||||
|
||||
InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy);
|
||||
RemoveFromLeaf(leftLeaf, last, strategy);
|
||||
|
||||
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var leftInternal = left.AsInternal();
|
||||
var rightInternal = right.AsInternal();
|
||||
int last = leftInternal.Header.Count - 1;
|
||||
|
||||
K sep = parent.Keys[separatorIndex];
|
||||
InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy);
|
||||
|
||||
parent.Keys[separatorIndex] = leftInternal.Keys[last];
|
||||
if (strategy.UsesPrefixes)
|
||||
{
|
||||
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(leftInternal.Keys[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++;
|
||||
|
||||
if (index < leaf.Header.Count)
|
||||
{
|
||||
nextKey = leaf.Keys[index];
|
||||
nextValue = leaf.Values[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = depth - 1; i >= 0; i--)
|
||||
{
|
||||
if (indices[i] < path[i].Header.Count)
|
||||
{
|
||||
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>
|
||||
{
|
||||
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 > 0)
|
||||
{
|
||||
prevKey = leaf.Keys[index - 1];
|
||||
prevValue = leaf.Values[index - 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = depth - 1; i >= 0; i--)
|
||||
{
|
||||
if (indices[i] > 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
197
PersistentOrderedMap/BaseOrderedMap.cs
Normal file
197
PersistentOrderedMap/BaseOrderedMap.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
internal Node<K> _root;
|
||||
internal readonly TStrategy _strategy;
|
||||
|
||||
public int Count { get; protected set; }
|
||||
|
||||
protected BaseOrderedMap(Node<K> root, TStrategy strategy, int count)
|
||||
{
|
||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
|
||||
Count = count;
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Read Operations (Shared)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public bool TryGetValue(K key, out V value)
|
||||
{
|
||||
return BTreeFunctions.TryGetValue(_root, key, _strategy, out value);
|
||||
}
|
||||
|
||||
public bool ContainsKey(K key)
|
||||
{
|
||||
return BTreeFunctions.TryGetValue<K,V, TStrategy>(_root, key, _strategy, out _);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Bootstrap / Factory Helpers
|
||||
// ---------------------------------------------------------
|
||||
|
||||
public static PersistentOrderedMap<K, V, TStrategy> Create(TStrategy strategy)
|
||||
{
|
||||
// Start with an empty leaf owned by None so the first write triggers CoW.
|
||||
var emptyRoot = new LeafNode<K, V>(OwnerId.None, strategy.UsesPrefixes);
|
||||
return new PersistentOrderedMap<K, V, TStrategy>(emptyRoot, strategy, 0);
|
||||
}
|
||||
|
||||
public static TransientOrderedMap<K, V, TStrategy> CreateTransient(TStrategy strategy)
|
||||
{
|
||||
var emptyRoot = new LeafNode<K, V>(OwnerId.None, strategy.UsesPrefixes);
|
||||
return new TransientOrderedMap<K, V, TStrategy>(emptyRoot, strategy,0);
|
||||
}
|
||||
|
||||
|
||||
public BTreeEnumerator<K, V, TStrategy> GetEnumerator()
|
||||
{
|
||||
return AsEnumerable().GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<K, V>> IEnumerable<KeyValuePair<K, V>>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
// 1. Full Scan
|
||||
public BTreeEnumerable<K, V, TStrategy> AsEnumerable()
|
||||
=> new(_root, _strategy, false, default, false, default);
|
||||
|
||||
// 2. Exact Range
|
||||
public BTreeEnumerable<K, V, TStrategy> Range(K min, K max)
|
||||
=> new(_root, _strategy, true, min, true, max);
|
||||
|
||||
// 3. Start From (Open Ended)
|
||||
public BTreeEnumerable<K, V, TStrategy> From(K min) => new(_root, _strategy, true, min, false, default);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
247
PersistentOrderedMap/Iterator.cs
Normal file
247
PersistentOrderedMap/Iterator.cs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
|
||||
|
||||
public struct BTreeEnumerable<K, V, TStrategy> : IEnumerable<KeyValuePair<K, V>>
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
private readonly Node<K> _root;
|
||||
private readonly TStrategy _strategy;
|
||||
private readonly K _min, _max;
|
||||
private readonly bool _hasMin, _hasMax;
|
||||
|
||||
public BTreeEnumerable(Node<K> root, TStrategy strategy, bool hasMin, K min, bool hasMax, K max)
|
||||
{
|
||||
_root = root; _strategy = strategy;
|
||||
_hasMin = hasMin; _min = min;
|
||||
_hasMax = hasMax; _max = max;
|
||||
}
|
||||
|
||||
public BTreeEnumerator<K, V, TStrategy> GetEnumerator()
|
||||
{
|
||||
return new BTreeEnumerator<K, V, TStrategy>(_root, _strategy, _hasMin, _min, _hasMax, _max);
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<K, V>> IEnumerable<KeyValuePair<K, V>>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
// Fixed-size buffer for the path.
|
||||
// Depth 16 * 32 (branching factor) = Exabytes of capacity.
|
||||
[InlineArray(16)]
|
||||
internal struct IterNodeBuffer<K>
|
||||
{
|
||||
private Node<K> _element0;
|
||||
}
|
||||
|
||||
[InlineArray(16)]
|
||||
internal struct IterIndexBuffer<K>
|
||||
{
|
||||
private int _element0;
|
||||
}
|
||||
|
||||
public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
|
||||
where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
private readonly TStrategy _strategy;
|
||||
private readonly Node<K> _root;
|
||||
|
||||
// --- BOUNDS ---
|
||||
private readonly bool _hasMax;
|
||||
private readonly K _maxKey;
|
||||
private readonly bool _hasMin;
|
||||
private readonly K _minKey;
|
||||
|
||||
// --- INLINE STACK ---
|
||||
private IterNodeBuffer<K> _nodeStack;
|
||||
private IterIndexBuffer<K> _indexStack;
|
||||
private int _depth;
|
||||
|
||||
// --- STATE ---
|
||||
private LeafNode<K, V>? _currentLeaf;
|
||||
private int _currentLeafIndex;
|
||||
private KeyValuePair<K, V> _current;
|
||||
|
||||
// Unified Constructor
|
||||
// We use boolean flags because 'K' might be a struct where 'null' is impossible.
|
||||
public BTreeEnumerator(Node<K> root, TStrategy strategy, bool hasMin, K minKey, bool hasMax, K maxKey)
|
||||
{
|
||||
_root = root;
|
||||
_strategy = strategy;
|
||||
_hasMax = hasMax;
|
||||
_maxKey = maxKey;
|
||||
_hasMin = hasMin;
|
||||
_minKey = minKey;
|
||||
|
||||
_nodeStack = new IterNodeBuffer<K>();
|
||||
_indexStack = new IterIndexBuffer<K>(); // Explicit struct init
|
||||
_depth = 0;
|
||||
_currentLeaf = null;
|
||||
_currentLeafIndex = -1;
|
||||
_current = default;
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
if (hasMin)
|
||||
{
|
||||
Seek(minKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
DiveLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logic 1: Unbounded Start (Go to very first item)
|
||||
private void DiveLeft()
|
||||
{
|
||||
Node<K> node = _root;
|
||||
_depth = 0;
|
||||
|
||||
while (!node.IsLeaf)
|
||||
{
|
||||
var internalNode = node.AsInternal();
|
||||
_nodeStack[_depth] = internalNode;
|
||||
_indexStack[_depth] = 0; // Always take left-most child
|
||||
_depth++;
|
||||
node = internalNode.Children[0]!;
|
||||
}
|
||||
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
_currentLeafIndex = -1; // Position before the first element (0)
|
||||
}
|
||||
|
||||
// Logic 2: Bounded Start (Go to specific key)
|
||||
private void Seek(K key)
|
||||
{
|
||||
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, keyPrefix, _strategy);
|
||||
|
||||
_nodeStack[_depth] = internalNode;
|
||||
_indexStack[_depth] = idx;
|
||||
_depth++;
|
||||
|
||||
node = internalNode.Children[idx]!;
|
||||
}
|
||||
|
||||
// Find index in Leaf
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
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;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_currentLeaf == null) return false;
|
||||
|
||||
// 1. Try to advance in current leaf
|
||||
if (++_currentLeafIndex < _currentLeaf.Header.Count)
|
||||
{
|
||||
// OPTIMIZATION: Check Max Bound (if active)
|
||||
if (_hasMax)
|
||||
{
|
||||
// If Current Key > Max Key, we are done.
|
||||
if (_strategy.Compare(_currentLeaf.Keys[_currentLeafIndex], _maxKey) > 0)
|
||||
{
|
||||
_currentLeaf = null; // Close iterator
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_current = new KeyValuePair<K, V>(_currentLeaf.Keys[_currentLeafIndex], _currentLeaf.Values[_currentLeafIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Leaf exhausted. Find next leaf.
|
||||
if (FindNextLeaf())
|
||||
{
|
||||
// Found new leaf, index reset to 0.
|
||||
// Check Max Bound immediately for the first item
|
||||
if (_hasMax)
|
||||
{
|
||||
if (_strategy.Compare(_currentLeaf!.Keys[0], _maxKey) > 0)
|
||||
{
|
||||
_currentLeaf = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_current = new KeyValuePair<K, V>(_currentLeaf.Keys[0], _currentLeaf.Values[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool FindNextLeaf()
|
||||
{
|
||||
while (_depth > 0)
|
||||
{
|
||||
_depth--;
|
||||
var internalNode = _nodeStack[_depth].AsInternal();
|
||||
int currentIndex = _indexStack[_depth];
|
||||
|
||||
if (currentIndex < internalNode.Header.Count)
|
||||
{
|
||||
int nextIndex = currentIndex + 1;
|
||||
_indexStack[_depth] = nextIndex;
|
||||
_depth++;
|
||||
|
||||
Node<K> node = internalNode.Children[nextIndex]!;
|
||||
while (!node.IsLeaf)
|
||||
{
|
||||
_nodeStack[_depth] = node;
|
||||
_indexStack[_depth] = 0;
|
||||
_depth++;
|
||||
node = node.AsInternal().Children[0]!;
|
||||
}
|
||||
|
||||
_currentLeaf = node.AsLeaf<V>();
|
||||
_currentLeafIndex = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public KeyValuePair<K, V> Current => _current;
|
||||
object IEnumerator.Current => _current;
|
||||
public void Reset()
|
||||
{
|
||||
// 1. Clear current state
|
||||
_depth = 0;
|
||||
_currentLeaf = null;
|
||||
_currentLeafIndex = -1;
|
||||
_current = default;
|
||||
|
||||
// 2. Re-initialize based on how the iterator was created
|
||||
if (_root != null)
|
||||
{
|
||||
if (_hasMin)
|
||||
{
|
||||
// If we had a start range, find it again
|
||||
Seek(_minKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, go back to the very first leaf
|
||||
DiveLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
67
PersistentOrderedMap/KeyStrategies.cs
Normal file
67
PersistentOrderedMap/KeyStrategies.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
public interface IKeyStrategy<K>
|
||||
{
|
||||
int Compare(K x, K y);
|
||||
long GetPrefix(K key);
|
||||
|
||||
bool UsesPrefixes => true;
|
||||
|
||||
bool IsLossless => false;
|
||||
bool UseBinarySearch => false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public struct UnicodeStrategy : IKeyStrategy<string>
|
||||
{
|
||||
|
||||
public bool UsesPrefixes => true;
|
||||
public bool UseBinarySearch => false;
|
||||
public bool IsLossLess => false;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(string? x, string? y) => string.CompareOrdinal(x, y);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return long.MinValue;
|
||||
|
||||
// 1. Prepare Buffer (8 bytes)
|
||||
// stackalloc is virtually free (pointer bump)
|
||||
Span<byte> utf8Bytes = stackalloc byte[8];
|
||||
|
||||
// 2. Transcode (The "Safe" Magic)
|
||||
// This intrinsic handles ASCII efficiently and converts Surrogates/Chinese
|
||||
// into bytes that maintain the correct "Magnitude" (Sort Order).
|
||||
// Invalid surrogates become 0xEF (Replacement Char), which sorts > ASCII.
|
||||
System.Text.Unicode.Utf8.FromUtf16(
|
||||
key.AsSpan(0, Math.Min(key.Length, 8)),
|
||||
utf8Bytes,
|
||||
out _,
|
||||
out _,
|
||||
replaceInvalidSequences: true); // True ensures we get 0xEF for broken chars
|
||||
|
||||
// 3. Load as Big Endian Long
|
||||
long packed = BinaryPrimitives.ReadInt64BigEndian(utf8Bytes);
|
||||
|
||||
// 4. Sign Toggle
|
||||
// Maps the byte range 0x00..0xFF to the signed long range Min..Max
|
||||
// Essential for the < and > operators to work correctly.
|
||||
return packed ^ unchecked((long)0x8080808080808080);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
17
PersistentOrderedMap/KeyStrategies/ComparableStrategy.cs
Normal file
17
PersistentOrderedMap/KeyStrategies/ComparableStrategy.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace PersistentOrderedMap;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// This is a comparable strategy that may squeeze some extra time out of value types
|
||||
|
||||
public readonly struct ComparableStrategy<K> : IKeyStrategy<K> where K : IComparable<K>
|
||||
{
|
||||
public bool UsesPrefixes => false;
|
||||
public bool UseBinarySearch => true;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(K key) => 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(K x, K y) => x.CompareTo(y);
|
||||
}
|
||||
34
PersistentOrderedMap/KeyStrategies/DoubleStrategy.cs
Normal file
34
PersistentOrderedMap/KeyStrategies/DoubleStrategy.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace PersistentOrderedMap;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
public struct DoubleStrategy : IKeyStrategy<double>
|
||||
{
|
||||
public bool IsLossless => true;
|
||||
// Use the standard comparison for the fallback/refine step
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(double x, double y) => x.CompareTo(y);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(double key)
|
||||
{
|
||||
// 1. Bit Cast to Long (0 cost)
|
||||
long bits = Unsafe.As<double, long>(ref key);
|
||||
|
||||
// 2. The Magic Twist
|
||||
// If the sign bit (MSB) is set (negative), we flip ALL bits.
|
||||
// If the sign bit is clear (positive), we flip ONLY the sign bit.
|
||||
// This maps:
|
||||
// -Negative Max -> 0
|
||||
// -0 -> Midpoint
|
||||
// +Negative Max -> Max
|
||||
|
||||
long mask = (bits >> 63); // 0 for positive, -1 (All 1s) for negative
|
||||
|
||||
// If negative: bits ^ -1 = ~bits (Flip All)
|
||||
// If positive: bits ^ 0 = bits (Flip None)
|
||||
// Then we toggle the sign bit (0x8000...) to shift the range to signed long.
|
||||
|
||||
return (bits ^ (mask & 0x7FFFFFFFFFFFFFFF)) ^ unchecked((long)0x8000000000000000);
|
||||
}
|
||||
}
|
||||
|
||||
168
PersistentOrderedMap/KeyStrategies/IntScanner.cs
Normal file
168
PersistentOrderedMap/KeyStrategies/IntScanner.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
public static class IntScanner
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int FindFirstGreaterOrEqual(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
// Fallback for short arrays or unsupported hardware.
|
||||
// AVX2 processes 8 integers at a time.
|
||||
if (!Avx2.IsSupported || keys.Length < 8)
|
||||
return LinearScan(keys, target);
|
||||
|
||||
return Avx512F.IsSupported
|
||||
? ScanAvx512(keys, target)
|
||||
: ScanAvx2(keys, target);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int LinearScan(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
for (var i = 0; i < keys.Length; i++)
|
||||
if (keys[i] >= target)
|
||||
return i;
|
||||
return keys.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx2(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
// AVX2 lacks a native GreaterOrEqual for 32-bit integers.
|
||||
// We use GreaterThan(Data, target - 1).
|
||||
var vTarget = Vector256.Create(target - 1);
|
||||
var i = 0;
|
||||
var len = keys.Length;
|
||||
|
||||
for (; i <= len - 8; i += 8)
|
||||
{
|
||||
fixed (int* ptr = keys)
|
||||
{
|
||||
var vData = Avx2.LoadVector256(ptr + i);
|
||||
var vResult = Avx2.CompareGreaterThan(vData, vTarget);
|
||||
|
||||
// MoveMask creates a 32-bit integer from the most significant bit of each byte.
|
||||
var mask = (uint)Avx2.MoveMask(vResult.AsByte());
|
||||
|
||||
if (mask != 0)
|
||||
{
|
||||
// Since an int is 4 bytes, MoveMask sets 4 bits per matching element.
|
||||
// Dividing the trailing zero count by 4 maps the byte offset back to the integer index.
|
||||
return i + (BitOperations.TrailingZeroCount(mask) / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScan(keys.Slice(i), target) + i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx512(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
// AVX-512 processes 16 integers (512 bits) per instruction.
|
||||
var vTarget = Vector512.Create(target);
|
||||
var i = 0;
|
||||
var len = keys.Length;
|
||||
|
||||
for (; i <= len - 16; i += 16)
|
||||
{
|
||||
fixed (int* ptr = keys)
|
||||
{
|
||||
var vData = Avx512F.LoadVector512(ptr + i);
|
||||
|
||||
// Vector512 API is used directly here to cleanly get the mask
|
||||
var mask = Vector512.GreaterThanOrEqual(vData, vTarget);
|
||||
|
||||
if (mask != Vector512<int>.Zero)
|
||||
{
|
||||
uint m = (uint)mask.ExtractMostSignificantBits();
|
||||
return i + BitOperations.TrailingZeroCount(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScan(keys.Slice(i), target) + i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int FindFirstGreater(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
if (!Avx2.IsSupported || keys.Length < 8)
|
||||
return LinearScanGreater(keys, target);
|
||||
|
||||
return Avx512F.IsSupported
|
||||
? ScanAvx512Greater(keys, target)
|
||||
: ScanAvx2Greater(keys, target);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int LinearScanGreater(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
for (var i = 0; i < keys.Length; i++)
|
||||
if (keys[i] > target)
|
||||
return i;
|
||||
return keys.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx2Greater(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
// For > target, AVX2 CompareGreaterThan works directly without the (target - 1) offset
|
||||
var vTarget = Vector256.Create(target);
|
||||
var i = 0;
|
||||
var len = keys.Length;
|
||||
|
||||
for (; i <= len - 8; i += 8)
|
||||
{
|
||||
fixed (int* ptr = keys)
|
||||
{
|
||||
var vData = Avx2.LoadVector256(ptr + i);
|
||||
var vResult = Avx2.CompareGreaterThan(vData, vTarget);
|
||||
|
||||
var mask = (uint)Avx2.MoveMask(vResult.AsByte());
|
||||
|
||||
if (mask != 0)
|
||||
{
|
||||
return i + (BitOperations.TrailingZeroCount(mask) / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScanGreater(keys.Slice(i), target) + i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx512Greater(ReadOnlySpan<int> keys, int target)
|
||||
{
|
||||
var vTarget = Vector512.Create(target);
|
||||
var i = 0;
|
||||
var len = keys.Length;
|
||||
|
||||
for (; i <= len - 16; i += 16)
|
||||
{
|
||||
fixed (int* ptr = keys)
|
||||
{
|
||||
var vData = Avx512F.LoadVector512(ptr + i);
|
||||
|
||||
// Use GreaterThan instead of GreaterThanOrEqual
|
||||
var mask = Vector512.GreaterThan(vData, vTarget);
|
||||
|
||||
if (mask != Vector512<int>.Zero)
|
||||
{
|
||||
uint m = (uint)mask.ExtractMostSignificantBits();
|
||||
return i + BitOperations.TrailingZeroCount(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScanGreater(keys.Slice(i), target) + i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
16
PersistentOrderedMap/KeyStrategies/IntStrategy.cs
Normal file
16
PersistentOrderedMap/KeyStrategies/IntStrategy.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace PersistentOrderedMap;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
public struct IntStrategy : IKeyStrategy<int>
|
||||
{
|
||||
public bool UsesPrefixes => false;
|
||||
public bool IsLossless => true;
|
||||
public bool UseBinarySearch => false;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(int x, int y) => x.CompareTo(y);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(int key) => 0; // Unused
|
||||
}
|
||||
101
PersistentOrderedMap/KeyStrategies/PrefixScanner.cs
Normal file
101
PersistentOrderedMap/KeyStrategies/PrefixScanner.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
namespace PersistentOrderedMap;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86; // For AVX2
|
||||
using System.Numerics;
|
||||
/// <summary>
|
||||
/// Helper for SIMD accelerated prefix scanning.
|
||||
/// </summary>
|
||||
public static class PrefixScanner
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int FindFirstGreaterOrEqual(ReadOnlySpan<long> prefixes, long targetPrefix)
|
||||
{
|
||||
|
||||
// Fallback for short arrays or unsupported hardware
|
||||
if (!Avx2.IsSupported || prefixes.Length < 4)
|
||||
return LinearScan(prefixes, targetPrefix);
|
||||
|
||||
return Avx512F.IsSupported
|
||||
? ScanAvx512(prefixes, targetPrefix)
|
||||
: ScanAvx2(prefixes, targetPrefix);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int LinearScan(ReadOnlySpan<long> prefixes, long target)
|
||||
{
|
||||
for (var i = 0; i < prefixes.Length; i++)
|
||||
if (prefixes[i] >= target)
|
||||
return i;
|
||||
return prefixes.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx2(ReadOnlySpan<long> prefixes, long target)
|
||||
{
|
||||
// Create a vector where every element is the target prefix
|
||||
var vTarget = Vector256.Create(target);
|
||||
var i = 0;
|
||||
var len = prefixes.Length;
|
||||
|
||||
// Process 4 longs at a time (256 bits)
|
||||
for (; i <= len - 4; i += 4)
|
||||
fixed (long* ptr = prefixes)
|
||||
{
|
||||
var vData = Avx2.LoadVector256(ptr + i);
|
||||
|
||||
// Compare: result is -1 (all 1s) if true, 0 if false
|
||||
// We want Data >= Target.
|
||||
// AVX2 CompareGreaterThan is for signed. Longs should be treated carefully,
|
||||
// but for text prefixes (positive), signed compare is usually sufficient.
|
||||
// Effectively: !(Data < Target) could be safer if signs vary,
|
||||
// but here we assume prefixes are derived from unsigned chars.
|
||||
// Standard AVX2 hack for CompareGreaterOrEqual (Signed):
|
||||
// No native _mm256_cmpge_epi64 in AVX2.
|
||||
// Use CompareGreaterThan(Data, Target - 1)
|
||||
var vResult = Avx2.CompareGreaterThan(vData, Vector256.Create(target - 1));
|
||||
|
||||
var mask = Avx2.MoveMask(vResult.AsByte());
|
||||
|
||||
if (mask != 0)
|
||||
{
|
||||
// Identify the first set bit corresponding to a 64-bit element
|
||||
// MoveMask returns 32 bits (1 per byte). Each long is 8 bytes.
|
||||
// We check bits 0, 8, 16, 24.
|
||||
if ((mask & 0xFF) != 0) return i + 0;
|
||||
if ((mask & 0xFF00) != 0) return i + 1;
|
||||
if ((mask & 0xFF0000) != 0) return i + 2;
|
||||
return i + 3;
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScan(prefixes.Slice(i), target) + i;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ScanAvx512(ReadOnlySpan<long> prefixes, long target)
|
||||
{
|
||||
var vTarget = Vector512.Create(target);
|
||||
var i = 0;
|
||||
var len = prefixes.Length;
|
||||
|
||||
for (; i <= len - 8; i += 8)
|
||||
fixed (long* ptr = prefixes)
|
||||
{
|
||||
var vData = Avx512F.LoadVector512(ptr + i);
|
||||
// AVX512 has dedicated Compare Greater Than or Equal Long
|
||||
var mask = Avx512F.CompareGreaterThanOrEqual(vData, vTarget);
|
||||
|
||||
if (mask != Vector512<long>.Zero)
|
||||
{
|
||||
// Extract most significant bit mask
|
||||
var m = mask.ExtractMostSignificantBits();
|
||||
// Count trailing zeros to find the index
|
||||
return i + BitOperations.TrailingZeroCount(m);
|
||||
}
|
||||
}
|
||||
|
||||
return LinearScan(prefixes.Slice(i), target) + i;
|
||||
}
|
||||
}
|
||||
53
PersistentOrderedMap/KeyStrategies/StandardStrategy.cs
Normal file
53
PersistentOrderedMap/KeyStrategies/StandardStrategy.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
namespace PersistentOrderedMap;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
/// <summary>
|
||||
/// A universal key strategy for any type that relies on standard comparisons
|
||||
/// (IComparable, IComparer, or custom StringComparers) without SIMD prefixes.
|
||||
/// </summary>
|
||||
public readonly struct StandardStrategy<K> : IKeyStrategy<K>
|
||||
{
|
||||
private readonly IComparer<K> _comparer;
|
||||
|
||||
// If no comparer is provided, it defaults to Comparer<K>.Default
|
||||
// which automatically uses IComparable<K> if the type implements it.
|
||||
|
||||
public StandardStrategy()
|
||||
{
|
||||
_comparer = Comparer<K>.Default;
|
||||
}
|
||||
|
||||
public StandardStrategy(IComparer<K>? comparer)
|
||||
{
|
||||
_comparer = comparer ?? Comparer<K>.Default;
|
||||
}
|
||||
// Tell the B-Tree to skip SIMD routing and just use LinearSearch
|
||||
public bool UsesPrefixes => false;
|
||||
public bool UseBinarySearch => true;
|
||||
// This will never be called because UsesPrefixes is false,
|
||||
// but we must satisfy the interface.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetPrefix(K key) => 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(K x, K y)
|
||||
{
|
||||
return _comparer.Compare(x, y);
|
||||
}
|
||||
}
|
||||
public readonly struct StandardStrategy2<K, TComparer> : IKeyStrategy<K>
|
||||
where TComparer : struct, IComparer<K>
|
||||
{
|
||||
private readonly TComparer _comparer;
|
||||
|
||||
public StandardStrategy2(TComparer comparer) => _comparer = comparer;
|
||||
|
||||
public bool UsesPrefixes => false;
|
||||
public bool UseBinarySearch => true;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Compare(K x, K y) => _comparer.Compare(x, y);
|
||||
|
||||
public long GetPrefix(K key) => 0;
|
||||
}
|
||||
|
||||
333
PersistentOrderedMap/Nodes.cs
Normal file
333
PersistentOrderedMap/Nodes.cs
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
[Flags]
|
||||
public enum NodeFlags : byte
|
||||
{
|
||||
None = 0,
|
||||
IsLeaf = 1 << 0,
|
||||
IsRoot = 1 << 1,
|
||||
HasPrefixes = 1 << 2
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct NodeHeader
|
||||
{
|
||||
// 6 Bytes: OwnerId for Copy-on-Write (CoW)
|
||||
public OwnerId Owner;
|
||||
|
||||
// 1 Byte: Number of items currently used
|
||||
public byte Count;
|
||||
|
||||
// 1 Byte: Type flags (Leaf, Root, etc.)
|
||||
public NodeFlags Flags;
|
||||
|
||||
public NodeHeader(OwnerId owner, byte count, NodeFlags flags)
|
||||
{
|
||||
Owner = owner;
|
||||
Count = count;
|
||||
Flags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
[InlineArray(32)]
|
||||
public struct KeyBuffer<K>
|
||||
{
|
||||
private K _element0;
|
||||
}
|
||||
|
||||
// Constraint: Internal Nodes fixed at 32 children.
|
||||
// This removes the need for a separate array allocation for children references.
|
||||
[InlineArray(32)]
|
||||
public struct NodeBuffer<V>
|
||||
{
|
||||
private Node<V>? _element0;
|
||||
}
|
||||
|
||||
[InlineArray(32)]
|
||||
internal struct InternalPrefixBuffer
|
||||
{
|
||||
private long _element0;
|
||||
}
|
||||
|
||||
public abstract class Node<K>
|
||||
{
|
||||
public NodeHeader Header;
|
||||
|
||||
protected Node(OwnerId owner, NodeFlags flags)
|
||||
{
|
||||
Header = new NodeHeader(owner, 0, flags);
|
||||
}
|
||||
|
||||
public abstract Span<K> GetKeys();
|
||||
|
||||
// Abstract access to prefixes regardless of storage backing
|
||||
public abstract Span<long> AllPrefixes { get; }
|
||||
|
||||
public Span<long> Prefixes => AllPrefixes.Slice(0, Header.Count);
|
||||
|
||||
|
||||
public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0;
|
||||
|
||||
public abstract Node<K> EnsureEditable(OwnerId transactionId);
|
||||
|
||||
public void SetCount(int newCount) => Header.Count = (byte)newCount;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public LeafNode<K, V> AsLeaf<V>()
|
||||
{
|
||||
// Zero-overhead cast. Assumes you checked IsLeaf or know logic flow.
|
||||
return Unsafe.As<LeafNode<K, V>>(this);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public InternalNode<K> AsInternal()
|
||||
{
|
||||
// Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow.
|
||||
return Unsafe.As<InternalNode<K>>(this);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public PrefixInternalNode<K> AsPrefixInternal()
|
||||
{
|
||||
return Unsafe.As<PrefixInternalNode<K>>(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LeafNode<K, V> : Node<K>
|
||||
{
|
||||
public const int Capacity = 64;
|
||||
public const int MergeThreshold = 8;
|
||||
|
||||
public K[]? Keys;
|
||||
public V[] Values;
|
||||
|
||||
internal long[]? _prefixes;
|
||||
|
||||
public override Span<long> AllPrefixes => _prefixes != null ? _prefixes : Span<long>.Empty;
|
||||
|
||||
public LeafNode(OwnerId owner, bool usePrefixes) : base(owner, NodeFlags.IsLeaf | (usePrefixes ? NodeFlags.HasPrefixes : NodeFlags.None))
|
||||
{
|
||||
Keys = new K[Capacity];
|
||||
Values = new V[Capacity];
|
||||
if (usePrefixes)
|
||||
{
|
||||
_prefixes = new long[Capacity];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy Constructor for CoW
|
||||
private LeafNode(LeafNode<K, V> original, OwnerId newOwner)
|
||||
: base(newOwner, original.Header.Flags)
|
||||
{
|
||||
Keys = new K[Capacity];
|
||||
Values = new V[Capacity];
|
||||
Header.Count = original.Header.Count; _prefixes = new long[Capacity];
|
||||
|
||||
// Copy data
|
||||
Array.Copy(original.Keys, Keys, original.Header.Count);
|
||||
Array.Copy(original.Values, Values, original.Header.Count);
|
||||
if (original._prefixes != null)
|
||||
Array.Copy(original._prefixes, _prefixes, original.Header.Count);
|
||||
}
|
||||
|
||||
public override Node<K> EnsureEditable(OwnerId transactionId)
|
||||
{
|
||||
// CASE 1: Persistent Mode (transactionId is None).
|
||||
// We MUST create a copy, because we cannot distinguish "Shared Immutable Node (0)"
|
||||
// from "New Mutable Node (0)" based on ID alone.
|
||||
// However, since BTreeFunctions only calls this once before descending,
|
||||
// we won't copy the same fresh node twice.
|
||||
if (transactionId == OwnerId.None)
|
||||
{
|
||||
return new LeafNode<K, V>(this, OwnerId.None);
|
||||
}
|
||||
|
||||
// CASE 2: Transient Mode.
|
||||
// If we own the node, return it.
|
||||
if (Header.Owner == transactionId)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
// CASE 3: CoW needed (Ownership mismatch).
|
||||
return new LeafNode<K, V>(this, transactionId);
|
||||
}
|
||||
|
||||
public override Span<K> GetKeys()
|
||||
{
|
||||
return Keys.AsSpan(0, Header.Count);
|
||||
}
|
||||
|
||||
public Span<V> GetValues()
|
||||
{
|
||||
return Values.AsSpan(0, Header.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public class InternalNode<K> : Node<K>
|
||||
{
|
||||
public const int Capacity = 32;
|
||||
|
||||
public KeyBuffer<K> Keys;
|
||||
public NodeBuffer<K> Children;
|
||||
|
||||
public override Span<long> AllPrefixes => Span<long>.Empty;
|
||||
|
||||
public InternalNode(OwnerId owner, NodeFlags flags = NodeFlags.None)
|
||||
: base(owner, flags)
|
||||
{
|
||||
}
|
||||
|
||||
// Fixed CoW Constructor
|
||||
protected InternalNode(InternalNode<K> original, OwnerId newOwner, NodeFlags flags)
|
||||
: base(newOwner, flags)
|
||||
{
|
||||
Header.Count = original.Header.Count;
|
||||
|
||||
// Fast struct blit for both Keys and Children.
|
||||
// No loop required for InlineArrays!
|
||||
this.Keys = original.Keys;
|
||||
this.Children = original.Children;
|
||||
}
|
||||
|
||||
// The missing method needed by BTreeFunctions for routing
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<Node<K>> GetChildren()
|
||||
{
|
||||
// An internal node always has (Count + 1) children
|
||||
return MemoryMarshal.CreateSpan(ref Children[0], Header.Count + 1);
|
||||
}
|
||||
|
||||
public override Span<K> GetKeys() => MemoryMarshal.CreateSpan(ref Keys[0], Header.Count);
|
||||
|
||||
public override Node<K> EnsureEditable(OwnerId transactionId)
|
||||
{
|
||||
if (transactionId == OwnerId.None) return new InternalNode<K>(this, OwnerId.None, Header.Flags);
|
||||
if (Header.Owner == transactionId) return this;
|
||||
return new InternalNode<K>(this, transactionId, Header.Flags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class PrefixInternalNode<K> : InternalNode<K>
|
||||
{
|
||||
internal InternalPrefixBuffer _prefixBuffer;
|
||||
|
||||
public override Span<long> AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity);
|
||||
|
||||
public PrefixInternalNode(OwnerId owner)
|
||||
: base(owner, NodeFlags.HasPrefixes)
|
||||
{
|
||||
}
|
||||
|
||||
// CoW Constructor
|
||||
private PrefixInternalNode(PrefixInternalNode<K> original, OwnerId newOwner)
|
||||
: base(original, newOwner, original.Header.Flags)
|
||||
{
|
||||
// Copy the base Keys and Children, then blit the prefix buffer
|
||||
this._prefixBuffer = original._prefixBuffer;
|
||||
}
|
||||
|
||||
public override Node<K> EnsureEditable(OwnerId transactionId)
|
||||
{
|
||||
if (transactionId == OwnerId.None) return new PrefixInternalNode<K>(this, OwnerId.None);
|
||||
if (Header.Owner == transactionId) return this;
|
||||
return new PrefixInternalNode<K>(this, transactionId);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Auto, Pack = 1)]
|
||||
public readonly struct OwnerId(uint id, ushort gen) : IEquatable<OwnerId>
|
||||
{
|
||||
private const int BatchSize = 100;
|
||||
|
||||
// The max of allocated IDs globally.
|
||||
// Starts at 0, so the first batch reserves IDs 1 to 100.
|
||||
private static long _globalHighWaterMark;
|
||||
|
||||
|
||||
// These fields are unique to each thread. They initialize to 0/default.
|
||||
// The current ID value this thread is handing out.
|
||||
[ThreadStatic] private static long _localCurrentId;
|
||||
|
||||
// How many IDs are left in this thread's current batch.
|
||||
[ThreadStatic] private static int _localRemaining;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Instance Data (6 Bytes)
|
||||
// ---------------------------------------------------------
|
||||
private readonly uint Id = id; // 4 bytes
|
||||
private readonly ushort Gen = gen; // 2 bytes
|
||||
|
||||
/// <summary>
|
||||
/// Generates the next unique OwnerId.
|
||||
/// mostly non-blocking (thread-local), hits Interlocked only once per 100 IDs.
|
||||
/// </summary>
|
||||
public static OwnerId Next()
|
||||
{
|
||||
// We have IDs remaining in our local batch.
|
||||
// This executes with zero locking overhead.
|
||||
if (_localRemaining > 0)
|
||||
{
|
||||
_localRemaining--;
|
||||
var val = ++_localCurrentId;
|
||||
return new OwnerId((uint)val, (ushort)(val >> 32));
|
||||
}
|
||||
|
||||
// SLOW PATH: We ran out (or this is the thread's first call).
|
||||
return NextBatch();
|
||||
}
|
||||
|
||||
private static OwnerId NextBatch()
|
||||
{
|
||||
// Atomically reserve a new block of IDs from the global counter.
|
||||
// Only one thread contends for this cache line at a time.
|
||||
var reservedEnd = Interlocked.Add(ref _globalHighWaterMark, BatchSize);
|
||||
|
||||
// Calculate the start of our new range.
|
||||
var reservedStart = reservedEnd - BatchSize + 1;
|
||||
|
||||
// Reset the local cache.
|
||||
// We set _localCurrentId to (start - 1) so that the first increment
|
||||
// inside the logic below lands exactly on 'reservedStart'.
|
||||
_localCurrentId = reservedStart - 1;
|
||||
_localRemaining = BatchSize;
|
||||
|
||||
// Perform the generation logic (same as Fast Path)
|
||||
_localRemaining--;
|
||||
var val = ++_localCurrentId;
|
||||
return new OwnerId((uint)val, (ushort)(val >> 32));
|
||||
}
|
||||
|
||||
public static readonly OwnerId None = new(0, 0);
|
||||
|
||||
public bool IsNone => Id == 0 && Gen == 0;
|
||||
|
||||
public bool Equals(OwnerId other)
|
||||
{
|
||||
return Id == other.Id && Gen == other.Gen;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is OwnerId other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id, Gen);
|
||||
}
|
||||
|
||||
public static bool operator ==(OwnerId left, OwnerId right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(OwnerId left, OwnerId right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
44
PersistentOrderedMap/PersistentOrderedMap.cs
Normal file
44
PersistentOrderedMap/PersistentOrderedMap.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
public sealed class PersistentOrderedMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy>, IEnumerable, IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
|
||||
{
|
||||
internal PersistentOrderedMap(Node<K> root, TStrategy strategy, int count)
|
||||
: base(root, strategy, count) { }
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Immutable Write API (Returns new Map)
|
||||
// ---------------------------------------------------------
|
||||
public PersistentOrderedMap<K, V, TStrategy> Set(K key, V value)
|
||||
{
|
||||
// OPTIMIZATION: Use OwnerId.None (0).
|
||||
// This signals EnsureEditable to always copy the root path,
|
||||
// producing a new tree of nodes that also have OwnerId.None.
|
||||
var newRoot = BTreeFunctions.Set(_root, key, value, _strategy, OwnerId.None, out bool countChanged);
|
||||
return new PersistentOrderedMap<K, V, TStrategy>(newRoot, _strategy, countChanged ? Count + 1 : Count);
|
||||
}
|
||||
|
||||
public static PersistentOrderedMap<K, V, TStrategy> Empty(TStrategy strategy)
|
||||
{
|
||||
// Create an empty Leaf Node.
|
||||
// 'default(OwnerId)' (usually 0) marks this node as Immutable/Persistent.
|
||||
// This ensures that any subsequent Set/Remove will clone this node
|
||||
// instead of modifying it in place.
|
||||
var emptyRoot = new LeafNode<K, V>(default(OwnerId), strategy.UsesPrefixes);
|
||||
|
||||
return new PersistentOrderedMap<K, V, TStrategy>(emptyRoot, strategy, 0);
|
||||
}
|
||||
|
||||
public PersistentOrderedMap<K, V, TStrategy> Remove(K key)
|
||||
{
|
||||
var newRoot = BTreeFunctions.Remove<K,V, TStrategy>(_root, key, _strategy, OwnerId.None, out bool removed);
|
||||
if (!removed) return this;
|
||||
return new PersistentOrderedMap<K, V, TStrategy>(newRoot, _strategy, Count - 1);
|
||||
}
|
||||
|
||||
public TransientOrderedMap<K, V, TStrategy> ToTransient()
|
||||
{
|
||||
return new TransientOrderedMap<K, V, TStrategy>(_root, _strategy, Count);
|
||||
}
|
||||
}
|
||||
11
PersistentOrderedMap/PersistentOrderedMap.csproj
Normal file
11
PersistentOrderedMap/PersistentOrderedMap.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>
|
||||
623
PersistentOrderedMap/Readme.org
Normal file
623
PersistentOrderedMap/Readme.org
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
* 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. The primary use case is for when editing operations are in bulk. Updating single elements many times will fail to distinguish this collection from other more mature collections. Bulk writes are very fast using transient interfaces, random reads are fastish depending on key type and entropy, Sequential reads and min/max queries are very fast.
|
||||
|
||||
** Features
|
||||
- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tre 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 have a very nice API, and it also has a performance model that is easy to understand. If you look at the performance characteristics of it below, it scales much more linear with collection size. For example: I don't know why PersistentMap lookups suddenly becomes so much slower when we reach 100000 integer keys. There might be a gazillion things like that, that make PersistentMap much slower for real world usage.
|
||||
|
||||
The general version of this, using =StandardStrategy<K>= does not benefit from the prefix optimization, although might benefit from a usage of binary search in the future.
|
||||
|
||||
** 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. This does not edit an existing map, but will make bulk operations a lot faster on nodes "owned" by the current map.
|
||||
|
||||
#+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 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_sr
|
||||
|
||||
*** 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 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
|
||||
|
||||
** Benchmarks
|
||||
These benchmarks tries a variety of operations. The Int benchmarks (the first ones) use avx for lookups. On a computer without avx this is bound to be slower. In benchmarks where there are many writes to the tree, the transient version is used, since this is the workload this datastructure is optimized for.
|
||||
|
||||
Build: builds a map of size N. I did not benchmark the builders used by the built in collections, but they are almost certainly at least as fast as the transients used by this library. For integers, the map was sorted (triggering a small optimization in PersistentMap). For the string benchmark it was random.
|
||||
|
||||
The retrieval benchmarks reads a subset of the keys in random order.
|
||||
|
||||
The update benchmarks updates a subset of the keys in random order.
|
||||
|
||||
The update and set benchmarks updates and sets keys in random order. Half of the keys are new.
|
||||
|
||||
The iteration benchmarks iterate from start to finish. The ordered collections are of course in order.
|
||||
|
||||
The removal benchmarks removes a subset of the keys in random order.
|
||||
|
||||
*** Integer keys
|
||||
This is pretty much the best case scenario for everyone. Key comparisons are fast, hashing is minimal. For b+trees this means we can do a lot of key comparisons at once using avx. The machine this is done on is an amd 5900x, which supports some kind of bastardized avx512. It is not really a gain over avx256 in this benchmark though on that processor. With regards to building, the microsoft collections have builders, and they are about as fast as the TransientMap, but a little little slower on my computer. LanguageExt lacks transients, and thus these comparisons are _not_ fair.
|
||||
|
||||
ImmDict is System.Collections.Immutable dictionary. ImmSortedDict is it's sorted sibling. ExtMap is the sorted map from LanguageExt. ExtHashMap is the unsorted HashMap from LanguageExt.
|
||||
|
||||
|
||||
#+begin_src
|
||||
| Method | N | Mean | Gen0 | Gen1 | Gen2 | Allocated |
|
||||
|-------------------------|--------|-----------------:|-----------:|----------:|--------:|------------:|
|
||||
| Build_ImmDict | 100 | 11,307.04 ns | 4.9744 | 0.0458 | - | 41688 B |
|
||||
| Build_ImmSortedDict | 100 | 8,493.79 ns | 4.4250 | 0.0458 | - | 37104 B |
|
||||
| Build_ExtMap | 100 | 8,519.63 ns | 5.3101 | 0.0458 | - | 44432 B |
|
||||
| Build_ExtHashMap | 100 | 9,855.33 ns | 7.5378 | 0.0458 | - | 63104 B |
|
||||
| Build_PersistentMap | 100 | 8,698.33 ns | 16.3879 | 0.1526 | - | 137072 B |
|
||||
| Build_TransientMap | 100 | 1,665.90 ns | 0.6332 | 0.0038 | - | 5304 B |
|
||||
| Retrieve_ImmDict | 100 | 39.19 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 100 | 64.32 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 100 | 117.61 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 100 | 84.19 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap | 100 | 47.25 ns | - | - | - | - |
|
||||
| Update_ImmDict | 100 | 1,145.75 ns | 0.4616 | 0.0019 | - | 3872 B |
|
||||
| Update_PersistentMap | 100 | 1.107 μs | 1.9398 | 0.0248 | - | 15.86 KB |
|
||||
| Update_TransientMap | 100 | 347.49 ns | 0.3576 | 0.0033 | - | 2992 B |
|
||||
| Update_ImmSortedDict | 100 | 849.39 ns | 0.3958 | 0.0010 | - | 3312 B |
|
||||
| Update_ExtMap | 100 | 642.50 ns | 0.3939 | 0.0010 | - | 3296 B |
|
||||
| Update_ExtHashMap | 100 | 541.84 ns | 0.5283 | 0.0010 | - | 4424 B |
|
||||
| UpdateSet_ImmDict | 100 | 1,236.82 ns | 0.5226 | 0.0019 | - | 4376 B |
|
||||
| UpdateSet_PersistentMap | 100 | 1189 ns | 1.9398 | 0.0248 | - | 15.86 KB |
|
||||
| UpdateSet_TransientMap | 100 | 380.70 ns | 0.3576 | 0.0033 | - | 2992 B |
|
||||
| UpdateSet_ImmSortedDict | 100 | 887.07 ns | 0.4587 | 0.0010 | - | 3840 B |
|
||||
| UpdateSet_ExtMap | 100 | 856.76 ns | 0.4797 | 0.0010 | - | 4016 B |
|
||||
| UpdateSet_ExtHashMap | 100 | 582.31 ns | 0.5312 | 0.0019 | - | 4448 B |
|
||||
| Iterate_ImmDict | 100 | 1,324.41 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap | 100 | 175.98 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 100 | 488.69 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 100 | 337.40 ns | 0.0038 | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 100 | 1,209.77 ns | 0.2518 | - | - | 2112 B |
|
||||
| Remove_ImmDict | 100 | 899.57 ns | 0.4425 | 0.0010 | - | 3704 B |
|
||||
| Remove_TransientMap | 100 | 433.52 ns | 0.3290 | 0.0029 | - | 2752 B |
|
||||
| Remove_ImmSortedDict | 100 | 728.77 ns | 0.3786 | 0.0010 | - | 3168 B |
|
||||
| Remove_ExtMap | 100 | 653.72 ns | 0.3767 | 0.0010 | - | 3152 B |
|
||||
| Remove_ExtHashMap | 100 | 589.03 ns | 0.5178 | - | - | 4336 B |
|
||||
| Build_ImmDict | 1000 | 168,692.47 ns | 71.5332 | 6.8359 | - | 598712 B |
|
||||
| Build_ImmSortedDict | 1000 | 125,591.01 ns | 62.9883 | 5.1270 | - | 526896 B |
|
||||
| Build_ExtMap | 1000 | 117,763.81 ns | 72.3877 | 6.1035 | - | 605936 B |
|
||||
| Build_ExtHashMap | 1000 | 64,443.19 ns | 67.5049 | 1.5869 | - | 564864 B |
|
||||
| Build_PersistentMap | 1000 | 133,156.05 ns | 192.1387 | 7.8125 | - | 1607744 B |
|
||||
| Build_TransientMap | 1000 | 25,945.72 ns | 4.2725 | 0.1526 | - | 35976 B |
|
||||
| Retrieve_ImmDict | 1000 | 686.48 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 1000 | 1,145.83 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 1000 | 2,276.07 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 1000 | 808.53 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap | 1000 | 680.50 ns | - | - | - | - |
|
||||
| Update_ImmDict | 1000 | 16,863.81 ns | 6.5613 | 0.2136 | - | 54960 B |
|
||||
| Update_PersistentMap | 1000 | 13,617.12 ns | 19.4092 | 1.1597 | - | 158.59 KB |
|
||||
| Update_TransientMap | 1000 | 3,611.03 ns | 2.5406 | 0.1564 | - | 21280 B |
|
||||
| Update_ImmSortedDict | 1000 | 12,428.90 ns | 5.5542 | 0.1526 | - | 46464 B |
|
||||
| Update_ExtMap | 1000 | 10,091.51 ns | 5.6000 | 0.1678 | - | 46880 B |
|
||||
| Update_ExtHashMap | 1000 | 6,758.96 ns | 7.9575 | 0.2136 | - | 66616 B |
|
||||
| UpdateSet_ImmDict | 1000 | 21,489.70 ns | 7.0496 | 0.1831 | - | 59160 B |
|
||||
| UpdateSet_PersistentMap | 1000 | 14,890.21 ns | 19.4855 | 1.0529 | - | 159.23 KB |
|
||||
| UpdateSet_TransientMap | 1000 | 5,063.11 ns | 2.4796 | 0.1450 | - | 20776 B |
|
||||
| UpdateSet_ImmSortedDict | 1000 | 13,333.61 ns | 6.3782 | 0.1526 | - | 53472 B |
|
||||
| UpdateSet_ExtMap | 1000 | 11,221.39 ns | 6.5918 | 0.1526 | - | 55184 B |
|
||||
| UpdateSet_ExtHashMap | 1000 | 15,967.43 ns | 13.1836 | 0.4578 | - | 110440 B |
|
||||
| Iterate_ImmDict | 1000 | 15,325.93 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap | 1000 | 1,574.20 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 1000 | 5,110.07 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 1000 | 3,432.88 ns | 0.0038 | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 1000 | 8,207.75 ns | 0.2441 | - | - | 2112 B |
|
||||
| Remove_ImmDict | 1000 | 15,205.95 ns | 6.4392 | 0.2136 | - | 54064 B |
|
||||
| Remove_TransientMap | 1000 | 4,036.18 ns | 2.2507 | 0.1373 | - | 18880 B |
|
||||
| Remove_ImmSortedDict | 1000 | 10,664.14 ns | 5.7068 | 0.1678 | - | 47760 B |
|
||||
| Remove_ExtMap | 1000 | 9,993.90 ns | 5.5084 | 0.1526 | - | 46160 B |
|
||||
| Remove_ExtHashMap | 1000 | 7,475.24 ns | 7.7209 | 0.1907 | - | 64608 B |
|
||||
| Build_ImmDict | 10000 | 2,571,753.12 ns | 41.4063 | 390.6250 | - | 7882552 B |
|
||||
| Build_ImmSortedDict | 10000 | 1,975,364.14 ns | 820.3125 | 296.8750 | - | 6893616 B |
|
||||
| Build_ExtMap | 10000 | 1,866,221.83 ns | 917.9688 | 320.3125 | - | 7692272 B |
|
||||
| Build_ExtHashMap | 10000 | 1,215,103.58 ns | 1009.7656 | 240.2344 | - | 8446080 B |
|
||||
| Build_PersistentMap | 10000 | 1,930,457.96 ns | 2345.7031 | 494.1406 | - | 19626728 B |
|
||||
| Build_TransientMap | 10000 | 640,413.08 ns | 41.0156 | 8.7891 | - | 347344 B |
|
||||
| Retrieve_ImmDict | 10000 | 14,880.32 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 10000 | 15,595.68 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 10000 | 36,225.60 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 10000 | 11,987.70 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap | 10000 | 10,227.73 ns | - | - | - | - |
|
||||
| Update_ImmDict | 10000 | 318,905.00 ns | 86.9141 | 23.4375 | - | 730200 B |
|
||||
| Update_PersistentMap | 10000 | 202,244.42 ns | 243.6523 | 73.2422 | - | 1992.19 KB |
|
||||
| Update_TransientMap | 10000 | 73,203.50 ns | 24.9023 | 7.3242 | - | 209056 B |
|
||||
| Update_ImmSortedDict | 10000 | 216,638.87 ns | 77.1484 | 16.1133 | - | 645360 B |
|
||||
| Update_ExtMap | 10000 | 176,737.24 ns | 74.4629 | 17.5781 | - | 623600 B |
|
||||
| Update_ExtHashMap | 10000 | 105,445.84 ns | 97.2900 | 17.3340 | - | 814376 B |
|
||||
| UpdateSet_ImmDict | 10000 | 333,260.72 ns | 92.2852 | 19.0430 | - | 775784 B |
|
||||
| UpdateSet_PersistentMap | 10000 | 221,958.91 ns | 244.6289 | 71.2891 | - | 1998.95 KB |
|
||||
| UpdateSet_TransientMap | 10000 | 93,484.07 ns | 24.9023 | 7.3242 | - | 209072 B |
|
||||
| UpdateSet_ImmSortedDict | 10000 | 224,214.31 ns | 83.2520 | 14.6484 | - | 697920 B |
|
||||
| UpdateSet_ExtMap | 10000 | 186,761.55 ns | 83.7402 | 14.4043 | - | 700880 B |
|
||||
| UpdateSet_ExtHashMap | 10000 | 112,371.27 ns | 97.7783 | 20.2637 | - | 818240 B |
|
||||
| Iterate_ImmDict | 10000 | 152,686.50 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap | 10000 | 14,841.56 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 10000 | 53,372.05 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 10000 | 38,673.93 ns | - | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 10000 | 111,676.15 ns | 8.0566 | - | - | 67648 B |
|
||||
| Remove_ImmDict | 10000 | 303,798.22 ns | 86.4258 | 19.5313 | - | 726560 B |
|
||||
| Remove_TransientMap | 10000 | 58,890.93 ns | 22.0947 | 6.5308 | - | 185056 B |
|
||||
| Remove_ImmSortedDict | 10000 | 219,974.63 ns | 77.8809 | 15.1367 | - | 653184 B |
|
||||
| Remove_ExtMap | 10000 | 188,713.80 ns | 74.2188 | 14.4043 | - | 621248 B |
|
||||
| Remove_ExtHashMap | 10000 | 120,113.97 ns | 95.9473 | 15.9912 | - | 802944 B |
|
||||
| Build_ImmDict | 100000 | 38,394,437.95 ns | 11714.2857 | 1071.4286 | 71.4286 | 97460075 B |
|
||||
| Build_ImmSortedDict | 100000 | 30,860,676.12 ns | 10187.5000 | 906.2500 | 62.5000 | 84908636 B |
|
||||
| Build_ExtMap | 100000 | 28,415,796.22 ns | 11156.2500 | 937.5000 | 62.5000 | 92907004 B |
|
||||
| Build_ExtHashMap | 100000 | 29,149,824.12 ns | 15375.0000 | 2750.0000 | 62.5000 | 128198060 B |
|
||||
| Build_PersistentMap | 100000 | 24,745,757.59 ns | 27687.5000 | 375.0000 | - | 231722008 B |
|
||||
| Build_TransientMap | 100000 | 9,137,195.74 ns | 406.2500 | 234.3750 | - | 3460512 B |
|
||||
| Retrieve_ImmDict | 100000 | 1,259,618.31 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 100000 | 975,518.14 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 100000 | 1,535,487.85 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 100000 | 284,590.55 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap | 100000 | 429,001.27 ns | - | - | - | - |
|
||||
| Update_ImmDict | 100000 | 5,705,786.31 ns | 1093.7500 | 906.2500 | - | 9183488 B |
|
||||
| Update_PersistentMap | 100000 | 4,056,612.12 ns | 2945.3125 | 2781.2500 | 15.6250 | 23984.39 KB |
|
||||
| Update_TransientMap | 100000 | 1,145,551.13 ns | 248.0469 | 199.2188 | - | 2081568 B |
|
||||
| Update_ImmSortedDict | 100000 | 4,433,611.11 ns | 953.1250 | 796.8750 | - | 8021136 B |
|
||||
| Update_ExtMap | 100000 | 3,901,065.86 ns | 937.5000 | 789.0625 | - | 7848704 B |
|
||||
| Update_ExtHashMap | 100000 | 2,696,228.39 ns | 1289.0625 | 960.9375 | - | 10805952 B |
|
||||
| UpdateSet_ImmDict | 100000 | 5,340,382.88 ns | 1109.3750 | 867.1875 | - | 9318896 B |
|
||||
| UpdateSet_PersistentMap | 100000 | 4,629,564.21 ns | 2984.3750 | 1906.2500 | 39.0625 | 24060.44 KB |
|
||||
| UpdateSet_TransientMap | 100000 | 1,332,859.76 ns | 250.0000 | 208.9844 | - | 2099520 B |
|
||||
| UpdateSet_ImmSortedDict | 100000 | 4,418,076.49 ns | 1000.0000 | 992.1875 | - | 8396544 B |
|
||||
| UpdateSet_ExtMap | 100000 | 3,107,339.72 ns | 996.0938 | 507.8125 | - | 8349248 B |
|
||||
| UpdateSet_ExtHashMap | 100000 | 2,630,473.81 ns | 1292.9688 | 976.5625 | - | 10845480 B |
|
||||
| Iterate_ImmDict | 100000 | 1,550,040.28 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap | 100000 | 149,743.16 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 100000 | 723,978.27 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 100000 | 504,204.91 ns | - | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 100000 | 1,936,574.10 ns | 257.8125 | - | - | 2164800 B |
|
||||
| Remove_ImmDict | 100000 | 5,419,879.00 ns | 1093.7500 | 914.0625 | - | 9149160 B |
|
||||
| Remove_TransientMap | 100000 | 951,332.63 ns | 219.7266 | 155.2734 | - | 1839264 B |
|
||||
| Remove_ImmSortedDict | 100000 | 4,203,794.51 ns | 953.1250 | 781.2500 | - | 8028144 B |
|
||||
| Remove_ExtMap | 100000 | 3,896,109.04 ns | 929.6875 | 789.0625 | - | 7824560 B |
|
||||
| Remove_ExtHashMap | 100000 | 2,816,957.99 ns | 1277.3438 | 914.0625 | - | 10709360 B |
|
||||
|
||||
#+end_src
|
||||
|
||||
* String keys
|
||||
|
||||
These benchmarks act like above, but do not insert keys in a specific order. Sorting them before will yield a speed boost. One uses the standardkeystrategy (does a binary search) and one uses the unicodstrategy which encodes the first 8 bytes as a long and uses avx to search for keys.
|
||||
|
||||
|
||||
#+begin_src
|
||||
|
||||
|
||||
```
|
||||
| Method | N | StringLength | Mean | Gen0 | Gen1 | Gen2 | Allocated |
|
||||
|----------------------------------|------------|--------------|---------------------:|-------------:|-------------:|---------:|--------------:|
|
||||
| **Build_TransientMap_Standard** | **100** | **8** | **5,170.76 ns** | **0.7401** | **0.0076** | **-** | **6200 B** |
|
||||
| Build_TransientMap_Unicode | 100 | 8 | 12,251.17 ns | 0.8850 | 0.0153 | - | 7528 B |
|
||||
| Build_ImmDict | 100 | 8 | 13,926.78 ns | 5.3253 | 0.0610 | - | 44640 B |
|
||||
| Build_ImmSortedDict | 100 | 8 | 21,789.13 ns | 4.2114 | 0.0305 | - | 35472 B |
|
||||
| Build_ExtMap | 100 | 8 | 22,791.98 ns | 5.6152 | 0.0610 | - | 47104 B |
|
||||
| Build_ExtHashMap | 100 | 8 | 9,993.13 ns | 4.0894 | 0.0305 | - | 34216 B |
|
||||
| Retrieve_ImmDict | 100 | 8 | 78.83 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 100 | 8 | 105.97 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 100 | 8 | 149.07 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 100 | 8 | 1,203.47 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 100 | 8 | 1,297.20 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 100 | 8 | 189.50 ns | - | - | - | - |
|
||||
| Update_ImmDict | 100 | 8 | 1,343.93 ns | 0.5207 | 0.0019 | - | 4368 B |
|
||||
| Update_PersistentMap_Standard | 100 | 8 | 1,593.27 ns | 2.3994 | 0.0401 | - | 20080 B |
|
||||
| Update_PersistentMap_Unicode | 100 | 8 | 1,913.07 ns | 2.7046 | 0.0496 | - | 22640 B |
|
||||
| Update_TransientMap_Standard | 100 | 8 | 520.49 ns | 0.4339 | 0.0048 | - | 3632 B |
|
||||
| Update_TransientMap_Unicode | 100 | 8 | 690.74 ns | 0.4644 | 0.0057 | - | 3888 B |
|
||||
| Update_ImmSortedDict | 100 | 8 | 1,748.06 ns | 0.3662 | - | - | 3072 B |
|
||||
| Update_ExtMap | 100 | 8 | 1,793.32 ns | 0.4120 | - | - | 3456 B |
|
||||
| Update_ExtHashMap | 100 | 8 | 754.97 ns | 0.5264 | 0.0010 | - | 4408 B |
|
||||
| UpdateSet_ImmDict | 100 | 8 | 1,361.32 ns | 0.5207 | 0.0019 | - | 4368 B |
|
||||
| UpdateSet_PersistentMap_Standard | 100 | 8 | 1,838.15 ns | 2.3994 | 0.0420 | - | 20080 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 100 | 8 | 2,427.91 ns | 2.7046 | 0.0534 | - | 22640 B |
|
||||
| UpdateSet_TransientMap_Standard | 100 | 8 | 711.17 ns | 0.4339 | 0.0048 | - | 3632 B |
|
||||
| UpdateSet_TransientMap_Unicode | 100 | 8 | 1,247.32 ns | 0.4635 | 0.0057 | - | 3888 B |
|
||||
| UpdateSet_ImmSortedDict | 100 | 8 | 2,399.76 ns | 0.4578 | - | - | 3840 B |
|
||||
| UpdateSet_ExtMap | 100 | 8 | 2,202.70 ns | 0.4730 | - | - | 3960 B |
|
||||
| UpdateSet_ExtHashMap | 100 | 8 | 773.22 ns | 0.5274 | 0.0019 | - | 4416 B |
|
||||
| Iterate_ImmDict | 100 | 8 | 1,335.18 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap_Standard | 100 | 8 | 189.19 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 100 | 8 | 485.69 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 100 | 8 | 327.67 ns | 0.0038 | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 100 | 8 | 1,089.89 ns | 0.2480 | - | - | 2088 B |
|
||||
| Iterate_PersistentMap_Unicode | 100 | 8 | 187.62 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 100 | 8 | 1,272.43 ns | 0.5131 | - | - | 4304 B |
|
||||
| Remove_PersistentMap_Standard | 100 | 8 | 1,969.67 ns | 2.3689 | 0.0381 | - | 19840 B |
|
||||
| Remove_PersistentMap_Unicode | 100 | 8 | 2,487.99 ns | 2.6779 | 0.0496 | - | 22400 B |
|
||||
| Remove_TransientMap_Standard | 100 | 8 | 836.95 ns | 0.4101 | 0.0048 | - | 3432 B |
|
||||
| Remove_TransientMap_Unicode | 100 | 8 | 1,333.99 ns | 0.4406 | 0.0057 | - | 3688 B |
|
||||
| Remove_ImmSortedDict | 100 | 8 | 1,946.19 ns | 0.3777 | - | - | 3168 B |
|
||||
| Remove_ExtMap | 100 | 8 | 2,221.31 ns | 0.5798 | - | - | 4856 B |
|
||||
| Remove_ExtHashMap | 100 | 8 | 770.43 ns | 0.4721 | 0.0019 | - | 3952 B |
|
||||
| **Build_TransientMap_Standard** | **100** | **50** | **5,216.44 ns** | **0.7401** | **0.0076** | **-** | **6200 B** |
|
||||
| Build_TransientMap_Unicode | 100 | 50 | 11,944.76 ns | 0.8850 | 0.0153 | - | 7528 B |
|
||||
| Build_ImmDict | 100 | 50 | 15,771.58 ns | 5.3101 | 0.0610 | - | 44576 B |
|
||||
| Build_ImmSortedDict | 100 | 50 | 22,171.62 ns | 4.2114 | 0.0305 | - | 35232 B |
|
||||
| Build_ExtMap | 100 | 50 | 27,705.49 ns | 5.6458 | 0.0610 | - | 47328 B |
|
||||
| Build_ExtHashMap | 100 | 50 | 15,207.38 ns | 4.4861 | 0.0305 | - | 37672 B |
|
||||
| Retrieve_ImmDict | 100 | 50 | 286.21 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 100 | 50 | 103.79 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 100 | 50 | 151.04 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 100 | 50 | 1,317.60 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 100 | 50 | 1,162.96 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 100 | 50 | 412.76 ns | - | - | - | - |
|
||||
| Update_ImmDict | 100 | 50 | 1,359.95 ns | 0.4368 | - | - | 3664 B |
|
||||
| Update_PersistentMap_Standard | 100 | 50 | 1,583.22 ns | 2.3994 | 0.0420 | - | 20080 B |
|
||||
| Update_PersistentMap_Unicode | 100 | 50 | 1,861.53 ns | 2.7065 | 0.0515 | - | 22640 B |
|
||||
| Update_TransientMap_Standard | 100 | 50 | 543.81 ns | 0.4339 | 0.0048 | - | 3632 B |
|
||||
| Update_TransientMap_Unicode | 100 | 50 | 633.60 ns | 0.4644 | 0.0057 | - | 3888 B |
|
||||
| Update_ImmSortedDict | 100 | 50 | 1,986.02 ns | 0.4120 | - | - | 3456 B |
|
||||
| Update_ExtMap | 100 | 50 | 1,892.16 ns | 0.4253 | - | - | 3568 B |
|
||||
| Update_ExtHashMap | 100 | 50 | 1,088.83 ns | 0.4997 | - | - | 4184 B |
|
||||
| UpdateSet_ImmDict | 100 | 50 | 1,799.24 ns | 0.5817 | 0.0019 | - | 4880 B |
|
||||
| UpdateSet_PersistentMap_Standard | 100 | 50 | 1,847.71 ns | 2.3994 | 0.0401 | - | 20080 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 100 | 50 | 2,510.91 ns | 2.7046 | 0.0496 | - | 22640 B |
|
||||
| UpdateSet_TransientMap_Standard | 100 | 50 | 754.14 ns | 0.4339 | 0.0048 | - | 3632 B |
|
||||
| UpdateSet_TransientMap_Unicode | 100 | 50 | 1,274.76 ns | 0.4635 | 0.0057 | - | 3888 B |
|
||||
| UpdateSet_ImmSortedDict | 100 | 50 | 2,463.36 ns | 0.4730 | - | - | 3984 B |
|
||||
| UpdateSet_ExtMap | 100 | 50 | 2,364.62 ns | 0.5913 | - | - | 4968 B |
|
||||
| UpdateSet_ExtHashMap | 100 | 50 | 1,026.26 ns | 0.5074 | - | - | 4248 B |
|
||||
| Iterate_ImmDict | 100 | 50 | 1,223.85 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap_Standard | 100 | 50 | 187.31 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 100 | 50 | 484.33 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 100 | 50 | 358.22 ns | 0.0038 | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 100 | 50 | 1,149.01 ns | 0.2575 | - | - | 2160 B |
|
||||
| Iterate_PersistentMap_Unicode | 100 | 50 | 187.47 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 100 | 50 | 1,589.38 ns | 0.5283 | 0.0019 | - | 4432 B |
|
||||
| Remove_PersistentMap_Standard | 100 | 50 | 1,976.63 ns | 2.3689 | 0.0381 | - | 19840 B |
|
||||
| Remove_PersistentMap_Unicode | 100 | 50 | 2,568.69 ns | 2.6779 | 0.0458 | - | 22400 B |
|
||||
| Remove_TransientMap_Standard | 100 | 50 | 839.46 ns | 0.4101 | 0.0048 | - | 3432 B |
|
||||
| Remove_TransientMap_Unicode | 100 | 50 | 1,399.50 ns | 0.4406 | 0.0057 | - | 3688 B |
|
||||
| Remove_ImmSortedDict | 100 | 50 | 2,069.17 ns | 0.3891 | - | - | 3264 B |
|
||||
| Remove_ExtMap | 100 | 50 | 2,124.59 ns | 0.4387 | - | - | 3680 B |
|
||||
| Remove_ExtHashMap | 100 | 50 | 1,029.42 ns | 0.5112 | - | - | 4288 B |
|
||||
| **Build_TransientMap_Standard** | **1000** | **8** | **102,292.92 ns** | **5.7373** | **0.3662** | **-** | **48592 B** |
|
||||
| Build_TransientMap_Unicode | 1000 | 8 | 172,854.88 ns | 7.3242 | 0.7324 | - | 62248 B |
|
||||
| Build_ImmDict | 1000 | 8 | 247,732.93 ns | 79.1016 | 8.3008 | - | 662016 B |
|
||||
| Build_ImmSortedDict | 1000 | 8 | 429,391.31 ns | 61.0352 | 4.8828 | - | 513312 B |
|
||||
| Build_ExtMap | 1000 | 8 | 416,823.27 ns | 79.1016 | 7.3242 | - | 662448 B |
|
||||
| Build_ExtHashMap | 1000 | 8 | 154,417.86 ns | 70.8008 | 4.6387 | - | 592920 B |
|
||||
| Retrieve_ImmDict | 1000 | 8 | 1,043.45 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 1000 | 8 | 1,783.56 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 1000 | 8 | 2,026.44 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 1000 | 8 | 21,724.33 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 1000 | 8 | 24,401.52 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 1000 | 8 | 2,472.28 ns | - | - | - | - |
|
||||
| Update_ImmDict | 1000 | 8 | 19,547.84 ns | 7.2937 | 0.2747 | - | 61024 B |
|
||||
| Update_PersistentMap_Standard | 1000 | 8 | 19,846.20 ns | 23.9868 | 2.2278 | - | 200800 B |
|
||||
| Update_PersistentMap_Unicode | 1000 | 8 | 27,309.69 ns | 27.0386 | 2.5330 | - | 226400 B |
|
||||
| Update_TransientMap_Standard | 1000 | 8 | 6,888.79 ns | 4.3945 | 0.4501 | - | 36768 B |
|
||||
| Update_TransientMap_Unicode | 1000 | 8 | 7,801.38 ns | 4.4250 | 0.4425 | - | 37024 B |
|
||||
| Update_ImmSortedDict | 1000 | 8 | 32,192.51 ns | 5.7373 | 0.1221 | - | 48288 B |
|
||||
| Update_ExtMap | 1000 | 8 | 31,972.71 ns | 6.4087 | 0.1831 | - | 53824 B |
|
||||
| Update_ExtHashMap | 1000 | 8 | 10,476.16 ns | 7.2021 | 0.2289 | - | 60272 B |
|
||||
| UpdateSet_ImmDict | 1000 | 8 | 22,622.37 ns | 8.2703 | 0.3967 | - | 69280 B |
|
||||
| UpdateSet_PersistentMap_Standard | 1000 | 8 | 22,274.40 ns | 23.9868 | 2.3499 | - | 200800 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 1000 | 8 | 32,897.61 ns | 27.0386 | 2.6855 | - | 226400 B |
|
||||
| UpdateSet_TransientMap_Standard | 1000 | 8 | 8,218.75 ns | 4.5624 | 0.4272 | - | 38176 B |
|
||||
| UpdateSet_TransientMap_Unicode | 1000 | 8 | 12,847.07 ns | 4.5929 | 0.4883 | - | 38432 B |
|
||||
| UpdateSet_ImmSortedDict | 1000 | 8 | 37,683.95 ns | 6.3477 | 0.2441 | - | 53232 B |
|
||||
| UpdateSet_ExtMap | 1000 | 8 | 38,195.16 ns | 7.6904 | 0.3052 | - | 64576 B |
|
||||
| UpdateSet_ExtHashMap | 1000 | 8 | 14,327.00 ns | 7.7057 | 0.2747 | - | 64480 B |
|
||||
| Iterate_ImmDict | 1000 | 8 | 12,971.77 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap_Standard | 1000 | 8 | 1,615.06 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 1000 | 8 | 4,905.20 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 1000 | 8 | 3,282.20 ns | 0.0038 | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 1000 | 8 | 14,214.03 ns | 2.6093 | - | - | 21888 B |
|
||||
| Iterate_PersistentMap_Unicode | 1000 | 8 | 1,644.30 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 1000 | 8 | 19,504.24 ns | 7.5989 | 0.3052 | - | 63648 B |
|
||||
| Remove_PersistentMap_Standard | 1000 | 8 | 24,227.34 ns | 23.7122 | 2.0752 | - | 198400 B |
|
||||
| Remove_PersistentMap_Unicode | 1000 | 8 | 29,622.70 ns | 26.7639 | 2.4414 | - | 224000 B |
|
||||
| Remove_TransientMap_Standard | 1000 | 8 | 9,476.68 ns | 3.7689 | 0.3510 | - | 31592 B |
|
||||
| Remove_TransientMap_Unicode | 1000 | 8 | 14,237.22 ns | 3.7994 | 0.3662 | - | 31848 B |
|
||||
| Remove_ImmSortedDict | 1000 | 8 | 33,197.78 ns | 5.6763 | 0.1221 | - | 47616 B |
|
||||
| Remove_ExtMap | 1000 | 8 | 39,241.19 ns | 6.8970 | 0.1831 | - | 57744 B |
|
||||
| Remove_ExtHashMap | 1000 | 8 | 11,374.77 ns | 7.5378 | 0.2136 | - | 63056 B |
|
||||
| **Build_TransientMap_Standard** | **1000** | **50** | **104,538.75 ns** | **5.4932** | **0.3662** | **-** | **46784 B** |
|
||||
| Build_TransientMap_Unicode | 1000 | 50 | 175,661.70 ns | 7.0801 | 0.4883 | - | 59368 B |
|
||||
| Build_ImmDict | 1000 | 50 | 278,290.76 ns | 79.5898 | 8.7891 | - | 669696 B |
|
||||
| Build_ImmSortedDict | 1000 | 50 | 417,827.13 ns | 61.0352 | 5.3711 | - | 511344 B |
|
||||
| Build_ExtMap | 1000 | 50 | 409,994.29 ns | 78.1250 | 7.3242 | - | 656288 B |
|
||||
| Build_ExtHashMap | 1000 | 50 | 181,354.06 ns | 70.3125 | 4.6387 | - | 588328 B |
|
||||
| Retrieve_ImmDict | 1000 | 50 | 3,132.97 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 1000 | 50 | 1,794.95 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 1000 | 50 | 2,065.82 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 1000 | 50 | 21,834.49 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 1000 | 50 | 23,236.99 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 1000 | 50 | 4,931.32 ns | - | - | - | - |
|
||||
| Update_ImmDict | 1000 | 50 | 22,234.72 ns | 7.5073 | 0.3052 | - | 62816 B |
|
||||
| Update_PersistentMap_Standard | 1000 | 50 | 20,313.66 ns | 23.9868 | 2.0142 | - | 200800 B |
|
||||
| Update_PersistentMap_Unicode | 1000 | 50 | 27,829.75 ns | 27.0386 | 2.2888 | - | 226400 B |
|
||||
| Update_TransientMap_Standard | 1000 | 50 | 6,827.30 ns | 4.0588 | 0.4044 | - | 33952 B |
|
||||
| Update_TransientMap_Unicode | 1000 | 50 | 7,654.44 ns | 4.0894 | 0.3967 | - | 34208 B |
|
||||
| Update_ImmSortedDict | 1000 | 50 | 32,637.97 ns | 5.6763 | 0.1221 | - | 47952 B |
|
||||
| Update_ExtMap | 1000 | 50 | 32,523.61 ns | 6.5308 | 0.1831 | - | 54720 B |
|
||||
| Update_ExtHashMap | 1000 | 50 | 12,993.79 ns | 7.2479 | 0.2441 | - | 60720 B |
|
||||
| UpdateSet_ImmDict | 1000 | 50 | 25,835.15 ns | 8.3313 | 0.3967 | - | 69728 B |
|
||||
| UpdateSet_PersistentMap_Standard | 1000 | 50 | 22,970.95 ns | 24.1089 | 2.2278 | - | 201704 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 1000 | 50 | 34,226.66 ns | 27.2217 | 2.6245 | - | 227840 B |
|
||||
| UpdateSet_TransientMap_Standard | 1000 | 50 | 7,701.61 ns | 4.3335 | 0.4578 | - | 36264 B |
|
||||
| UpdateSet_TransientMap_Unicode | 1000 | 50 | 13,115.82 ns | 4.4250 | 0.4578 | - | 37056 B |
|
||||
| UpdateSet_ImmSortedDict | 1000 | 50 | 37,636.40 ns | 6.2866 | 0.1831 | - | 53088 B |
|
||||
| UpdateSet_ExtMap | 1000 | 50 | 38,985.88 ns | 7.8735 | 0.3052 | - | 66200 B |
|
||||
| UpdateSet_ExtHashMap | 1000 | 50 | 17,008.89 ns | 7.6294 | 0.2441 | - | 63936 B |
|
||||
| Iterate_ImmDict | 1000 | 50 | 13,396.48 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap_Standard | 1000 | 50 | 1,626.01 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 1000 | 50 | 4,912.18 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 1000 | 50 | 3,126.26 ns | 0.0038 | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 1000 | 50 | 14,857.12 ns | 2.7924 | - | - | 23472 B |
|
||||
| Iterate_PersistentMap_Unicode | 1000 | 50 | 1,654.97 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 1000 | 50 | 22,385.84 ns | 7.6904 | 0.3052 | - | 64352 B |
|
||||
| Remove_PersistentMap_Standard | 1000 | 50 | 25,269.13 ns | 23.7122 | 1.9531 | - | 198400 B |
|
||||
| Remove_PersistentMap_Unicode | 1000 | 50 | 30,307.99 ns | 26.7639 | 2.2583 | - | 224000 B |
|
||||
| Remove_TransientMap_Standard | 1000 | 50 | 9,482.18 ns | 3.2654 | 0.2747 | - | 27368 B |
|
||||
| Remove_TransientMap_Unicode | 1000 | 50 | 13,754.52 ns | 3.2959 | 0.2899 | - | 27624 B |
|
||||
| Remove_ImmSortedDict | 1000 | 50 | 32,695.64 ns | 5.6763 | 0.1221 | - | 47664 B |
|
||||
| Remove_ExtMap | 1000 | 50 | 37,495.23 ns | 6.9580 | 0.2441 | - | 58640 B |
|
||||
| Remove_ExtHashMap | 1000 | 50 | 14,713.28 ns | 7.9193 | 0.2594 | - | 66264 B |
|
||||
| **Build_TransientMap_Standard** | **10000** | **8** | **1,680,964.28 ns** | **52.7344** | **15.6250** | **-** | **452352 B** |
|
||||
| Build_TransientMap_Unicode | 10000 | 8 | 2,275,404.91 ns | 66.4063 | 19.5313 | - | 576584 B |
|
||||
| Build_ImmDict | 10000 | 8 | 4,364,880.86 ns | 1046.8750 | 507.8125 | - | 8766016 B |
|
||||
| Build_ImmSortedDict | 10000 | 8 | 6,551,472.85 ns | 804.6875 | 281.2500 | - | 6767232 B |
|
||||
| Build_ExtMap | 10000 | 8 | 6,411,766.35 ns | 1015.6250 | 437.5000 | - | 8542480 B |
|
||||
| Build_ExtHashMap | 10000 | 8 | 1,913,707.20 ns | 945.3125 | 320.3125 | - | 7912992 B |
|
||||
| Retrieve_ImmDict | 10000 | 8 | 23,538.66 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 10000 | 8 | 44,522.29 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 10000 | 8 | 33,878.51 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 10000 | 8 | 385,003.57 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 10000 | 8 | 415,526.12 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 10000 | 8 | 38,484.21 ns | - | - | - | - |
|
||||
| Update_ImmDict | 10000 | 8 | 395,683.89 ns | 101.5625 | 26.3672 | - | 849792 B |
|
||||
| Update_PersistentMap_Standard | 10000 | 8 | 348,111.33 ns | 304.6875 | 131.3477 | - | 2552000 B |
|
||||
| Update_PersistentMap_Unicode | 10000 | 8 | 415,082.82 ns | 366.2109 | 160.1563 | - | 3064000 B |
|
||||
| Update_TransientMap_Standard | 10000 | 8 | 138,925.69 ns | 40.5273 | 13.6719 | - | 339232 B |
|
||||
| Update_TransientMap_Unicode | 10000 | 8 | 155,075.00 ns | 40.7715 | 16.6016 | - | 341792 B |
|
||||
| Update_ImmSortedDict | 10000 | 8 | 576,796.03 ns | 76.1719 | 15.6250 | - | 640368 B |
|
||||
| Update_ExtMap | 10000 | 8 | 566,570.69 ns | 87.8906 | 22.4609 | - | 735640 B |
|
||||
| Update_ExtHashMap | 10000 | 8 | 169,597.22 ns | 103.0273 | 22.7051 | - | 862144 B |
|
||||
| UpdateSet_ImmDict | 10000 | 8 | 430,876.44 ns | 108.3984 | 28.3203 | - | 907136 B |
|
||||
| UpdateSet_PersistentMap_Standard | 10000 | 8 | 390,932.56 ns | 306.6406 | 136.7188 | - | 2566136 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 10000 | 8 | 485,184.77 ns | 368.6523 | 154.2969 | - | 3086432 B |
|
||||
| UpdateSet_TransientMap_Standard | 10000 | 8 | 183,267.74 ns | 42.2363 | 16.3574 | - | 354776 B |
|
||||
| UpdateSet_TransientMap_Unicode | 10000 | 8 | 222,767.01 ns | 43.7012 | 16.3574 | - | 365632 B |
|
||||
| UpdateSet_ImmSortedDict | 10000 | 8 | 648,381.31 ns | 82.0313 | 19.5313 | - | 687648 B |
|
||||
| UpdateSet_ExtMap | 10000 | 8 | 637,483.97 ns | 99.6094 | 25.3906 | - | 833752 B |
|
||||
| UpdateSet_ExtHashMap | 10000 | 8 | 189,210.57 ns | 104.9805 | 23.6816 | - | 878600 B |
|
||||
| Iterate_ImmDict | 10000 | 8 | 176,031.53 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap_Standard | 10000 | 8 | 17,218.51 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 10000 | 8 | 56,553.28 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 10000 | 8 | 66,099.89 ns | - | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 10000 | 8 | 175,854.49 ns | 20.0195 | - | - | 168696 B |
|
||||
| Iterate_PersistentMap_Unicode | 10000 | 8 | 17,469.24 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 10000 | 8 | 395,433.28 ns | 102.5391 | 26.3672 | - | 858560 B |
|
||||
| Remove_PersistentMap_Standard | 10000 | 8 | 417,267.98 ns | 302.2461 | 112.7930 | - | 2529408 B |
|
||||
| Remove_PersistentMap_Unicode | 10000 | 8 | 489,859.89 ns | 363.2813 | 180.6641 | - | 3041408 B |
|
||||
| Remove_TransientMap_Standard | 10000 | 8 | 198,908.02 ns | 37.8418 | 13.6719 | - | 316680 B |
|
||||
| Remove_TransientMap_Unicode | 10000 | 8 | 223,427.67 ns | 38.0859 | 15.1367 | - | 319240 B |
|
||||
| Remove_ImmSortedDict | 10000 | 8 | 602,358.94 ns | 77.1484 | 15.6250 | - | 652944 B |
|
||||
| Remove_ExtMap | 10000 | 8 | 614,493.62 ns | 91.7969 | 20.5078 | - | 774000 B |
|
||||
| Remove_ExtHashMap | 10000 | 8 | 179,222.20 ns | 104.0039 | 20.2637 | - | 870432 B |
|
||||
| **Build_TransientMap_Standard** | **10000** | **50** | **1,799,942.31 ns** | **52.7344** | **13.6719** | **-** | **446352 B** |
|
||||
| Build_TransientMap_Unicode | 10000 | 50 | 2,336,360.10 ns | 66.4063 | 23.4375 | - | 567112 B |
|
||||
| Build_ImmDict | 10000 | 50 | 4,618,106.24 ns | 1046.8750 | 515.6250 | - | 8772288 B |
|
||||
| Build_ImmSortedDict | 10000 | 50 | 6,614,766.71 ns | 804.6875 | 265.6250 | - | 6751584 B |
|
||||
| Build_ExtMap | 10000 | 50 | 6,524,244.50 ns | 1015.6250 | 429.6875 | - | 8544720 B |
|
||||
| Build_ExtHashMap | 10000 | 50 | 2,195,902.51 ns | 945.3125 | 312.5000 | - | 7921128 B |
|
||||
| Retrieve_ImmDict | 10000 | 50 | 53,644.62 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 10000 | 50 | 51,772.04 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 10000 | 50 | 35,183.58 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 10000 | 50 | 384,954.19 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 10000 | 50 | 419,295.20 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 10000 | 50 | 68,364.13 ns | - | - | - | - |
|
||||
| Update_ImmDict | 10000 | 50 | 416,499.71 ns | 100.5859 | 24.9023 | - | 842880 B |
|
||||
| Update_PersistentMap_Standard | 10000 | 50 | 352,007.58 ns | 304.6875 | 131.3477 | - | 2552000 B |
|
||||
| Update_PersistentMap_Unicode | 10000 | 50 | 419,485.01 ns | 366.2109 | 158.2031 | - | 3064000 B |
|
||||
| Update_TransientMap_Standard | 10000 | 50 | 145,767.49 ns | 39.3066 | 13.6719 | - | 328832 B |
|
||||
| Update_TransientMap_Unicode | 10000 | 50 | 152,884.18 ns | 39.5508 | 13.9160 | - | 331136 B |
|
||||
| Update_ImmSortedDict | 10000 | 50 | 581,461.95 ns | 76.1719 | 14.6484 | - | 641616 B |
|
||||
| Update_ExtMap | 10000 | 50 | 562,336.86 ns | 86.9141 | 20.5078 | - | 732896 B |
|
||||
| Update_ExtHashMap | 10000 | 50 | 189,986.29 ns | 103.0273 | 22.2168 | - | 863280 B |
|
||||
| UpdateSet_ImmDict | 10000 | 50 | 457,611.78 ns | 108.8867 | 30.7617 | - | 912128 B |
|
||||
| UpdateSet_PersistentMap_Standard | 10000 | 50 | 400,214.07 ns | 306.6406 | 129.8828 | - | 2565560 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 10000 | 50 | 497,552.64 ns | 368.1641 | 152.3438 | - | 3085600 B |
|
||||
| UpdateSet_TransientMap_Standard | 10000 | 50 | 191,918.16 ns | 41.0156 | 13.6719 | - | 343800 B |
|
||||
| UpdateSet_TransientMap_Unicode | 10000 | 50 | 222,651.45 ns | 42.2363 | 15.8691 | - | 354144 B |
|
||||
| UpdateSet_ImmSortedDict | 10000 | 50 | 656,130.18 ns | 82.0313 | 19.5313 | - | 691728 B |
|
||||
| UpdateSet_ExtMap | 10000 | 50 | 644,220.45 ns | 98.6328 | 23.4375 | - | 829552 B |
|
||||
| UpdateSet_ExtHashMap | 10000 | 50 | 213,717.48 ns | 104.7363 | 23.6816 | - | 876664 B |
|
||||
| Iterate_ImmDict | 10000 | 50 | 172,329.11 ns | - | - | - | - |
|
||||
| Iterate_PersistentMap_Standard | 10000 | 50 | 17,538.70 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 10000 | 50 | 52,626.12 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 10000 | 50 | 75,440.14 ns | - | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 10000 | 50 | 176,467.34 ns | 20.0195 | - | - | 168192 B |
|
||||
| Iterate_PersistentMap_Unicode | 10000 | 50 | 17,931.81 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 10000 | 50 | 420,385.25 ns | 102.5391 | 26.3672 | - | 858432 B |
|
||||
| Remove_PersistentMap_Standard | 10000 | 50 | 417,831.54 ns | 302.2461 | 113.7695 | - | 2530816 B |
|
||||
| Remove_PersistentMap_Unicode | 10000 | 50 | 488,933.92 ns | 363.2813 | 158.2031 | - | 3042816 B |
|
||||
| Remove_TransientMap_Standard | 10000 | 50 | 215,079.21 ns | 37.1094 | 13.4277 | - | 310504 B |
|
||||
| Remove_TransientMap_Unicode | 10000 | 50 | 221,947.80 ns | 37.3535 | 13.1836 | - | 312808 B |
|
||||
| Remove_ImmSortedDict | 10000 | 50 | 607,856.65 ns | 77.1484 | 15.6250 | - | 651840 B |
|
||||
| Remove_ExtMap | 10000 | 50 | 628,754.81 ns | 91.7969 | 21.4844 | - | 771256 B |
|
||||
| Remove_ExtHashMap | 10000 | 50 | 203,488.31 ns | 104.2480 | 21.4844 | - | 873560 B |
|
||||
| **Build_TransientMap_Standard** | **100000** | **8** | **27,358,697.21 ns** | **531.2500** | **406.2500** | **-** | **4540216 B** |
|
||||
| Build_TransientMap_Unicode | 100000 | 8 | 30,530,320.71 ns | 687.5000 | 593.7500 | - | 5798728 B |
|
||||
| Build_ImmDict | 100000 | 8 | 90,877,970.56 ns | 13166.6667 | 4500.0000 | 166.6667 | 109369973 B |
|
||||
| Build_ImmSortedDict | 100000 | 8 | 116,689,394.37 ns | 10000.0000 | 3800.0000 | - | 83946432 B |
|
||||
| Build_ExtMap | 100000 | 8 | 117,813,711.80 ns | 12400.0000 | 5600.0000 | - | 104660688 B |
|
||||
| Build_ExtHashMap | 100000 | 8 | 44,164,471.41 ns | 12000.0000 | 2583.3333 | 83.3333 | 99774201 B |
|
||||
| Retrieve_ImmDict | 100000 | 8 | 1,475,338.26 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 100000 | 8 | 1,774,506.52 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 100000 | 8 | 1,336,091.68 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 100000 | 8 | 6,136,773.68 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 100000 | 8 | 6,552,400.29 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 100000 | 8 | 1,008,726.75 ns | - | - | - | - |
|
||||
| Update_ImmDict | 100000 | 8 | 7,725,041.14 ns | 1265.6250 | 1070.3125 | - | 10622656 B |
|
||||
| Update_PersistentMap_Standard | 100000 | 8 | 9,751,403.12 ns | 3734.3750 | 2984.3750 | 46.8750 | 30960042 B |
|
||||
| Update_PersistentMap_Unicode | 100000 | 8 | 11,106,373.87 ns | 4671.8750 | 2468.7500 | 62.5000 | 38640073 B |
|
||||
| Update_TransientMap_Standard | 100000 | 8 | 3,072,594.67 ns | 410.1563 | 347.6563 | - | 3458240 B |
|
||||
| Update_TransientMap_Unicode | 100000 | 8 | 2,678,497.53 ns | 414.0625 | 351.5625 | - | 3487168 B |
|
||||
| Update_ImmSortedDict | 100000 | 8 | 9,958,696.31 ns | 953.1250 | 781.2500 | - | 8020080 B |
|
||||
| Update_ExtMap | 100000 | 8 | 10,091,372.46 ns | 1093.7500 | 937.5000 | - | 9246400 B |
|
||||
| Update_ExtHashMap | 100000 | 8 | 3,713,620.94 ns | 1269.5313 | 996.0938 | - | 10625016 B |
|
||||
| UpdateSet_ImmDict | 100000 | 8 | 8,447,925.20 ns | 1343.7500 | 1062.5000 | - | 11239232 B |
|
||||
| UpdateSet_PersistentMap_Standard | 100000 | 8 | 10,256,666.84 ns | 3750.0000 | 2609.3750 | 46.8750 | 31066796 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 100000 | 8 | 12,170,123.49 ns | 4687.5000 | 2250.0000 | 62.5000 | 38809425 B |
|
||||
| UpdateSet_TransientMap_Standard | 100000 | 8 | 3,437,362.03 ns | 421.8750 | 351.5625 | - | 3545288 B |
|
||||
| UpdateSet_TransientMap_Unicode | 100000 | 8 | 3,401,903.92 ns | 433.5938 | 386.7188 | - | 3636832 B |
|
||||
| UpdateSet_ImmSortedDict | 100000 | 8 | 11,287,098.80 ns | 1015.6250 | 875.0000 | - | 8495856 B |
|
||||
| UpdateSet_ExtMap | 100000 | 8 | 11,577,275.47 ns | 1218.7500 | 1015.6250 | - | 10287328 B |
|
||||
| UpdateSet_ExtHashMap | 100000 | 8 | 3,795,803.13 ns | 1285.1563 | 996.0938 | - | 10752160 B |
|
||||
| Iterate_ImmDict | 100000 | 8 | 2,176,936.15 ns | - | - | - | 192 B |
|
||||
| Iterate_PersistentMap_Standard | 100000 | 8 | 240,376.55 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 100000 | 8 | 713,292.36 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 100000 | 8 | 1,041,016.32 ns | - | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 100000 | 8 | 2,609,178.31 ns | 277.3438 | - | - | 2321200 B |
|
||||
| Iterate_PersistentMap_Unicode | 100000 | 8 | 228,758.17 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 100000 | 8 | 7,633,500.34 ns | 1281.2500 | 1070.3125 | - | 10733056 B |
|
||||
| Remove_PersistentMap_Standard | 100000 | 8 | 10,634,178.02 ns | 3718.7500 | 3093.7500 | 46.8750 | 30732714 B |
|
||||
| Remove_PersistentMap_Unicode | 100000 | 8 | 11,815,412.68 ns | 4640.6250 | 2875.0000 | 62.5000 | 38412734 B |
|
||||
| Remove_TransientMap_Standard | 100000 | 8 | 3,501,181.09 ns | 378.9063 | 312.5000 | - | 3197160 B |
|
||||
| Remove_TransientMap_Unicode | 100000 | 8 | 3,113,607.62 ns | 382.8125 | 324.2188 | - | 3226088 B |
|
||||
| Remove_ImmSortedDict | 100000 | 8 | 10,204,663.55 ns | 953.1250 | 781.2500 | - | 8031888 B |
|
||||
| Remove_ExtMap | 100000 | 8 | 11,026,369.80 ns | 1140.6250 | 937.5000 | - | 9630448 B |
|
||||
| Remove_ExtHashMap | 100000 | 8 | 3,801,747.29 ns | 1265.6250 | 937.5000 | - | 10608872 B |
|
||||
| **Build_TransientMap_Standard** | **100000** | **50** | **31,414,873.58 ns** | **500.0000** | **312.5000** | **-** | **4518440 B** |
|
||||
| Build_TransientMap_Unicode | 100000 | 50 | 34,272,872.95 ns | 666.6667 | 466.6667 | - | 5763784 B |
|
||||
| Build_ImmDict | 100000 | 50 | 89,991,315.99 ns | 13166.6667 | 4833.3333 | 166.6667 | 109418176 B |
|
||||
| Build_ImmSortedDict | 100000 | 50 | 131,306,574.64 ns | 10000.0000 | 4000.0000 | - | 83731632 B |
|
||||
| Build_ExtMap | 100000 | 50 | 128,889,148.68 ns | 12250.0000 | 5250.0000 | - | 104371560 B |
|
||||
| Build_ExtHashMap | 100000 | 50 | 48,924,428.69 ns | 11909.0909 | 2272.7273 | - | 99798944 B |
|
||||
| Retrieve_ImmDict | 100000 | 50 | 1,789,773.89 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Standard | 100000 | 50 | 2,280,540.33 ns | - | - | - | - |
|
||||
| Retrieve_PersistentMap_Unicode | 100000 | 50 | 1,447,748.76 ns | - | - | - | - |
|
||||
| Retrieve_ImmSortedDict | 100000 | 50 | 7,097,150.11 ns | - | - | - | - |
|
||||
| Retrieve_ExtMap | 100000 | 50 | 7,013,962.09 ns | - | - | - | - |
|
||||
| Retrieve_ExtHashMap | 100000 | 50 | 1,327,343.45 ns | - | - | - | - |
|
||||
| Update_ImmDict | 100000 | 50 | 8,075,323.36 ns | 1265.6250 | 1062.5000 | - | 10593088 B |
|
||||
| Update_PersistentMap_Standard | 100000 | 50 | 11,441,201.31 ns | 3734.3750 | 3421.8750 | 46.8750 | 30960051 B |
|
||||
| Update_PersistentMap_Unicode | 100000 | 50 | 11,646,138.25 ns | 4656.2500 | 2765.6250 | 46.8750 | 38640052 B |
|
||||
| Update_TransientMap_Standard | 100000 | 50 | 3,785,331.39 ns | 406.2500 | 347.6563 | - | 3410592 B |
|
||||
| Update_TransientMap_Unicode | 100000 | 50 | 2,931,528.16 ns | 410.1563 | 343.7500 | - | 3440288 B |
|
||||
| Update_ImmSortedDict | 100000 | 50 | 11,055,325.40 ns | 953.1250 | 781.2500 | - | 8012928 B |
|
||||
| Update_ExtMap | 100000 | 50 | 10,703,171.07 ns | 1093.7500 | 906.2500 | - | 9233632 B |
|
||||
| Update_ExtHashMap | 100000 | 50 | 4,100,682.76 ns | 1265.6250 | 976.5625 | - | 10623184 B |
|
||||
| UpdateSet_ImmDict | 100000 | 50 | 8,848,378.05 ns | 1343.7500 | 1046.8750 | - | 11255552 B |
|
||||
| UpdateSet_PersistentMap_Standard | 100000 | 50 | 12,223,495.13 ns | 3734.3750 | 2640.6250 | 31.2500 | 31083384 B |
|
||||
| UpdateSet_PersistentMap_Unicode | 100000 | 50 | 12,917,217.90 ns | 4687.5000 | 2437.5000 | 46.8750 | 38835954 B |
|
||||
| UpdateSet_TransientMap_Standard | 100000 | 50 | 4,490,436.88 ns | 414.0625 | 343.7500 | - | 3528320 B |
|
||||
| UpdateSet_TransientMap_Unicode | 100000 | 50 | 3,756,285.03 ns | 433.5938 | 367.1875 | - | 3630560 B |
|
||||
| UpdateSet_ImmSortedDict | 100000 | 50 | 12,490,770.48 ns | 1015.6250 | 890.6250 | - | 8501136 B |
|
||||
| UpdateSet_ExtMap | 100000 | 50 | 12,674,840.15 ns | 1218.7500 | 1015.6250 | - | 10265544 B |
|
||||
| UpdateSet_ExtHashMap | 100000 | 50 | 4,395,939.08 ns | 1281.2500 | 992.1875 | - | 10756792 B |
|
||||
| Iterate_ImmDict | 100000 | 50 | 2,416,834.88 ns | - | - | - | 96 B |
|
||||
| Iterate_PersistentMap_Standard | 100000 | 50 | 214,203.41 ns | - | - | - | - |
|
||||
| Iterate_ImmSortedDict | 100000 | 50 | 712,519.08 ns | - | - | - | - |
|
||||
| Iterate_ExtMap | 100000 | 50 | 1,091,987.30 ns | - | - | - | 32 B |
|
||||
| Iterate_ExtHashMap | 100000 | 50 | 2,669,431.61 ns | 273.4375 | - | - | 2314072 B |
|
||||
| Iterate_PersistentMap_Unicode | 100000 | 50 | 211,247.16 ns | - | - | - | - |
|
||||
| Remove_ImmDict | 100000 | 50 | 8,089,556.02 ns | 1281.2500 | 1031.2500 | - | 10743040 B |
|
||||
| Remove_PersistentMap_Standard | 100000 | 50 | 11,874,296.78 ns | 3703.1250 | 2750.0000 | 31.2500 | 30722852 B |
|
||||
| Remove_PersistentMap_Unicode | 100000 | 50 | 11,948,106.52 ns | 4625.0000 | 2859.3750 | 46.8750 | 38402877 B |
|
||||
| Remove_TransientMap_Standard | 100000 | 50 | 4,337,489.41 ns | 375.0000 | 304.6875 | - | 3176264 B |
|
||||
| Remove_TransientMap_Unicode | 100000 | 50 | 3,278,674.84 ns | 382.8125 | 312.5000 | - | 3205960 B |
|
||||
| Remove_ImmSortedDict | 100000 | 50 | 11,338,022.46 ns | 953.1250 | 781.2500 | - | 8032656 B |
|
||||
| Remove_ExtMap | 100000 | 50 | 11,912,777.70 ns | 1140.6250 | 921.8750 | - | 9617232 B |
|
||||
| Remove_ExtHashMap | 100000 | 50 | 4,223,945.68 ns | 1265.6250 | 937.5000 | - | 10621824 B |#+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.
|
||||
40
PersistentOrderedMap/TransientOrderedMap.cs
Normal file
40
PersistentOrderedMap/TransientOrderedMap.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace PersistentOrderedMap;
|
||||
|
||||
public sealed class TransientOrderedMap<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;
|
||||
|
||||
public TransientOrderedMap(Node<K> root, TStrategy strategy, int count)
|
||||
: base(root, strategy, count)
|
||||
{
|
||||
_transactionId = OwnerId.Next();
|
||||
}
|
||||
|
||||
public void Set(K key, V value)
|
||||
{
|
||||
_root = BTreeFunctions.Set(_root, key, value, _strategy, _transactionId, out bool countChanged);
|
||||
if (countChanged) Count++;
|
||||
}
|
||||
|
||||
public void Remove(K key)
|
||||
{
|
||||
_root = BTreeFunctions.Remove<K,V, TStrategy>(_root, key, _strategy, _transactionId, out bool removed);
|
||||
if (removed) Count--;
|
||||
}
|
||||
|
||||
public PersistentOrderedMap<K, V, TStrategy> ToPersistent()
|
||||
{
|
||||
// 1. Create the snapshot by copying all relevant information
|
||||
|
||||
var snapshot = new PersistentOrderedMap<K, V, TStrategy>(_root, _strategy, Count);
|
||||
|
||||
// 2. Protect the snapshot from THIS TransientOrderedMap by getting a new ownerId
|
||||
// so that future edits will be done by CoW
|
||||
_transactionId = OwnerId.Next();
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue