Compare commits

..

No commits in common. "master" and "fix_prefixes_again" have entirely different histories.

41 changed files with 2528 additions and 3261 deletions

View file

@ -2,7 +2,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentOrderedMap", "PersistentOrderedMap\PersistentOrderedMap.csproj", "{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentMap", "PersistentMap\PersistentMap.csproj", "{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{9E499000-5E37-42F8-89D2-E18A53F0EF0C}"
EndProject
@ -10,7 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstImmutableDict", "benchmarks\AgainstImmutableDict\AgainstImmutableDict.csproj", "{13304F19-7ED3-4C40-9A08-46D539667D50}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyBenchMarks", "benchmarks\MyBenchMarks\MyBenchMarks.csproj", "{769E1CEA-7E01-405B-80A2-95CBF432A2BA}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstLanguageExt", "benchmarks\AgainstLanguageExt\AgainstLanguageExt.csproj", "{6C16526B-5139-4EA3-BF74-E6320F467198}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -20,7 +20,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE} = {B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}
{13304F19-7ED3-4C40-9A08-46D539667D50} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
{769E1CEA-7E01-405B-80A2-95CBF432A2BA} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
{6C16526B-5139-4EA3-BF74-E6320F467198} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -35,9 +35,9 @@ Global
{13304F19-7ED3-4C40-9A08-46D539667D50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.Build.0 = Release|Any CPU
{769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Release|Any CPU.Build.0 = Release|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,939 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PersistentMap
{
public static class BTreeFunctions
{
// ---------------------------------------------------------
// Public API
// ---------------------------------------------------------
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
where TStrategy : IKeyStrategy<K>
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
Node<K> current = root;
while (true)
{
if (current.IsLeaf)
{
var leaf = current.AsLeaf<V>();
// Leaf uses standard FindIndex (Lower Bound) to find exact match
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
{
// FIX: Internal uses FindRoutingIndex (Upper Bound)
var internalNode = current.AsInternal();
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
current = internalNode.Children[index]!;
}
}
}
// Public API
public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool countChanged)
{
root = root.EnsureEditable(owner);
var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged);
if (splitResult != null)
{
var newRoot = new InternalNode<K>(owner);
newRoot.Children[0] = root;
newRoot.Keys[0] = splitResult.Separator;
newRoot.Children[1] = splitResult.NewNode;
newRoot.SetCount(1);
if (strategy.UsesPrefixes)
newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator);
return newRoot;
}
return root;
}
// Recursive Helper
private static SplitResult<K>? InsertRecursive<K, V>(Node<K> node, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool added)
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
leaf.Values[index] = value;
added = false; // Key existed, value updated. Count does not change.
return null;
}
added = true; // New key. Count +1.
if (leaf.Header.Count < LeafNode<K, V>.Capacity)
{
InsertIntoLeaf(leaf, index, key, value, strategy);
return null;
}
else
{
return SplitLeaf(leaf, index, key, value, strategy, owner);
}
}
else
{
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 API
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged)
where TStrategy : IKeyStrategy<K>
{
root = root.EnsureEditable(owner);
bool rebalanceNeeded = RemoveRecursive<K, V, TStrategy>(root, key, strategy, owner, out countChanged);
if (rebalanceNeeded)
{
if (!root.IsLeaf)
{
var internalRoot = root.AsInternal();
if (internalRoot.Header.Count == 0)
{
return internalRoot.Children[0]!;
}
}
}
return root;
}
// Recursive Helper
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed)
where TStrategy : IKeyStrategy<K>
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
RemoveFromLeaf(leaf, index, strategy);
removed = true; // Item removed. Count -1.
return leaf.Header.Count <LeafNode<K, V>.MergeThreshold;
}
removed = false; // Item not found.
return false;
}
else
{
var internalNode = node.AsInternal();
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
var child = internalNode.Children[index]!.EnsureEditable(owner);
internalNode.Children[index] = child;
bool childUnderflow = RemoveRecursive<K, V, TStrategy>(child, key, strategy, owner, out removed);
if (removed && childUnderflow)
{
return HandleUnderflow<K, V, TStrategy>(internalNode, index, strategy, owner);
}
return false;
}
}
// ---------------------------------------------------------
// Internal Helpers: Search
// ---------------------------------------------------------
// Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
if (strategy.UsesPrefixes)
{
// Use the pre-calculated prefix here!
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
return RefineSearch(index, node.GetKeys(), key, strategy);
}
return LinearSearchKeys(node.GetKeys(), key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchKeys<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = 0;
// Standard linear scan on the keys array
while (i < keys.Length && strategy.Compare(keys[i], key) < 0)
{
i++;
}
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RefineSearch<K, TStrategy>(int startIndex, Span<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = startIndex;
// JIT can now inline 'strategy.Compare' here!
while (i < keys.Length && strategy.Compare(keys[i], key) < 0)
{
i++;
}
return i;
}
// Used by Internal Nodes: Finds the child index to descend into.
// If Key == Separator, we must go RIGHT (index + 1), so we need (Upper Bound).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
if (!strategy.UsesPrefixes)
{
return LinearSearchRouting(node.GetKeys(), key, strategy);
}
// Use the pre-calculated prefix here!
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = 0;
// Routing: Skip everything that is LessOrEqual.
// We stop at the first item that is Greater.
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0)
{
i++;
}
return i;
}
// Overload for primitive types (avoids IComparer call overhead in fallback)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<T>(Span<T> keys, T key) where T : struct, IComparable<T>
{
int i = 0;
while (i < keys.Length && keys[i].CompareTo(key) <= 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RefineRouting<K, TStrategy>(int startIndex, K[] keys, int count, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = startIndex;
// DIFFERENCE: We continue past valid matches.
// We want the first key STRICTLY GREATER than target.
// If keys[i] == key, we increment (go to right child).
while (i < count && strategy.Compare(keys[i], key) <= 0)
{
i++;
}
return i;
}
// ---------------------------------------------------------
// 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 SplitResult<K>? InsertRecur2sive<K, V, TStrategy>(Node<K> node, K key, V value, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<K>
{
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
// --- LEAF CASE ---
if (node.IsLeaf)
{
var leaf = node.AsLeaf<V>();
// Leaf uses FindIndex
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
leaf.Values[index] = value;
return null;
}
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);
}
}
// --- INTERNAL CASE ---
var internalNode = node.AsInternal();
// FIX: Internal uses FindRoutingIndex
int childIndex = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
var child = internalNode.Children[childIndex]!.EnsureEditable(owner);
internalNode.Children[childIndex] = child;
var split = InsertRecursive(child, key, value, strategy, owner, out _);
if (split != null)
{
// ... checks ...
// Use childIndex here
if (internalNode.Header.Count < InternalNode<K>.Capacity - 1)
{
InsertIntoInternal(internalNode, childIndex, split.Separator, split.NewNode, strategy);
return null;
}
else
{
return SplitInternal(internalNode, childIndex, split.Separator, split.NewNode, strategy, owner);
}
}
return null;
}
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;
// Fast Span memory moves
leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1));
leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1));
if (strategy.UsesPrefixes)
{
leaf.AllPrefixes.Slice(index, count - index)
.CopyTo(leaf.AllPrefixes.Slice(index + 1));
}
}
leaf.Keys[index] = key;
// This fails if leaf.Values is a Span<V> of length 'count'
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);
int totalCount = left.Header.Count;
// Heuristics
int splitPoint;
if (insertIndex == totalCount) splitPoint = totalCount; // Append: Keep all in Left (90/10 logic effectively)
else if (insertIndex == 0) splitPoint = 0; // Prepend: Right gets all
else splitPoint = totalCount / 2;
// Move items to Right
int moveCount = totalCount - splitPoint;
if (moveCount > 0)
{
// Fast Span memory moves
left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0));
left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0));
// Manually copy prefixes if needed or re-calculate
if (strategy.UsesPrefixes)
for (int i = 0; i < moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + i];
}
// Update Counts
left.SetCount(splitPoint);
right.SetCount(moveCount);
// Insert the New Item into the correct node
if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0))
{
InsertIntoLeaf(left, insertIndex, key, value, strategy);
}
else
{
InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy);
}
// In B+ Tree, the separator is the first key of the right node
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;
// Shift Keys and Prefixes
if (index < count)
{
int moveCount = count - index;
// Fast Span memory moves
node.Keys.AsSpan(index, moveCount).CopyTo(node.Keys.AsSpan(index + 1));
// FIX: Shift raw prefix array
if (strategy.UsesPrefixes)
{
node.AllPrefixes.Slice(index, count - index)
.CopyTo(node.AllPrefixes.Slice(index + 1));
}
}
// Shift Children
// Children buffer is indexable like an array but requires manual loop or Unsafe copy
// if we don't want to use unsafe pointers.
// Since it's a small struct buffer (size 33), a loop is fine/fast.
for (int i = count + 1; i > index + 1; i--)
{
node.Children[i] = node.Children[i - 1];
}
node.Keys[index] = separator;
// FIX: Write to raw array
if (strategy.UsesPrefixes)
node.AllPrefixes![index] = strategy.GetPrefix(separator);
node.Children[index + 1] = newChild;
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 = new InternalNode<K>(owner);
int count = left.Header.Count;
int splitPoint = count / 2; // Internal nodes usually split 50/50 to keep tree fat
// The key at splitPoint moves UP to become the separator.
// Keys > splitPoint move to Right.
K upKey = left.Keys[splitPoint];
// Move Keys/Prefixes to Right
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
// Fast Span memory moves
if (moveCount > 0)
{
left.Keys.AsSpan(splitPoint + 1, moveCount).CopyTo(right.Keys.AsSpan(0));
if (strategy.UsesPrefixes)
{
left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0));
}
}
// Left has children 0..splitPoint. Right has children splitPoint+1..End
for (int i = 0; i <= moveCount; i++)
{
right.Children[i] = left.Children[splitPoint + 1 + i];
}
left.SetCount(splitPoint);
right.SetCount(moveCount);
// Determine where to insert the new Separator/Child
// Note: We extracted 'upKey' from the original array.
// We now have to compare the *incoming* separator with 'upKey'
// to see if it goes Left or Right.
if (insertIndex == splitPoint)
{
// Special case: The new key is exactly the one pushing up?
// Usually easier to insert into temp buffer and split,
// but here we can branch:
// If insertIndex <= splitPoint, insert left. Else right.
}
// Simplified insertion into split nodes:
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
// ---------------------------------------------------------
// ---------------------------------------------------------
// Removal Logic (Fixed Type Inference & Casting)
// ---------------------------------------------------------
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)
{
// Fast Span memory moves
leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index));
leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index));
if (strategy.UsesPrefixes)
{
// Replaced manual 'for' loop with native slice copy
leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index));
}
}
leaf.SetCount(count - 1);
}
// FIX 3: Added <V> to HandleUnderflow
private static bool HandleUnderflow<K, V, TStrategy>(InternalNode<K> parent, int childIndex, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<K>
{
// Try to borrow from Right Sibling
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;
}
}
// Try to borrow from Left Sibling
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 Left and Current. Note separator index is 'childIndex - 1'
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)
{
// Note: LeafNode<K, V>.MergeThreshold is constant 8, so we can access it statically or via 8
return node.Header.Count > 8 + 1;
}
// FIX 4: Added <V> to Merge/Rotate so we can cast to LeafNode<K, V> successfully.
private static void Merge<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
// Case A: Merging Leaves
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);
}
// Case B: Merging Internal Nodes
else
{
var leftInternal = left.AsInternal();
var rightInternal = right.AsInternal();
// Pull separator from parent
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;
rightInternal.Keys.AsSpan(0, rCount).CopyTo(leftInternal.Keys.AsSpan(lCount + 1));
if (strategy.UsesPrefixes)
{
rightInternal.AllPrefixes.Slice(0, rCount)
.CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1));
}
for (int i = 0; i <= rCount; i++)
{
leftInternal.Children[lCount + 1 + i] = rightInternal.Children[i];
}
leftInternal.SetCount(lCount + 1 + rCount);
}
// Remove Separator and Right Child from Parent
int pCount = parent.Header.Count;
int moveCount = pCount - separatorIndex - 1;
if (moveCount > 0)
{
parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex));
if (strategy.UsesPrefixes)
{
// Replaced manual 'for' loop with native slice copy
parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex));
}
}
for (int i = separatorIndex + 2; i <= pCount; i++)
{
parent.Children[i - 1] = parent.Children[i];
}
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>
{
// Move one item from Right to Left
if (left.IsLeaf)
{
var leftLeaf = left.AsLeaf<V>();
var rightLeaf = right.AsLeaf<V>();
// Move first of right to end of left
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
RemoveFromLeaf(rightLeaf, 0, strategy);
// Update Parent Separator
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();
// 1. Move Parent Separator to Left End
K sep = parent.Keys[separatorIndex];
InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy);
// 2. Move Right[0] Key to Parent
parent.Keys[separatorIndex] = rightInternal.Keys[0];
if (strategy.UsesPrefixes)
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]);
// 3. Fix Right (Remove key 0 and shift child 0 out)
// We basically remove key at 0. Child 0 was moved to left. Child 1 becomes Child 0.
// Re-using Remove logic implies shifts.
// Manual shift for performance:
int rCount = rightInternal.Header.Count;
// Shift children
for (int i = 0; i < rCount; i++) rightInternal.Children[i] = rightInternal.Children[i + 1];
if (rCount > 1)
{
// Fast Span memory moves (Replaces Array.Copy & manual loop)
rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0));
if (strategy.UsesPrefixes)
{
rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0));
}
}
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>
{
// Move one item from Left to Right
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 = (InternalNode<K>)left;
var rightInternal = (InternalNode<K>)right;
int last = leftInternal.Header.Count - 1;
// 1. Move Parent Separator to Right Start
K sep = parent.Keys[separatorIndex];
// The child moving to right is the *last* child of left (index count)
InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy);
// 2. Move Left[last] Key to Parent
parent.Keys[separatorIndex] = leftInternal.Keys[last];
if (strategy.UsesPrefixes)
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
// 3. Truncate Left
leftInternal.SetCount(last);
}
}
public static bool TryGetMin<K, V>(Node<K> root, out K key, out V value)
{
var current = root;
while (!current.IsLeaf)
{
current = current.AsInternal().Children[0]!;
}
var leaf = current.AsLeaf<V>();
if (leaf.Header.Count == 0)
{
key = default!;
value = default!;
return false;
}
key = leaf.Keys![0];
value = leaf.Values[0];
return true;
}
public static bool TryGetMax<K, V>(Node<K> root, out K key, out V value)
{
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
current = internalNode.Children[internalNode.Header.Count]!;
}
var leaf = current.AsLeaf<V>();
if (leaf.Header.Count == 0)
{
key = default!;
value = default!;
return false;
}
int last = leaf.Header.Count - 1;
key = leaf.Keys![last];
value = leaf.Values[last];
return true;
}
public static bool TryGetSuccessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K nextKey, out V nextValue)
where TStrategy : IKeyStrategy<K>
{
InternalNode<K>[] path = new InternalNode<K>[32];
int[] indices = new int[32];
int depth = 0;
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
path[depth] = internalNode;
indices[depth] = idx;
depth++;
current = internalNode.Children[idx]!;
}
var leaf = current.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) index++;
// 1. Successor is in the same leaf
if (index < leaf.Header.Count)
{
nextKey = leaf.Keys![index];
nextValue = leaf.Values[index];
return true;
}
// 2. Successor is in the next leaf (We must backtrack up the tree!)
for (int i = depth - 1; i >= 0; i--)
{
// If we haven't reached the right-most child of this parent
if (indices[i] < path[i].Header.Count)
{
// Take one step right, then go absolute left all the way down
current = path[i].Children[indices[i] + 1]!;
while (!current.IsLeaf)
{
current = current.AsInternal().Children[0]!;
}
var targetLeaf = current.AsLeaf<V>();
nextKey = targetLeaf.Keys![0];
nextValue = targetLeaf.Values[0];
return true;
}
}
nextKey = default!;
nextValue = default!;
return false;
}
public static bool TryGetPredecessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K prevKey, out V prevValue)
where TStrategy : IKeyStrategy<K>
{
// Max depth of a B-Tree is small, preallocate a small array to track the descent path.
InternalNode<K>[] path = new InternalNode<K>[32];
int[] indices = new int[32];
int depth = 0;
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
path[depth] = internalNode;
indices[depth] = idx;
depth++;
current = internalNode.Children[idx]!;
}
var leaf = current.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy);
// Easy case: Predecessor is in the same leaf
if (index > 0)
{
prevKey = leaf.Keys![index - 1];
prevValue = leaf.Values[index - 1];
return true;
}
// Hard case: We need to backtrack to find the first left branch we ignored
for (int i = depth - 1; i >= 0; i--)
{
if (indices[i] > 0)
{
// Jump to the left sibling branch, then take the absolute right-most path down
current = path[i].Children[indices[i] - 1]!;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
current = internalNode.Children[internalNode.Header.Count]!;
}
var targetLeaf = current.AsLeaf<V>();
int last = targetLeaf.Header.Count - 1;
prevKey = targetLeaf.Keys![last];
prevValue = targetLeaf.Values[last];
return true;
}
}
prevKey = default!;
prevValue = default!;
return false;
}
}
}

View file

