Compare commits

...

2 commits

Author SHA1 Message Date
9242c1c751 perf: Optimize non-prefix key strategies and
memory usage

- Conditionally allocate prefix buffers in `LeafNode` and introduce `PrefixInternalNode` to reduce memory overhead when prefixes are disabled.
- Bypass prefix calculation and logic entirely when `UsesPrefixes` is false.
- Add a binary search fallback for key scanning.
- Implement a dedicated `int` scanning fast-path, removing SIMD prefix usage from `IntStrategy`.
- Reorganize key strategies into separate files.
- Add a new benchmark project specifically for string keys.
2026-04-22 15:55:33 +02:00
570a736606 Refactor key strategies 2026-04-21 08:41:59 +02:00
13 changed files with 1452 additions and 675 deletions

View file

@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstImmutableDict", "ben
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstLanguageExt", "benchmarks\AgainstLanguageExt\AgainstLanguageExt.csproj", "{6C16526B-5139-4EA3-BF74-E6320F467198}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstLanguageExt", "benchmarks\AgainstLanguageExt\AgainstLanguageExt.csproj", "{6C16526B-5139-4EA3-BF74-E6320F467198}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyBenchMarks", "benchmarks\MyBenchMarks\MyBenchMarks.csproj", "{769E1CEA-7E01-405B-80A2-95CBF432A2BA}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -21,6 +23,7 @@ Global
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE} = {B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE} {CA49AA3C-0CE6-4735-887F-FB3631D63CEE} = {B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}
{13304F19-7ED3-4C40-9A08-46D539667D50} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53} {13304F19-7ED3-4C40-9A08-46D539667D50} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
{6C16526B-5139-4EA3-BF74-E6320F467198} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53} {6C16526B-5139-4EA3-BF74-E6320F467198} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
{769E1CEA-7E01-405B-80A2-95CBF432A2BA} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -39,5 +42,9 @@ Global
{6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.Build.0 = Release|Any CPU {6C16526B-5139-4EA3-BF74-E6320F467198}.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
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View file

@ -13,7 +13,6 @@ namespace PersistentMap
public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value) public static bool TryGetValue<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out V value)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
Node<K> current = root; Node<K> current = root;
@ -22,7 +21,6 @@ namespace PersistentMap
if (current.IsLeaf) if (current.IsLeaf)
{ {
var leaf = current.AsLeaf<V>(); var leaf = current.AsLeaf<V>();
// Leaf uses standard FindIndex (Lower Bound) to find exact match
int index = FindIndex(leaf, key, keyPrefix, strategy); int index = FindIndex(leaf, key, keyPrefix, strategy);
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{ {
@ -34,7 +32,6 @@ namespace PersistentMap
} }
else else
{ {
// FIX: Internal uses FindRoutingIndex (Upper Bound)
var internalNode = current.AsInternal(); var internalNode = current.AsInternal();
int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy);
current = internalNode.Children[index]!; current = internalNode.Children[index]!;
@ -42,7 +39,6 @@ namespace PersistentMap
} }
} }
// Public API
public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool countChanged) public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> strategy, OwnerId owner, out bool countChanged)
{ {
root = root.EnsureEditable(owner); root = root.EnsureEditable(owner);
@ -51,13 +47,19 @@ namespace PersistentMap
if (splitResult != null) if (splitResult != null)
{ {
var newRoot = new InternalNode<K>(owner); var newRoot = strategy.UsesPrefixes
newRoot.Children[0] = root; ? new PrefixInternalNode<K>(owner)
: new InternalNode<K>(owner);
newRoot.Keys[0] = splitResult.Separator; newRoot.Keys[0] = splitResult.Separator;
newRoot.Children[0] = root;
newRoot.Children[1] = splitResult.NewNode; newRoot.Children[1] = splitResult.NewNode;
newRoot.SetCount(1); newRoot.SetCount(1);
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{
newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator); newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator);
}
return newRoot; return newRoot;
} }
@ -65,10 +67,8 @@ namespace PersistentMap
return root; 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) 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; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf) if (node.IsLeaf)
@ -79,11 +79,11 @@ namespace PersistentMap
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{ {
leaf.Values[index] = value; leaf.Values[index] = value;
added = false; // Key existed, value updated. Count does not change. added = false;
return null; return null;
} }
added = true; // New key. Count +1. added = true;
if (leaf.Header.Count < LeafNode<K, V>.Capacity) if (leaf.Header.Count < LeafNode<K, V>.Capacity)
{ {
InsertIntoLeaf(leaf, index, key, value, strategy); InsertIntoLeaf(leaf, index, key, value, strategy);
@ -120,9 +120,8 @@ namespace PersistentMap
} }
} }
// Public API
public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged) public static Node<K> Remove<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, OwnerId owner, out bool countChanged)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
root = root.EnsureEditable(owner); root = root.EnsureEditable(owner);
@ -143,11 +142,9 @@ namespace PersistentMap
return root; return root;
} }
// Recursive Helper
private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed) private static bool RemoveRecursive<K, V, TStrategy>(Node<K> node, K key, TStrategy strategy, OwnerId owner, out bool removed)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
// 1. Calculate ONCE
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
if (node.IsLeaf) if (node.IsLeaf)
@ -158,11 +155,11 @@ namespace PersistentMap
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{ {
RemoveFromLeaf(leaf, index, strategy); RemoveFromLeaf(leaf, index, strategy);
removed = true; // Item removed. Count -1. removed = true;
return leaf.Header.Count <LeafNode<K, V>.MergeThreshold; return leaf.Header.Count < LeafNode<K, V>.MergeThreshold;
} }
removed = false; // Item not found. removed = false;
return false; return false;
} }
else else
@ -187,101 +184,129 @@ namespace PersistentMap
// Internal Helpers: Search // Internal Helpers: Search
// --------------------------------------------------------- // ---------------------------------------------------------
// Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound)
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, long keyPrefix, TStrategy strategy) internal static int FindIndex<K, TStrategy>(Node<K> node, K key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
if (typeof(K) == typeof(int))
{
Span<K> keys = node.GetKeys();
ref K firstKeyRef = ref MemoryMarshal.GetReference(keys);
ref int firstIntRef = ref Unsafe.As<K, int>(ref firstKeyRef);
ReadOnlySpan<int> intKeys = MemoryMarshal.CreateReadOnlySpan(ref firstIntRef, keys.Length);
int intKey = Unsafe.As<K, int>(ref key);
return IntScanner.FindFirstGreaterOrEqual(intKeys, intKey);
}
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
// Use the pre-calculated prefix here!
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
return RefineSearch(index, node.GetKeys(), key, strategy); return RefineSearch(index, node.GetKeys(), key, strategy);
} }
return LinearSearchKeys(node.GetKeys(), key, strategy); return FallbackSearchKeys(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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, long keyPrefix, TStrategy strategy) internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, long keyPrefix, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
if (!strategy.UsesPrefixes) if (!strategy.UsesPrefixes)
{ {
return LinearSearchRouting(node.GetKeys(), key, strategy); return FallbackRoutingKeys(node.GetKeys(), key, strategy);
} }
// Use the pre-calculated prefix here!
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy); return RefineRouting(index, node.GetKeys(), key, strategy);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<K, TStrategy>(Span<K> keys, K key, TStrategy strategy) private static int RefineSearch<K, TStrategy>(int startIndex, ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K> 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; int i = startIndex;
// DIFFERENCE: We continue past valid matches. while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++;
// 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; return i;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RefineRouting<K, TStrategy>(int startIndex, ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = startIndex;
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FallbackSearchKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
return strategy.UseBinarySearch
? BinarySearchKeys(keys, key, strategy)
: LinearSearchKeys(keys, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FallbackRoutingKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
return strategy.UseBinarySearch
? BinaryRoutingKeys(keys, key, strategy)
: LinearRoutingKeys(keys, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = 0;
while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearRoutingKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = 0;
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int BinarySearchKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int low = 0;
int high = keys.Length - 1;
while (low <= high)
{
int mid = low + ((high - low) >> 1);
int cmp = strategy.Compare(keys[mid], key);
if (cmp == 0) return mid;
if (cmp < 0) low = mid + 1;
else high = mid - 1;
}
return low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int BinaryRoutingKeys<K, TStrategy>(ReadOnlySpan<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int low = 0;
int high = keys.Length - 1;
while (low <= high)
{
int mid = low + ((high - low) >> 1);
int cmp = strategy.Compare(keys[mid], key);
if (cmp <= 0) low = mid + 1;
else high = mid - 1;
}
return low;
}
// --------------------------------------------------------- // ---------------------------------------------------------
// Insertion Logic // Insertion Logic
// --------------------------------------------------------- // ---------------------------------------------------------
@ -290,226 +315,137 @@ namespace PersistentMap
{ {
public Node<K> NewNode; public Node<K> NewNode;
public K Separator; public K Separator;
public SplitResult(Node<K> newNode, K separator) { NewNode = newNode; Separator = 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) private static void InsertIntoLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, K key, V value, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
int count = leaf.Header.Count; int count = leaf.Header.Count;
if (index < count) if (index < count)
{ {
int moveCount = count - index; int moveCount = count - index;
// Fast Span memory moves
leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1)); leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1));
leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1)); leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1));
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
leaf.AllPrefixes.Slice(index, count - index) leaf.AllPrefixes.Slice(index, moveCount).CopyTo(leaf.AllPrefixes.Slice(index + 1));
.CopyTo(leaf.AllPrefixes.Slice(index + 1));
} }
} }
leaf.Keys[index] = key; leaf.Keys[index] = key;
// This fails if leaf.Values is a Span<V> of length 'count'
leaf.Values[index] = value; leaf.Values[index] = value;
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
leaf.AllPrefixes![index] = strategy.GetPrefix(key); {
leaf.AllPrefixes[index] = strategy.GetPrefix(key);
}
leaf.SetCount(count + 1); leaf.SetCount(count + 1);
} }
private static SplitResult<K> SplitLeaf<K, V, TStrategy>(LeafNode<K, V> left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner) private static SplitResult<K> SplitLeaf<K, V, TStrategy>(LeafNode<K, V> left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
var right = new LeafNode<K, V>(owner); var right = new LeafNode<K, V>(owner, strategy.UsesPrefixes);
int totalCount = left.Header.Count; int totalCount = left.Header.Count;
// Heuristics int splitPoint = (insertIndex == totalCount) ? totalCount : (insertIndex == 0 ? 0 : totalCount / 2);
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; int moveCount = totalCount - splitPoint;
if (moveCount > 0) if (moveCount > 0)
{ {
// Fast Span memory moves
left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0)); left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0));
left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0)); left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0));
// Manually copy prefixes if needed or re-calculate
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
for (int i = 0; i < moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + i]; {
left.AllPrefixes.Slice(splitPoint, moveCount).CopyTo(right.AllPrefixes);
}
} }
// Update Counts
left.SetCount(splitPoint); left.SetCount(splitPoint);
right.SetCount(moveCount); right.SetCount(moveCount);
// Insert the New Item into the correct node
if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0)) if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0))
{ {
InsertIntoLeaf(left, insertIndex, key, value, strategy); InsertIntoLeaf(left, insertIndex, key, value, strategy);
} }
else else
{ {
InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy); 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]); 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) private static void InsertIntoInternal<K, TStrategy>(InternalNode<K> node, int index, K separator, Node<K> newChild, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
int count = node.Header.Count; int count = node.Header.Count;
// Shift Keys and Prefixes
if (index < count) if (index < count)
{ {
int moveCount = count - index; int moveCount = count - index;
Span<K> keysSpan = node.Keys;
keysSpan.Slice(index, moveCount).CopyTo(keysSpan.Slice(index + 1));
Span<Node<K>> childrenSpan = node.Children;
childrenSpan.Slice(index + 1, moveCount).CopyTo(childrenSpan.Slice(index + 2));
// Fast Span memory moves
node.Keys.AsSpan(index, moveCount).CopyTo(node.Keys.AsSpan(index + 1));
// FIX: Shift raw prefix array
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
node.AllPrefixes.Slice(index, count - index) node.AllPrefixes.Slice(index, moveCount).CopyTo(node.AllPrefixes.Slice(index + 1));
.CopyTo(node.AllPrefixes.Slice(index + 1));
} }
} }
// Shift Children node.Keys[index] = separator;
// Children buffer is indexable like an array but requires manual loop or Unsafe copy node.Children[index + 1] = newChild;
// if we don't want to use unsafe pointers.
// Since it's a small struct buffer (size 33), a loop is fine/fast. if (strategy.UsesPrefixes)
for (int i = count + 1; i > index + 1; i--)
{ {
node.Children[i] = node.Children[i - 1]; node.AllPrefixes[index] = strategy.GetPrefix(separator);
} }
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); 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) 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> where TStrategy : IKeyStrategy<K>
{ {
var right = new InternalNode<K>(owner); var right = strategy.UsesPrefixes
? new PrefixInternalNode<K>(owner)
: new InternalNode<K>(owner);
int count = left.Header.Count; int count = left.Header.Count;
int splitPoint = count / 2; // Internal nodes usually split 50/50 to keep tree fat int splitPoint = count / 2;
// The key at splitPoint moves UP to become the separator.
// Keys > splitPoint move to Right.
K upKey = left.Keys[splitPoint]; K upKey = left.Keys[splitPoint];
int moveCount = count - splitPoint - 1;
// Move Keys/Prefixes to Right
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
// Fast Span memory moves
if (moveCount > 0) if (moveCount > 0)
{ {
left.Keys.AsSpan(splitPoint + 1, moveCount).CopyTo(right.Keys.AsSpan(0)); Span<K> leftKeys = left.Keys;
Span<K> rightKeys = right.Keys;
leftKeys.Slice(splitPoint + 1, moveCount).CopyTo(rightKeys);
Span<Node<K>> leftChildren = left.Children;
Span<Node<K>> rightChildren = right.Children;
leftChildren.Slice(splitPoint + 1, moveCount + 1).CopyTo(rightChildren);
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0)); left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes);
} }
} }
// 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); left.SetCount(splitPoint);
right.SetCount(moveCount); 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) if (insertIndex <= splitPoint)
{ {
InsertIntoInternal(left, insertIndex, separator, newChild, strategy); InsertIntoInternal(left, insertIndex, separator, newChild, strategy);
@ -526,26 +462,19 @@ namespace PersistentMap
// Removal Logic // Removal Logic
// --------------------------------------------------------- // ---------------------------------------------------------
// ---------------------------------------------------------
// Removal Logic (Fixed Type Inference & Casting)
// ---------------------------------------------------------
private static void RemoveFromLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, TStrategy strategy) private static void RemoveFromLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
int count = leaf.Header.Count; int count = leaf.Header.Count;
int moveCount = count - index - 1; int moveCount = count - index - 1;
if (moveCount > 0) if (moveCount > 0)
{ {
// Fast Span memory moves
leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index)); leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index));
leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index)); leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index));
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
// Replaced manual 'for' loop with native slice copy
leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index)); leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index));
} }
} }
@ -553,11 +482,9 @@ namespace PersistentMap
leaf.SetCount(count - 1); 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) private static bool HandleUnderflow<K, V, TStrategy>(InternalNode<K> parent, int childIndex, TStrategy strategy, OwnerId owner)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
// Try to borrow from Right Sibling
if (childIndex < parent.Header.Count) if (childIndex < parent.Header.Count)
{ {
var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner); var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner);
@ -575,7 +502,6 @@ namespace PersistentMap
return parent.Header.Count < LeafNode<K, V>.MergeThreshold; return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
} }
} }
// Try to borrow from Left Sibling
else if (childIndex > 0) else if (childIndex > 0)
{ {
var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner); var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner);
@ -589,7 +515,6 @@ namespace PersistentMap
} }
else else
{ {
// Merge Left and Current. Note separator index is 'childIndex - 1'
Merge<K, V, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy); Merge<K, V, TStrategy>(parent, childIndex - 1, leftSibling, rightChild, strategy);
return parent.Header.Count < LeafNode<K, V>.MergeThreshold; return parent.Header.Count < LeafNode<K, V>.MergeThreshold;
} }
@ -600,15 +525,12 @@ namespace PersistentMap
private static bool CanBorrow<K>(Node<K> node) 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; 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) private static void Merge<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
// Case A: Merging Leaves
if (left.IsLeaf) if (left.IsLeaf)
{ {
var leftLeaf = left.AsLeaf<V>(); var leftLeaf = left.AsLeaf<V>();
@ -616,117 +538,113 @@ namespace PersistentMap
int lCount = leftLeaf.Header.Count; int lCount = leftLeaf.Header.Count;
int rCount = rightLeaf.Header.Count; int rCount = rightLeaf.Header.Count;
rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount)); rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount));
rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount)); rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount));
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
rightLeaf.AllPrefixes.Slice(0, rCount) rightLeaf.AllPrefixes.Slice(0, rCount).CopyTo(leftLeaf.AllPrefixes.Slice(lCount));
.CopyTo(leftLeaf.AllPrefixes.Slice(lCount));
} }
leftLeaf.SetCount(lCount + rCount); leftLeaf.SetCount(lCount + rCount);
} }
// Case B: Merging Internal Nodes
else else
{ {
var leftInternal = left.AsInternal(); var leftInternal = left.AsInternal();
var rightInternal = right.AsInternal(); var rightInternal = right.AsInternal();
// Pull separator from parent
K separator = parent.Keys[separatorIndex]; K separator = parent.Keys[separatorIndex];
int lCount = leftInternal.Header.Count; int lCount = leftInternal.Header.Count;
leftInternal.Keys[lCount] = separator; leftInternal.Keys[lCount] = separator;
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{
leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator); leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator);
}
int rCount = rightInternal.Header.Count; int rCount = rightInternal.Header.Count;
rightInternal.Keys.AsSpan(0, rCount).CopyTo(leftInternal.Keys.AsSpan(lCount + 1)); Span<K> rightKeys = rightInternal.Keys;
Span<K> leftKeys = leftInternal.Keys;
rightKeys.Slice(0, rCount).CopyTo(leftKeys.Slice(lCount + 1));
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
rightInternal.AllPrefixes.Slice(0, rCount) rightInternal.AllPrefixes.Slice(0, rCount).CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1));
.CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1));
} }
for (int i = 0; i <= rCount; i++) Span<Node<K>> rightChildren = rightInternal.Children;
{ Span<Node<K>> leftChildren = leftInternal.Children;
leftInternal.Children[lCount + 1 + i] = rightInternal.Children[i]; rightChildren.Slice(0, rCount + 1).CopyTo(leftChildren.Slice(lCount + 1));
}
leftInternal.SetCount(lCount + 1 + rCount); leftInternal.SetCount(lCount + 1 + rCount);
} }
// Remove Separator and Right Child from Parent
int pCount = parent.Header.Count; int pCount = parent.Header.Count;
int moveCount = pCount - separatorIndex - 1; int moveCount = pCount - separatorIndex - 1;
if (moveCount > 0) if (moveCount > 0)
{ {
parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex)); Span<K> parentKeys = parent.Keys;
parentKeys.Slice(separatorIndex + 1, moveCount).CopyTo(parentKeys.Slice(separatorIndex));
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
// Replaced manual 'for' loop with native slice copy
parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex)); parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex));
} }
}
for (int i = separatorIndex + 2; i <= pCount; i++) Span<Node<K>> parentChildren = parent.Children;
{ parentChildren.Slice(separatorIndex + 2, moveCount).CopyTo(parentChildren.Slice(separatorIndex + 1));
parent.Children[i - 1] = parent.Children[i];
} }
parent.SetCount(pCount - 1); parent.SetCount(pCount - 1);
} }
private static void RotateLeft<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy) private static void RotateLeft<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
// Move one item from Right to Left
if (left.IsLeaf) if (left.IsLeaf)
{ {
var leftLeaf = left.AsLeaf<V>(); var leftLeaf = left.AsLeaf<V>();
var rightLeaf = right.AsLeaf<V>(); var rightLeaf = right.AsLeaf<V>();
// Move first of right to end of left
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy); InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
RemoveFromLeaf(rightLeaf, 0, strategy); RemoveFromLeaf(rightLeaf, 0, strategy);
// Update Parent Separator
parent.Keys[separatorIndex] = rightLeaf.Keys[0]; parent.Keys[separatorIndex] = rightLeaf.Keys[0];
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
}
} }
else else
{ {
var leftInternal = left.AsInternal(); var leftInternal = left.AsInternal();
var rightInternal = right.AsInternal(); var rightInternal = right.AsInternal();
// 1. Move Parent Separator to Left End
K sep = parent.Keys[separatorIndex]; K sep = parent.Keys[separatorIndex];
InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy); InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy);
// 2. Move Right[0] Key to Parent
parent.Keys[separatorIndex] = rightInternal.Keys[0]; parent.Keys[separatorIndex] = rightInternal.Keys[0];
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]); parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]);
}
// 3. Fix Right (Remove key 0 and shift child 0 out)
// 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; int rCount = rightInternal.Header.Count;
// Shift children Span<Node<K>> rightChildren = rightInternal.Children;
for (int i = 0; i < rCount; i++) rightInternal.Children[i] = rightInternal.Children[i + 1]; rightChildren.Slice(1, rCount).CopyTo(rightChildren);
if (rCount > 1) if (rCount > 1)
{ {
// Fast Span memory moves (Replaces Array.Copy & manual loop) Span<K> rightKeys = rightInternal.Keys;
rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0)); rightKeys.Slice(1, rCount - 1).CopyTo(rightKeys);
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
{ {
rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0)); rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes);
} }
} }
@ -735,9 +653,8 @@ namespace PersistentMap
} }
private static void RotateRight<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy) private static void RotateRight<K, V, TStrategy>(InternalNode<K> parent, int separatorIndex, Node<K> left, Node<K> right, TStrategy strategy)
where TStrategy : IKeyStrategy<K> where TStrategy : IKeyStrategy<K>
{ {
// Move one item from Left to Right
if (left.IsLeaf) if (left.IsLeaf)
{ {
var leftLeaf = left.AsLeaf<V>(); var leftLeaf = left.AsLeaf<V>();
@ -749,31 +666,29 @@ namespace PersistentMap
parent.Keys[separatorIndex] = rightLeaf.Keys[0]; parent.Keys[separatorIndex] = rightLeaf.Keys[0];
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); {
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
}
} }
else else
{ {
var leftInternal = (InternalNode<K>)left; var leftInternal = left.AsInternal();
var rightInternal = (InternalNode<K>)right; var rightInternal = right.AsInternal();
int last = leftInternal.Header.Count - 1; int last = leftInternal.Header.Count - 1;
// 1. Move Parent Separator to Right Start
K sep = parent.Keys[separatorIndex]; K sep = parent.Keys[separatorIndex];
// The child moving to right is the *last* child of left (index count)
InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy); InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy);
// 2. Move Left[last] Key to Parent
parent.Keys[separatorIndex] = leftInternal.Keys[last]; parent.Keys[separatorIndex] = leftInternal.Keys[last];
if (strategy.UsesPrefixes) if (strategy.UsesPrefixes)
parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); {
parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
}
// 3. Truncate Left
leftInternal.SetCount(last); leftInternal.SetCount(last);
} }
} }
public static bool TryGetMin<K, V>(Node<K> root, out K key, out V value) public static bool TryGetMin<K, V>(Node<K> root, out K key, out V value)
{ {
var current = root; var current = root;
@ -790,7 +705,7 @@ namespace PersistentMap
return false; return false;
} }
key = leaf.Keys![0]; key = leaf.Keys[0];
value = leaf.Values[0]; value = leaf.Values[0];
return true; return true;
} }
@ -813,7 +728,7 @@ namespace PersistentMap
} }
int last = leaf.Header.Count - 1; int last = leaf.Header.Count - 1;
key = leaf.Keys![last]; key = leaf.Keys[last];
value = leaf.Values[last]; value = leaf.Values[last];
return true; return true;
} }
@ -826,7 +741,6 @@ namespace PersistentMap
int depth = 0; int depth = 0;
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
var current = root; var current = root;
while (!current.IsLeaf) while (!current.IsLeaf)
{ {
@ -841,23 +755,19 @@ namespace PersistentMap
var leaf = current.AsLeaf<V>(); var leaf = current.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy); 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 && strategy.Compare(leaf.Keys[index], key) == 0) index++;
// 1. Successor is in the same leaf
if (index < leaf.Header.Count) if (index < leaf.Header.Count)
{ {
nextKey = leaf.Keys![index]; nextKey = leaf.Keys[index];
nextValue = leaf.Values[index]; nextValue = leaf.Values[index];
return true; return true;
} }
// 2. Successor is in the next leaf (We must backtrack up the tree!)
for (int i = depth - 1; i >= 0; i--) 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) 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]!; current = path[i].Children[indices[i] + 1]!;
while (!current.IsLeaf) while (!current.IsLeaf)
{ {
@ -865,7 +775,7 @@ namespace PersistentMap
} }
var targetLeaf = current.AsLeaf<V>(); var targetLeaf = current.AsLeaf<V>();
nextKey = targetLeaf.Keys![0]; nextKey = targetLeaf.Keys[0];
nextValue = targetLeaf.Values[0]; nextValue = targetLeaf.Values[0];
return true; return true;
} }
@ -879,13 +789,11 @@ namespace PersistentMap
public static bool TryGetPredecessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K prevKey, out V prevValue) public static bool TryGetPredecessor<K, V, TStrategy>(Node<K> root, K key, TStrategy strategy, out K prevKey, out V prevValue)
where TStrategy : IKeyStrategy<K> 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]; InternalNode<K>[] path = new InternalNode<K>[32];
int[] indices = new int[32]; int[] indices = new int[32];
int depth = 0; int depth = 0;
long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0;
var current = root; var current = root;
while (!current.IsLeaf) while (!current.IsLeaf)
{ {
@ -900,20 +808,17 @@ namespace PersistentMap
var leaf = current.AsLeaf<V>(); var leaf = current.AsLeaf<V>();
int index = FindIndex(leaf, key, keyPrefix, strategy); int index = FindIndex(leaf, key, keyPrefix, strategy);
// Easy case: Predecessor is in the same leaf
if (index > 0) if (index > 0)
{ {
prevKey = leaf.Keys![index - 1]; prevKey = leaf.Keys[index - 1];
prevValue = leaf.Values[index - 1]; prevValue = leaf.Values[index - 1];
return true; return true;
} }
// Hard case: We need to backtrack to find the first left branch we ignored
for (int i = depth - 1; i >= 0; i--) for (int i = depth - 1; i >= 0; i--)
{ {
if (indices[i] > 0) 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]!; current = path[i].Children[indices[i] - 1]!;
while (!current.IsLeaf) while (!current.IsLeaf)
{ {
@ -923,7 +828,7 @@ namespace PersistentMap
var targetLeaf = current.AsLeaf<V>(); var targetLeaf = current.AsLeaf<V>();
int last = targetLeaf.Header.Count - 1; int last = targetLeaf.Header.Count - 1;
prevKey = targetLeaf.Keys![last]; prevKey = targetLeaf.Keys[last];
prevValue = targetLeaf.Values[last]; prevValue = targetLeaf.Values[last];
return true; return true;
} }
@ -934,6 +839,4 @@ namespace PersistentMap
return false; return false;
} }
} }
} }