@ -1,18 +1,18 @@
using System.Collections;
namespace PersistentOrderedMap;
namespace PersistentMap;
public abstract class BaseOrderedMap<TK, TV, TStrategy> : IEnumerable<KeyValuePair<TK, TV>> where TStrategy : IKeyStrategy<TK>
public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
{
internal Node<TK> Root;
internal readonly TStrategy Strategy;
internal Node<K> _root;
internal readonly TStrategy _strategy;
public int Count { get; protected set; }
protected BaseOrderedMap(Node<TK> root, TStrategy strategy, int count)
protected BaseOrderedMap(Node<K> root, TStrategy strategy, int count)
{
Root = root ?? throw new ArgumentNullException(nameof(root));
Strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
_root = root ?? throw new ArgumentNullException(nameof(root));
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
Count = count;
}
@ -21,14 +21,14 @@ public abstract class BaseOrderedMap<TK, TV, TStrategy> : IEnumerable<KeyValuePa
// Read Operations (Shared)
// ---------------------------------------------------------
public bool TryGetValue(TK key, out TV value)
public bool TryGetValue(K key, out V value)
{
return BTreeFunctions.TryGetValue(Root, key, Strategy, out value);
return BTreeFunctions.TryGetValue(_root, key, _strategy, out value);
}
public bool ContainsKey(TK key)
public bool ContainsKey(K key)
{
return BTreeFunctions.TryGetValue<TK,TV, TStrategy>(Root, key, Strategy, out _);
return BTreeFunctions.TryGetValue<K,V, TStrategy>(_root, key, _strategy, out _);
}
@ -37,26 +37,26 @@ public abstract class BaseOrderedMap<TK, TV, TStrategy> : IEnumerable<KeyValuePa
// Bootstrap / Factory Helpers
// ---------------------------------------------------------
public static PersistentOrderedMap<TK, TV, TStrategy> Create(TStrategy strategy)
public static PersistentMap<K, V, TStrategy> Create(TStrategy strategy)
{
// Start with an empty leaf owned by None so the first write triggers CoW.
var emptyRoot = new LeafNode<TK, TV>(OwnerId.None, strategy.UsesPrefixes);
return new PersistentOrderedMap<TK, TV, TStrategy>(emptyRoot, strategy, 0);
var emptyRoot = new LeafNode<K, V>(OwnerId.None);
return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0);
}
public static TransientOrderedMap<TK, TV, TStrategy> CreateTransient(TStrategy strategy)
public static TransientMap<K, V, TStrategy> CreateTransient(TStrategy strategy)
{
var emptyRoot = new LeafNode<TK, TV>(OwnerId.None, strategy.UsesPrefixes);
return new TransientOrderedMap<TK, TV, TStrategy>(emptyRoot, strategy,0);
var emptyRoot = new LeafNode<K, V>(OwnerId.None);
return new TransientMap<K, V, TStrategy>(emptyRoot, strategy,0);
}
public BTreeEnumerator<TK, TV, TStrategy> GetEnumerator()
public BTreeEnumerator<K, V, TStrategy> GetEnumerator()
{
return AsEnumerable().GetEnumerator();
}
IEnumerator<KeyValuePair<TK, TV>> IEnumerable<KeyValuePair<TK, TV>>.GetEnumerator()
IEnumerator<KeyValuePair<K, V>> IEnumerable<KeyValuePair<K, V>>.GetEnumerator()
{
return GetEnumerator();
}
@ -67,19 +67,19 @@ public abstract class BaseOrderedMap<TK, TV, TStrategy> : IEnumerable<KeyValuePa
}
// 1. Full Scan
public BTreeEnumerable<TK, TV, TStrategy> AsEnumerable()
=> new(Root, Strategy, false, default!, false, default!);
public BTreeEnumerable<K, V, TStrategy> AsEnumerable()
=> new(_root, _strategy, false, default, false, default);
// 2. Exact Range
public BTreeEnumerable<TK, TV, TStrategy> Range(TK min, TK max)
=> new(Root, Strategy, true, min, true, max);
public BTreeEnumerable<K, V, TStrategy> Range(K min, K max)
=> new(_root, _strategy, true, min, true, max);
// 3. Start From (Open Ended)
public BTreeEnumerable<TK, TV, TStrategy> From(TK min) => new(Root, Strategy, true, min, false, default!);
public BTreeEnumerable<K, V, TStrategy> From(K min) => new(_root, _strategy, true, min, false, default);
// 4. Until (Start at beginning)
public BTreeEnumerable<TK, TV, TStrategy> Until(TK max)
=> new(Root, Strategy, false, default!, true, max);
public BTreeEnumerable<K, V, TStrategy> Until(K max)
=> new(_root, _strategy, false, default, true, max);
@ -87,19 +87,19 @@ public abstract class BaseOrderedMap<TK, TV, TStrategy> : IEnumerable<KeyValuePa
// Navigation Operations
// ---------------------------------------------------------
public bool TryGetMin(out TK key, out TV value) => BTreeFunctions.TryGetMin(Root, out key, out value);
public bool TryGetMin(out K key, out V value) => BTreeFunctions.TryGetMin(_root, out key, out value);
public bool TryGetMax(out TK key, out TV value) => BTreeFunctions.TryGetMax(Root, out key, out value);
public bool TryGetMax(out K key, out V value) => BTreeFunctions.TryGetMax(_root, out key, out value);
public bool TryGetSuccessor(TK key, out TK nextKey, out TV nextValue) => BTreeFunctions.TryGetSuccessor(Root, key, Strategy, out nextKey, out nextValue);
public bool TryGetSuccessor(K key, out K nextKey, out V nextValue) => BTreeFunctions.TryGetSuccessor(_root, key, _strategy, out nextKey, out nextValue);
public bool TryGetPredecessor(TK key, out TK prevKey, out TV prevValue) => BTreeFunctions.TryGetPredecessor(Root, key, Strategy, out prevKey, out prevValue);
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<TK, TV>> Intersect(BaseOrderedMap<TK, TV, TStrategy> other)
public IEnumerable<KeyValuePair<K, V>> Intersect(BaseOrderedMap<K, V, TStrategy> other)
{
using var enum1 = this.GetEnumerator();
using var enum2 = other.GetEnumerator();
@ -109,7 +109,7 @@ public IEnumerable<KeyValuePair<TK, TV>> Intersect(BaseOrderedMap<TK, TV, TStrat
while (has1 && has2)
{
int cmp = Strategy.Compare(enum1.Current.Key, enum2.Current.Key);
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
if (cmp == 0)
{
yield return enum1.Current;
@ -121,7 +121,7 @@ public IEnumerable<KeyValuePair<TK, TV>> Intersect(BaseOrderedMap<TK, TV, TStrat
}
}
public IEnumerable<KeyValuePair<TK, TV>> Except(BaseOrderedMap<TK, TV, TStrategy> other)
public IEnumerable<KeyValuePair<K, V>> Except(BaseOrderedMap<K, V, TStrategy> other)
{
using var enum1 = this.GetEnumerator();
using var enum2 = other.GetEnumerator();
@ -131,7 +131,7 @@ public IEnumerable<KeyValuePair<TK, TV>> Except(BaseOrderedMap<TK, TV, TStrategy
while (has1 && has2)
{
int cmp = Strategy.Compare(enum1.Current.Key, enum2.Current.Key);
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
if (cmp == 0)
{
has1 = enum1.MoveNext();
@ -155,7 +155,7 @@ public IEnumerable<KeyValuePair<TK, TV>> Except(BaseOrderedMap<TK, TV, TStrategy
}
}
public IEnumerable<KeyValuePair<TK, TV>> SymmetricExcept(BaseOrderedMap<TK, TV, TStrategy> other)
public IEnumerable<KeyValuePair<K, V>> SymmetricExcept(BaseOrderedMap<K, V, TStrategy> other)
{
using var enum1 = this.GetEnumerator();
using var enum2 = other.GetEnumerator();
@ -165,7 +165,7 @@ public IEnumerable<KeyValuePair<TK, TV>> SymmetricExcept(BaseOrderedMap<TK, TV,
while (has1 && has2)
{
int cmp = Strategy.Compare(enum1.Current.Key, enum2.Current.Key);
int cmp = _strategy.Compare(enum1.Current.Key, enum2.Current.Key);
if (cmp == 0)
{
has1 = enum1.MoveNext();

View file

@ -0,0 +1,111 @@
```
BenchmarkDotNet v0.15.8, Linux openSUSE Tumbleweed-Slowroll
AMD Ryzen 9 7900 3.02GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.100
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
Alloc Ratio=NA
```
| Method | KeyLength | CollectionSize | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|----------------------------------- |---------- |--------------- |--------------------:|------------------:|------------------:|------------:|------------:|----------:|-------------:|
| **&#39;Build: Sys.ImmutableDict&#39;** | **10** | **1000** | **80,197.68 ns** | **628.656 ns** | **557.288 ns** | **3.7842** | **0.6104** | **-** | **64072 B** |
| &#39;Build: LangExt.HashMap&#39; | 10 | 1000 | 101,031.57 ns | 842.539 ns | 703.558 ns | 16.8457 | 2.3193 | - | 282816 B |
| &#39;Build: LangExt.SortedMap&#39; | 10 | 1000 | 231,169.60 ns | 4,303.018 ns | 3,814.513 ns | 3.1738 | 0.4883 | - | 56088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 10 | 1000 | 247,805.45 ns | 4,848.561 ns | 7,106.959 ns | 138.1836 | 15.1367 | - | 2314008 B |
| &#39;Build: PersistentMap (Transient)&#39; | 10 | 1000 | 85,655.04 ns | 826.536 ns | 690.195 ns | 3.5400 | 0.3662 | - | 59624 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 10 | 1000 | 11.26 ns | 0.040 ns | 0.035 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 10 | 1000 | 132.93 ns | 1.110 ns | 0.984 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 10 | 1000 | 24.35 ns | 0.220 ns | 0.206 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 10 | 1000 | 191.15 ns | 0.908 ns | 0.758 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 10 | 1000 | 25.57 ns | 0.185 ns | 0.173 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **10** | **100000** | **26,453,597.57 ns** | **369,042.136 ns** | **345,202.243 ns** | **375.0000** | **281.2500** | **-** | **6400128 B** |
| &#39;Build: LangExt.HashMap&#39; | 10 | 100000 | 30,466,067.88 ns | 607,708.598 ns | 568,451.000 ns | 2125.0000 | 1125.0000 | 125.0000 | 33872183 B |
| &#39;Build: LangExt.SortedMap&#39; | 10 | 100000 | 57,320,977.00 ns | 294,859.779 ns | 246,221.270 ns | 333.3333 | 222.2222 | - | 5600088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 10 | 100000 | 61,269,640.88 ns | 944,908.467 ns | 883,867.966 ns | 22375.0000 | 15375.0000 | 125.0000 | 373286500 B |
| &#39;Build: PersistentMap (Transient)&#39; | 10 | 100000 | 17,053,104.34 ns | 75,075.340 ns | 66,552.333 ns | 343.7500 | 218.7500 | - | 5764192 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 10 | 100000 | 14.23 ns | 0.048 ns | 0.043 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 10 | 100000 | 317.13 ns | 1.974 ns | 1.750 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 10 | 100000 | 30.39 ns | 0.115 ns | 0.102 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 10 | 100000 | 367.82 ns | 1.219 ns | 1.080 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 10 | 100000 | 44.58 ns | 0.191 ns | 0.178 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **10** | **1000000** | **565,648,044.07 ns** | **7,804,426.380 ns** | **7,300,265.281 ns** | **3000.0000** | **2000.0000** | **-** | **64006400 B** |
| &#39;Build: LangExt.HashMap&#39; | 10 | 1000000 | 705,337,758.36 ns | 8,815,356.221 ns | 7,814,583.679 ns | 22000.0000 | 20000.0000 | - | 371442296 B |
| &#39;Build: LangExt.SortedMap&#39; | 10 | 1000000 | 1,090,945,766.47 ns | 15,976,157.606 ns | 14,944,107.744 ns | 3000.0000 | 2000.0000 | - | 56000088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 10 | 1000000 | 1,869,359,967.60 ns | 13,028,258.852 ns | 12,186,641.419 ns | 248000.0000 | 134000.0000 | 2000.0000 | 4116022328 B |
| &#39;Build: PersistentMap (Transient)&#39; | 10 | 1000000 | 327,151,058.39 ns | 6,192,298.711 ns | 6,625,690.250 ns | 3000.0000 | 2500.0000 | - | 57911880 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 10 | 1000000 | 16.76 ns | 0.329 ns | 0.323 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 10 | 1000000 | 229.91 ns | 0.557 ns | 0.521 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 10 | 1000000 | 30.30 ns | 0.095 ns | 0.089 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 10 | 1000000 | 439.20 ns | 3.921 ns | 3.668 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 10 | 1000000 | 57.93 ns | 0.262 ns | 0.219 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **100** | **1000** | **152,297.84 ns** | **1,453.902 ns** | **1,359.981 ns** | **3.6621** | **0.4883** | **-** | **64072 B** |
| &#39;Build: LangExt.HashMap&#39; | 100 | 1000 | 162,130.87 ns | 1,572.067 ns | 1,470.513 ns | 17.0898 | 2.1973 | - | 286248 B |
| &#39;Build: LangExt.SortedMap&#39; | 100 | 1000 | 227,901.82 ns | 3,599.541 ns | 3,190.900 ns | 3.1738 | 0.4883 | - | 56088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 100 | 1000 | 246,870.77 ns | 4,663.995 ns | 4,362.704 ns | 138.4277 | 15.1367 | - | 2316904 B |
| &#39;Build: PersistentMap (Transient)&#39; | 100 | 1000 | 87,058.89 ns | 995.531 ns | 831.314 ns | 3.6621 | 0.3662 | - | 62520 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 100 | 1000 | 51.85 ns | 0.344 ns | 0.287 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 100 | 1000 | 126.84 ns | 0.807 ns | 0.755 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 100 | 1000 | 59.05 ns | 0.193 ns | 0.161 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 100 | 1000 | 172.41 ns | 1.498 ns | 1.401 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 100 | 1000 | 26.71 ns | 0.094 ns | 0.088 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **100** | **100000** | **33,894,492.92 ns** | **510,741.665 ns** | **477,748.070 ns** | **333.3333** | **266.6667** | **-** | **6400184 B** |
| &#39;Build: LangExt.HashMap&#39; | 100 | 100000 | 42,503,012.72 ns | 844,933.727 ns | 1,854,650.153 ns | 2066.6667 | 1066.6667 | 66.6667 | 33877456 B |
| &#39;Build: LangExt.SortedMap&#39; | 100 | 100000 | 58,627,288.83 ns | 659,710.519 ns | 550,888.162 ns | 333.3333 | 222.2222 | - | 5600088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 100 | 100000 | 69,999,451.30 ns | 1,366,779.941 ns | 1,824,612.118 ns | 22625.0000 | 15625.0000 | 125.0000 | 377408145 B |
| &#39;Build: PersistentMap (Transient)&#39; | 100 | 100000 | 19,221,436.72 ns | 376,157.061 ns | 628,473.997 ns | 343.7500 | 281.2500 | - | 5791360 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 100 | 100000 | 55.38 ns | 0.349 ns | 0.309 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 100 | 100000 | 298.94 ns | 3.088 ns | 2.888 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 100 | 100000 | 81.90 ns | 0.693 ns | 0.649 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 100 | 100000 | 401.93 ns | 2.570 ns | 2.278 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 100 | 100000 | 49.00 ns | 0.523 ns | 0.490 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **100** | **1000000** | **679,242,633.13 ns** | **13,320,688.340 ns** | **12,460,180.143 ns** | **3000.0000** | **2000.0000** | **-** | **64006792 B** |
| &#39;Build: LangExt.HashMap&#39; | 100 | 1000000 | 905,145,222.68 ns | 16,013,156.606 ns | 19,665,596.105 ns | 22000.0000 | 20000.0000 | - | 371420160 B |
| &#39;Build: LangExt.SortedMap&#39; | 100 | 1000000 | 1,311,013,675.64 ns | 21,946,824.587 ns | 19,455,288.355 ns | 3000.0000 | 2000.0000 | - | 56000088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 100 | 1000000 | 1,781,838,551.67 ns | 21,016,398.450 ns | 19,658,752.159 ns | 249000.0000 | 134000.0000 | 1000.0000 | 4157329544 B |
| &#39;Build: PersistentMap (Transient)&#39; | 100 | 1000000 | 378,070,334.14 ns | 7,546,360.889 ns | 19,747,512.605 ns | 3000.0000 | 2000.0000 | - | 57879232 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 100 | 1000000 | 56.94 ns | 0.391 ns | 0.366 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 100 | 1000000 | 299.78 ns | 4.283 ns | 4.006 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 100 | 1000000 | 81.32 ns | 0.714 ns | 0.633 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 100 | 1000000 | 371.55 ns | 1.288 ns | 1.142 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 100 | 1000000 | 57.35 ns | 0.203 ns | 0.180 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **1000** | **1000** | **862,166.32 ns** | **6,173.897 ns** | **5,775.067 ns** | **2.9297** | **-** | **-** | **64072 B** |
| &#39;Build: LangExt.HashMap&#39; | 1000 | 1000 | 715,350.87 ns | 5,832.760 ns | 5,455.967 ns | 16.6016 | 1.9531 | - | 288824 B |
| &#39;Build: LangExt.SortedMap&#39; | 1000 | 1000 | 231,953.07 ns | 4,633.161 ns | 4,957.431 ns | 3.1738 | 0.4883 | - | 56088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 1000 | 1000 | 244,027.59 ns | 4,381.202 ns | 3,883.821 ns | 138.1836 | 14.6484 | - | 2312560 B |
| &#39;Build: PersistentMap (Transient)&#39; | 1000 | 1000 | 85,141.37 ns | 997.399 ns | 832.873 ns | 3.4180 | 0.2441 | - | 58176 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 1000 | 1000 | 463.86 ns | 2.281 ns | 1.905 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 1000 | 1000 | 141.16 ns | 1.373 ns | 1.285 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 1000 | 1000 | 482.30 ns | 2.993 ns | 2.499 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 1000 | 1000 | 151.26 ns | 0.522 ns | 0.463 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 1000 | 1000 | 25.14 ns | 0.100 ns | 0.089 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **1000** | **100000** | **110,681,036.36 ns** | **2,111,588.232 ns** | **2,073,861.990 ns** | **285.7143** | **142.8571** | **-** | **6400072 B** |
| &#39;Build: LangExt.HashMap&#39; | 1000 | 100000 | 97,557,350.10 ns | 551,991.120 ns | 516,332.837 ns | 2000.0000 | 1000.0000 | - | 33861848 B |
| &#39;Build: LangExt.SortedMap&#39; | 1000 | 100000 | 64,549,795.52 ns | 1,182,362.259 ns | 1,105,982.391 ns | 250.0000 | 125.0000 | - | 5600088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 1000 | 100000 | 67,619,915.03 ns | 537,449.664 ns | 502,730.749 ns | 22500.0000 | 15500.0000 | - | 377887656 B |
| &#39;Build: PersistentMap (Transient)&#39; | 1000 | 100000 | 18,935,946.67 ns | 148,299.648 ns | 138,719.583 ns | 343.7500 | 250.0000 | - | 5797496 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 1000 | 100000 | 468.15 ns | 4.581 ns | 4.285 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 1000 | 100000 | 314.21 ns | 0.812 ns | 0.720 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 1000 | 100000 | 485.12 ns | 3.011 ns | 2.817 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 1000 | 100000 | 312.35 ns | 1.905 ns | 1.782 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 1000 | 100000 | 45.20 ns | 0.272 ns | 0.254 ns | - | - | - | - |
| | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **1000** | **1000000** | **1,243,324,259.58 ns** | **23,435,444.565 ns** | **26,048,434.545 ns** | **3000.0000** | **2000.0000** | **-** | **64006232 B** |
| &#39;Build: LangExt.HashMap&#39; | 1000 | 1000000 | 1,622,230,008.71 ns | 12,545,963.874 ns | 11,121,670.194 ns | 22000.0000 | 20000.0000 | - | 371481912 B |
| &#39;Build: LangExt.SortedMap&#39; | 1000 | 1000000 | 1,405,640,022.27 ns | 20,672,528.624 ns | 19,337,096.110 ns | 3000.0000 | 2000.0000 | - | 56000088 B |
| &#39;Build: PersistentMap (Iterative)&#39; | 1000 | 1000000 | 1,576,250,726.60 ns | 21,132,287.760 ns | 19,767,155.091 ns | 250000.0000 | 134000.0000 | - | 4196398744 B |
| &#39;Build: PersistentMap (Transient)&#39; | 1000 | 1000000 | 399,073,585.07 ns | 7,329,442.854 ns | 6,855,965.396 ns | 3000.0000 | 2000.0000 | - | 57944840 B |
| &#39;Lookup: Sys.ImmutableDict&#39; | 1000 | 1000000 | 469.34 ns | 2.373 ns | 2.104 ns | - | - | - | - |
| &#39;Lookup: Sys.SortedDict&#39; | 1000 | 1000000 | 328.66 ns | 1.561 ns | 1.384 ns | - | - | - | - |
| &#39;Lookup: LangExt.HashMap&#39; | 1000 | 1000000 | 484.32 ns | 3.425 ns | 3.204 ns | - | - | - | - |
| &#39;Lookup: LangExt.SortedMap&#39; | 1000 | 1000000 | 400.19 ns | 2.649 ns | 2.349 ns | - | - | - | - |
| &#39;Lookup: PersistentMap&#39; | 1000 | 1000000 | 56.25 ns | 0.147 ns | 0.130 ns | - | - | - | - |

View file

@ -0,0 +1,78 @@
```
BenchmarkDotNet v0.15.8, Linux openSUSE Tumbleweed-Slowroll
AMD Ryzen 9 7900 3.02GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.100
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
```
| Method | Keysize | CollectionSize | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|----------------------------------- |----------|--------------- |--------------:|-------------:|-------------:|--------------:|--------:|---------:|--------:|----------:|------------:|
| **&#39;Build: Sys.ImmutableDict&#39;** | **10** | **1000** | **82,965.51 ns** | **1,541.386 ns** | **1,582.889 ns** | **6,986.62** | **146.96** | **3.7842** | **0.6104** | **64072 B** | **NA** |
| &#39;Build: LangExt.HashMap&#39; | 10 | 1000 | 107,355.82 ns | 1,551.663 ns | 1,451.427 ns | 9,040.55 | 148.52 | 17.2119 | 2.4414 | 289808 B | NA |
| &#39;Build: LangExt.SortedMap&#39; | 10 | 1000 | 228,660.43 ns | 2,218.079 ns | 1,852.196 ns | 19,255.74 | 243.15 | 3.1738 | 0.4883 | 56088 B | NA |
| &#39;Build: PersistentMap (Iterative)&#39; | 10 | 1000 | 244,752.37 ns | 2,794.335 ns | 2,333.396 ns | 20,610.86 | 278.75 | 138.1836 | 15.6250 | 2314008 B | NA |
| &#39;Build: PersistentMap (Transient)&#39; | 10 | 1000 | 86,370.48 ns | 1,150.505 ns | 1,076.183 ns | 7,273.35 | 113.63 | 3.5400 | 0.3662 | 59624 B | NA |
| &#39;Lookup: Sys.ImmutableDict&#39; | 10 | 1000 | 11.88 ns | 0.130 ns | 0.122 ns | 1.00 | 0.01 | - | - | - | NA |
| &#39;Lookup: Sys.SortedDict&#39; | 10 | 1000 | 129.81 ns | 1.686 ns | 1.494 ns | 10.93 | 0.16 | - | - | - | NA |
| &#39;Lookup: LangExt.HashMap&#39; | 10 | 1000 | 24.52 ns | 0.293 ns | 0.274 ns | 2.07 | 0.03 | - | - | - | NA |
| &#39;Lookup: LangExt.SortedMap&#39; | 10 | 1000 | 202.85 ns | 0.770 ns | 0.682 ns | 17.08 | 0.18 | - | - | - | NA |
| &#39;Lookup: PersistentMap&#39; | 10 | 1000 | 25.24 ns | 0.159 ns | 0.148 ns | 2.13 | 0.02 | - | - | - | NA |
| | | | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **100** | **1000** | **151,217.34 ns** | **1,687.298 ns** | **1,578.300 ns** | **2,930.42** | **31.69** | **3.6621** | **0.4883** | **64072 B** | **NA** |
| &#39;Build: LangExt.HashMap&#39; | 100 | 1000 | 167,490.07 ns | 2,525.502 ns | 2,362.356 ns | 3,245.77 | 46.06 | 17.3340 | 2.1973 | 293096 B | NA |
| &#39;Build: LangExt.SortedMap&#39; | 100 | 1000 | 228,731.62 ns | 4,424.010 ns | 4,733.641 ns | 4,432.57 | 90.96 | 3.1738 | 0.4883 | 56088 B | NA |
| &#39;Build: PersistentMap (Iterative)&#39; | 100 | 1000 | 256,683.18 ns | 2,564.193 ns | 2,141.218 ns | 4,974.24 | 44.31 | 138.1836 | 15.1367 | 2316904 B | NA |
| &#39;Build: PersistentMap (Transient)&#39; | 100 | 1000 | 87,260.94 ns | 1,667.741 ns | 1,712.648 ns | 1,691.02 | 32.92 | 3.6621 | 0.3662 | 62520 B | NA |
| &#39;Lookup: Sys.ImmutableDict&#39; | 100 | 1000 | 51.60 ns | 0.263 ns | 0.205 ns | 1.00 | 0.01 | - | - | - | NA |
| &#39;Lookup: Sys.SortedDict&#39; | 100 | 1000 | 126.22 ns | 0.747 ns | 0.662 ns | 2.45 | 0.02 | - | - | - | NA |
| &#39;Lookup: LangExt.HashMap&#39; | 100 | 1000 | 65.88 ns | 0.353 ns | 0.295 ns | 1.28 | 0.01 | - | - | - | NA |
| &#39;Lookup: LangExt.SortedMap&#39; | 100 | 1000 | 168.63 ns | 0.768 ns | 0.718 ns | 3.27 | 0.02 | - | - | - | NA |
| &#39;Lookup: PersistentMap&#39; | 100 | 1000 | 26.67 ns | 0.149 ns | 0.132 ns | 0.52 | 0.00 | - | - | - | NA |
| | | | | | | | | | | | |
| **&#39;Build: Sys.ImmutableDict&#39;** | **1000** | **1000** | **858,405.87 ns** | **3,122.202 ns** | **2,437.610 ns** | **1,837.79** | **11.46** | **2.9297** | **-** | **64072 B** | **NA** |
| &#39;Build: LangExt.HashMap&#39; | 1000 | 1000 | 712,616.81 ns | 3,284.251 ns | 2,742.498 ns | 1,525.66 | 10.25 | 16.6016 | 1.9531 | 293600 B | NA |
| &#39;Build: LangExt.SortedMap&#39; | 1000 | 1000 | 227,785.60 ns | 2,218.172 ns | 1,966.352 ns | 487.67 | 4.90 | 3.1738 | 0.4883 | 56088 B | NA |
| &#39;Build: PersistentMap (Iterative)&#39; | 1000 | 1000 | 249,242.56 ns | 4,789.243 ns | 7,313.681 ns | 533.61 | 15.71 | 138.1836 | 14.6484 | 2312560 B | NA |
| &#39;Build: PersistentMap (Transient)&#39; | 1000 | 1000 | 85,297.82 ns | 1,186.402 ns | 990.700 ns | 182.62 | 2.29 | 3.4180 | 0.2441 | 58176 B | NA |
| &#39;Lookup: Sys.ImmutableDict&#39; | 1000 | 1000 | 467.10 ns | 3.062 ns | 2.714 ns | 1.00 | 0.01 | - | - | - | NA |
| &#39;Lookup: Sys.SortedDict&#39; | 1000 | 1000 | 131.87 ns | 1.521 ns | 1.348 ns | 0.28 | 0.00 | - | - | - | NA |
| &#39;Lookup: LangExt.HashMap&#39; | 1000 | 1000 | 478.53 ns | 2.786 ns | 2.606 ns | 1.02 | 0.01 | - | - | - | NA |
| &#39;Lookup: LangExt.SortedMap&#39; | 1000 | 1000 | 151.49 ns | 1.183 ns | 1.049 ns | 0.32 | 0.00 | - | - | - | NA |
| &#39;Lookup: PersistentMap&#39; | 1000 | 1000 | 24.98 ns | 0.209 ns | 0.195 ns | 0.05 | 0.00 |
Here are some better lookup benchmarks that do not just try to lookup the same index:
| Method | CollectionSize | N | Mean | Error | StdDev | Allocated |
|-------------------------- |--------------- |----- |----------:|----------:|----------:|----------:|
| 'Cyclic: PersistentMap' | 1024 | 10 | 26.68 ns | 0.246 ns | 0.230 ns | - |
| 'Cyclic: Sys.Sorted' | 1024 | 10 | 153.12 ns | 1.591 ns | 1.410 ns | - |
| 'Cyclic: LangExt.HashMap' | 1024 | 10 | 24.80 ns | 0.140 ns | 0.131 ns | - |
| 'Cyclic: LangExt.Sorted' | 1024 | 10 | 180.45 ns | 1.695 ns | 1.415 ns | - |
| | | | | | | |
| 'Cyclic: PersistentMap' | 1024 | 100 | 27.09 ns | 0.142 ns | 0.126 ns | - |
| 'Cyclic: Sys.Sorted' | 1024 | 100 | 154.13 ns | 1.729 ns | 1.444 ns | - |
| 'Cyclic: LangExt.HashMap' | 1024 | 100 | 66.44 ns | 0.501 ns | 0.468 ns | - |
| 'Cyclic: LangExt.Sorted' | 1024 | 100 | 180.61 ns | 3.244 ns | 3.034 ns | - |
| | | | | | | |
| 'Cyclic: PersistentMap' | 1024 | 1000 | 26.84 ns | 0.131 ns | 0.110 ns | - |
| 'Cyclic: Sys.Sorted' | 1024 | 1000 | 171.48 ns | 1.828 ns | 1.710 ns | - |
| 'Cyclic: LangExt.HashMap' | 1024 | 1000 | 497.32 ns | 9.698 ns | 9.071 ns | - |
| 'Cyclic: LangExt.Sorted' | 1024 | 1000 | 180.88 ns | 2.297 ns | 1.918 ns | - |
| | | | | | | |
| 'Cyclic: PersistentMap' | 131072 | 10 | 103.80 ns | 1.740 ns | 1.628 ns | - |
| 'Cyclic: Sys.Sorted' | 131072 | 10 | 459.04 ns | 4.579 ns | 4.283 ns | - |
| 'Cyclic: LangExt.HashMap' | 131072 | 10 | 56.64 ns | 0.654 ns | 0.612 ns | - |
| 'Cyclic: LangExt.Sorted' | 131072 | 10 | 525.38 ns | 10.281 ns | 11.840 ns | - |
| | | | | | | |
| 'Cyclic: PersistentMap' | 131072 | 100 | 118.92 ns | 2.222 ns | 2.967 ns | - |
| 'Cyclic: Sys.Sorted' | 131072 | 100 | 552.77 ns | 10.983 ns | 12.648 ns | - |
| 'Cyclic: LangExt.HashMap' | 131072 | 100 | 169.08 ns | 1.478 ns | 1.234 ns | - |
| 'Cyclic: LangExt.Sorted' | 131072 | 100 | 588.64 ns | 11.473 ns | 11.782 ns | - |
| | | | | | | |
| 'Cyclic: PersistentMap' | 131072 | 1000 | 151.38 ns | 1.432 ns | 1.269 ns | - |
| 'Cyclic: Sys.Sorted' | 131072 | 1000 | 606.19 ns | 9.281 ns | 8.228 ns | - |
| 'Cyclic: LangExt.HashMap' | 131072 | 1000 | 732.79 ns | 6.556 ns | 5.812 ns | - |
| 'Cyclic: LangExt.Sorted' | 131072 | 1000 | 653.56 ns | 9.363 ns | 8.300 ns | - |

View file

@ -0,0 +1,65 @@
This uses integer keys, and thus there are no hashing penalties. This is more or less raw overhead of the data structure.
```
BenchmarkDotNet v0.15.8, Linux openSUSE Tumbleweed-Slowroll
AMD Ryzen 9 7900 3.02GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.100
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
ShortRun : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
```
| Method | N | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|------------------------------- |------- |-----------------:|-----------------:|---------------:|----------:|----------:|--------:|-----------:|
| 'Build: NiceBTree (Transient)' | 100 | 3,056.96 ns | 404.797 ns | 22.188 ns | 0.2861 | - | - | 4800 B |
| 'Build: MS Sorted (Builder)' | 100 | 3,099.28 ns | 551.017 ns | 30.203 ns | 0.2899 | 0.0038 | - | 4864 B |
| 'Build: LanguageExt Map (AVL)' | 100 | 6,376.44 ns | 2,154.744 ns | 118.109 ns | 2.2736 | 0.0229 | - | 38144 B |
| 'Build: LanguageExt HashMap' | 100 | 4,577.22 ns | 1,143.256 ns | 62.666 ns | 1.9684 | 0.0076 | - | 33024 B |
| 'Read: NiceBTree' | 100 | 1,281.41 ns | 326.069 ns | 17.873 ns | - | - | - | - |
| 'Read: MS Sorted' | 100 | 484.55 ns | 176.528 ns | 9.676 ns | - | - | - | - |
| 'Read: LanguageExt Map' | 100 | 1,284.80 ns | 770.679 ns | 42.244 ns | - | - | - | - |
| 'Read: LanguageExt HashMap' | 100 | 641.44 ns | 33.663 ns | 1.845 ns | - | - | - | - |
| 'Iterate: NiceBTree' | 100 | 136.77 ns | 14.772 ns | 0.810 ns | - | - | - | - |
| 'Iterate: MS Sorted' | 100 | 425.40 ns | 78.190 ns | 4.286 ns | - | - | - | - |
| 'Iterate: LanguageExt Map' | 100 | 291.45 ns | 113.262 ns | 6.208 ns | 0.0019 | - | - | 32 B |
| 'Iterate: LanguageExt HashMap' | 100 | 763.45 ns | 72.014 ns | 3.947 ns | 0.0648 | - | - | 1088 B |
| 'Set: NiceBTree' | 100 | 60.85 ns | 11.258 ns | 0.617 ns | 0.0678 | 0.0002 | - | 1136 B |
| 'Set: MS Sorted' | 100 | 73.15 ns | 23.712 ns | 1.300 ns | 0.0229 | - | - | 384 B |
| 'Set: LanguageExt Map' | 100 | 58.56 ns | 1.750 ns | 0.096 ns | 0.0219 | - | - | 368 B |
| 'Set: LanguageExt HashMap' | 100 | 36.32 ns | 14.171 ns | 0.777 ns | 0.0206 | - | - | 344 B |
| 'Build: NiceBTree (Transient)' | 1000 | 42,256.55 ns | 4,394.007 ns | 240.850 ns | 2.3804 | 0.1221 | - | 40176 B |
| 'Build: MS Sorted (Builder)' | 1000 | 49,147.19 ns | 3,136.972 ns | 171.948 ns | 2.8687 | 0.4272 | - | 48064 B |
| 'Build: LanguageExt Map (AVL)' | 1000 | 103,207.86 ns | 38,017.513 ns | 2,083.868 ns | 34.6680 | 3.1738 | - | 580688 B |
| 'Build: LanguageExt HashMap' | 1000 | 118,382.76 ns | 8,091.969 ns | 443.548 ns | 45.4102 | 3.2959 | - | 760096 B |
| 'Read: NiceBTree' | 1000 | 13,839.35 ns | 680.579 ns | 37.305 ns | - | - | - | - |
| 'Read: MS Sorted' | 1000 | 8,663.56 ns | 1,007.067 ns | 55.201 ns | - | - | - | - |
| 'Read: LanguageExt Map' | 1000 | 22,507.35 ns | 2,405.937 ns | 131.878 ns | - | - | - | - |
| 'Read: LanguageExt HashMap' | 1000 | 9,727.15 ns | 1,226.266 ns | 67.216 ns | - | - | - | - |
| 'Iterate: NiceBTree' | 1000 | 1,216.36 ns | 264.964 ns | 14.524 ns | - | - | - | - |
| 'Iterate: MS Sorted' | 1000 | 3,870.96 ns | 280.519 ns | 15.376 ns | - | - | - | - |
| 'Iterate: LanguageExt Map' | 1000 | 2,571.58 ns | 422.239 ns | 23.144 ns | - | - | - | 32 B |
| 'Iterate: LanguageExt HashMap' | 1000 | 12,252.69 ns | 2,654.186 ns | 145.485 ns | 1.9226 | - | - | 32320 B |
| 'Set: NiceBTree' | 1000 | 122.89 ns | 31.121 ns | 1.706 ns | 0.0677 | 0.0002 | - | 1136 B |
| 'Set: MS Sorted' | 1000 | 94.78 ns | 68.248 ns | 3.741 ns | 0.0315 | - | - | 528 B |
| 'Set: LanguageExt Map' | 1000 | 81.47 ns | 39.825 ns | 2.183 ns | 0.0305 | - | - | 512 B |
| 'Set: LanguageExt HashMap' | 1000 | 58.48 ns | 22.323 ns | 1.224 ns | 0.0368 | 0.0001 | - | 616 B |
| 'Build: NiceBTree (Transient)' | 100000 | 8,648,357.47 ns | 528,464.456 ns | 28,966.920 ns | 234.3750 | 125.0000 | - | 3952352 B |
| 'Build: MS Sorted (Builder)' | 100000 | 16,518,142.94 ns | 754,623.461 ns | 41,363.458 ns | 281.2500 | 250.0000 | - | 4800064 B |
| 'Build: LanguageExt Map (AVL)' | 100000 | 41,025,632.97 ns | 4,549,124.512 ns | 249,352.866 ns | 5333.3333 | 3333.3333 | - | 89959040 B |
| 'Build: LanguageExt HashMap' | 100000 | 21,273,736.10 ns | 1,167,411.035 ns | 63,989.738 ns | 5781.2500 | 2937.5000 | 31.2500 | 96555424 B |
| 'Read: NiceBTree' | 100000 | 5,469,322.19 ns | 351,350.121 ns | 19,258.686 ns | - | - | - | - |
| 'Read: MS Sorted' | 100000 | 8,066,906.24 ns | 709,413.540 ns | 38,885.350 ns | - | - | - | - |
| 'Read: LanguageExt Map' | 100000 | 10,104,086.04 ns | 1,534,486.048 ns | 84,110.359 ns | - | - | - | - |
| 'Read: LanguageExt HashMap' | 100000 | 1,932,000.56 ns | 247,929.366 ns | 13,589.845 ns | - | - | - | - |
| 'Iterate: NiceBTree' | 100000 | 144,689.84 ns | 57,439.701 ns | 3,148.464 ns | - | - | - | - |
| 'Iterate: MS Sorted' | 100000 | 1,087,465.02 ns | 240,535.824 ns | 13,184.580 ns | - | - | - | - |
| 'Iterate: LanguageExt Map' | 100000 | 774,299.86 ns | 55,922.321 ns | 3,065.291 ns | - | - | - | 32 B |
| 'Iterate: LanguageExt HashMap' | 100000 | 1,230,437.83 ns | 47,039.229 ns | 2,578.379 ns | 64.4531 | - | - | 1082432 B |
| 'Set: NiceBTree' | 100000 | 201.88 ns | 431.379 ns | 23.645 ns | 0.1223 | 0.0007 | - | 2048 B |
| 'Set: MS Sorted' | 100000 | 144.35 ns | 75.135 ns | 4.118 ns | 0.0458 | - | - | 768 B |
| 'Set: LanguageExt Map' | 100000 | 121.02 ns | 31.124 ns | 1.706 ns | 0.0448 | - | - | 752 B |
| 'Set: LanguageExt HashMap' | 100000 | 84.70 ns | 56.667 ns | 3.106 ns | 0.0583 | - | - | 976 B |

View file

@ -1,86 +1,83 @@
using System.Collections;
using System.Runtime.CompilerServices;
namespace PersistentOrderedMap;
namespace PersistentMap;
public struct BTreeEnumerable<TK, TV, TStrategy> : IEnumerable<KeyValuePair<TK, TV>>
where TStrategy : IKeyStrategy<TK>
public struct BTreeEnumerable<K, V, TStrategy> : IEnumerable<KeyValuePair<K, V>>
where TStrategy : IKeyStrategy<K>
{
private readonly Node<TK> _root;
private readonly Node<K> _root;
private readonly TStrategy _strategy;
private readonly TK _min, _max;
private readonly K _min, _max;
private readonly bool _hasMin, _hasMax;
public BTreeEnumerable(Node<TK> root, TStrategy strategy, bool hasMin, TK min, bool hasMax, TK max)
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<TK, TV, TStrategy> GetEnumerator()
public BTreeEnumerator<K, V, TStrategy> GetEnumerator()
{
return new BTreeEnumerator<TK, TV, TStrategy>(_root, _strategy, _hasMin, _min, _hasMax, _max);
return new BTreeEnumerator<K, V, TStrategy>(_root, _strategy, _hasMin, _min, _hasMax, _max);
}
IEnumerator<KeyValuePair<TK, TV>> IEnumerable<KeyValuePair<TK, TV>>.GetEnumerator() => GetEnumerator();
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.
// The B tree is, theoretically, not limited by the size of an int, like
// all int-indexed data structures (or bit partitioned ones).
// This should be enough for anyone.
[InlineArray(16)]
internal struct IterNodeBuffer<TK>
internal struct IterNodeBuffer<K>
{
private Node<TK> _element0;
private Node<K> _element0;
}
[InlineArray(16)]
internal struct IterIndexBuffer
internal struct IterIndexBuffer<K>
{
private int _element0;
}
public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK, TV>>
where TStrategy : IKeyStrategy<TK>
public struct BTreeEnumerator<K, V, TStrategy> : IEnumerator<KeyValuePair<K, V>>
where TStrategy : IKeyStrategy<K>
{
private readonly TStrategy _strategy;
private readonly Node<TK> _root;
private readonly Node<K> _root;
// --- BOUNDS ---
private readonly bool _hasMax;
private readonly TK _maxKey;
private readonly K _maxKey;
private readonly bool _hasMin;
private readonly TK _minKey;
private readonly K _minKey;
// --- INLINE STACK ---
private IterNodeBuffer<TK> _nodeStack;
private IterIndexBuffer _indexStack;
private IterNodeBuffer<K> _nodeStack;
private IterIndexBuffer<K> _indexStack;
private int _depth;
// --- STATE ---
private LeafNode<TK, TV>? _currentLeaf;
private LeafNode<K, V>? _currentLeaf;
private int _currentLeafIndex;
private KeyValuePair<TK, TV> _current;
private KeyValuePair<K, V> _current;
// Unified Constructor
// We use boolean flags because 'K' might be a struct where 'null' is impossible.
public BTreeEnumerator(Node<TK>? root, TStrategy strategy, bool hasMin, TK minKey, bool hasMax, TK maxKey)
public BTreeEnumerator(Node<K> root, TStrategy strategy, bool hasMin, K minKey, bool hasMax, K maxKey)
{
_root = root!;
_root = root;
_strategy = strategy;
_hasMax = hasMax;
_maxKey = maxKey;
_hasMin = hasMin;
_minKey = minKey;
_nodeStack = new IterNodeBuffer<TK>();
_indexStack = new IterIndexBuffer(); // Explicit struct init
_nodeStack = new IterNodeBuffer<K>();
_indexStack = new IterIndexBuffer<K>(); // Explicit struct init
_depth = 0;
_currentLeaf = null;
_currentLeafIndex = -1;
@ -102,7 +99,7 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
// Logic 1: Unbounded Start (Go to very first item)
private void DiveLeft()
{
Node<TK> node = _root;
Node<K> node = _root;
_depth = 0;
while (!node.IsLeaf)
@ -114,14 +111,14 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
node = internalNode.Children[0]!;
}
_currentLeaf = node.AsLeaf<TV>();
_currentLeaf = node.AsLeaf<V>();
_currentLeafIndex = -1; // Position before the first element (0)
}
// Logic 2: Bounded Start (Go to specific key)
private void Seek(TK key)
private void Seek(K key)
{
Node<TK> node = _root;
Node<K> node = _root;
_depth = 0;
long keyPrefix = _strategy.UsesPrefixes ? _strategy.GetPrefix(key) : 0;
@ -130,7 +127,7 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
while (!node.IsLeaf)
{
var internalNode = node.AsInternal();
int idx = BTreeFunctions.FindRoutingIndex(internalNode, key, keyPrefix, _strategy);
int idx = BTreeFunctions.FindRoutingIndex<K, TStrategy>(internalNode, key, keyPrefix, _strategy);
_nodeStack[_depth] = internalNode;
_indexStack[_depth] = idx;
@ -140,8 +137,8 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
}
// Find index in Leaf
_currentLeaf = node.AsLeaf<TV>();
int index = BTreeFunctions.FindIndex(_currentLeaf, key, keyPrefix, _strategy);
_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;
@ -158,14 +155,14 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
if (_hasMax)
{
// If Current Key > Max Key, we are done.
if (_strategy.Compare(_currentLeaf.Keys![_currentLeafIndex], _maxKey) > 0)
if (_strategy.Compare(_currentLeaf.Keys[_currentLeafIndex], _maxKey) > 0)
{
_currentLeaf = null; // Close iterator
return false;
}
}
_current = new KeyValuePair<TK, TV>(_currentLeaf.Keys![_currentLeafIndex], _currentLeaf.Values[_currentLeafIndex]);
_current = new KeyValuePair<K, V>(_currentLeaf.Keys[_currentLeafIndex], _currentLeaf.Values[_currentLeafIndex]);
return true;
}
@ -176,14 +173,14 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
// Check Max Bound immediately for the first item
if (_hasMax)
{
if (_strategy.Compare(_currentLeaf.Keys![0], _maxKey) > 0)
if (_strategy.Compare(_currentLeaf!.Keys[0], _maxKey) > 0)
{
_currentLeaf = null;
return false;
}
}
_current = new KeyValuePair<TK, TV>(_currentLeaf.Keys![0], _currentLeaf.Values[0]);
_current = new KeyValuePair<K, V>(_currentLeaf.Keys[0], _currentLeaf.Values[0]);
return true;
}
@ -204,7 +201,7 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
_indexStack[_depth] = nextIndex;
_depth++;
Node<TK> node = internalNode.Children[nextIndex]!;
Node<K> node = internalNode.Children[nextIndex]!;
while (!node.IsLeaf)
{
_nodeStack[_depth] = node;
@ -213,7 +210,7 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
node = node.AsInternal().Children[0]!;
}
_currentLeaf = node.AsLeaf<TV>();
_currentLeaf = node.AsLeaf<V>();
_currentLeafIndex = 0;
return true;
}
@ -221,7 +218,7 @@ public struct BTreeEnumerator<TK, TV, TStrategy> : IEnumerator<KeyValuePair<TK,
return false;
}
public KeyValuePair<TK, TV> Current => _current;
public KeyValuePair<K, V> Current => _current;
object IEnumerator.Current => _current;
public void Reset()
{

View file

@ -0,0 +1,241 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace PersistentMap;
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;
}
/// <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(IComparer<K>? comparer = null)
{
_comparer = comparer ?? Comparer<K>.Default;
}
// Tell the B-Tree to skip SIMD routing and just use LinearSearch
public bool UsesPrefixes => false;
// 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 struct UnicodeStrategy : IKeyStrategy<string>
{
[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);
}
}
public struct IntStrategy : IKeyStrategy<int>
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(int x, int y) => x.CompareTo(y);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetPrefix(int key)
{
// Pack the 32-bit int into the high 32-bits of the long.
// This preserves sorting order when scanning the long array.
// Cast to uint first to prevent sign extension confusion during the shift,
// though standard int shifting usually works fine for direct mapping.
return (long)key << 32;
}
}
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);
}
}
/// <summary>
/// Helper for SIMD accelerated prefix scanning.
/// </summary>
public static class PrefixScanner
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int FindFirstGreaterOrEqual(ReadOnlySpan<long> prefixes, long targetPrefix)
{
// Handle MinValue specifically to avoid underflow in (target - 1) logic
// If target is MinValue, any value in prefixes is >= target.
// So the first element (index 0) is the match.
// TODO: evaluate if this is needed.
//if (targetPrefix == long.MinValue)
//{
// return 0;
//}
// 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;
}
}

View file

@ -1,7 +1,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PersistentOrderedMap;
namespace PersistentMap;
[Flags]
public enum NodeFlags : byte
@ -32,18 +32,12 @@ public struct NodeHeader
}
}
[InlineArray(32)]
public struct KeyBuffer<TK>
{
private TK _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<TV>
public struct NodeBuffer<V>
{
private Node<TV>? _element0;
private Node<V>? _element0;
}
[InlineArray(32)]
@ -52,7 +46,7 @@ internal struct InternalPrefixBuffer
private long _element0;
}
public abstract class Node<TK>
public abstract class Node<K>
{
public NodeHeader Header;
@ -61,7 +55,7 @@ public abstract class Node<TK>
Header = new NodeHeader(owner, 0, flags);
}
public abstract Span<TK> GetKeys();
public abstract Span<K> GetKeys();
// Abstract access to prefixes regardless of storage backing
public abstract Span<long> AllPrefixes { get; }
@ -71,69 +65,61 @@ public abstract class Node<TK>
public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0;
public abstract Node<TK> EnsureEditable(OwnerId transactionId);
public abstract Node<K> EnsureEditable(OwnerId transactionId);
public void SetCount(int newCount) => Header.Count = (byte)newCount;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public LeafNode<TK, TV> AsLeaf<TV>()
public LeafNode<K, V> AsLeaf<V>()
{
// Zero-overhead cast. Assumes you checked IsLeaf or know logic flow.
return Unsafe.As<LeafNode<TK, TV>>(this);
return Unsafe.As<LeafNode<K, V>>(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public InternalNode<TK> AsInternal()
public InternalNode<K> AsInternal()
{
// Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow.
return Unsafe.As<InternalNode<TK>>(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PrefixInternalNode<TK> AsPrefixInternal()
{
return Unsafe.As<PrefixInternalNode<TK>>(this);
return Unsafe.As<InternalNode<K>>(this);
}
}
public sealed class LeafNode<TK, TV> : Node<TK>
public sealed class LeafNode<K, V> : Node<K>
{
public const int Capacity = 64;
public const int MergeThreshold = 8;
public TK[]? Keys;
public TV[] Values;
public K[]? Keys;
public V[] Values;
private long[]? _prefixes;
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 TK[Capacity];
Values = new TV[Capacity];
if (usePrefixes)
public LeafNode(OwnerId owner) : base(owner, NodeFlags.IsLeaf | NodeFlags.HasPrefixes)
{
Keys = new K[Capacity];
Values = new V[Capacity];
_prefixes = new long[Capacity];
}
}
// Copy Constructor for CoW
private LeafNode(LeafNode<TK, TV> original, OwnerId newOwner)
private LeafNode(LeafNode<K, V> original, OwnerId newOwner)
: base(newOwner, original.Header.Flags)
{
Keys = new TK[Capacity];
Values = new TV[Capacity];
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.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<TK> EnsureEditable(OwnerId transactionId)
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)"
@ -142,7 +128,7 @@ public sealed class LeafNode<TK, TV> : Node<TK>
// we won't copy the same fresh node twice.
if (transactionId == OwnerId.None)
{
return new LeafNode<TK, TV>(this, OwnerId.None);
return new LeafNode<K, V>(this, OwnerId.None);
}
// CASE 2: Transient Mode.
@ -153,89 +139,81 @@ public sealed class LeafNode<TK, TV> : Node<TK>
}
// CASE 3: CoW needed (Ownership mismatch).
return new LeafNode<TK, TV>(this, transactionId);
return new LeafNode<K, V>(this, transactionId);
}
public override Span<TK> GetKeys()
public override Span<K> GetKeys()
{
return Keys.AsSpan(0, Header.Count);
}
public Span<TV> GetValues()
public Span<V> GetValues()
{
return Values.AsSpan(0, Header.Count);
}
}
public class InternalNode<TK> : Node<TK>
public sealed class InternalNode<K> : Node<K>
{
public const int Capacity = 32;
public KeyBuffer<TK> Keys;
public NodeBuffer<TK> Children;
// InlineArray storage
internal InternalPrefixBuffer _prefixBuffer;
public NodeBuffer<K> Children;
public override Span<long> AllPrefixes => Span<long>.Empty;
public K[]? Keys;
public InternalNode(OwnerId owner, NodeFlags flags = NodeFlags.None)
: base(owner, flags)
public override Span<long> AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity);
public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes)
{
Keys = new K[Capacity];
// Children buffer is a struct, zero-initialized by default
}
// Fixed CoW Constructor
protected InternalNode(InternalNode<TK> original, OwnerId newOwner, NodeFlags flags)
: base(newOwner, flags)
// Copy Constructor for CoW
private InternalNode(InternalNode<K> original, OwnerId newOwner)
: base(newOwner, original.Header.Flags)
{
Header.Count = original.Header.Count;
Keys = new K[Capacity];
Array.Copy(original.Keys, Keys, original.Header.Count);
// Fast struct blit for both Keys and Children.
// No loop required for InlineArrays!
this.Keys = original.Keys;
this.Children = original.Children;
// Fast struct blit for prefixes
this._prefixBuffer = original._prefixBuffer;
var srcChildren = original.GetChildren();
for (var i = 0; i < srcChildren.Length; i++) Children[i] = srcChildren[i];
}
// The missing method needed by BTreeFunctions for routing
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<Node<TK>> GetChildren()
public override Node<K> EnsureEditable(OwnerId transactionId)
{
// An internal node always has (Count + 1) children
return MemoryMarshal.CreateSpan(ref Children[0]!, Header.Count + 1);
if (transactionId == OwnerId.None)
{
return new InternalNode<K>(this, OwnerId.None);
}
public override Span<TK> GetKeys() => MemoryMarshal.CreateSpan(ref Keys[0], Header.Count);
public override Node<TK> EnsureEditable(OwnerId transactionId)
if (Header.Owner == transactionId)
{
if (transactionId == OwnerId.None) return new InternalNode<TK>(this, OwnerId.None, Header.Flags);
if (Header.Owner == transactionId) return this;
return new InternalNode<TK>(this, transactionId, Header.Flags);
}
return this;
}
public sealed class PrefixInternalNode<TK> : InternalNode<TK>
{
internal InternalPrefixBuffer PrefixBuffer;
public override Span<long> AllPrefixes => MemoryMarshal.CreateSpan(ref PrefixBuffer[0], Capacity);
public PrefixInternalNode(OwnerId owner)
: base(owner, NodeFlags.HasPrefixes)
return new InternalNode<K>(this, transactionId);
}
public override Span<K> GetKeys()
{
return Keys.AsSpan(0, Header.Count);
}
// CoW Constructor
private PrefixInternalNode(PrefixInternalNode<TK> original, OwnerId newOwner)
: base(original, newOwner, original.Header.Flags)
// Exposes the InlineArray as a Span
public Span<Node<K>?> GetChildren()
{
// Copy the base Keys and Children, then blit the prefix buffer
this.PrefixBuffer = original.PrefixBuffer;
return MemoryMarshal.CreateSpan<Node<K>?>(ref Children[0]!, Header.Count + 1);
}
public override Node<TK> EnsureEditable(OwnerId transactionId)
public void SetChild(int index, Node<K> node)
{
if (transactionId == OwnerId.None) return new PrefixInternalNode<TK>(this, OwnerId.None);
if (Header.Owner == transactionId) return this;
return new PrefixInternalNode<TK>(this, transactionId);
Children[index] = node;
}
}