View file

@ -40,13 +40,13 @@ public abstract class BaseOrderedMap<K, V, TStrategy> : IEnumerable<KeyValuePair
public static PersistentMap<K, V, 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. // Start with an empty leaf owned by None so the first write triggers CoW.
var emptyRoot = new LeafNode<K, V>(OwnerId.None); var emptyRoot = new LeafNode<K, V>(OwnerId.None, strategy.UsesPrefixes);
return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0); return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0);
} }
public static TransientMap<K, V, TStrategy> CreateTransient(TStrategy strategy) public static TransientMap<K, V, TStrategy> CreateTransient(TStrategy strategy)
{ {
var emptyRoot = new LeafNode<K, V>(OwnerId.None); var emptyRoot = new LeafNode<K, V>(OwnerId.None, strategy.UsesPrefixes);
return new TransientMap<K, V, TStrategy>(emptyRoot, strategy,0); return new TransientMap<K, V, TStrategy>(emptyRoot, strategy,0);
} }

View file

@ -18,38 +18,10 @@ public interface IKeyStrategy<K>
// //
bool IsLossless => false; bool IsLossless => false;
bool UseBinarySearch => 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> public struct UnicodeStrategy : IKeyStrategy<string>
@ -89,159 +61,5 @@ public struct UnicodeStrategy : IKeyStrategy<string>
public bool UsesPrefixes => true; public bool UsesPrefixes => true;
} }
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 bool UsesPrefixes => true;
public bool IsLossless => true;
}
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