View file

@ -0,0 +1,44 @@
using System.Collections;
namespace PersistentMap;
public sealed class PersistentMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy>, IEnumerable, IEnumerable<KeyValuePair<K, V>> where TStrategy : IKeyStrategy<K>
{
internal PersistentMap(Node<K> root, TStrategy strategy, int count)
: base(root, strategy, count) { }
// ---------------------------------------------------------
// Immutable Write API (Returns new Map)
// ---------------------------------------------------------
public PersistentMap<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 PersistentMap<K, V, TStrategy>(newRoot, _strategy, countChanged ? Count + 1 : Count);
}
public static PersistentMap<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));
return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0);
}
public PersistentMap<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 PersistentMap<K, V, TStrategy>(newRoot, _strategy, Count - 1);
}
public TransientMap<K, V, TStrategy> ToTransient()
{
return new TransientMap<K, V, TStrategy>(_root, _strategy, Count);
}
}

89
PersistentMap/Readme.md Normal file
View file

@ -0,0 +1,89 @@
# PersistentMap
A high-performance, persistent (immutable) B-Tree map implementation for .NET, designed for scenarios requiring efficient snapshots and transactional updates.
## Features
* **Persistent (Immutable) by Default:** Operations on `PersistentMap` return a new instance, sharing structure with the previous version. This makes it trivial to keep historical snapshots or implement undo/redo.
* **Transient (Mutable) Phase:** Supports a `TransientMap` for high-performance batch updates. This allows you to perform multiple mutations (Set/Remove) without the overhead of allocating new path nodes for every single operation, similar to Clojure's transients or Scala's builders.
* **Optimized B-Tree:** Uses a B-Tree structure optimized for modern CPU caches and SIMD instructions (AVX2/AVX512) for key prefix scanning.
* **Custom Key Strategies:** Flexible `IKeyStrategy<K>` interface allows defining custom comparison and prefix generation logic (e.g., for strings, integers, or custom types).
## Usage
### When should I use this?
Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does not have any problems key collisions, which will slow this map down by a lot.
It is also faster for just about every key that isn't a more-than-30-char-with-few-common-prefixes string.
### Basic Persistent Operations
```csharp
using PersistentMap;
// 1. Create an empty map with a strategy (e.g., for strings)
var map0 = PersistentMap<string, string, UnicodeStrategy>.Empty(new UnicodeStrategy());
// 2. Add items (returns a new map)
var map1 = map0.Set("key1", "value1");
var map2 = map1.Set("key2", "value2");
// map0 is still empty
// map1 has "key1"
// map2 has "key1" and "key2"
// 3. Remove items
var map3 = map2.Remove("key1");
// map3 has only "key2"
```
### Efficient Batch Updates (Transients)
When you need to perform many updates at once (e.g., initial load, bulk import), use `ToTransient()` to switch to a mutable mode, and `ToPersistent()` to seal it back.
```csharp
// 1. Start with a persistent map
var initialMap = PersistentMap<int, string, IntStrategy>.Empty(new IntStrategy());
// 2. Convert to transient (mutable)
var transientMap = initialMap.ToTransient();
// 3. Perform batch mutations (in-place, fast)
for (int i = 0; i < 10000; i++)
{
transientMap.Set(i, $"Value {i}");
}
// 4. Convert back to persistent (immutable)
// This "seals" the current state. The transient map rolls its transaction ID,
// so subsequent writes to 'transientMap' won't affect 'finalMap'.
var finalMap = transientMap.ToPersistent();
```
## Key Strategies
The library uses `IKeyStrategy<K>` to handle key comparisons and optimization.
* **`UnicodeStrategy`**: Optimized for `string` keys. Uses SIMD to pack the first 8 bytes of the string into a `long` prefix for fast scanning.
* **`IntStrategy`**: Optimized for `int` keys.
You can implement `IKeyStrategy<K>` for your own types.
## Performance Notes
* **Structure Sharing:** `PersistentMap` shares unchanged nodes between versions, minimizing memory overhead.
* **Transients:** `TransientMap` uses an internal `OwnerId` (transaction ID) to track ownership. Nodes created within the same transaction are mutated in-place. `ToPersistent()` ensures that any future writes to the transient map will copy nodes instead of mutating the shared ones. This leads to very fast building times compared to using persistent updates.
* **SIMD:** The `PrefixScanner` uses AVX2/AVX512 (if available) to scan node keys efficiently.
### Key strategies
For string keys, the prefix optimization lets the library have really fast lookups. For mostly-ascii string keys, we are faster than most persistent hash maps once you pass a certain key size or collection size depending on implementation strategy. The B tree is shallow and has fewer cache misses, meaning it can be faster than either deep trees or hash maps despite doing linear searches.
## Project Structure
* `PersistentMap.cs`: The main immutable map implementation.
* `TransientMap.cs`: The mutable builder for batch operations.
* `Nodes.cs`: Internal B-Tree node definitions.
* `KeyStrategies.cs`: implementations of key comparison and prefixing.

115
PersistentMap/Readme.org Normal file
View file

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

View file

@ -0,0 +1,40 @@
using System.Collections;
namespace PersistentMap;
public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrategy> where TStrategy : IKeyStrategy<K>
{
// This is mutable, but we treat it as readonly for the ID generation logic usually.
private OwnerId _transactionId;
public TransientMap(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 PersistentMap<K, V, TStrategy> ToPersistent()
{
// 1. Create the snapshot by copying all relevant information
var snapshot = new PersistentMap<K, V, TStrategy>(_root, _strategy, Count);
// 2. Protect the snapshot from THIS TransientMap by getting a new ownerId
// so that future edits will be done by CoW
_transactionId = OwnerId.Next();
return snapshot;
}
}

View file

@ -1,852 +0,0 @@
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<TK, TV, TStrategy>(Node<TK> root, TK key, TStrategy strategy, out TV value)
where TStrategy : IKeyStrategy<TK>
{
// We always get a strategy to avoid branching already here
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
Node<TK> current = root;
while (true)
{
if (current.IsLeaf)
{
var leaf = current.AsLeaf<TV>();
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<TK> Set<TK, TV, TStrategy>(Node<TK> root, TK key, TV value, TStrategy strategy, OwnerId owner, out bool countChanged)
where TStrategy : IKeyStrategy<TK>
{
root = root.EnsureEditable(owner);
// Todo, this should really be made a tuple return value to not stress the GC
var (newNode, sep) = InsertRecursive(root, key, value, strategy, owner, out countChanged);
if (newNode != null)
{
var newRoot = strategy.UsesPrefixes
? new PrefixInternalNode<TK>(owner)
: new InternalNode<TK>(owner);
newRoot.Keys[0] = sep;
newRoot.Children[0] = root;
newRoot.Children[1] = newNode;
newRoot.SetCount(1);
if (strategy.UsesPrefixes)
{
newRoot.AllPrefixes[0] = strategy.GetPrefix(sep);
}
return newRoot;
}
return root;
}
private static (Node<TK>? newNode, TK separator ) InsertRecursive<TK, TV>(Node<TK> node, TK key, TV value, IKeyStrategy<TK> strategy, OwnerId owner, out bool added)
{
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf)
{
var leaf = node.AsLeaf<TV>();
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, default)!;
}
added = true;
if (leaf.Header.Count < LeafNode<TK, TV>.Capacity)
{
InsertIntoLeaf(leaf, index, key, value, strategy);
return (null, default)!;
}
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 (newNode, sep) = InsertRecursive(child, key, value, strategy, owner, out added);
if (newNode != null)
{
if (internalNode.Header.Count < InternalNode<TK>.Capacity - 1)
{
InsertIntoInternal(internalNode, index, sep, newNode, strategy);
return (null, default)!;
}
return SplitInternal(internalNode, index, sep, newNode, strategy, owner);
}
return (null, default)!;
}
}
public static Node<TK> Remove<TK, TV, TStrategy>(Node<TK> root, TK key, TStrategy strategy, OwnerId owner, out bool countChanged)
where TStrategy : IKeyStrategy<TK>
{
root = root.EnsureEditable(owner);
bool rebalanceNeeded = RemoveRecursive<TK, TV, 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<TK, TV, TStrategy>(Node<TK> node, TK key, TStrategy strategy, OwnerId owner, out bool removed)
where TStrategy : IKeyStrategy<TK>
{
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf)
{
var leaf = node.AsLeaf<TV>();
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<TK, TV>.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<TK, TV, TStrategy>(child, key, strategy, owner, out removed);
if (removed && childUnderflow)
{
return HandleUnderflow<TK, TV, TStrategy>(internalNode, index, strategy, owner);
}
return false;
}
}
// ---------------------------------------------------------
// Internal Helpers: Search
// ---------------------------------------------------------
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindIndex<TK,TV, TStrategy>(LeafNode<TK,TV> node, TK key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
if (typeof(TK) == typeof(int))
{
Span<TK> keys = node.GetKeys();
ref TK firstKeyRef = ref MemoryMarshal.GetReference(keys);
ref int firstIntRef = ref Unsafe.As<TK, int>(ref firstKeyRef);
ReadOnlySpan<int> intKeys = MemoryMarshal.CreateReadOnlySpan(ref firstIntRef, keys.Length);
int intKey = Unsafe.As<TK, 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<TK, TStrategy>(InternalNode<TK> node, TK key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
if (typeof(TK) == typeof(int))
{
Span<TK> keys = node.GetKeys();
ref TK firstKeyRef = ref MemoryMarshal.GetReference(keys);
ref int firstIntRef = ref Unsafe.As<TK, int>(ref firstKeyRef);
ReadOnlySpan<int> intKeys = MemoryMarshal.CreateReadOnlySpan(ref firstIntRef, keys.Length);
int intKey = Unsafe.As<TK, 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<TK, TStrategy>(int startIndex, ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int i = startIndex;
while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RefineRouting<TK, TStrategy>(int startIndex, ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int i = startIndex;
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FallbackSearchKeys<TK, TStrategy>(ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
return strategy.UseBinarySearch
? BinarySearchKeys(keys, key, strategy)
: LinearSearchKeys(keys, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FallbackRoutingKeys<TK, TStrategy>(ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
return strategy.UseBinarySearch
? BinaryRoutingKeys(keys, key, strategy)
: LinearRoutingKeys(keys, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchKeys<TK, TStrategy>(ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int i = 0;
while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearRoutingKeys<TK, TStrategy>(ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int i = 0;
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int BinarySearchKeys<TK, TStrategy>(ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int low = 0;
int high = keys.Length - 1;
ref TK keysRef = ref MemoryMarshal.GetReference(keys);
while (low <= high)
{
int mid = low + ((high - low) >> 1);
TK 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<TK, TStrategy>(ReadOnlySpan<TK> keys, TK key, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int low = 0;
int high = keys.Length - 1;
ref TK keysRef = ref MemoryMarshal.GetReference(keys);
while (low <= high)
{
int mid = low + ((high - low) >> 1);
TK 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 static void InsertIntoLeaf<TK, TV, TStrategy>(LeafNode<TK, TV> leaf, int index, TK key, TV value, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
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 (Node<TK>, TK) SplitLeaf<TK, TV, TStrategy>(LeafNode<TK, TV> left, int insertIndex, TK key, TV value, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<TK>
{
var right = new LeafNode<TK, TV>(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 (right, right.Keys![0]);
}
private static void InsertIntoInternal<TK, TStrategy>(InternalNode<TK> node, int index, TK separator, Node<TK> newChild, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
int count = node.Header.Count;
if (index < count)
{
int moveCount = count - index;
Span<TK> keysSpan = node.Keys;
keysSpan.Slice(index, moveCount).CopyTo(keysSpan.Slice(index + 1));
Span<Node<TK>> 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 (Node<TK>, TK) SplitInternal<TK, TStrategy>(InternalNode<TK> left, int insertIndex, TK separator, Node<TK> newChild, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<TK>
{
var right = strategy.UsesPrefixes
? new PrefixInternalNode<TK>(owner)
: new InternalNode<TK>(owner);
int count = left.Header.Count;
int splitPoint = count / 2;
TK upKey = left.Keys[splitPoint];
int moveCount = count - splitPoint - 1;
if (moveCount > 0)
{
Span<TK> leftKeys = left.Keys;
Span<TK> rightKeys = right.Keys;
leftKeys.Slice(splitPoint + 1, moveCount).CopyTo(rightKeys);
Span<Node<TK>> leftChildren = left.Children!;
Span<Node<TK>> 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 (right, upKey);
}
// ---------------------------------------------------------
// Removal Logic
// ---------------------------------------------------------
private static void RemoveFromLeaf<TK, TV, TStrategy>(LeafNode<TK, TV> leaf, int index, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
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<TK, TV, TStrategy>(InternalNode<TK> parent, int childIndex, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<TK>
{
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<TK, TV, TStrategy>(parent, childIndex, leftChild, rightSibling, strategy);
return false;
}
else
{
Merge<TK, TV, TStrategy>(parent, childIndex, leftChild, rightSibling, strategy);
return parent.Header.Count < LeafNode<TK, TV>.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<TK, TV, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
return false;
}
else
{
Merge<TK, TV, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
return parent.Header.Count < LeafNode<TK, TV>.MergeThreshold;
}
}
return true;
}
private static bool CanBorrow<TK>(Node<TK> node)
{
return node.Header.Count > 8 + 1;
}
private static void Merge<TK, TV, TStrategy>(InternalNode<TK> parent, int separatorIndex, Node<TK> left, Node<TK> right, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
if (left.IsLeaf)
{
var leftLeaf = left.AsLeaf<TV>();
var rightLeaf = right.AsLeaf<TV>();
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();
TK 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<TK> rightKeys = rightInternal.Keys;
Span<TK> 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<TK>> rightChildren = rightInternal.Children!;
Span<Node<TK>> 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<TK> 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<TK>> parentChildren = parent.Children!;
parentChildren.Slice(separatorIndex + 2, moveCount).CopyTo(parentChildren.Slice(separatorIndex + 1));
}
parent.SetCount(pCount - 1);
}
private static void RotateLeft<TK, TV, TStrategy>(InternalNode<TK> parent, int separatorIndex, Node<TK> left, Node<TK> right, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
if (left.IsLeaf)
{
var leftLeaf = left.AsLeaf<TV>();
var rightLeaf = right.AsLeaf<TV>();
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();
TK 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<TK>> rightChildren = rightInternal.Children!;
rightChildren.Slice(1, rCount).CopyTo(rightChildren);
if (rCount > 1)
{
Span<TK> 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<TK, TV, TStrategy>(InternalNode<TK> parent, int separatorIndex, Node<TK> left, Node<TK> right, TStrategy strategy)
where TStrategy : IKeyStrategy<TK>
{
if (left.IsLeaf)
{
var leftLeaf = left.AsLeaf<TV>();
var rightLeaf = right.AsLeaf<TV>();
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;
TK 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<TK, TV>(Node<TK> root, out TK key, out TV value)
{
var current = root;
while (!current.IsLeaf)
{
current = current.AsInternal().Children[0]!;
}
var leaf = current.AsLeaf<TV>();
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<TK, TV>(Node<TK> root, out TK key, out TV value)
{
var current = root;
while (!current.IsLeaf)
{
var internalNode = current.AsInternal();
current = internalNode.Children[internalNode.Header.Count]!;
}
var leaf = current.AsLeaf<TV>();
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<TK, TV, TStrategy>(Node<TK> root, TK key, TStrategy strategy, out TK nextKey, out TV nextValue)
where TStrategy : IKeyStrategy<TK>
{
InternalNode<TK>[] path = new InternalNode<TK>[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<TV>();
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<TV>();
nextKey = targetLeaf.Keys![0];
nextValue = targetLeaf.Values[0];
return true;
}
}
nextKey = default!;
nextValue = default!;
return false;
}
public static bool TryGetPredecessor<TK, TV, TStrategy>(Node<TK> root, TK key, TStrategy strategy, out TK prevKey, out TV prevValue)
where TStrategy : IKeyStrategy<TK>
{
InternalNode<TK>[] path = new InternalNode<TK>[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<TV>();
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<TV>();
int last = targetLeaf.Header.Count - 1;
prevKey = targetLeaf.Keys![last];
prevValue = targetLeaf.Values[last];
return true;
}
}
prevKey = default!;
prevValue = default!;
return false;
}
}
}

View file

@ -1,63 +0,0 @@
namespace PersistentOrderedMap;
using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
public interface IKeyStrategy<in TK>
{
int Compare(TK x, TK y);
long GetPrefix(TK 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);
}
}

View file

@ -1,17 +0,0 @@
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<TK> : IKeyStrategy<TK> where TK : IComparable<TK>
{
public bool UsesPrefixes => false;
public bool UseBinarySearch => true;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetPrefix(TK key) => 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(TK x, TK y) => x.CompareTo(y);
}

View file

@ -1,34 +0,0 @@
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);
}
}

View file

@ -1,168 +0,0 @@
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;
}
}

View file

@ -1,16 +0,0 @@
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
}

View file

@ -1,101 +0,0 @@
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;
}
}

View file

@ -1,53 +0,0 @@
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<TK> : IKeyStrategy<TK>
{
private readonly IComparer<TK> _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<TK>.Default;
}
public StandardStrategy(IComparer<TK>? comparer)
{
_comparer = comparer ?? Comparer<TK>.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(TK key) => 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(TK x, TK y)
{
return _comparer.Compare(x, y);
}
}
public readonly struct StandardStrategy2<TK, TComparer> : IKeyStrategy<TK>
where TComparer : struct, IComparer<TK>
{
private readonly TComparer _comparer;
public StandardStrategy2(TComparer comparer) => _comparer = comparer;
public bool UsesPrefixes => false;
public bool UseBinarySearch => true;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(TK x, TK y) => _comparer.Compare(x, y);
public long GetPrefix(TK key) => 0;
}

View file

@ -1,43 +0,0 @@
namespace PersistentOrderedMap;
public sealed class PersistentOrderedMap<TK, TV, TStrategy> : BaseOrderedMap<TK, TV, TStrategy> where TStrategy : IKeyStrategy<TK>
{
internal PersistentOrderedMap(Node<TK> root, TStrategy strategy, int count)
: base(root, strategy, count) { }
// ---------------------------------------------------------
// Immutable Write API (Returns new Map)
// ---------------------------------------------------------
public PersistentOrderedMap<TK, TV, TStrategy> Set(TK key, TV 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<TK, TV, TStrategy>(newRoot, Strategy, countChanged ? Count + 1 : Count);
}
public static PersistentOrderedMap<TK, TV, 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<TK, TV>(default(OwnerId), strategy.UsesPrefixes);
return new PersistentOrderedMap<TK, TV, TStrategy>(emptyRoot, strategy, 0);
}
public PersistentOrderedMap<TK, TV, TStrategy> Remove(TK key)
{
var newRoot = BTreeFunctions.Remove<TK,TV, TStrategy>(Root, key, Strategy, OwnerId.None, out bool removed);
if (!removed) return this;
return new PersistentOrderedMap<TK, TV, TStrategy>(newRoot, Strategy, Count - 1);
}
public TransientOrderedMap<TK, TV, TStrategy> ToTransient()
{
return new TransientOrderedMap<TK, TV, TStrategy>(Root, Strategy, Count);
}
}

View file

@ -1,623 +0,0 @@
* 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.

View file

@ -1,39 +0,0 @@
namespace PersistentOrderedMap;
public sealed class TransientOrderedMap<TK, TV, TStrategy> : BaseOrderedMap<TK, TV, TStrategy> where TStrategy : IKeyStrategy<TK>
{
// This is mutable, but we treat it as readonly for the ID generation logic usually.
private OwnerId _transactionId;
public TransientOrderedMap(Node<TK> root, TStrategy strategy, int count)
: base(root, strategy, count)
{
_transactionId = OwnerId.Next();
}
public void Set(TK key, TV value)
{
Root = BTreeFunctions.Set(Root, key, value, Strategy, _transactionId, out bool countChanged);
if (countChanged) Count++;
}
public void Remove(TK key)
{
Root = BTreeFunctions.Remove<TK,TV, TStrategy>(Root, key, Strategy, _transactionId, out bool removed);
if (removed) Count--;
}
public PersistentOrderedMap<TK, TV, TStrategy> ToPersistent()
{
// 1. Create the snapshot by copying all relevant information
var snapshot = new PersistentOrderedMap<TK, TV, 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;
}
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
using PersistentOrderedMap;
using PersistentMap;
public class BTreeFuzzTests
{
@ -19,25 +19,25 @@ public class BTreeFuzzTests
public void Fuzz_Insert_And_Remove_consistency()
{
// CONFIGURATION
const int iterations = 100_000; // High enough to trigger all splits/merges
const int keyRange = 5000; // Small enough to cause frequent collisions
const int Iterations = 100_000; // High enough to trigger all splits/merges
const int KeyRange = 5000; // Small enough to cause frequent collisions
const bool showOps = false;
int seed = 2135974; // Environment.TickCount;
int Seed = 2135974; // Environment.TickCount;
// ORACLES
var reference = new SortedDictionary<int, int>();
var subject = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
var random = new Random(seed);
_output.WriteLine($"Starting Fuzz Test with Seed: {seed}");
var random = new Random(Seed);
_output.WriteLine($"Starting Fuzz Test with Seed: {Seed}");
try
{
for (int i = 0; i < iterations; i++)
for (int i = 0; i < Iterations; i++)
{
// 1. Pick an Action: 70% Insert/Update, 30% Remove
bool isInsert = random.NextDouble() < 0.7;
int key = random.Next(keyRange);
int key = random.Next(KeyRange);
int val = key * 100;
if (isInsert)
@ -82,7 +82,7 @@ public class BTreeFuzzTests
}
catch (Exception)
{
_output.WriteLine($"FAILED at iteration with SEED: {seed}");
_output.WriteLine($"FAILED at iteration with SEED: {Seed}");
throw; // Re-throw to fail the test
}
}
@ -91,28 +91,28 @@ public class BTreeFuzzTests
public void Fuzz_Range_Queries()
{
// Validates that your Range Enumerator matches LINQ on the reference
const int iterations = 1000;
const int keyRange = 2000;
int seed = Environment.TickCount;
const int Iterations = 1000;
const int KeyRange = 2000;
int Seed = Environment.TickCount;
var reference = new SortedDictionary<int, int>();
var subject = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
var random = new Random(seed);
var random = new Random(Seed);
// Fill Data
for(int i=0; i<keyRange; i++)
for(int i=0; i<KeyRange; i++)
{
int k = random.Next(keyRange);
int k = random.Next(KeyRange);
reference[k] = k;
subject.Set(k, k);
}
var persistent = subject.ToPersistent();
for (int i = 0; i < iterations; i++)
for (int i = 0; i < Iterations; i++)
{
int min = random.Next(keyRange);
int max = min + random.Next(keyRange - min); // Ensure max >= min
int min = random.Next(KeyRange);
int max = min + random.Next(KeyRange - min); // Ensure max >= min
// 1. Reference Result (LINQ)
// Note: SortedDictionary doesn't have a direct Range query, so we filter memory.
@ -137,7 +137,7 @@ public class BTreeFuzzTests
}
}
private void AssertConsistency(SortedDictionary<int, int> expected, TransientOrderedMap<int, int, IntStrategy> actual)
private void AssertConsistency(SortedDictionary<int, int> expected, TransientMap<int, int, IntStrategy> actual)
{
// 1. Count
if (expected.Count != actual.Count)

View file

@ -1,170 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
using PersistentOrderedMap;
public class BTreeFuzzTestStandardStrategy
{
private readonly ITestOutputHelper _output;
private readonly StandardStrategy<int> _strategy = new();
public BTreeFuzzTestStandardStrategy(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void Fuzz_Insert_And_Remove_consistency()
{
// CONFIGURATION
const int iterations = 100_000; // High enough to trigger all splits/merges
const int keyRange = 5000; // Small enough to cause frequent collisions
const bool showOps = false;
int seed = 2135974; // Environment.TickCount;
// ORACLES
var reference = new SortedDictionary<int, int>();
var subject = BaseOrderedMap<int, int, StandardStrategy<int>>.CreateTransient(_strategy);
var random = new Random(seed);
_output.WriteLine($"Starting Fuzz Test with Seed: {seed}");
try
{
for (int i = 0; i < iterations; i++)
{
// 1. Pick an Action: 70% Insert/Update, 30% Remove
bool isInsert = random.NextDouble() < 0.7;
int key = random.Next(keyRange);
int val = key * 100;
if (isInsert)
{
// ACTION: INSERT
if (showOps)Console.WriteLine($"insert: {key} : {val}");
if (key == 4436)
{
Console.WriteLine("BP");
}
reference[key] = val;
subject.Set(key, val);
}
else
{
// ACTION: REMOVE
if (reference.ContainsKey(key))
{
if (showOps)Console.WriteLine($"remove ${key}");
reference.Remove(key);
subject.Remove(key);
}
else
{
// Try removing non-existent key (should be safe)
subject.Remove(key);
}
}
// 2. VERIFY CONSISTENCY (Expensive but necessary)
// We check consistency every 1000 ops or if the tree is small,
// to keep the test fast enough.
//if (i % 1000 == 0 || reference.Count < 100)
//{
AssertConsistency(reference, subject);
//}
}
// Final check
AssertConsistency(reference, subject);
}
catch (Exception)
{
_output.WriteLine($"FAILED at iteration with SEED: {seed}");
throw; // Re-throw to fail the test
}
}
[Fact]
public void Fuzz_Range_Queries()
{
// Validates that your Range Enumerator matches LINQ on the reference
const int iterations = 1000;
const int keyRange = 2000;
int seed = Environment.TickCount;
var reference = new SortedDictionary<int, int>();
var subject = BaseOrderedMap<int, int, StandardStrategy<int>>.CreateTransient(_strategy);
var random = new Random(seed);
// Fill Data
for(int i=0; i<keyRange; i++)
{
int k = random.Next(keyRange);
reference[k] = k;
subject.Set(k, k);
}
var persistent = subject.ToPersistent();
for (int i = 0; i < iterations; i++)
{
int min = random.Next(keyRange);
int max = min + random.Next(keyRange - min); // Ensure max >= min
// 1. Reference Result (LINQ)
// Note: SortedDictionary doesn't have a direct Range query, so we filter memory.
var expected = reference
.Where(kv => kv.Key >= min && kv.Key <= max)
.Select(kv => kv.Key)
.ToList();
// 2. Subject Result
var actual = persistent.Range(min, max)
.Select(kv => kv.Key)
.ToList();
// 3. Compare
if (!expected.SequenceEqual(actual))
{
_output.WriteLine($"Range Mismatch! Range: [{min}, {max}]");
_output.WriteLine($"Expected: {string.Join(",", expected)}");
_output.WriteLine($"Actual: {string.Join(",", actual)}");
Assert.Fail("Range query results differ.");
}
}
}
private void AssertConsistency(SortedDictionary<int, int> expected, TransientOrderedMap<int, int, StandardStrategy<int>> actual)
{
// 1. Count
if (expected.Count != actual.Count)
{
Console.WriteLine("BP");
throw new Exception($"Count Mismatch! Expected {expected.Count}, Got {actual.Count}");
}
// 2. Full Scan Verification
using var enumerator = actual.GetEnumerator();
foreach (var kvp in expected)
{
if (!enumerator.MoveNext())
{
throw new Exception("Enumerator ended too early!");
}
if (enumerator.Current.Key != kvp.Key || enumerator.Current.Value != kvp.Value)
{
Console.WriteLine("BP");
throw new Exception($"Content Mismatch! Expected [{kvp.Key}:{kvp.Value}], Got [{enumerator.Current.Key}:{enumerator.Current.Value}]");
}
}
if (enumerator.MoveNext())
{
throw new Exception("Enumerator has extra items!");
}
}
}

View file

@ -1,5 +1,5 @@
using Xunit;
using PersistentOrderedMap;
using PersistentMap;
using System.Linq;
using System.Collections.Generic;
@ -8,8 +8,8 @@ public class EnumeratorTests
// Use IntStrategy for simple numeric testing
private readonly IntStrategy _strategy = new();
// Helper to create a populated PersistentOrderedMap quickly
private PersistentOrderedMap<int, int, IntStrategy> CreateMap(params int[] keys)
// Helper to create a populated PersistentMap quickly
private PersistentMap<int, int, IntStrategy> CreateMap(params int[] keys)
{
var map = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
foreach (var k in keys)
@ -22,7 +22,7 @@ public class EnumeratorTests
[Fact]
public void EmptyMap_EnumeratesNothing()
{
var map = PersistentOrderedMap<int, int, IntStrategy>.Empty(_strategy);
var map = PersistentMap<int, int, IntStrategy>.Empty(_strategy);
var list = new List<int>();
foreach(var kv in map) list.Add(kv.Key);

View file

@ -1,13 +1,13 @@
using System.Linq;
using Xunit;
using PersistentOrderedMap;
using PersistentMap;
namespace PersistentMap.Tests
{
public class BTreeExtendedOperationsTests
{
// Helper to quickly spin up a populated map
private TransientOrderedMap<int, string, IntStrategy> CreateMap(params int[] keys)
private TransientMap<int, string, IntStrategy> CreateMap(params int[] keys)
{
var map = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
foreach (var key in keys)

View file

@ -1,5 +1,5 @@
namespace TestProject1;
using PersistentOrderedMap;
using PersistentMap;
public class PersistenceTests
{
private readonly UnicodeStrategy _strategy = new UnicodeStrategy();

View file

@ -1,38 +0,0 @@
namespace PersistentMap.Tests;
using PersistentOrderedMap;
using System.Linq;
using Xunit;
public class StandardStrategy
{
private static string GenerateRandomString(int length, Random rnd)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length).Select(s => s[rnd.Next(s.Length)]).ToArray());
}
[Fact]
public void Setup()
{
var n = 1000;
var stdStrategy = new StandardStrategy<string>();
var uniStrategy = new UnicodeStrategy();
var rnd = new Random(42);
var stringLength = 10;
// Build random strings
var allKeys = Enumerable.Range(0, n).Select(_ => GenerateRandomString(stringLength, rnd)).Distinct().ToArray();
// Regenerate if Distinct() reduced array size (highly unlikely with length 8/50, but safe)
while (allKeys.Length < n)
{
allKeys = allKeys.Concat(new[] { GenerateRandomString(stringLength, rnd) }).Distinct().ToArray();
}
var transStd = BaseOrderedMap<string, int, StandardStrategy<string>>.CreateTransient(stdStrategy);
var transUni = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(uniStrategy);
for (int i = 0; i < allKeys.Length; i++)
{
transStd.Set(allKeys[i], i);
transUni.Set(allKeys[i], i);
}
}
}

View file

@ -1,6 +1,6 @@
namespace TestProject1;
using PersistentOrderedMap;
using PersistentMap;
public class StressTests
{

View file

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PersistentOrderedMap\PersistentOrderedMap.csproj" />
<ProjectReference Include="..\PersistentMap\PersistentMap.csproj" />
</ItemGroup>
</Project>

View file

@ -1,7 +1,7 @@
namespace TestProject1;
using Xunit;
using PersistentOrderedMap;
using PersistentMap;
public class BasicTests
{

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using PersistentMap;
// Ensure your PersistentMap namespace is included here
// using MyProject.Collections;
[MemoryDiagnoser]
public class ImmutableBenchmark
{
[Params(10, 100)]
public int N { get; set; }
[Params(10000)]
public int CollectionSize { get; set; }
private ImmutableDictionary<string, string> _immutableDict;
private ImmutableSortedDictionary<string, string> _immutableSortedDict;
// 1. Add field for your map
private PersistentMap<string, string, UnicodeStrategy> _persistentMap;
private string[] _searchKeys;
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
var data = new Dictionary<string, string>();
while (data.Count < CollectionSize)
{
string key = GenerateRandomString(random, N);
if (!data.ContainsKey(key))
{
data[key] = "value";
}
}
_immutableDict = data.ToImmutableDictionary();
_immutableSortedDict = data.ToImmutableSortedDictionary();
// 2. Initialize your map.
// ASSUMPTION: Standard immutable pattern (Add returns new instance).
// Adjust if you have a bulk loader like .ToPersistentMap() or a constructor.
_persistentMap = PersistentMap<string, string, UnicodeStrategy>.Empty(new UnicodeStrategy());
foreach (var kvp in data)
{
_persistentMap = _persistentMap.Set(kvp.Key, kvp.Value);
}
_searchKeys = data.Keys.ToArray();
}
[Benchmark(Baseline = true)]
public string ImmutableDict_Lookup()
{
var key = _searchKeys[CollectionSize / 2];
_immutableDict.TryGetValue(key, out var value);
return value;
}
[Benchmark]
public string ImmutableSortedDict_Lookup()
{
var key = _searchKeys[CollectionSize / 2];
_immutableSortedDict.TryGetValue(key, out var value);
return value;
}
// 3. Add the benchmark case
[Benchmark]
public string PersistentMap_Lookup()
{
var key = _searchKeys[CollectionSize / 2];
// Adjust API call if your map uses a different method (e.g. Find, Get, indexer)
_persistentMap.TryGetValue(key, out var value);
return value;
}
private string GenerateRandomString(Random rng, int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var buffer = new char[length];
for (int i = 0; i < length; i++)
{
buffer[i] = chars[rng.Next(chars.Length)];
}
return new string(buffer);
}
}

View file

@ -0,0 +1,198 @@
using System.Collections.Immutable;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using LanguageExt;
using PersistentMap;
namespace AgainstLanguageExt;
// Mocking your library types for the sake of the example to ensure compilation.
// Replace these with your actual namespace imports.
[MemoryDiagnoser]
[HideColumns("Ratio", "RatioSD", "Alloc Ratio")]
public class MapBenchmarks
{
[Params(10, 100, 1000)]
public int KeyLength { get; set; } // Key length
[Params(1000, 100000, 1000000)]
public int CollectionSize { get; set; }
// Data Source
private KeyValuePair<string, int>[] _data;
private string[] _searchKeys;
private int _index = 0;
private int _mask;
// Comparison Targets (for Lookup)
private ImmutableDictionary<string, int> _sysDict;
private ImmutableSortedDictionary<string, int> _sysSorted;
private LanguageExt.HashMap<string, int> _langExtHash;
private LanguageExt.Map<string, int> _langExtSorted; // Map<K,V> is Sorted in LangExt
private PersistentMap<string, int, UnicodeStrategy> _persistentMap;
[GlobalSetup]
public void Setup()
{
var random = new Random(42);
var dict = new Dictionary<string, int>();
// 1. Generate Data
while (dict.Count < CollectionSize)
{
string key = GenerateRandomString(random, KeyLength);
if (!dict.ContainsKey(key))
{
dict[key] = random.Next();
}
}
_data = dict.ToArray();
_searchKeys = dict.Keys.ToArray();
// 2. Pre-build maps for the Lookup benchmarks
_sysDict = dict.ToImmutableDictionary();
_sysSorted = dict.ToImmutableSortedDictionary();
_langExtHash = Prelude.toHashMap(dict);
_langExtSorted = Prelude.toMap(dict);
// Build PersistentMap for lookup test
var trans = PersistentMap<string, int, UnicodeStrategy>.Empty(new UnicodeStrategy()).ToTransient();
foreach (var kvp in _data)
{
trans.Set(kvp.Key, kvp.Value);
}
_persistentMap = trans.ToPersistent();
}
// ==========================================
// BUILD BENCHMARKS
// ==========================================
[Benchmark(Description = "Lookup: PersistentMap (Cyclic)")]
public int Lookup_Persistent_Cyclic()
{
// Fast wrap-around
var key = _searchKeys[_index++ & _mask];
_persistentMap.TryGetValue(key, out var value);
return value;
}
[Benchmark(Description = "Lookup: Sys.Sorted (Cyclic)")]
public int Lookup_SysSorted_Cyclic()
{
var key = _searchKeys[_index++ & _mask];
_sysSorted.TryGetValue(key, out var value);
return value;
}
[Benchmark(Description = "Build: Sys.ImmutableDict")]
public ImmutableDictionary<string, int> Build_SysImmutable()
{
// Using CreateRange/ToImmutable is usually the standard 'bulk' build
return _data.ToImmutableDictionary();
}
[Benchmark(Description = "Build: LangExt.HashMap")]
public LanguageExt.HashMap<string, int> Build_LangExtHash()
{
return Prelude.toHashMap(_data);
}
[Benchmark(Description = "Build: LangExt.SortedMap")]
public LanguageExt.Map<string, int> Build_LangExtSorted()
{
return Prelude.toMap(_data);
}
[Benchmark(Description = "Build: PersistentMap (Iterative)")]
public PersistentMap<string, int, UnicodeStrategy> Build_Persistent_Iterative()
{
// Simulating naive immutable building (O(n log n) or worse due to copying)
var map = PersistentMap<string, int, UnicodeStrategy>.Empty(new UnicodeStrategy());
foreach (var item in _data)
{
map = map.Set(item.Key, item.Value);
}
return map;
}
[Benchmark(Description = "Build: PersistentMap (Transient)")]
public PersistentMap<string, int, UnicodeStrategy> Build_Persistent_Transient()
{
// Simulating efficient mutable build -> freeze
var trans = PersistentMap<string, int, UnicodeStrategy>.Empty(new UnicodeStrategy()).ToTransient();
foreach (var item in _data)
{
trans.Set(item.Key, item.Value);
}
return trans.ToPersistent();
}
// ==========================================
// LOOKUP BENCHMARKS
// ==========================================
[Benchmark(Baseline = true, Description = "Lookup: Sys.ImmutableDict")]
public int Lookup_SysImmutable()
{
var key = _searchKeys[CollectionSize / 2];
_sysDict.TryGetValue(key, out var value);
return value;
}
[Benchmark(Description = "Lookup: Sys.SortedDict")]
public int Lookup_SysSorted()
{
var key = _searchKeys[CollectionSize / 2];
_sysSorted.TryGetValue(key, out var value);
return value;
}
[Benchmark(Description = "Lookup: LangExt.HashMap")]
public int Lookup_LangExtHash()
{
var key = _searchKeys[CollectionSize / 2];
// LanguageExt often uses Find which returns Option, or [] operator
// Assuming TryGetValue-like behavior or using match for fairness
return _langExtHash.Find(key).IfNone(0);
}
[Benchmark(Description = "Lookup: LangExt.SortedMap")]
public int Lookup_LangExtSorted()
{
var key = _searchKeys[CollectionSize / 2];
return _langExtSorted.Find(key).IfNone(0);
}
[Benchmark(Description = "Lookup: PersistentMap")]
public int Lookup_PersistentMap()
{
var key = _searchKeys[CollectionSize / 2];
_persistentMap.TryGetValue(key, out var value);
return value;
}
// Helper
private string GenerateRandomString(Random rng, int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var buffer = new char[length];
for (int i = 0; i < length; i++)
{
buffer[i] = chars[rng.Next(chars.Length)];
}
return new string(buffer);
}
}
public class Program
{
public static void Main(string[] args)
{
// This scans the assembly and lets the command line (args) decide what to run
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}

View file

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using LanguageExt;
using static LanguageExt.Prelude;
using PersistentMap;
[MemoryDiagnoser]
[HideColumns("Ratio", "RatioSD", "Alloc Ratio")]
public class CyclicMapBenchmarks
{
// Powers of 2 for fast bitwise masking
[Params(1024, 131072)]
public int CollectionSize { get; set; }
[Params(10, 100, 1000)]
public int N { get; set; }
// Collections
private PersistentMap<string, int, UnicodeStrategy> _persistentMap;
private ImmutableSortedDictionary<string, int> _sysSorted;
private LanguageExt.HashMap<string, int> _langExtHash;
private LanguageExt.Map<string, int> _langExtSorted;
// Lookup scaffolding
private string[] _searchKeys;
private int _index = 0;
private int _mask;
[GlobalSetup]
public void Setup()
{
_mask = CollectionSize - 1;
var random = new Random(42);
var data = new Dictionary<string, int>();
// 1. Generate Data
while (data.Count < CollectionSize)
{
string key = GenerateRandomString(random, N);
if (!data.ContainsKey(key))
{
data[key] = random.Next();
}
}
// 2. Build Collections
// PersistentMap
var builder = PersistentMap<string, int, UnicodeStrategy>.Empty(new UnicodeStrategy()).ToTransient();
foreach (var kvp in data) builder.Set(kvp.Key, kvp.Value);
_persistentMap = builder.ToPersistent();
// System
_sysSorted = data.ToImmutableSortedDictionary();
// LanguageExt
_langExtHash = toHashMap(data);
_langExtSorted = toMap(data);
// 3. Setup Cyclic Keys
_searchKeys = data.Keys.ToArray();
// Shuffle to defeat branch prediction / cache pre-fetching
Random.Shared.Shuffle(_searchKeys);
}
[Benchmark(Description = "Cyclic: PersistentMap")]
public int Lookup_Persistent()
{
var key = _searchKeys[_index++ & _mask];
_persistentMap.TryGetValue(key, out var value);
return value;
}
[Benchmark(Description = "Cyclic: Sys.Sorted")]
public int Lookup_SysSorted()
{
var key = _searchKeys[_index++ & _mask];
_sysSorted.TryGetValue(key, out var value);
return value;
}
[Benchmark(Description = "Cyclic: LangExt.HashMap")]
public int Lookup_LangExtHash()
{
var key = _searchKeys[_index++ & _mask];
// Option<T> struct return, overhead is minimal but present
return _langExtHash.Find(key).IfNone(0);
}
[Benchmark(Description = "Cyclic: LangExt.Sorted")]
public int Lookup_LangExtSorted()
{
var key = _searchKeys[_index++ & _mask];
// AVL Tree traversal
return _langExtSorted.Find(key).IfNone(0);
}
private string GenerateRandomString(Random rng, int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var buffer = new char[length];
for (int i = 0; i < length; i++)
{
buffer[i] = chars[rng.Next(chars.Length)];
}
return new string(buffer);
}
}

View file

@ -0,0 +1,219 @@
using System;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using LanguageExt; // Your Namespace
using LanguageExt;
using LanguageExt;
using PersistentMap; // NuGet: LanguageExt.Core
[MemoryDiagnoser]
public class ImmutableCollectionBenchmarks
{
[Params(100, 1000, 100_000)]
public int N;
private int[] _keys;
private int[] _values;
// --- 1. Your Collections ---
private PersistentMap<int, int, IntStrategy> _niceMap;
private IntStrategy _strategy;
// --- 2. Microsoft Collections ---
private System.Collections.Immutable.ImmutableSortedDictionary<int, int> _msSortedMap;
private System.Collections.Immutable.ImmutableDictionary<int, int> _msHashMap;
// --- 3. LanguageExt Collections ---
private LanguageExt.Map<int, int> _leMap; // AVL Tree (Sorted)
private LanguageExt.HashMap<int, int> _leHashMap; // Hash Array Mapped Trie (Unsorted)
[GlobalSetup]
public void Setup()
{
// Generate Data
var rand = new Random(42);
_keys = Enumerable.Range(0, N).Select(x => x * 2).ToArray();
rand.Shuffle(_keys);
_values = _keys.Select(k => k * 100).ToArray();
_strategy = new IntStrategy();
// 1. Setup NiceBTree
var transient = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
for (int i = 0; i < N; i++) transient.Set(_keys[i], _values[i]);
_niceMap = transient.ToPersistent();
// 2. Setup MS Immutable
var msBuilder = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder<int, int>();
for (int i = 0; i < N; i++) msBuilder.Add(_keys[i], _values[i]);
_msSortedMap = msBuilder.ToImmutable();
_msHashMap = System.Collections.Immutable.ImmutableDictionary.CreateRange(
_keys.Zip(_values, (k, v) => new System.Collections.Generic.KeyValuePair<int, int>(k, v)));
// 3. Setup LanguageExt
// Note: LanguageExt performs best when bulk-loaded from tuples
var tuples = _keys.Zip(_values, (k, v) => (k, v));
_leMap = new LanguageExt.Map<int,int>(tuples);
_leHashMap = new HashMap<int, int>(tuples);
}
// =========================================================
// 1. BUILD (Item by Item)
// Note: LanguageExt has no "Mutable Builder", so this tests
// the cost of pure immutable inserts vs your Transient/Builder.
// =========================================================
[Benchmark(Description = "Build: NiceBTree (Transient)")]
public int Build_NiceBTree()
{
var t = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_strategy);
for (int i = 0; i < N; i++) t.Set(_keys[i], _values[i]);
return t.Count;
}
[Benchmark(Description = "Build: MS Sorted (Builder)")]
public int Build_MsSorted()
{
var b = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder<int, int>();
for (int i = 0; i < N; i++) b.Add(_keys[i], _values[i]);
return b.Count;
}
[Benchmark(Description = "Build: LanguageExt Map (AVL)")]
public int Build_LanguageExt_Map()
{
var map = LanguageExt.Map<int, int>.Empty;
for (int i = 0; i < N; i++)
{
// Pure immutable add
map = map.Add(_keys[i], _values[i]);
}
return map.Count;
}
[Benchmark(Description = "Build: LanguageExt HashMap")]
public int Build_LanguageExt_HashMap()
{
var map = LanguageExt.HashMap<int, int>.Empty;
for (int i = 0; i < N; i++)
{
map = map.Add(_keys[i], _values[i]);
}
return map.Count;
}
// =========================================================
// 2. READ (Lookup)
// =========================================================
[Benchmark(Description = "Read: NiceBTree")]
public int Read_NiceBTree()
{
int found = 0;
for (int i = 0; i < N; i++)
{
if (_niceMap.TryGetValue(_keys[i], out _)) found++;
}
return found;
}
[Benchmark(Description = "Read: MS Sorted")]
public int Read_MsSorted()
{
int found = 0;
for (int i = 0; i < N; i++)
{
if (_msSortedMap.ContainsKey(_keys[i])) found++;
}
return found;
}
[Benchmark(Description = "Read: LanguageExt Map")]
public int Read_LanguageExt_Map()
{
int found = 0;
for (int i = 0; i < N; i++)
{
// Find returns Option<V>, IsSome checks if it exists
if (_leMap.Find(_keys[i]).IsSome) found++;
}
return found;
}
[Benchmark(Description = "Read: LanguageExt HashMap")]
public int Read_LanguageExt_HashMap()
{
int found = 0;
for (int i = 0; i < N; i++)
{
if (_leHashMap.Find(_keys[i]).IsSome) found++;
}
return found;
}
// =========================================================
// 3. ITERATE (Foreach)
// =========================================================
[Benchmark(Description = "Iterate: NiceBTree")]
public int Iterate_NiceBTree()
{
int sum = 0;
foreach (var kvp in _niceMap) sum += kvp.Key;
return sum;
}
[Benchmark(Description = "Iterate: MS Sorted")]
public int Iterate_MsSorted()
{
int sum = 0;
foreach (var kvp in _msSortedMap) sum += kvp.Key;
return sum;
}
[Benchmark(Description = "Iterate: LanguageExt Map")]
public int Iterate_LanguageExt_Map()
{
int sum = 0;
// LanguageExt Map is IEnumerable<(Key, Value)>
foreach (var item in _leMap) sum += item.Key;
return sum;
}
[Benchmark(Description = "Iterate: LanguageExt HashMap")]
public int Iterate_LanguageExt_HashMap()
{
int sum = 0;
foreach (var item in _leHashMap) sum += item.Key;
return sum;
}
// =========================================================
// 4. SET (Persistent / Immutable Update)
// =========================================================
[Benchmark(Description = "Set: NiceBTree")]
public PersistentMap<int, int, IntStrategy> Set_NiceBTree()
{
return _niceMap.Set(_keys[N / 2], -1);
}
[Benchmark(Description = "Set: MS Sorted")]
public System.Collections.Immutable.ImmutableSortedDictionary<int, int> Set_MsSorted()
{
return _msSortedMap.SetItem(_keys[N / 2], -1);
}
[Benchmark(Description = "Set: LanguageExt Map")]
public LanguageExt.Map<int, int> Set_LanguageExt_Map()
{
return _leMap.SetItem(_keys[N / 2], -1);
}
[Benchmark(Description = "Set: LanguageExt HashMap")]
public LanguageExt.HashMap<int, int> Set_LanguageExt_HashMap()
{
return _leHashMap.SetItem(_keys[N / 2], -1);
}
}

View file

@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PersistentOrderedMap\PersistentOrderedMap.csproj" />
</ItemGroup>
</Project>

View file

@ -1,368 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using LanguageExt;
using PersistentOrderedMap;
namespace MapBenchmarks;
[MemoryDiagnoser]
public class IntMapBenchmarks
{
[Params(100, 1000, 10000, 100000)]
public int N { get; set; }
private int[] _allKeys;
private int[] _retrieveKeys;
private int[] _updateKeys;
private int[] _removeKeys;
private int[] _mixedKeys; // Half existing, half new
// Pre-built collections for read/update/remove tests
private ImmutableDictionary<int, int> _immDict;
private ImmutableSortedDictionary<int, int> _immSortedDict;
private LanguageExt.Map<int, int> _extMap;
private LanguageExt.HashMap<int, int> _extHashMap;
private PersistentOrderedMap<int, int, IntStrategy> _persistentOrderedMap;
private readonly IntStrategy _intStrategy = new IntStrategy();
[GlobalSetup]
public void Setup()
{
var rnd = new Random(42);
// Build integer keys (inserted sorted)
_allKeys = Enumerable.Range(0, N).ToArray();
int subsetSize = Math.Max(1, N / 10);
// Subsets for different operations
var shuffled = _allKeys.OrderBy(x => rnd.Next()).ToArray();
_retrieveKeys = shuffled.Take(subsetSize).ToArray();
_updateKeys = shuffled.Skip(subsetSize).Take(subsetSize).ToArray();
_removeKeys = shuffled.Skip(subsetSize * 2).Take(subsetSize).ToArray();
// Mixed keys: half existing, half completely new (N + 1 to N + subsetSize/2)
var existingHalf = shuffled.Skip(subsetSize * 3).Take(subsetSize / 2).ToArray();
var newHalf = Enumerable.Range(N + 1, subsetSize - (subsetSize / 2)).ToArray();
_mixedKeys = existingHalf.Concat(newHalf).OrderBy(x => rnd.Next()).ToArray();
// Pre-build collections
_immDict = ImmutableDictionary.CreateRange(_allKeys.Select(k => new KeyValuePair<int, int>(k, k)));
_immSortedDict = ImmutableSortedDictionary.CreateRange(_allKeys.Select(k => new KeyValuePair<int, int>(k, k)));
_extMap = LanguageExt.Map.empty<int, int>();
_extHashMap = LanguageExt.HashMap.empty<int, int>();
foreach (var k in _allKeys)
{
_extMap = _extMap.AddOrUpdate(k, k);
_extHashMap = _extHashMap.AddOrUpdate(k, k);
}
var transient = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_intStrategy);
foreach (var k in _allKeys) transient.Set(k, k);
_persistentOrderedMap = transient.ToPersistent();
}
// --- 1. BUILD ---
[Benchmark]
public ImmutableDictionary<int, int> Build_ImmDict()
{
var map = ImmutableDictionary<int, int>.Empty;
foreach (var k in _allKeys) map = map.Add(k, k);
return map;
}
[Benchmark]
public ImmutableSortedDictionary<int, int> Build_ImmSortedDict()
{
var map = ImmutableSortedDictionary<int, int>.Empty;
foreach (var k in _allKeys) map = map.Add(k, k);
return map;
}
[Benchmark]
public LanguageExt.Map<int, int> Build_ExtMap()
{
var map = LanguageExt.Map.empty<int, int>();
foreach (var k in _allKeys) map = map.AddOrUpdate(k, k);
return map;
}
[Benchmark]
public LanguageExt.HashMap<int, int> Build_ExtHashMap()
{
var map = LanguageExt.HashMap.empty<int, int>();
foreach (var k in _allKeys) map = map.AddOrUpdate(k, k);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> Build_PersistentMap()
{
var map = PersistentOrderedMap<int, int, IntStrategy>.Empty(_intStrategy);
foreach (var k in _allKeys) map = map.Set(k, k);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> Build_TransientMap()
{
var map = BaseOrderedMap<int, int, IntStrategy>.CreateTransient(_intStrategy);
foreach (var k in _allKeys) map.Set(k, k);
return map.ToPersistent();
}
// --- 2. RETRIEVAL ---
[Benchmark]
public int Retrieve_ImmDict()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_immDict.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_ImmSortedDict()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_immSortedDict.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_ExtMap()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_extMap.Find(k).IsSome) count++;
return count;
}
[Benchmark]
public int Retrieve_ExtHashMap()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_extHashMap.Find(k).IsSome) count++;
return count;
}
[Benchmark]
public int Retrieve_PersistentMap()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_persistentOrderedMap.TryGetValue(k, out _)) count++;
return count;
}
// --- 3. UPDATING ---
[Benchmark]
public ImmutableDictionary<int, int> Update_ImmDict()
{
var map = _immDict;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> Update_PersistentMap()
{
var map = _persistentOrderedMap;
foreach (var k in _updateKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> Update_TransientMap()
{
var transient = _persistentOrderedMap.ToTransient();
foreach (var k in _updateKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public ImmutableSortedDictionary<int, int> Update_ImmSortedDict()
{
var map = _immSortedDict;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public LanguageExt.Map<int, int> Update_ExtMap()
{
var map = _extMap;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public LanguageExt.HashMap<int, int> Update_ExtHashMap()
{
var map = _extHashMap;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
// --- 4. UPDATE & SET (MIXED) ---
[Benchmark]
public ImmutableDictionary<int, int> UpdateSet_ImmDict()
{
var map = _immDict;
foreach (var k in _mixedKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> UpdateSet_PersistentMap()
{
var map = _persistentOrderedMap;
foreach (var k in _mixedKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> UpdateSet_TransientMap()
{
var transient = _persistentOrderedMap.ToTransient();
foreach (var k in _mixedKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public ImmutableSortedDictionary<int, int> UpdateSet_ImmSortedDict()
{
var map = _immSortedDict;
foreach (var k in _mixedKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public LanguageExt.Map<int, int> UpdateSet_ExtMap()
{
var map = _extMap;
foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999);
return map;
}
[Benchmark]
public LanguageExt.HashMap<int, int> UpdateSet_ExtHashMap()
{
var map = _extHashMap;
foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999);
return map;
}
// --- 5. ITERATION ---
[Benchmark]
public int Iterate_ImmDict()
{
int sum = 0;
foreach (var kvp in _immDict) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_PersistentMap()
{
int sum = 0;
foreach (var kvp in _persistentOrderedMap) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_ImmSortedDict()
{
int sum = 0;
foreach (var kvp in _immSortedDict) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_ExtMap()
{
int sum = 0;
foreach (var kvp in _extMap) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_ExtHashMap()
{
int sum = 0;
foreach (var kvp in _extHashMap) sum += kvp.Value;
return sum;
}
// --- 6. REMOVAL ---
[Benchmark]
public ImmutableDictionary<int, int> Remove_ImmDict()
{
var map = _immDict;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> Remove_PersistentMap()
{
var map = _persistentOrderedMap;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentOrderedMap<int, int, IntStrategy> Remove_TransientMap()
{
var transient = _persistentOrderedMap.ToTransient();
foreach (var k in _removeKeys) transient.Remove(k);
return transient.ToPersistent();
}
[Benchmark]
public ImmutableSortedDictionary<int, int> Remove_ImmSortedDict()
{
var map = _immSortedDict;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public LanguageExt.Map<int, int> Remove_ExtMap()
{
var map = _extMap;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public LanguageExt.HashMap<int, int> Remove_ExtHashMap()
{
var map = _extHashMap;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
public static void Main(string[] args)
{
BenchmarkSwitcher
.FromAssembly(typeof(IntMapBenchmarks).Assembly)
.Run(args);
}
}

View file

@ -1,451 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Attributes;
using LanguageExt;
using PersistentOrderedMap;
using System.Runtime.CompilerServices;
namespace MapBenchmarks;
public readonly struct OrdinalComparer : IComparer<string>
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(string? x, string? y) => string.CompareOrdinal(x, y);
}
[MemoryDiagnoser]
public class StringMapBenchmarks
{
[Params(100, 1000, 10000, 100000)]
public int N { get; set; }
[Params(8, 50)]
public int StringLength { get; set; }
private string[] _allKeys;
private string[] _retrieveKeys;
private string[] _updateKeys;
private string[] _removeKeys;
private string[] _mixedKeys;
private ImmutableDictionary<string, int> _immDict;
private ImmutableSortedDictionary<string, int> _immSortedDict;
private LanguageExt.Map<string, int> _extMap;
private LanguageExt.HashMap<string, int> _extHashMap;
private PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> _persistentOrderedMapStandard;
private PersistentOrderedMap<string, int, UnicodeStrategy> _persistentOrderedMapUnicode;
private readonly StandardStrategy2<string, OrdinalComparer> _stdStrategy = new StandardStrategy2<string, OrdinalComparer>(new OrdinalComparer());
private readonly UnicodeStrategy _uniStrategy = new UnicodeStrategy();
[GlobalSetup]
public void Setup()
{
var rnd = new Random(42);
// Build random strings
_allKeys = Enumerable.Range(0, N).Select(_ => GenerateRandomString(StringLength, rnd)).Distinct().ToArray();
// Regenerate if Distinct() reduced array size (highly unlikely with length 8/50, but safe)
while (_allKeys.Length < N)
{
_allKeys = _allKeys.Concat(new[] { GenerateRandomString(StringLength, rnd) }).Distinct().ToArray();
}
int subsetSize = Math.Max(1, N / 10);
var shuffled = _allKeys.OrderBy(x => rnd.Next()).ToArray();
_retrieveKeys = shuffled.Take(subsetSize).ToArray();
_updateKeys = shuffled.Skip(subsetSize).Take(subsetSize).ToArray();
_removeKeys = shuffled.Skip(subsetSize * 2).Take(subsetSize).ToArray();
var existingHalf = shuffled.Skip(subsetSize * 3).Take(subsetSize / 2).ToArray();
var newHalf = Enumerable.Range(0, subsetSize - (subsetSize / 2)).Select(_ => GenerateRandomString(StringLength, rnd)).ToArray();
_mixedKeys = existingHalf.Concat(newHalf).OrderBy(x => rnd.Next()).ToArray();
// Pre-build collections
_immDict = ImmutableDictionary.CreateRange(_allKeys.Select((k, i) => new KeyValuePair<string, int>(k, i)));
_immSortedDict = ImmutableSortedDictionary.CreateRange(_allKeys.Select((k, i) => new KeyValuePair<string, int>(k, i)));
_extMap = LanguageExt.Map.empty<string, int>();
_extHashMap = LanguageExt.HashMap.empty<string, int>();
for (int i = 0; i < _allKeys.Length; i++)
{
_extMap = _extMap.AddOrUpdate(_allKeys[i], i);
_extHashMap = _extHashMap.AddOrUpdate(_allKeys[i], i);
}
var transStd = BaseOrderedMap<string, int, StandardStrategy2<string,OrdinalComparer>>.CreateTransient(_stdStrategy);
var transUni = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_uniStrategy);
for (int i = 0; i < _allKeys.Length; i++)
{
transStd.Set(_allKeys[i], i);
transUni.Set(_allKeys[i], i);
}
_persistentOrderedMapStandard = transStd.ToPersistent();
_persistentOrderedMapUnicode = transUni.ToPersistent();
}
private static string GenerateRandomString(int length, Random rnd)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length).Select(s => s[rnd.Next(s.Length)]).ToArray());
}
// --- 1. BUILD ---
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string,OrdinalComparer>> Build_TransientMap_Standard()
{
var map = BaseOrderedMap<string, int, StandardStrategy2<string,OrdinalComparer>>.CreateTransient(_stdStrategy);
for (int i = 0; i < _allKeys.Length; i++) map.Set(_allKeys[i], i);
return map.ToPersistent();
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> Build_TransientMap_Unicode()
{
var map = BaseOrderedMap<string, int, UnicodeStrategy>.CreateTransient(_uniStrategy);
for (int i = 0; i < _allKeys.Length; i++) map.Set(_allKeys[i], i);
return map.ToPersistent();
}
[Benchmark]
public ImmutableDictionary<string, int> Build_ImmDict()
{
var map = ImmutableDictionary<string, int>.Empty;
for (int i = 0; i < _allKeys.Length; i++) map = map.Add(_allKeys[i], i);
return map;
}
// --- 1. BUILD (Missing) ---
[Benchmark]
public ImmutableSortedDictionary<string, int> Build_ImmSortedDict()
{
var map = ImmutableSortedDictionary<string, int>.Empty;
for (int i = 0; i < _allKeys.Length; i++) map = map.Add(_allKeys[i], i);
return map;
}
[Benchmark]
public LanguageExt.Map<string, int> Build_ExtMap()
{
var map = LanguageExt.Map.empty<string, int>();
for (int i = 0; i < _allKeys.Length; i++) map = map.AddOrUpdate(_allKeys[i], i);
return map;
}
[Benchmark]
public LanguageExt.HashMap<string, int> Build_ExtHashMap()
{
var map = LanguageExt.HashMap.empty<string, int>();
for (int i = 0; i < _allKeys.Length; i++) map = map.AddOrUpdate(_allKeys[i], i);
return map;
}
// --- 2. RETRIEVAL ---
[Benchmark]
public int Retrieve_ImmDict()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_immDict.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_PersistentMap_Standard()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_persistentOrderedMapStandard.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_PersistentMap_Unicode()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_persistentOrderedMapUnicode.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_ImmSortedDict()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_immSortedDict.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_ExtMap()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_extMap.Find(k).IsSome) count++;
return count;
}
[Benchmark]
public int Retrieve_ExtHashMap()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_extHashMap.Find(k).IsSome) count++;
return count;
}
// --- 3. UPDATING ---
[Benchmark]
public ImmutableDictionary<string, int> Update_ImmDict()
{
var map = _immDict;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> Update_PersistentMap_Standard()
{
var map = _persistentOrderedMapStandard;
foreach (var k in _updateKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> Update_PersistentMap_Unicode()
{
var map = _persistentOrderedMapUnicode;
foreach (var k in _updateKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> Update_TransientMap_Standard()
{
var transient = _persistentOrderedMapStandard.ToTransient();
foreach (var k in _updateKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> Update_TransientMap_Unicode()
{
var transient = _persistentOrderedMapUnicode.ToTransient();
foreach (var k in _updateKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public ImmutableSortedDictionary<string, int> Update_ImmSortedDict()
{
var map = _immSortedDict;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public LanguageExt.Map<string, int> Update_ExtMap()
{
var map = _extMap;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public LanguageExt.HashMap<string, int> Update_ExtHashMap()
{
var map = _extHashMap;
foreach (var k in _updateKeys) map = map.SetItem(k, 999);
return map;
}
// --- 4. UPDATE & SET (MIXED) ---
[Benchmark]
public ImmutableDictionary<string, int> UpdateSet_ImmDict()
{
var map = _immDict;
foreach (var k in _mixedKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> UpdateSet_PersistentMap_Standard()
{
var map = _persistentOrderedMapStandard;
foreach (var k in _mixedKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> UpdateSet_PersistentMap_Unicode()
{
var map = _persistentOrderedMapUnicode;
foreach (var k in _mixedKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> UpdateSet_TransientMap_Standard()
{
var transient = _persistentOrderedMapStandard.ToTransient();
foreach (var k in _mixedKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> UpdateSet_TransientMap_Unicode()
{
var transient = _persistentOrderedMapUnicode.ToTransient();
foreach (var k in _mixedKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public ImmutableSortedDictionary<string, int> UpdateSet_ImmSortedDict()
{
var map = _immSortedDict;
foreach (var k in _mixedKeys) map = map.SetItem(k, 999);
return map;
}
[Benchmark]
public LanguageExt.Map<string, int> UpdateSet_ExtMap()
{
var map = _extMap;
foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999);
return map;
}
[Benchmark]
public LanguageExt.HashMap<string, int> UpdateSet_ExtHashMap()
{
var map = _extHashMap;
foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999);
return map;
}
// --- 5. ITERATION ---
[Benchmark]
public int Iterate_ImmDict()
{
int sum = 0;
foreach (var kvp in _immDict) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_PersistentMap_Standard()
{
int sum = 0;
foreach (var kvp in _persistentOrderedMapStandard) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_ImmSortedDict()
{
int sum = 0;
foreach (var kvp in _immSortedDict) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_ExtMap()
{
int sum = 0;
foreach (var kvp in _extMap) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_ExtHashMap()
{
int sum = 0;
foreach (var kvp in _extHashMap) sum += kvp.Value;
return sum;
}
[Benchmark]
public int Iterate_PersistentMap_Unicode()
{
int sum = 0;
foreach (var kvp in _persistentOrderedMapUnicode) sum += kvp.Value;
return sum;
}
// --- 6. REMOVAL ---
[Benchmark]
public ImmutableDictionary<string, int> Remove_ImmDict()
{
var map = _immDict;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> Remove_PersistentMap_Standard()
{
var map = _persistentOrderedMapStandard;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> Remove_PersistentMap_Unicode()
{
var map = _persistentOrderedMapUnicode;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentOrderedMap<string, int, StandardStrategy2<string, OrdinalComparer>> Remove_TransientMap_Standard()
{
var transient = _persistentOrderedMapStandard.ToTransient();
foreach (var k in _removeKeys) transient.Remove(k);
return transient.ToPersistent();
}
[Benchmark]
public PersistentOrderedMap<string, int, UnicodeStrategy> Remove_TransientMap_Unicode()
{
var transient = _persistentOrderedMapUnicode.ToTransient();
foreach (var k in _removeKeys) transient.Remove(k);
return transient.ToPersistent();
}
[Benchmark]
public ImmutableSortedDictionary<string, int> Remove_ImmSortedDict()
{
var map = _immSortedDict;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public LanguageExt.Map<string, int> Remove_ExtMap()
{
var map = _extMap;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public LanguageExt.HashMap<string, int> Remove_ExtHashMap()
{
var map = _extHashMap;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
}