@ -0,0 +1,34 @@
namespace PersistentMap;
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

@ -0,0 +1,16 @@
namespace PersistentMap;
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

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

@ -0,0 +1,37 @@
namespace PersistentMap;
using System.Runtime.CompilerServices;
/// <summary>
/// A universal key strategy for any type that relies on standard comparisons
/// (IComparable, IComparer, or custom StringComparers) without SIMD prefixes.
/// </summary>
public readonly struct StandardStrategy<K> : IKeyStrategy<K>
{
private readonly IComparer<K> _comparer;
// If no comparer is provided, it defaults to Comparer<K>.Default
// which automatically uses IComparable<K> if the type implements it.
public StandardStrategy()
{
_comparer = Comparer<K>.Default;
}
public StandardStrategy(IComparer<K>? comparer)
{
_comparer = comparer ?? Comparer<K>.Default;
}
// Tell the B-Tree to skip SIMD routing and just use LinearSearch
public bool UsesPrefixes => false;
// 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);
}
}

View file

@ -32,6 +32,12 @@ public struct NodeHeader
} }
} }
[InlineArray(32)]
public struct KeyBuffer<K>
{
private K _element0;
}
// Constraint: Internal Nodes fixed at 32 children. // Constraint: Internal Nodes fixed at 32 children.
// This removes the need for a separate array allocation for children references. // This removes the need for a separate array allocation for children references.
[InlineArray(32)] [InlineArray(32)]
@ -82,6 +88,12 @@ public abstract class Node<K>
// Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow. // Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow.
return Unsafe.As<InternalNode<K>>(this); return Unsafe.As<InternalNode<K>>(this);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PrefixInternalNode<K> AsPrefixInternal()
{
return Unsafe.As<PrefixInternalNode<K>>(this);
}
} }
public sealed class LeafNode<K, V> : Node<K> public sealed class LeafNode<K, V> : Node<K>
@ -96,12 +108,14 @@ public sealed class LeafNode<K, V> : Node<K>
public override Span<long> AllPrefixes => _prefixes != null ? _prefixes : Span<long>.Empty; public override Span<long> AllPrefixes => _prefixes != null ? _prefixes : Span<long>.Empty;
public LeafNode(OwnerId owner) : base(owner, NodeFlags.IsLeaf | NodeFlags.HasPrefixes) public LeafNode(OwnerId owner, bool usePrefixes) : base(owner, NodeFlags.IsLeaf | (usePrefixes ? NodeFlags.HasPrefixes : NodeFlags.None))
{ {
Keys = new K[Capacity]; Keys = new K[Capacity];
Values = new V[Capacity]; Values = new V[Capacity];
_prefixes = new long[Capacity]; if (usePrefixes)
{
_prefixes = new long[Capacity];
}
} }
// Copy Constructor for CoW // Copy Constructor for CoW
@ -153,67 +167,75 @@ public sealed class LeafNode<K, V> : Node<K>
} }
} }
public sealed class InternalNode<K> : Node<K> public class InternalNode<K> : Node<K>
{ {
public const int Capacity = 32; public const int Capacity = 32;
// InlineArray storage public KeyBuffer<K> Keys;
internal InternalPrefixBuffer _prefixBuffer;
public NodeBuffer<K> Children; public NodeBuffer<K> Children;
public K[]? Keys;
public override Span<long> AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity);
public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes) public override Span<long> AllPrefixes => Span<long>.Empty;
public InternalNode(OwnerId owner, NodeFlags flags = NodeFlags.None)
: base(owner, flags)
{ {
Keys = new K[Capacity];
// Children buffer is a struct, zero-initialized by default
} }
// Copy Constructor for CoW // Fixed CoW Constructor
private InternalNode(InternalNode<K> original, OwnerId newOwner) protected InternalNode(InternalNode<K> original, OwnerId newOwner, NodeFlags flags)
: base(newOwner, original.Header.Flags) : base(newOwner, flags)
{ {
Header.Count = original.Header.Count; Header.Count = original.Header.Count;
Keys = new K[Capacity];
Array.Copy(original.Keys, Keys, original.Header.Count);
// Fast struct blit for prefixes // Fast struct blit for both Keys and Children.
this._prefixBuffer = original._prefixBuffer; // No loop required for InlineArrays!
this.Keys = original.Keys;
this.Children = original.Children;
}
var srcChildren = original.GetChildren(); // The missing method needed by BTreeFunctions for routing
for (var i = 0; i < srcChildren.Length; i++) Children[i] = srcChildren[i]; [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<Node<K>> GetChildren()
{
// An internal node always has (Count + 1) children
return MemoryMarshal.CreateSpan(ref Children[0], Header.Count + 1);
}
public override Span<K> GetKeys() => MemoryMarshal.CreateSpan(ref Keys[0], Header.Count);
public override Node<K> EnsureEditable(OwnerId transactionId)
{
if (transactionId == OwnerId.None) return new InternalNode<K>(this, OwnerId.None, Header.Flags);
if (Header.Owner == transactionId) return this;
return new InternalNode<K>(this, transactionId, Header.Flags);
}
}
public sealed class PrefixInternalNode<K> : InternalNode<K>
{
internal InternalPrefixBuffer _prefixBuffer;
public override Span<long> AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity);
public PrefixInternalNode(OwnerId owner)
: base(owner, NodeFlags.HasPrefixes)
{
}
// CoW Constructor
private PrefixInternalNode(PrefixInternalNode<K> original, OwnerId newOwner)
: base(original, newOwner, original.Header.Flags)
{
// Copy the base Keys and Children, then blit the prefix buffer
this._prefixBuffer = original._prefixBuffer;
} }
public override Node<K> EnsureEditable(OwnerId transactionId) public override Node<K> EnsureEditable(OwnerId transactionId)
{ {
if (transactionId == OwnerId.None) if (transactionId == OwnerId.None) return new PrefixInternalNode<K>(this, OwnerId.None);
{ if (Header.Owner == transactionId) return this;
return new InternalNode<K>(this, OwnerId.None); return new PrefixInternalNode<K>(this, transactionId);
}
if (Header.Owner == transactionId)
{
return this;
}
return new InternalNode<K>(this, transactionId);
}
public override Span<K> GetKeys()
{
return Keys.AsSpan(0, Header.Count);
}
// Exposes the InlineArray as a Span
public Span<Node<K>?> GetChildren()
{
return MemoryMarshal.CreateSpan<Node<K>?>(ref Children[0]!, Header.Count + 1);
}
public void SetChild(int index, Node<K> node)
{
Children[index] = node;
} }
} }

View file

@ -25,7 +25,7 @@ public sealed class PersistentMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrat
// 'default(OwnerId)' (usually 0) marks this node as Immutable/Persistent. // 'default(OwnerId)' (usually 0) marks this node as Immutable/Persistent.
// This ensures that any subsequent Set/Remove will clone this node // This ensures that any subsequent Set/Remove will clone this node
// instead of modifying it in place. // instead of modifying it in place.
var emptyRoot = new LeafNode<K, V>(default(OwnerId)); var emptyRoot = new LeafNode<K, V>(default(OwnerId), strategy.UsesPrefixes);
return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0); return new PersistentMap<K, V, TStrategy>(emptyRoot, strategy, 0);
} }

View file

@ -5,16 +5,16 @@ 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. 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 ** Features
- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tree yields a new version while sharing unmodified nodes. - *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. - *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. - *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. - *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? ** 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. 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. 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 ** Quick Start
@ -36,8 +36,8 @@ if (map2.TryGetValue(2, out var value))
} }
#+end_src #+end_src
*** 2. Transient Mode (Bulk Mutations) *** 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. 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 #+begin_src csharp
var transientMap = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy()); var transientMap = BaseOrderedMap<int, string, IntStrategy>.CreateTransient(new IntStrategy());
@ -53,7 +53,7 @@ var persistentSnapshot = transientMap.ToPersistent();
#+end_src #+end_src
*** 3. Range Queries and Iteration *** 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. Because it is a B+ tree range queries require zero allocations and simply walk the leaves.
#+begin_src csharp #+begin_src csharp
var map = GetPopulatedMap(); var map = GetPopulatedMap();
@ -68,7 +68,7 @@ foreach (var kvp in map.Range(min: 10, max: 50))
var greaterThan100 = map.From(100); var greaterThan100 = map.From(100);
var lessThan50 = map.Until(50); var lessThan50 = map.Until(50);
var allElements = map.AsEnumerable(); var allElements = map.AsEnumerable();
#+end_src #+end_sr
*** 4. Tree Navigation *** 4. Tree Navigation
Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound. Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound.
@ -91,7 +91,7 @@ if (map.TryGetPredecessor(42, out int prevKey, out string prevVal))
#+end_src #+end_src
*** 5. Set Operations *** 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. Set operations take advantage of the tree's underlying sorted structure to merge trees in linear $O(N+M)$ time.
#+begin_src csharp #+begin_src csharp
var mapA = CreateMap(1, 2, 3, 4); var mapA = CreateMap(1, 2, 3, 4);
@ -108,146 +108,502 @@ var symmetricDiff = mapA.SymmetricExcept(mapB);
#+end_src #+end_src
** Benchmarks ** Benchmarks
This is going to be all over the place, but here is a small comparison to other immutable sequences. Due to how the prefix optimization works, this persistent map will be the absolutely most performant when there is high entropy in the first 8 bytes of the key. The following is pretty much the best scenario we can have since we probably only look at the first 8 characters (this is for reading a value). 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.
#+begin_src 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.
| Method | CollectionSize | KeySize | Mean | Gen0 | Allocated |
|-----------------|----------------|----------|--------------:|-----------:|----------:|
| PersistentMap | **1024** | **10** | **25.61 ns** | **0.0043** | **72 B** |
| Sys.Sorted | 1024 | 10 | 153.18 ns | - | - |
| LangExt.HashMap | 1024 | 10 | 24.80 ns | - | - |
| LangExtSorted | 1024 | 10 | 176.90 ns | - | - |
| PersistentMap | **1024** | **100** | **26.43 ns** | **0.0043** | **72 B** |
| SysSorted | 1024 | 100 | 154.77 ns | - | - |
| LangExt.HashMap | 1024 | 100 | 66.30 ns | - | - |
| LangExtSorted | 1024 | 100 | 177.28 ns | - | - |
| PersistentMap | **1024** | **1000** | **26.17 ns** | **0.0043** | **72 B** |
| SysSorted | 1024 | 1000 | 155.68 ns | - | - |
| LangExt.HashMap | 1024 | 1000 | 491.97 ns | - | - |
| LangExtSorted | 1024 | 1000 | 181.58 ns | - | - |
| PersistentMap | **131072** | **10** | **109.34 ns** | **0.0072** | **120 B** |
| SysSorted | 131072 | 10 | 460.22 ns | - | - |
| LangExt.HashMap | 131072 | 10 | 60.35 ns | - | - |
| LangExtSorted | 131072 | 10 | 555.17 ns | - | - |
| PersistentMap | **131072** | **100** | **147.30 ns** | **0.0072** | **120 B** |
| SysSorted | 131072 | 100 | 556.39 ns | - | - |
| LangExt.HashMap | 131072 | 100 | 162.81 ns | - | - |
| LangExtSorted | 131072 | 100 | 605.15 ns | - | - |
| PersistentMap | **131072** | **1000** | **170.16 ns** | **0.0072** | **120 B** |
| SysSorted | 131072 | 1000 | 625.78 ns | - | - |
| LangExt.HashMap | 131072 | 1000 | 763.75 ns | - | - |
| LangExtSorted | 131072 | 1000 | 692.92 ns | - | - |
The retrieval benchmarks reads a subset of the keys in random order.
#+end_src The update benchmarks updates a subset of the keys in random order.
To look at pure overhead, here is a benchmark using integers as keys. This is also a good fit for this BTree, since it can utilize a key strategy that compares integers using AVX. The hash based alternatives are going to have a huge advantage here. As you can see, reading a single value isn't great, and setting a single value after building the btree is also pretty awful (setting many should probably be done using transients). Iterating a building (using transients. The only valid comparison here is probably to MS SortedDict) is however plenty fast. 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 #+begin_src
| Method | N | Mean | Gen0 | Gen1 | Gen2 | Allocated | | Method | N | Mean | Gen0 | Gen1 | Gen2 | Allocated |
|-------------------------------------|--------|-----------------:|----------:|----------:|--------:|------------:| |-------------------------|--------|-----------------:|-----------:|----------:|--------:|------------:|
| 'Build: PersistentMap (Transient)' | 100 | 3,764.63 ns | 0.3929 | 0.0038 | - | 6632 B | | Build_ImmDict | 100 | 11,307.04 ns | 4.9744 | 0.0458 | - | 41688 B |
| 'Build: MS Sorted (Builder)' | 100 | 3,096.11 ns | 0.2899 | 0.0038 | - | 4864 B | | Build_ImmSortedDict | 100 | 8,493.79 ns | 4.4250 | 0.0458 | - | 37104 B |
| 'Build: LanguageExt Map (AVL)' | 100 | 6,967.02 ns | 2.2736 | 0.0229 | - | 38144 B | | Build_ExtMap | 100 | 8,519.63 ns | 5.3101 | 0.0458 | - | 44432 B |
| 'Build: LanguageExt HashMap' | 100 | 4,594.07 ns | 1.9684 | 0.0076 | - | 33024 B | | Build_ExtHashMap | 100 | 9,855.33 ns | 7.5378 | 0.0458 | - | 63104 B |
| 'Read: PersistentMap' | 100 | 1,596.68 ns | 0.4292 | - | - | 7200 B | | Build_PersistentMap | 100 | 8,698.33 ns | 16.3879 | 0.1526 | - | 137072 B |
| 'Read: MS Sorted' | 100 | 474.54 ns | - | - | - | - | | Build_TransientMap | 100 | 1,665.90 ns | 0.6332 | 0.0038 | - | 5304 B |
| 'Read: LanguageExt Map' | 100 | 1,311.31 ns | - | - | - | - | | Retrieve_ImmDict | 100 | 39.19 ns | - | - | - | - |
| 'Read: LanguageExt HashMap' | 100 | 641.22 ns | - | - | - | - | | Retrieve_ImmSortedDict | 100 | 64.32 ns | - | - | - | - |
| 'Iterate: PersistentMap' | 100 | 135.41 ns | - | - | - | - | | Retrieve_ExtMap | 100 | 117.61 ns | - | - | - | - |
| 'Iterate: MS Sorted' | 100 | 372.31 ns | - | - | - | - | | Retrieve_ExtHashMap | 100 | 84.19 ns | - | - | - | - |
| 'Iterate: LanguageExt Map' | 100 | 287.33 ns | 0.0019 | - | - | 32 B | | Retrieve_PersistentMap | 100 | 47.25 ns | - | - | - | - |
| 'Iterate: LanguageExt HashMap' | 100 | 781.56 ns | 0.0648 | - | - | 1088 B | | Update_ImmDict | 100 | 1,145.75 ns | 0.4616 | 0.0019 | - | 3872 B |
| 'Set: PersistentMap' | 100 | 85.68 ns | 0.1142 | 0.0007 | - | 1912 B | | Update_PersistentMap | 100 | 1.107 μs | 1.9398 | 0.0248 | - | 15.86 KB |
| 'Set: MS Sorted' | 100 | 66.44 ns | 0.0229 | - | - | 384 B | | Update_TransientMap | 100 | 347.49 ns | 0.3576 | 0.0033 | - | 2992 B |
| 'Set: LanguageExt Map' | 100 | 60.04 ns | 0.0219 | - | - | 368 B | | Update_ImmSortedDict | 100 | 849.39 ns | 0.3958 | 0.0010 | - | 3312 B |
| 'Set: LanguageExt HashMap' | 100 | 36.62 ns | 0.0206 | - | - | 344 B | | Update_ExtMap | 100 | 642.50 ns | 0.3939 | 0.0010 | - | 3296 B |
| 'Build: PersistentMap (Transient)' | 1000 | 49,445.56 ns | 3.1738 | 0.2441 | - | 53096 B | | Update_ExtHashMap | 100 | 541.84 ns | 0.5283 | 0.0010 | - | 4424 B |
| 'Build: MS Sorted (Builder)' | 1000 | 50,163.19 ns | 2.8687 | 0.4272 | - | 48064 B | | UpdateSet_ImmDict | 100 | 1,236.82 ns | 0.5226 | 0.0019 | - | 4376 B |
| 'Build: LanguageExt Map (AVL)' | 1000 | 103,877.98 ns | 34.6680 | 3.1738 | - | 580688 B | | UpdateSet_PersistentMap | 100 | 1189 ns | 1.9398 | 0.0248 | - | 15.86 KB |
| 'Build: LanguageExt HashMap' | 1000 | 124,339.17 ns | 45.4102 | 3.2959 | - | 760096 B | | UpdateSet_TransientMap | 100 | 380.70 ns | 0.3576 | 0.0033 | - | 2992 B |
| 'Read: PersistentMap' | 1000 | 17,671.71 ns | 4.3030 | - | - | 72000 B | | UpdateSet_ImmSortedDict | 100 | 887.07 ns | 0.4587 | 0.0010 | - | 3840 B |
| 'Read: MS Sorted' | 1000 | 7,911.72 ns | - | - | - | - | | UpdateSet_ExtMap | 100 | 856.76 ns | 0.4797 | 0.0010 | - | 4016 B |
| 'Read: LanguageExt Map' | 1000 | 20,187.52 ns | - | - | - | - | | UpdateSet_ExtHashMap | 100 | 582.31 ns | 0.5312 | 0.0019 | - | 4448 B |
| 'Read: LanguageExt HashMap' | 1000 | 9,740.28 ns | - | - | - | - | | Iterate_ImmDict | 100 | 1,324.41 ns | - | - | - | - |
| 'Iterate: PersistentMap' | 1000 | 1,217.47 ns | - | - | - | - | | Iterate_PersistentMap | 100 | 175.98 ns | - | - | - | - |
| 'Iterate: MS Sorted' | 1000 | 3,875.47 ns | - | - | - | - | | Iterate_ImmSortedDict | 100 | 488.69 ns | - | - | - | - |
| 'Iterate: LanguageExt Map' | 1000 | 2,862.82 ns | - | - | - | 32 B | | Iterate_ExtMap | 100 | 337.40 ns | 0.0038 | - | - | 32 B |
| 'Iterate: LanguageExt HashMap' | 1000 | 11,974.93 ns | 1.9226 | - | - | 32320 B | | Iterate_ExtHashMap | 100 | 1,209.77 ns | 0.2518 | - | - | 2112 B |
| 'Set: PersistentMap' | 1000 | 121.01 ns | 0.1142 | 0.0007 | - | 1912 B | | Remove_ImmDict | 100 | 899.57 ns | 0.4425 | 0.0010 | - | 3704 B |
| 'Set: MS Sorted' | 1000 | 91.62 ns | 0.0315 | - | - | 528 B | | Remove_TransientMap | 100 | 433.52 ns | 0.3290 | 0.0029 | - | 2752 B |
| 'Set: LanguageExt Map' | 1000 | 82.26 ns | 0.0305 | - | - | 512 B | | Remove_ImmSortedDict | 100 | 728.77 ns | 0.3786 | 0.0010 | - | 3168 B |
| 'Set: LanguageExt HashMap' | 1000 | 57.02 ns | 0.0367 | - | - | 616 B | | Remove_ExtMap | 100 | 653.72 ns | 0.3767 | 0.0010 | - | 3152 B |
| 'Build: PersistentMap (Transient)' | 100000 | 10,808,233.62 ns | 296.8750 | 218.7500 | - | 5185832 B | | Remove_ExtHashMap | 100 | 589.03 ns | 0.5178 | - | - | 4336 B |
| 'Build: MS Sorted (Builder)' | 100000 | 16,655,882.43 ns | 281.2500 | 250.0000 | - | 4800064 B | | Build_ImmDict | 1000 | 168,692.47 ns | 71.5332 | 6.8359 | - | 598712 B |
| 'Build: LanguageExt Map (AVL)' | 100000 | 39,932,734.83 ns | 5333.3333 | 3333.3333 | - | 89959040 B | | Build_ImmSortedDict | 1000 | 125,591.01 ns | 62.9883 | 5.1270 | - | 526896 B |
| 'Build: LanguageExt HashMap' | 100000 | 21,220,179.10 ns | 5781.2500 | 2968.7500 | 31.2500 | 96555422 B | | Build_ExtMap | 1000 | 117,763.81 ns | 72.3877 | 6.1035 | - | 605936 B |
| 'Read: PersistentMap' | 100000 | 7,359,807.97 ns | 710.9375 | - | - | 12000000 B | | Build_ExtHashMap | 1000 | 64,443.19 ns | 67.5049 | 1.5869 | - | 564864 B |
| 'Read: MS Sorted' | 100000 | 8,428,009.48 ns | - | - | - | - | | Build_PersistentMap | 1000 | 133,156.05 ns | 192.1387 | 7.8125 | - | 1607744 B |
| 'Read: LanguageExt Map' | 100000 | 10,268,884.43 ns | - | - | - | - | | Build_TransientMap | 1000 | 25,945.72 ns | 4.2725 | 0.1526 | - | 35976 B |
| 'Read: LanguageExt HashMap' | 100000 | 1,936,555.07 ns | - | - | - | - | | Retrieve_ImmDict | 1000 | 686.48 ns | - | - | - | - |
| 'Iterate: PersistentMap' | 100000 | 151,028.79 ns | - | - | - | - | | Retrieve_ImmSortedDict | 1000 | 1,145.83 ns | - | - | - | - |
| 'Iterate: MS Sorted' | 100000 | 1,068,072.16 ns | - | - | - | - | | Retrieve_ExtMap | 1000 | 2,276.07 ns | - | - | - | - |
| 'Iterate: LanguageExt Map' | 100000 | 837,677.39 ns | - | - | - | 32 B | | Retrieve_ExtHashMap | 1000 | 808.53 ns | - | - | - | - |
| 'Iterate: LanguageExt HashMap' | 100000 | 1,226,773.82 ns | 64.4531 | - | - | 1082432 B | | Retrieve_PersistentMap | 1000 | 680.50 ns | - | - | - | - |
| 'Set: PersistentMap' | 100000 | 208.61 ns | 0.1984 | 0.0024 | - | 3320 B | | Update_ImmDict | 1000 | 16,863.81 ns | 6.5613 | 0.2136 | - | 54960 B |
| 'Set: MS Sorted' | 100000 | 138.82 ns | 0.0458 | - | - | 768 B | | Update_PersistentMap | 1000 | 13,617.12 ns | 19.4092 | 1.1597 | - | 158.59 KB |
| 'Set: LanguageExt Map' | 100000 | 128.28 ns | 0.0448 | - | - | 752 B | | Update_TransientMap | 1000 | 3,611.03 ns | 2.5406 | 0.1564 | - | 21280 B |
| 'Set: LanguageExt HashMap' | 100000 | 84.33 ns | 0.0583 | - | - | 976 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 #+end_src
Lastly, here is a comparison of how things look compared to itself for when the prefixes are turned off for strings. This relies on regular linear string searches. This is however STILL a pretty good benchmark for all ordered dicts, since the strings are random, meaning the string comparison can stop almost immediately. For real world keys, all hash based dicts will be better, with everything regarding getting or setting a single key. * 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 linear string comparisons) and one uses the unicodstrategy which encodes the first 8 bytes as a long and uses avx to search for keys.
#+begin_src #+begin_src
| Method | N | KeyLength | Mean | Gen0 | Gen1 | Allocated | ```
|--------------------------- |------ |---------- |----------------:|---------:|---------:|----------:|
| 'Build: NiceBTree' | 10000 | 10 | 2,037,851.45 ns | 35.1563 | 15.6250 | 644600 B | BenchmarkDotNet v0.15.8, Linux Fedora Linux 43 (Container Image)
| 'Build: MS HashDict' | 10000 | 10 | 1,647,876.61 ns | 37.1094 | 15.6250 | 640096 B | AMD Ryzen 9 5900X 3.69GHz, 1 CPU, 24 logical and 12 physical cores
| 'Build: MS SortedDict' | 10000 | 10 | 3,853,709.48 ns | 31.2500 | 11.7188 | 560112 B | .NET SDK 10.0.104
| 'Build: LangExt HashMap' | 10000 | 10 | 1,612,117.07 ns | 472.6563 | 154.2969 | 7919328 B | [Host] : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3
| 'Build: LangExt Map' | 10000 | 10 | 5,363,298.26 ns | 507.8125 | 203.1250 | 8594784 B | ShortRun : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3
| 'Read: NiceBTree' | 10000 | 10 | 36.30 ns | - | - | - |
| 'Read: MS HashDict' | 10000 | 10 | 12.66 ns | - | - | - | Job=ShortRun IterationCount=3 LaunchCount=1
| 'Read: MS SortedDict' | 10000 | 10 | 233.59 ns | - | - | - | WarmupCount=3
| 'Read: LangExt HashMap' | 10000 | 10 | 28.61 ns | - | - | - |
| 'Read: LangExt Map' | 10000 | 10 | 268.13 ns | - | - | - | ```
| 'Iterate: NiceBTree' | 10000 | 10 | 12,630.95 ns | - | - | - | | Method | N | StringLength | Mean | Gen0 | Gen1 | Gen2 | Allocated |
| 'Iterate: MS HashDict' | 10000 | 10 | 151,314.44 ns | - | - | - | |--------------------------------- |------- |------------- |------------------:|-----------:|----------:|---------:|------------:|
| 'Iterate: MS SortedDict' | 10000 | 10 | 57,402.20 ns | - | - | - | | Build_TransientMap_Standard | 100 | 8 | 42,942.85 ns | 0.7324 | - | - | 6216 B |
| 'Iterate: LangExt HashMap' | 10000 | 10 | 148,980.47 ns | 10.0098 | - | 170712 B | | Build_TransientMap_Unicode | 100 | 8 | 12,289.81 ns | 0.8850 | 0.0153 | - | 7528 B |
| 'Iterate: LangExt Map' | 10000 | 10 | 34,428.07 ns | - | - | 32 B | | Build_ImmDict | 100 | 8 | 14,166.14 ns | 5.4016 | 0.0610 | - | 45280 B |
| 'Update: NiceBTree' | 10000 | 10 | 303.01 ns | 0.2027 | 0.0024 | 3392 B | | Build_ImmSortedDict | 100 | 8 | 22,037.60 ns | 4.2114 | 0.0305 | - | 35472 B |
| 'Update: MS HashDict' | 10000 | 10 | 48.36 ns | 0.0100 | - | 168 B | | Build_ExtMap | 100 | 8 | 22,837.07 ns | 5.6152 | 0.0610 | - | 47104 B |
| 'Update: MS SortedDict' | 10000 | 10 | 137.47 ns | 0.0196 | - | 328 B | | Build_ExtHashMap | 100 | 8 | 8,907.91 ns | 3.9673 | 0.0305 | - | 33240 B |
| 'Update: LangExt HashMap' | 10000 | 10 | 102.57 ns | 0.0502 | 0.0001 | 840 B | | Retrieve_ImmDict | 100 | 8 | 80.81 ns | - | - | - | - |
| 'Update: LangExt Map' | 10000 | 10 | 122.54 ns | 0.0186 | - | 312 B | | Retrieve_PersistentMap_Standard | 100 | 8 | 4,956.86 ns | 0.0534 | - | - | 480 B |
| 'Build: NiceBTree' | 10000 | 50 | 2,020,984.87 ns | 35.1563 | 11.7188 | 624248 B | | Retrieve_PersistentMap_Unicode | 100 | 8 | 148.61 ns | - | - | - | - |
| 'Build: MS HashDict' | 10000 | 50 | 1,811,186.24 ns | 37.1094 | 15.6250 | 640096 B | | Update_ImmDict | 100 | 8 | 1,089.03 ns | 0.4215 | - | - | 3536 B |
| 'Build: MS SortedDict' | 10000 | 50 | 3,883,214.25 ns | 31.2500 | 15.6250 | 560112 B | | Update_PersistentMap_Standard | 100 | 8 | 8,182.15 ns | 2.3956 | 0.0305 | - | 20160 B |
| 'Build: LangExt HashMap' | 10000 | 50 | 1,784,616.64 ns | 472.6563 | 154.2969 | 7926712 B | | Update_PersistentMap_Unicode | 100 | 8 | 1,954.07 ns | 2.7046 | 0.0496 | - | 22640 B |
| 'Build: LangExt Map' | 10000 | 50 | 5,248,030.22 ns | 507.8125 | 203.1250 | 8544720 B | | Update_TransientMap_Standard | 100 | 8 | 7,007.11 ns | 0.4349 | - | - | 3640 B |
| 'Read: NiceBTree' | 10000 | 50 | 40.64 ns | - | - | - | | Update_TransientMap_Unicode | 100 | 8 | 667.55 ns | 0.4644 | 0.0057 | - | 3888 B |
| 'Read: MS HashDict' | 10000 | 50 | 29.91 ns | - | - | - | | Update_ImmSortedDict | 100 | 8 | 1,741.49 ns | 0.3662 | - | - | 3072 B |
| 'Read: MS SortedDict' | 10000 | 50 | 255.55 ns | - | - | - | | Update_ExtMap | 100 | 8 | 1,812.71 ns | 0.4120 | - | - | 3456 B |
| 'Read: LangExt HashMap' | 10000 | 50 | 47.61 ns | - | - | - | | Update_ExtHashMap | 100 | 8 | 767.69 ns | 0.5360 | 0.0010 | - | 4488 B |
| 'Read: LangExt Map' | 10000 | 50 | 255.68 ns | - | - | - | | UpdateSet_ImmDict | 100 | 8 | 1,508.45 ns | 0.5589 | 0.0019 | - | 4688 B |
| 'Iterate: NiceBTree' | 10000 | 50 | 12,718.71 ns | - | - | - | | UpdateSet_PersistentMap_Standard | 100 | 8 | 6,877.98 ns | 2.4033 | 0.0381 | - | 20160 B |
| 'Iterate: MS HashDict' | 10000 | 50 | 170,815.59 ns | - | - | - | | UpdateSet_PersistentMap_Unicode | 100 | 8 | 2,660.42 ns | 2.7046 | 0.0534 | - | 22640 B |
| 'Iterate: MS SortedDict' | 10000 | 50 | 68,982.58 ns | - | - | - | | UpdateSet_TransientMap_Standard | 100 | 8 | 5,415.27 ns | 0.4349 | - | - | 3640 B |
| 'Iterate: LangExt HashMap' | 10000 | 50 | 144,442.27 ns | 9.7656 | - | 165600 B | | UpdateSet_TransientMap_Unicode | 100 | 8 | 1,212.47 ns | 0.4635 | 0.0057 | - | 3888 B |
| 'Iterate: LangExt Map' | 10000 | 50 | 35,082.49 ns | - | - | 32 B | | UpdateSet_ImmSortedDict | 100 | 8 | 2,424.14 ns | 0.4578 | - | - | 3840 B |
| 'Update: NiceBTree' | 10000 | 50 | 393.56 ns | 0.2027 | 0.0024 | 3392 B | | UpdateSet_ExtMap | 100 | 8 | 2,111.13 ns | 0.4730 | - | - | 3960 B |
| 'Update: MS HashDict' | 10000 | 50 | 114.57 ns | 0.0215 | - | 360 B | | UpdateSet_ExtHashMap | 100 | 8 | 876.06 ns | 0.5646 | 0.0019 | - | 4728 B |
| 'Update: MS SortedDict' | 10000 | 50 | 65.51 ns | 0.0129 | - | 216 B | | Iterate_ImmDict | 100 | 8 | 1,272.01 ns | - | - | - | - |
| 'Update: LangExt HashMap' | 10000 | 50 | 103.28 ns | 0.0535 | - | 896 B | | Iterate_PersistentMap_Standard | 100 | 8 | 191.98 ns | - | - | - | - |
| 'Update: LangExt Map' | 10000 | 50 | 67.62 ns | 0.0119 | - | 200 B | | Iterate_ImmSortedDict | 100 | 8 | 475.35 ns | - | - | - | - |
| Iterate_ExtMap | 100 | 8 | 322.36 ns | 0.0038 | - | - | 32 B |
| Iterate_ExtHashMap | 100 | 8 | 1,179.25 ns | 0.2747 | - | - | 2304 B |
| Iterate_PersistentMap_Unicode | 100 | 8 | 191.41 ns | - | - | - | - |
| Remove_ImmDict | 100 | 8 | 1,420.61 ns | 0.5360 | 0.0019 | - | 4496 B |
| Remove_PersistentMap_Standard | 100 | 8 | 8,294.72 ns | 2.4261 | 0.0305 | - | 20400 B |
| Remove_PersistentMap_Unicode | 100 | 8 | 2,597.23 ns | 2.6779 | 0.0496 | - | 22400 B |
| Remove_TransientMap_Standard | 100 | 8 | 7,178.94 ns | 0.4654 | - | - | 3928 B |
| Remove_TransientMap_Unicode | 100 | 8 | 1,446.12 ns | 0.4406 | 0.0057 | - | 3688 B |
| Remove_ImmSortedDict | 100 | 8 | 2,031.56 ns | 0.3777 | - | - | 3168 B |
| Remove_ExtMap | 100 | 8 | 2,274.33 ns | 0.5798 | - | - | 4856 B |
| Remove_ExtHashMap | 100 | 8 | 817.02 ns | 0.5102 | 0.0019 | - | 4272 B |
| Build_TransientMap_Standard | 100 | 50 | 43,518.39 ns | 0.7324 | - | - | 6216 B |
| Build_TransientMap_Unicode | 100 | 50 | 12,061.37 ns | 0.8850 | 0.0153 | - | 7528 B |
| Build_ImmDict | 100 | 50 | 16,497.93 ns | 5.4016 | 0.0610 | - | 45216 B |
| Build_ImmSortedDict | 100 | 50 | 22,820.20 ns | 4.2114 | 0.0305 | - | 35232 B |
| Build_ExtMap | 100 | 50 | 22,925.57 ns | 5.6458 | 0.0610 | - | 47328 B |
| Build_ExtHashMap | 100 | 50 | 13,019.93 ns | 4.3945 | 0.0305 | - | 36776 B |
| Retrieve_ImmDict | 100 | 50 | 286.77 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 100 | 50 | 4,620.16 ns | 0.0534 | - | - | 480 B |
| Retrieve_PersistentMap_Unicode | 100 | 50 | 158.67 ns | - | - | - | - |
| Update_ImmDict | 100 | 50 | 1,490.11 ns | 0.4902 | 0.0019 | - | 4112 B |
| Update_PersistentMap_Standard | 100 | 50 | 7,007.05 ns | 2.4033 | 0.0381 | - | 20160 B |
| Update_PersistentMap_Unicode | 100 | 50 | 1,976.44 ns | 2.7046 | 0.0534 | - | 22640 B |
| Update_TransientMap_Standard | 100 | 50 | 6,208.54 ns | 0.4349 | - | - | 3640 B |
| Update_TransientMap_Unicode | 100 | 50 | 665.76 ns | 0.4644 | 0.0057 | - | 3888 B |
| Update_ImmSortedDict | 100 | 50 | 2,133.72 ns | 0.4120 | - | - | 3456 B |
| Update_ExtMap | 100 | 50 | 1,842.17 ns | 0.4253 | - | - | 3568 B |
| Update_ExtHashMap | 100 | 50 | 971.20 ns | 0.5474 | 0.0019 | - | 4592 B |
| UpdateSet_ImmDict | 100 | 50 | 1,816.66 ns | 0.6046 | 0.0019 | - | 5072 B |
| UpdateSet_PersistentMap_Standard | 100 | 50 | 6,616.38 ns | 2.4033 | 0.0381 | - | 20160 B |
| UpdateSet_PersistentMap_Unicode | 100 | 50 | 2,517.24 ns | 2.7046 | 0.0496 | - | 22640 B |
| UpdateSet_TransientMap_Standard | 100 | 50 | 5,282.92 ns | 0.4349 | - | - | 3640 B |
| UpdateSet_TransientMap_Unicode | 100 | 50 | 1,214.38 ns | 0.4635 | 0.0057 | - | 3888 B |
| UpdateSet_ImmSortedDict | 100 | 50 | 2,531.36 ns | 0.4730 | - | - | 3984 B |
| UpdateSet_ExtMap | 100 | 50 | 2,399.90 ns | 0.5913 | - | - | 4968 B |
| UpdateSet_ExtHashMap | 100 | 50 | 1,041.57 ns | 0.5512 | 0.0019 | - | 4624 B |
| Iterate_ImmDict | 100 | 50 | 1,265.60 ns | - | - | - | - |
| Iterate_PersistentMap_Standard | 100 | 50 | 183.51 ns | - | - | - | - |
| Iterate_ImmSortedDict | 100 | 50 | 487.68 ns | - | - | - | - |
| Iterate_ExtMap | 100 | 50 | 313.58 ns | 0.0038 | - | - | 32 B |
| Iterate_ExtHashMap | 100 | 50 | 1,174.73 ns | 0.2747 | - | - | 2304 B |
| Iterate_PersistentMap_Unicode | 100 | 50 | 190.36 ns | - | - | - | - |
| Remove_ImmDict | 100 | 50 | 1,566.27 ns | 0.5283 | 0.0019 | - | 4432 B |
| Remove_PersistentMap_Standard | 100 | 50 | 7,539.09 ns | 2.4338 | 0.0381 | - | 20400 B |
| Remove_PersistentMap_Unicode | 100 | 50 | 2,572.60 ns | 2.6779 | 0.0458 | - | 22400 B |
| Remove_TransientMap_Standard | 100 | 50 | 6,450.13 ns | 0.4654 | - | - | 3928 B |
| Remove_TransientMap_Unicode | 100 | 50 | 1,408.78 ns | 0.4406 | 0.0057 | - | 3688 B |
| Remove_ImmSortedDict | 100 | 50 | 2,241.88 ns | 0.3891 | - | - | 3264 B |
| Remove_ExtMap | 100 | 50 | 2,156.69 ns | 0.4387 | - | - | 3680 B |
| Remove_ExtHashMap | 100 | 50 | 927.47 ns | 0.4807 | 0.0010 | - | 4024 B |
| Build_TransientMap_Standard | 1000 | 8 | 741,046.07 ns | 5.8594 | - | - | 49512 B |
| Build_TransientMap_Unicode | 1000 | 8 | 176,282.35 ns | 7.3242 | 0.7324 | - | 62248 B |
| Build_ImmDict | 1000 | 8 | 251,462.15 ns | 79.1016 | 8.3008 | - | 663488 B |
| Build_ImmSortedDict | 1000 | 8 | 420,903.44 ns | 61.0352 | 4.8828 | - | 513312 B |
| Build_ExtMap | 1000 | 8 | 426,942.00 ns | 79.1016 | 7.3242 | - | 662448 B |
| Build_ExtHashMap | 1000 | 8 | 154,509.50 ns | 70.3125 | 4.8828 | - | 589264 B |
| Retrieve_ImmDict | 1000 | 8 | 1,057.94 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 1000 | 8 | 71,081.84 ns | 0.4883 | - | - | 4800 B |
| Retrieve_PersistentMap_Unicode | 1000 | 8 | 2,056.30 ns | - | - | - | - |
| Update_ImmDict | 1000 | 8 | 20,613.44 ns | 7.4768 | 0.2747 | - | 62688 B |
| Update_PersistentMap_Standard | 1000 | 8 | 91,445.80 ns | 24.0479 | 2.3193 | - | 201600 B |
| Update_PersistentMap_Unicode | 1000 | 8 | 29,005.34 ns | 27.0386 | 2.5330 | - | 226400 B |
| Update_TransientMap_Standard | 1000 | 8 | 85,352.16 ns | 4.5166 | 0.4883 | - | 38184 B |
| Update_TransientMap_Unicode | 1000 | 8 | 7,968.40 ns | 4.4250 | 0.4425 | - | 37024 B |
| Update_ImmSortedDict | 1000 | 8 | 32,334.14 ns | 5.7373 | 0.1221 | - | 48288 B |
| Update_ExtMap | 1000 | 8 | 34,280.75 ns | 6.4087 | 0.1831 | - | 53824 B |
| Update_ExtHashMap | 1000 | 8 | 11,163.54 ns | 7.1716 | 0.2289 | - | 60016 B |
| UpdateSet_ImmDict | 1000 | 8 | 23,570.66 ns | 8.3618 | 0.3967 | - | 69984 B |
| UpdateSet_PersistentMap_Standard | 1000 | 8 | 103,655.77 ns | 24.0479 | 2.1973 | - | 201600 B |
| UpdateSet_PersistentMap_Unicode | 1000 | 8 | 33,858.71 ns | 27.0386 | 2.6245 | - | 226400 B |
| UpdateSet_TransientMap_Standard | 1000 | 8 | 86,914.93 ns | 4.1504 | 0.3662 | - | 35368 B |
| UpdateSet_TransientMap_Unicode | 1000 | 8 | 12,939.69 ns | 4.5929 | 0.4883 | - | 38432 B |
| UpdateSet_ImmSortedDict | 1000 | 8 | 37,789.08 ns | 6.3477 | 0.2441 | - | 53232 B |
| UpdateSet_ExtMap | 1000 | 8 | 38,304.46 ns | 7.6904 | 0.3052 | - | 64576 B |
| UpdateSet_ExtHashMap | 1000 | 8 | 14,695.01 ns | 7.7057 | 0.2747 | - | 64464 B |
| Iterate_ImmDict | 1000 | 8 | 13,240.07 ns | - | - | - | - |
| Iterate_PersistentMap_Standard | 1000 | 8 | 1,628.88 ns | - | - | - | - |
| Iterate_ImmSortedDict | 1000 | 8 | 4,933.29 ns | - | - | - | - |
| Iterate_ExtMap | 1000 | 8 | 3,191.58 ns | 0.0038 | - | - | 32 B |
| Iterate_ExtHashMap | 1000 | 8 | 14,380.64 ns | 2.6550 | - | - | 22320 B |
| Iterate_PersistentMap_Unicode | 1000 | 8 | 1,647.93 ns | - | - | - | - |
| Remove_ImmDict | 1000 | 8 | 20,527.32 ns | 7.6599 | 0.3052 | - | 64160 B |
| Remove_PersistentMap_Standard | 1000 | 8 | 106,140.73 ns | 24.2920 | 1.5869 | - | 204000 B |
| Remove_PersistentMap_Unicode | 1000 | 8 | 30,242.16 ns | 26.7639 | 2.4414 | - | 224000 B |
| Remove_TransientMap_Standard | 1000 | 8 | 91,057.33 ns | 4.6387 | 0.4883 | - | 39224 B |
| Remove_TransientMap_Unicode | 1000 | 8 | 14,218.28 ns | 3.7994 | 0.3662 | - | 31848 B |
| Remove_ImmSortedDict | 1000 | 8 | 33,413.24 ns | 5.6763 | 0.1221 | - | 47616 B |
| Remove_ExtMap | 1000 | 8 | 37,472.01 ns | 6.8970 | 0.1831 | - | 57744 B |
| Remove_ExtHashMap | 1000 | 8 | 12,185.93 ns | 7.9803 | 0.2441 | - | 66864 B |
| Build_TransientMap_Standard | 1000 | 50 | 727,350.80 ns | 4.8828 | - | - | 44992 B |
| Build_TransientMap_Unicode | 1000 | 50 | 176,948.61 ns | 7.0801 | 0.4883 | - | 59368 B |
| Build_ImmDict | 1000 | 50 | 285,938.59 ns | 78.6133 | 8.3008 | - | 660096 B |
| Build_ImmSortedDict | 1000 | 50 | 433,882.40 ns | 61.0352 | 5.3711 | - | 511344 B |
| Build_ExtMap | 1000 | 50 | 412,922.58 ns | 78.1250 | 7.3242 | - | 656288 B |
| Build_ExtHashMap | 1000 | 50 | 185,309.94 ns | 70.3125 | 4.8828 | - | 589128 B |
| Retrieve_ImmDict | 1000 | 50 | 3,151.77 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 1000 | 50 | 76,705.78 ns | 0.4883 | - | - | 4800 B |
| Retrieve_PersistentMap_Unicode | 1000 | 50 | 2,077.37 ns | - | - | - | - |
| Update_ImmDict | 1000 | 50 | 23,312.29 ns | 7.5073 | 0.3052 | - | 62880 B |
| Update_PersistentMap_Standard | 1000 | 50 | 93,945.90 ns | 24.0479 | 2.0752 | - | 201600 B |
| Update_PersistentMap_Unicode | 1000 | 50 | 29,787.05 ns | 27.0386 | 2.2888 | - | 226400 B |
| Update_TransientMap_Standard | 1000 | 50 | 85,349.42 ns | 3.7842 | 0.2441 | - | 32552 B |
| Update_TransientMap_Unicode | 1000 | 50 | 7,349.19 ns | 4.0894 | 0.4044 | - | 34208 B |
| Update_ImmSortedDict | 1000 | 50 | 32,705.50 ns | 5.6763 | 0.1221 | - | 47952 B |
| Update_ExtMap | 1000 | 50 | 33,742.93 ns | 6.5308 | 0.1831 | - | 54720 B |
| Update_ExtHashMap | 1000 | 50 | 13,720.77 ns | 7.2479 | 0.2289 | - | 60688 B |
| UpdateSet_ImmDict | 1000 | 50 | 24,725.01 ns | 8.0566 | 0.3967 | - | 67424 B |
| UpdateSet_PersistentMap_Standard | 1000 | 50 | 106,965.60 ns | 24.1699 | 1.9531 | - | 202504 B |
| UpdateSet_PersistentMap_Unicode | 1000 | 50 | 34,398.72 ns | 27.2217 | 2.6245 | - | 227840 B |
| UpdateSet_TransientMap_Standard | 1000 | 50 | 89,942.92 ns | 3.9063 | 0.3662 | - | 33456 B |
| UpdateSet_TransientMap_Unicode | 1000 | 50 | 13,555.52 ns | 4.4250 | 0.4578 | - | 37056 B |
| UpdateSet_ImmSortedDict | 1000 | 50 | 37,035.94 ns | 6.2866 | 0.1831 | - | 53088 B |
| UpdateSet_ExtMap | 1000 | 50 | 39,025.94 ns | 7.8735 | 0.3052 | - | 66200 B |
| UpdateSet_ExtHashMap | 1000 | 50 | 15,654.51 ns | 7.5989 | 0.2441 | - | 63608 B |
| Iterate_ImmDict | 1000 | 50 | 12,858.73 ns | - | - | - | - |
| Iterate_PersistentMap_Standard | 1000 | 50 | 1,674.98 ns | - | - | - | - |
| Iterate_ImmSortedDict | 1000 | 50 | 4,882.38 ns | - | - | - | - |
| Iterate_ExtMap | 1000 | 50 | 3,498.40 ns | 0.0038 | - | - | 32 B |
| Iterate_ExtHashMap | 1000 | 50 | 14,585.65 ns | 2.7008 | - | - | 22608 B |
| Iterate_PersistentMap_Unicode | 1000 | 50 | 1,654.17 ns | - | - | - | - |
| Remove_ImmDict | 1000 | 50 | 24,001.50 ns | 7.6904 | 0.3052 | - | 64480 B |
| Remove_PersistentMap_Standard | 1000 | 50 | 115,583.75 ns | 24.2920 | 1.7090 | - | 204000 B |
| Remove_PersistentMap_Unicode | 1000 | 50 | 30,177.90 ns | 26.7639 | 2.2583 | - | 224000 B |
| Remove_TransientMap_Standard | 1000 | 50 | 104,629.87 ns | 3.9063 | 0.2441 | - | 33592 B |
| Remove_TransientMap_Unicode | 1000 | 50 | 13,859.15 ns | 3.2959 | 0.2899 | - | 27624 B |
| Remove_ImmSortedDict | 1000 | 50 | 34,305.81 ns | 5.6763 | 0.1221 | - | 47664 B |
| Remove_ExtMap | 1000 | 50 | 36,962.41 ns | 6.9580 | 0.2441 | - | 58640 B |
| Remove_ExtHashMap | 1000 | 50 | 14,045.01 ns | 7.8583 | 0.1984 | - | 65800 B |
| Build_TransientMap_Standard | 10000 | 8 | 10,481,091.44 ns | 46.8750 | 15.6250 | - | 453272 B |
| Build_TransientMap_Unicode | 10000 | 8 | 2,288,777.17 ns | 66.4063 | 19.5313 | - | 576584 B |
| Build_ImmDict | 10000 | 8 | 4,451,716.58 ns | 1046.8750 | 500.0000 | - | 8790528 B |
| Build_ImmSortedDict | 10000 | 8 | 6,589,276.41 ns | 804.6875 | 281.2500 | - | 6767232 B |
| Build_ExtMap | 10000 | 8 | 6,466,222.77 ns | 1015.6250 | 437.5000 | - | 8542480 B |
| Build_ExtHashMap | 10000 | 8 | 1,987,067.22 ns | 945.3125 | 312.5000 | - | 7921664 B |
| Retrieve_ImmDict | 10000 | 8 | 23,653.66 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 10000 | 8 | 1,067,006.38 ns | 7.8125 | - | - | 72000 B |
| Retrieve_PersistentMap_Unicode | 10000 | 8 | 33,794.37 ns | - | - | - | - |
| Update_ImmDict | 10000 | 8 | 393,904.16 ns | 100.5859 | 25.3906 | - | 844992 B |
| Update_PersistentMap_Standard | 10000 | 8 | 1,373,414.67 ns | 304.6875 | 132.8125 | - | 2560000 B |
| Update_PersistentMap_Unicode | 10000 | 8 | 416,266.85 ns | 366.2109 | 160.1563 | - | 3064000 B |
| Update_TransientMap_Standard | 10000 | 8 | 1,160,717.93 ns | 41.0156 | 13.6719 | - | 346280 B |
| Update_TransientMap_Unicode | 10000 | 8 | 154,306.58 ns | 40.7715 | 16.6016 | - | 341792 B |
| Update_ImmSortedDict | 10000 | 8 | 583,839.85 ns | 76.1719 | 15.6250 | - | 640368 B |
| Update_ExtMap | 10000 | 8 | 571,924.12 ns | 87.8906 | 22.4609 | - | 735640 B |
| Update_ExtHashMap | 10000 | 8 | 164,331.80 ns | 102.7832 | 22.9492 | - | 860152 B |
| UpdateSet_ImmDict | 10000 | 8 | 433,669.48 ns | 108.3984 | 29.2969 | - | 907072 B |
| UpdateSet_PersistentMap_Standard | 10000 | 8 | 1,426,063.43 ns | 306.6406 | 130.8594 | - | 2572328 B |
| UpdateSet_PersistentMap_Unicode | 10000 | 8 | 502,233.19 ns | 368.1641 | 154.2969 | - | 3086432 B |
| UpdateSet_TransientMap_Standard | 10000 | 8 | 1,183,758.86 ns | 41.0156 | 15.6250 | - | 357200 B |
| UpdateSet_TransientMap_Unicode | 10000 | 8 | 224,362.69 ns | 43.7012 | 16.3574 | - | 365632 B |
| UpdateSet_ImmSortedDict | 10000 | 8 | 648,222.74 ns | 82.0313 | 19.5313 | - | 687648 B |
| UpdateSet_ExtMap | 10000 | 8 | 638,998.68 ns | 99.6094 | 25.3906 | - | 833752 B |
| UpdateSet_ExtHashMap | 10000 | 8 | 188,297.97 ns | 104.9805 | 23.9258 | - | 878640 B |
| Iterate_ImmDict | 10000 | 8 | 180,912.56 ns | - | - | - | - |
| Iterate_PersistentMap_Standard | 10000 | 8 | 17,134.49 ns | - | - | - | - |
| Iterate_ImmSortedDict | 10000 | 8 | 55,355.92 ns | - | - | - | - |
| Iterate_ExtMap | 10000 | 8 | 66,171.51 ns | - | - | - | 32 B |
| Iterate_ExtHashMap | 10000 | 8 | 173,270.08 ns | 20.2637 | - | - | 171360 B |
| Iterate_PersistentMap_Unicode | 10000 | 8 | 17,291.12 ns | - | - | - | - |
| Remove_ImmDict | 10000 | 8 | 398,833.85 ns | 102.0508 | 24.9023 | - | 857216 B |
| Remove_PersistentMap_Standard | 10000 | 8 | 1,488,081.64 ns | 310.5469 | 123.0469 | - | 2608000 B |
| Remove_PersistentMap_Unicode | 10000 | 8 | 492,391.59 ns | 363.2813 | 180.6641 | - | 3041408 B |
| Remove_TransientMap_Standard | 10000 | 8 | 1,334,741.70 ns | 44.9219 | 15.6250 | - | 388696 B |
| Remove_TransientMap_Unicode | 10000 | 8 | 224,694.52 ns | 38.0859 | 15.1367 | - | 319240 B |
| Remove_ImmSortedDict | 10000 | 8 | 624,219.05 ns | 77.1484 | 15.6250 | - | 652944 B |
| Remove_ExtMap | 10000 | 8 | 648,812.07 ns | 91.7969 | 20.5078 | - | 774000 B |
| Remove_ExtHashMap | 10000 | 8 | 187,697.63 ns | 104.2480 | 21.4844 | - | 873600 B |
| Build_TransientMap_Standard | 10000 | 50 | 11,121,008.03 ns | 46.8750 | 15.6250 | - | 453272 B |
| Build_TransientMap_Unicode | 10000 | 50 | 2,345,447.27 ns | 66.4063 | 23.4375 | - | 567112 B |
| Build_ImmDict | 10000 | 50 | 4,711,900.43 ns | 1039.0625 | 507.8125 | - | 8746752 B |
| Build_ImmSortedDict | 10000 | 50 | 6,774,329.88 ns | 804.6875 | 265.6250 | - | 6751584 B |
| Build_ExtMap | 10000 | 50 | 6,748,429.60 ns | 1015.6250 | 429.6875 | - | 8544720 B |
| Build_ExtHashMap | 10000 | 50 | 2,211,770.12 ns | 941.4063 | 332.0313 | - | 7894224 B |
| Retrieve_ImmDict | 10000 | 50 | 53,674.39 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 10000 | 50 | 1,099,621.23 ns | 7.8125 | - | - | 72000 B |
| Retrieve_PersistentMap_Unicode | 10000 | 50 | 34,282.21 ns | - | - | - | - |
| Update_ImmDict | 10000 | 50 | 416,110.39 ns | 101.0742 | 24.9023 | - | 845696 B |
| Update_PersistentMap_Standard | 10000 | 50 | 1,432,050.69 ns | 304.6875 | 125.0000 | - | 2560000 B |
| Update_PersistentMap_Unicode | 10000 | 50 | 482,376.79 ns | 366.2109 | 158.2031 | - | 3064000 B |
| Update_TransientMap_Standard | 10000 | 50 | 1,185,346.37 ns | 39.0625 | 11.7188 | - | 339240 B |
| Update_TransientMap_Unicode | 10000 | 50 | 153,479.49 ns | 39.5508 | 13.9160 | - | 331136 B |
| Update_ImmSortedDict | 10000 | 50 | 578,927.03 ns | 76.1719 | 14.6484 | - | 641616 B |
| Update_ExtMap | 10000 | 50 | 582,689.89 ns | 86.9141 | 20.5078 | - | 732896 B |
| Update_ExtHashMap | 10000 | 50 | 195,769.80 ns | 102.5391 | 20.5078 | - | 859328 B |
| UpdateSet_ImmDict | 10000 | 50 | 460,644.35 ns | 109.3750 | 30.2734 | - | 915264 B |
| UpdateSet_PersistentMap_Standard | 10000 | 50 | 1,488,123.25 ns | 306.6406 | 132.8125 | - | 2575040 B |
| UpdateSet_PersistentMap_Unicode | 10000 | 50 | 516,710.59 ns | 368.1641 | 152.3438 | - | 3085600 B |
| UpdateSet_TransientMap_Standard | 10000 | 50 | 1,257,184.91 ns | 41.0156 | 13.6719 | - | 355688 B |
| UpdateSet_TransientMap_Unicode | 10000 | 50 | 222,602.17 ns | 42.2363 | 15.8691 | - | 354144 B |
| UpdateSet_ImmSortedDict | 10000 | 50 | 659,407.65 ns | 82.0313 | 19.5313 | - | 691728 B |
| UpdateSet_ExtMap | 10000 | 50 | 652,772.00 ns | 98.6328 | 23.4375 | - | 829552 B |
| UpdateSet_ExtHashMap | 10000 | 50 | 216,637.22 ns | 104.9805 | 24.1699 | - | 878840 B |
| Iterate_ImmDict | 10000 | 50 | 172,211.95 ns | - | - | - | - |
| Iterate_PersistentMap_Standard | 10000 | 50 | 17,227.48 ns | - | - | - | - |
| Iterate_ImmSortedDict | 10000 | 50 | 56,105.18 ns | - | - | - | - |
| Iterate_ExtMap | 10000 | 50 | 70,322.76 ns | - | - | - | 32 B |
| Iterate_ExtHashMap | 10000 | 50 | 176,803.08 ns | 20.0195 | - | - | 168264 B |
| Iterate_PersistentMap_Unicode | 10000 | 50 | 17,473.52 ns | - | - | - | - |
| Remove_ImmDict | 10000 | 50 | 429,520.72 ns | 102.5391 | 28.3203 | - | 860032 B |
| Remove_PersistentMap_Standard | 10000 | 50 | 1,558,056.72 ns | 310.5469 | 125.0000 | - | 2610816 B |
| Remove_PersistentMap_Unicode | 10000 | 50 | 522,677.95 ns | 363.2813 | 158.2031 | - | 3042816 B |
| Remove_TransientMap_Standard | 10000 | 50 | 1,333,105.09 ns | 44.9219 | 15.6250 | - | 391512 B |
| Remove_TransientMap_Unicode | 10000 | 50 | 223,026.23 ns | 37.3535 | 13.1836 | - | 312808 B |
| Remove_ImmSortedDict | 10000 | 50 | 611,137.62 ns | 77.1484 | 15.6250 | - | 651840 B |
| Remove_ExtMap | 10000 | 50 | 630,117.55 ns | 91.7969 | 21.4844 | - | 771256 B |
| Remove_ExtHashMap | 10000 | 50 | 208,051.51 ns | 104.7363 | 20.7520 | - | 877216 B |
| Build_TransientMap_Standard | 100000 | 8 | 158,074,169.00 ns | 500.0000 | 250.0000 | - | 4524208 B |
| Build_TransientMap_Unicode | 100000 | 8 | 40,354,289.36 ns | 692.3077 | 538.4615 | - | 5798728 B |
| Build_ImmDict | 100000 | 8 | 95,840,918.80 ns | 13200.0000 | 4600.0000 | 200.0000 | 109352965 B |
| Build_ImmSortedDict | 100000 | 8 | 145,451,097.25 ns | 10000.0000 | 3750.0000 | - | 83946432 B |
| Build_ExtMap | 100000 | 8 | 121,026,729.25 ns | 12500.0000 | 5750.0000 | - | 104660688 B |
| Build_ExtHashMap | 100000 | 8 | 45,582,232.06 ns | 12000.0000 | 2583.3333 | 83.3333 | 99799303 B |
| Retrieve_ImmDict | 100000 | 8 | 1,518,946.53 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 100000 | 8 | 15,954,066.23 ns | 93.7500 | - | - | 960000 B |
| Retrieve_PersistentMap_Unicode | 100000 | 8 | 1,344,373.24 ns | - | - | - | - |
| Update_ImmDict | 100000 | 8 | 9,168,537.50 ns | 1265.6250 | 1062.5000 | - | 10588288 B |
| Update_PersistentMap_Standard | 100000 | 8 | 24,537,720.52 ns | 3718.7500 | 2906.2500 | 31.2500 | 31040034 B |
| Update_PersistentMap_Unicode | 100000 | 8 | 11,821,730.59 ns | 4671.8750 | 2500.0000 | 62.5000 | 38640073 B |
| Update_TransientMap_Standard | 100000 | 8 | 18,410,364.31 ns | 406.2500 | 312.5000 | - | 3415688 B |
| Update_TransientMap_Unicode | 100000 | 8 | 3,724,912.85 ns | 414.0625 | 351.5625 | - | 3487208 B |
| Update_ImmSortedDict | 100000 | 8 | 11,002,667.72 ns | 953.1250 | 781.2500 | - | 8020080 B |
| Update_ExtMap | 100000 | 8 | 11,289,391.77 ns | 1093.7500 | 937.5000 | - | 9246400 B |
| Update_ExtHashMap | 100000 | 8 | 3,795,553.44 ns | 1265.6250 | 968.7500 | - | 10624032 B |
| UpdateSet_ImmDict | 100000 | 8 | 9,324,688.19 ns | 1343.7500 | 1046.8750 | - | 11264256 B |
| UpdateSet_PersistentMap_Standard | 100000 | 8 | 28,227,498.72 ns | 3750.0000 | 2593.7500 | 31.2500 | 31143252 B |
| UpdateSet_PersistentMap_Unicode | 100000 | 8 | 12,980,484.49 ns | 4703.1250 | 2250.0000 | 62.5000 | 38809434 B |
| UpdateSet_TransientMap_Standard | 100000 | 8 | 18,455,887.50 ns | 406.2500 | 312.5000 | - | 3514688 B |
| UpdateSet_TransientMap_Unicode | 100000 | 8 | 4,599,394.95 ns | 429.6875 | 382.8125 | - | 3636872 B |
| UpdateSet_ImmSortedDict | 100000 | 8 | 11,611,425.71 ns | 1015.6250 | 875.0000 | - | 8495856 B |
| UpdateSet_ExtMap | 100000 | 8 | 13,256,339.44 ns | 1218.7500 | 1015.6250 | - | 10287328 B |
| UpdateSet_ExtHashMap | 100000 | 8 | 4,056,833.05 ns | 1281.2500 | 992.1875 | - | 10752664 B |
| Iterate_ImmDict | 100000 | 8 | 2,245,167.14 ns | - | - | - | 96 B |
| Iterate_PersistentMap_Standard | 100000 | 8 | 226,078.34 ns | - | - | - | - |
| Iterate_ImmSortedDict | 100000 | 8 | 752,117.01 ns | - | - | - | - |
| Iterate_ExtMap | 100000 | 8 | 1,153,701.70 ns | - | - | - | 32 B |
| Iterate_ExtHashMap | 100000 | 8 | 2,747,144.18 ns | 273.4375 | - | - | 2318432 B |
| Iterate_PersistentMap_Unicode | 100000 | 8 | 241,814.65 ns | - | - | - | - |
| Remove_ImmDict | 100000 | 8 | 8,922,500.09 ns | 1281.2500 | 1031.2500 | - | 10744896 B |
| Remove_PersistentMap_Standard | 100000 | 8 | 28,366,797.02 ns | 3812.5000 | 2750.0000 | 31.2500 | 31781143 B |
| Remove_PersistentMap_Unicode | 100000 | 8 | 12,499,542.97 ns | 4640.6250 | 2875.0000 | 62.5000 | 38412730 B |
| Remove_TransientMap_Standard | 100000 | 8 | 21,085,953.04 ns | 468.7500 | 343.7500 | - | 4134328 B |
| Remove_TransientMap_Unicode | 100000 | 8 | 3,227,138.07 ns | 382.8125 | 324.2188 | - | 3226088 B |
| Remove_ImmSortedDict | 100000 | 8 | 10,475,303.83 ns | 953.1250 | 781.2500 | - | 8031888 B |
| Remove_ExtMap | 100000 | 8 | 11,473,612.68 ns | 1140.6250 | 937.5000 | - | 9630448 B |
| Remove_ExtHashMap | 100000 | 8 | 3,920,078.08 ns | 1265.6250 | 914.0625 | - | 10624344 B |
| Build_TransientMap_Standard | 100000 | 50 | 186,991,900.50 ns | 500.0000 | - | - | 4511632 B |
| Build_TransientMap_Unicode | 100000 | 50 | 42,267,081.83 ns | 666.6667 | 500.0000 | - | 5763784 B |
| Build_ImmDict | 100000 | 50 | 91,363,990.94 ns | 13166.6667 | 4833.3333 | 166.6667 | 109457840 B |
| Build_ImmSortedDict | 100000 | 50 | 134,058,261.08 ns | 10000.0000 | 4000.0000 | - | 83731632 B |
| Build_ExtMap | 100000 | 50 | 130,083,384.42 ns | 12250.0000 | 5250.0000 | - | 104371560 B |
| Build_ExtHashMap | 100000 | 50 | 50,176,040.06 ns | 11909.0909 | 2545.4545 | - | 99721040 B |
| Retrieve_ImmDict | 100000 | 50 | 1,878,376.76 ns | - | - | - | - |
| Retrieve_PersistentMap_Standard | 100000 | 50 | 22,662,880.57 ns | 93.7500 | - | - | 960000 B |
| Retrieve_PersistentMap_Unicode | 100000 | 50 | 1,421,803.13 ns | - | - | - | - |
| Update_ImmDict | 100000 | 50 | 8,957,341.32 ns | 1265.6250 | 1062.5000 | - | 10646144 B |
| Update_PersistentMap_Standard | 100000 | 50 | 37,471,664.44 ns | 3692.3077 | 2769.2308 | - | 31040000 B |
| Update_PersistentMap_Unicode | 100000 | 50 | 12,856,950.49 ns | 4656.2500 | 2781.2500 | 46.8750 | 38640047 B |
| Update_TransientMap_Standard | 100000 | 50 | 26,563,266.79 ns | 406.2500 | 312.5000 | - | 3414056 B |
| Update_TransientMap_Unicode | 100000 | 50 | 4,173,539.70 ns | 406.2500 | 335.9375 | - | 3440328 B |
| Update_ImmSortedDict | 100000 | 50 | 11,812,577.59 ns | 953.1250 | 781.2500 | - | 8012928 B |
| Update_ExtMap | 100000 | 50 | 11,842,757.42 ns | 1093.7500 | 906.2500 | - | 9233632 B |
| Update_ExtHashMap | 100000 | 50 | 4,189,884.01 ns | 1265.6250 | 984.3750 | - | 10632736 B |
| UpdateSet_ImmDict | 100000 | 50 | 10,459,268.37 ns | 1343.7500 | 1031.2500 | - | 11256960 B |
| UpdateSet_PersistentMap_Standard | 100000 | 50 | 41,029,434.25 ns | 3666.6667 | 2500.0000 | - | 31141992 B |
| UpdateSet_PersistentMap_Unicode | 100000 | 50 | 14,028,512.79 ns | 4687.5000 | 2421.8750 | 46.8750 | 38835953 B |
| UpdateSet_TransientMap_Standard | 100000 | 50 | 28,314,403.39 ns | 406.2500 | 312.5000 | - | 3509008 B |
| UpdateSet_TransientMap_Unicode | 100000 | 50 | 5,267,848.18 ns | 429.6875 | 359.3750 | - | 3630600 B |
| UpdateSet_ImmSortedDict | 100000 | 50 | 14,089,168.54 ns | 1015.6250 | 890.6250 | - | 8501136 B |
| UpdateSet_ExtMap | 100000 | 50 | 12,785,268.31 ns | 1218.7500 | 1015.6250 | - | 10265544 B |
| UpdateSet_ExtHashMap | 100000 | 50 | 4,418,089.19 ns | 1281.2500 | 992.1875 | - | 10750000 B |
| Iterate_ImmDict | 100000 | 50 | 2,338,660.26 ns | - | - | - | 384 B |
| Iterate_PersistentMap_Standard | 100000 | 50 | 211,359.08 ns | - | - | - | - |
| Iterate_ImmSortedDict | 100000 | 50 | 715,600.57 ns | - | - | - | - |
| Iterate_ExtMap | 100000 | 50 | 1,117,575.94 ns | - | - | - | 32 B |
| Iterate_ExtHashMap | 100000 | 50 | 2,975,826.72 ns | 277.3438 | - | - | 2319904 B |
| Iterate_PersistentMap_Unicode | 100000 | 50 | 217,594.04 ns | - | - | - | - |
| Remove_ImmDict | 100000 | 50 | 9,190,906.43 ns | 1281.2500 | 1046.8750 | - | 10741824 B |
| Remove_PersistentMap_Standard | 100000 | 50 | 42,259,057.45 ns | 3727.2727 | 2636.3636 | - | 31774080 B |
| Remove_PersistentMap_Unicode | 100000 | 50 | 11,946,176.86 ns | 4625.0000 | 2859.3750 | 46.8750 | 38402868 B |
| Remove_TransientMap_Standard | 100000 | 50 | 26,120,804.07 ns | 468.7500 | 343.7500 | - | 4127064 B |
| Remove_TransientMap_Unicode | 100000 | 50 | 3,583,417.50 ns | 382.8125 | 312.5000 | - | 3205960 B |
| Remove_ImmSortedDict | 100000 | 50 | 14,164,094.39 ns | 953.1250 | 781.2500 | - | 8032656 B |
| Remove_ExtMap | 100000 | 50 | 13,861,593.95 ns | 1140.6250 | 921.8750 | - | 9617232 B |
| Remove_ExtHashMap | 100000 | 50 | 4,797,368.31 ns | 1265.6250 | 937.5000 | - | 10628320 B |
#+end_src #+end_src
** Architecture Notes: Key Strategies Architecture Notes: Key Strategies
NiceBtree uses =IKeyStrategy<K>= to map generic keys (like =string= or =double=) into sortable =long= prefixes. This achieves two things: 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. 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. 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.

View file

@ -0,0 +1,38 @@
namespace PersistentMap.Tests;
using PersistentMap;
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

@ -0,0 +1,445 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using BenchmarkDotNet.Attributes;
using LanguageExt;
using PersistentMap;
namespace MapBenchmarks;
[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 PersistentMap<string, int, StandardStrategy<string>> _persistentMapStandard;
private PersistentMap<string, int, UnicodeStrategy> _persistentMapUnicode;
private readonly StandardStrategy<string> _stdStrategy = new StandardStrategy<string>();
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, 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);
}
_persistentMapStandard = transStd.ToPersistent();
_persistentMapUnicode = 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 PersistentMap<string, int, StandardStrategy<string>> Build_TransientMap_Standard()
{
var map = BaseOrderedMap<string, int, StandardStrategy<string>>.CreateTransient(_stdStrategy);
for (int i = 0; i < _allKeys.Length; i++) map.Set(_allKeys[i], i);
return map.ToPersistent();
}
[Benchmark]
public PersistentMap<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 (_persistentMapStandard.TryGetValue(k, out _)) count++;
return count;
}
[Benchmark]
public int Retrieve_PersistentMap_Unicode()
{
int count = 0;
foreach (var k in _retrieveKeys)
if (_persistentMapUnicode.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 PersistentMap<string, int, StandardStrategy<string>> Update_PersistentMap_Standard()
{
var map = _persistentMapStandard;
foreach (var k in _updateKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentMap<string, int, UnicodeStrategy> Update_PersistentMap_Unicode()
{
var map = _persistentMapUnicode;
foreach (var k in _updateKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentMap<string, int, StandardStrategy<string>> Update_TransientMap_Standard()
{
var transient = _persistentMapStandard.ToTransient();
foreach (var k in _updateKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public PersistentMap<string, int, UnicodeStrategy> Update_TransientMap_Unicode()
{
var transient = _persistentMapUnicode.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 PersistentMap<string, int, StandardStrategy<string>> UpdateSet_PersistentMap_Standard()
{
var map = _persistentMapStandard;
foreach (var k in _mixedKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentMap<string, int, UnicodeStrategy> UpdateSet_PersistentMap_Unicode()
{
var map = _persistentMapUnicode;
foreach (var k in _mixedKeys) map = map.Set(k, 999);
return map;
}
[Benchmark]
public PersistentMap<string, int, StandardStrategy<string>> UpdateSet_TransientMap_Standard()
{
var transient = _persistentMapStandard.ToTransient();
foreach (var k in _mixedKeys) transient.Set(k, 999);
return transient.ToPersistent();
}
[Benchmark]
public PersistentMap<string, int, UnicodeStrategy> UpdateSet_TransientMap_Unicode()
{
var transient = _persistentMapUnicode.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 _persistentMapStandard) 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 _persistentMapUnicode) 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 PersistentMap<string, int, StandardStrategy<string>> Remove_PersistentMap_Standard()
{
var map = _persistentMapStandard;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentMap<string, int, UnicodeStrategy> Remove_PersistentMap_Unicode()
{
var map = _persistentMapUnicode;
foreach (var k in _removeKeys) map = map.Remove(k);
return map;
}
[Benchmark]
public PersistentMap<string, int, StandardStrategy<string>> Remove_TransientMap_Standard()
{
var transient = _persistentMapStandard.ToTransient();
foreach (var k in _removeKeys) transient.Remove(k);
return transient.ToPersistent();
}
[Benchmark]
public PersistentMap<string, int, UnicodeStrategy> Remove_TransientMap_Unicode()
{
var transient = _persistentMapUnicode.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;
}
}