fix prefixless search

This commit is contained in:
Linus Björnstam 2026-02-11 20:59:55 +01:00
parent 280177a9cb
commit a9b6ed9161
12 changed files with 1054 additions and 44 deletions

View file

@ -10,6 +10,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstImmutableDict", "benchmarks\AgainstImmutableDict\AgainstImmutableDict.csproj", "{13304F19-7ED3-4C40-9A08-46D539667D50}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstLanguageExt", "benchmarks\AgainstLanguageExt\AgainstLanguageExt.csproj", "{6C16526B-5139-4EA3-BF74-E6320F467198}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "benchmarks\ConsoleApp1\ConsoleApp1.csproj", "{F9DDC0AB-6962-40DA-BBE2-483F5DB677CE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,6 +22,8 @@ Global
GlobalSection(NestedProjects) = preSolution
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE} = {B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}
{13304F19-7ED3-4C40-9A08-46D539667D50} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
{6C16526B-5139-4EA3-BF74-E6320F467198} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
{F9DDC0AB-6962-40DA-BBE2-483F5DB677CE} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -32,5 +38,13 @@ Global
{13304F19-7ED3-4C40-9A08-46D539667D50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.Build.0 = Release|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.Build.0 = Release|Any CPU
{F9DDC0AB-6962-40DA-BBE2-483F5DB677CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F9DDC0AB-6962-40DA-BBE2-483F5DB677CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9DDC0AB-6962-40DA-BBE2-483F5DB677CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9DDC0AB-6962-40DA-BBE2-483F5DB677CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,4 +1,6 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PersistentMap
{
@ -51,7 +53,8 @@ public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> st
newRoot.Keys[0] = splitResult.Separator;
newRoot.Children[1] = splitResult.NewNode;
newRoot.SetCount(1);
newRoot._prefixes![0] = strategy.GetPrefix(splitResult.Separator);
if (strategy.UsesPrefixes)
newRoot._prefixes![0] = strategy.GetPrefix(splitResult.Separator);
return newRoot;
}
@ -145,7 +148,7 @@ where TStrategy : IKeyStrategy<K>
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
RemoveFromLeaf(leaf, index);
RemoveFromLeaf(leaf, index, strategy);
removed = true; // Item removed. Count -1.
return leaf.Header.Count < LeafNode<K, V>.MergeThreshold;
}
@ -181,11 +184,58 @@ where TStrategy : IKeyStrategy<K>
internal static int FindIndex<K, TStrategy>(Node<K> node, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
long keyPrefix = strategy.GetPrefix(key);
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
if (strategy.UsesPrefixes)
{
long keyPrefix = strategy.GetPrefix(key);
int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix);
// Pass strategy to Refine
return RefineSearch(index, node.GetKeys(), key, strategy);
// Pass strategy to Refine
return RefineSearch(index, node.GetKeys(), key, strategy);
}
Span<K> keys = node.GetKeys();
// ---------------------------------------------------------
// COMPILE-TIME DISPATCH (INT)
// ---------------------------------------------------------
if (typeof(K) == typeof(int))
{
// 1. Get pointer to start of keys
ref K startK = ref MemoryMarshal.GetReference(keys);
// 2. Cast pointer to int (Bypasses 'struct' constraint)
ref int startInt = ref Unsafe.As<K, int>(ref startK);
// 3. Create new Span<int> manually
var intKeys = MemoryMarshal.CreateSpan(ref startInt, keys.Length);
// 4. Run SIMD Search
return SearchNumericKeysSIMD(intKeys, Unsafe.As<K, int>(ref key));
}
else if (typeof(K) == typeof(long))
{
ref K startK = ref MemoryMarshal.GetReference(keys);
ref long startLong = ref Unsafe.As<K, long>(ref startK);
var longKeys = MemoryMarshal.CreateSpan(ref startLong, keys.Length);
return SearchNumericKeysSIMD(longKeys, Unsafe.As<K, long>(ref key));
}
return LinearSearchKeys(node.GetKeys(), key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchKeys<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = 0;
// Standard linear scan on the keys array
while (i < keys.Length && strategy.Compare(keys[i], key) < 0)
{
i++;
}
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -207,6 +257,32 @@ where TStrategy : IKeyStrategy<K>
internal static int FindRoutingIndex<K, TStrategy>(InternalNode<K> node, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
if (!strategy.UsesPrefixes)
{
// A. Optimize for INT
if (typeof(K) == typeof(int))
{
ref K startK = ref MemoryMarshal.GetReference(node.GetKeys());
ref int startInt = ref Unsafe.As<K, int>(ref startK);
var intKeys = MemoryMarshal.CreateSpan(ref startInt, node.GetKeys().Length);
return SearchNumericRoutingSIMD(intKeys, Unsafe.As<K, int>(ref key));
}
// B. Optimize for LONG (or Double via bit-casting)
else if (typeof(K) == typeof(long))
{
ref K startK = ref MemoryMarshal.GetReference(node.GetKeys());
ref long startLong = ref Unsafe.As<K, long>(ref startK);
var longKeys = MemoryMarshal.CreateSpan(ref startLong, node.GetKeys().Length);
return SearchNumericRoutingSIMD(longKeys, Unsafe.As<K, long>(ref key));
}
// C. Fallback
return LinearSearchRouting(node.GetKeys(), key, strategy);
}
long keyPrefix = strategy.GetPrefix(key);
// SIMD still finds >=.
@ -215,7 +291,141 @@ where TStrategy : IKeyStrategy<K>
// Refine using Upper Bound logic (<= 0 instead of < 0)
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SearchNumericKeysSIMD<T>(Span<T> keys, T key)
where T : struct, INumber<T> // Constraints ensure we deal with values
{
// 1. Vector Setup
int len = keys.Length;
int vectorSize = Vector<T>.Count;
int i = 0;
// Create a vector of [key, key, key...]
Vector<T> vKey = new Vector<T>(key);
// 2. Main SIMD Loop
ref T start = ref MemoryMarshal.GetReference(keys);
while (i <= len - vectorSize)
{
// Load data
Vector<T> vData = Unsafe.ReadUnaligned<Vector<T>>(
ref Unsafe.As<T, byte>(ref Unsafe.Add(ref start, i))
);
// Compare: GreaterThanOrEqual is not directly supported by Vector<T> on all hardware,
// but LessThan IS. So we invert: !(Data < Key)
// Wait! We want First GreaterOrEqual.
// Sorted array: [10, 20, 30, 40]. Search 25.
// 10 < 25 (True), 20 < 25 (True), 30 < 25 (False), 40 < 25 (False).
// The first "False" is our target.
Vector<T> vLessThan = Vector.LessThan(vData, vKey);
// If NOT all are less than key (i.e., some are >= key), we found the block.
if (vLessThan != Vector<T>.One) // Vector.One is all bits set (True)
{
// Iterate this small block to find the exact index
// (There are fancier bit-twiddling ways, but a tight loop over 4-8 items is instant)
for (int j = 0; j < vectorSize; j++)
{
// Re-check locally
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i + j), key) >= 0)
{
return i + j;
}
}
}
i += vectorSize;
}
// 3. Scalar Cleanup (Tail)
while (i < len)
{
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i), key) >= 0)
{
return i;
}
i++;
}
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SearchNumericRoutingSIMD<T>(Span<T> keys, T key)
where T : struct, INumber<T>
{
if (!Vector<T>.IsSupported) return LinearSearchRouting(keys, key);
int len = keys.Length;
int vectorSize = Vector<T>.Count;
int i = 0;
Vector<T> vKey = new Vector<T>(key);
ref T start = ref MemoryMarshal.GetReference(keys);
while (i <= len - vectorSize)
{
Vector<T> vData = Unsafe.ReadUnaligned<Vector<T>>(
ref Unsafe.As<T, byte>(ref Unsafe.Add(ref start, i))
);
// ROUTING LOGIC: We want STRICTLY GREATER (>).
// Vector.GreaterThan returns -1 (All 1s) for True, 0 for False.
Vector<T> vGreater = Vector.GreaterThan(vData, vKey);
if (vGreater != Vector<T>.Zero)
{
// Found a block with values > key. Find the exact one.
for (int j = 0; j < vectorSize; j++)
{
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i + j), key) > 0)
{
return i + j;
}
}
}
i += vectorSize;
}
// Tail cleanup
while (i < len)
{
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i), key) > 0)
{
return i;
}
i++;
}
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int i = 0;
// Routing: Skip everything that is LessOrEqual.
// We stop at the first item that is Greater.
while (i < keys.Length && strategy.Compare(keys[i], key) <= 0)
{
i++;
}
return i;
}
// Overload for primitive types (avoids IComparer call overhead in fallback)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<T>(Span<T> keys, T key) where T : struct, IComparable<T>
{
int i = 0;
while (i < keys.Length && keys[i].CompareTo(key) <= 0) i++;
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RefineRouting<K, TStrategy>(int startIndex, K[] keys, int count, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
@ -311,15 +521,16 @@ where TStrategy : IKeyStrategy<K>
// This fails if leaf.Values is a Span<V> of length 'count'
Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index);
Array.Copy(leaf._prefixes!, index, leaf._prefixes!, index + 1, count - index);
if (strategy.UsesPrefixes)
Array.Copy(leaf._prefixes!, index, leaf._prefixes!, index + 1, count - index);
}
leaf.Keys[index] = key;
// This fails if leaf.Values is a Span<V> of length 'count'
leaf.Values[index] = value;
leaf._prefixes![index] = strategy.GetPrefix(key);
if (strategy.UsesPrefixes)
leaf._prefixes![index] = strategy.GetPrefix(key);
leaf.SetCount(count + 1);
}
@ -342,7 +553,8 @@ where TStrategy : IKeyStrategy<K>
Array.Copy(left.Keys, splitPoint, right.Keys, 0, moveCount);
Array.Copy(left.Values, splitPoint, right.Values, 0, moveCount);
// Manually copy prefixes if needed or re-calculate
for(int i=0; i<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint+i];
if (strategy.UsesPrefixes)
for(int i=0; i<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint+i];
}
// Update Counts
@ -378,7 +590,8 @@ where TStrategy : IKeyStrategy<K>
Array.Copy(node.Keys, index, node.Keys, index + 1, count - index);
// FIX: Shift raw prefix array
Array.Copy(node._prefixes!, index, node._prefixes!, index + 1, count - index);
if (strategy.UsesPrefixes)
Array.Copy(node._prefixes!, index, node._prefixes!, index + 1, count - index);
}
// Shift Children
@ -393,7 +606,8 @@ where TStrategy : IKeyStrategy<K>
node.Keys[index] = separator;
// FIX: Write to raw array
node._prefixes![index] = strategy.GetPrefix(separator);
if (strategy.UsesPrefixes)
node._prefixes![index] = strategy.GetPrefix(separator);
node.Children[index + 1] = newChild;
node.SetCount(count + 1);
@ -414,7 +628,8 @@ where TStrategy : IKeyStrategy<K>
// Move Keys/Prefixes to Right
int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up
Array.Copy(left.Keys, splitPoint + 1, right.Keys, 0, moveCount);
for(int i=0; i<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint + 1 + i];
if (strategy.UsesPrefixes)
for(int i=0; i<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint + 1 + i];
// Move Children to Right
// Left has children 0..splitPoint. Right has children splitPoint+1..End
@ -472,7 +687,7 @@ where TStrategy : IKeyStrategy<K>
if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0)
{
RemoveFromLeaf(leaf, index);
RemoveFromLeaf(leaf, index, strategy);
return leaf.Header.Count < LeafNode<K, V>.MergeThreshold;
}
return false; // Key not found
@ -503,15 +718,19 @@ where TStrategy : IKeyStrategy<K>
}
}
private static void RemoveFromLeaf<K, V>(LeafNode<K, V> leaf, int index)
private static void RemoveFromLeaf<K, V, TStrategy>(LeafNode<K, V> leaf, int index, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
{
int count = leaf.Header.Count;
Array.Copy(leaf.Keys, index + 1, leaf.Keys, index, count - index - 1);
Array.Copy(leaf.Values, index + 1, leaf.Values, index, count - index - 1);
var p = leaf._prefixes;
for(int i=index; i<count-1; i++) p[i] = p[i+1];
if (strategy.UsesPrefixes)
{
var p = leaf._prefixes;
for (int i = index; i < count - 1; i++) p[i] = p[i + 1];
}
leaf.SetCount(count - 1);
}
@ -581,7 +800,8 @@ where TStrategy : IKeyStrategy<K>
Array.Copy(rightLeaf.Keys, 0, leftLeaf.Keys, lCount, rCount);
Array.Copy(rightLeaf.Values, 0, leftLeaf.Values, lCount, rCount);
Array.Copy(rightLeaf._prefixes.ToArray(), 0, leftLeaf._prefixes.ToArray(), lCount, rCount);
if (strategy.UsesPrefixes)
Array.Copy(rightLeaf._prefixes, 0, leftLeaf._prefixes, lCount, rCount);
leftLeaf.SetCount(lCount + rCount);
leftLeaf.Next = rightLeaf.Next;
@ -597,11 +817,13 @@ where TStrategy : IKeyStrategy<K>
int lCount = leftInternal.Header.Count;
leftInternal.Keys[lCount] = separator;
leftInternal._prefixes[lCount] = strategy.GetPrefix(separator);
if (strategy.UsesPrefixes)
leftInternal._prefixes[lCount] = strategy.GetPrefix(separator);
int rCount = rightInternal.Header.Count;
Array.Copy(rightInternal.Keys, 0, leftInternal.Keys, lCount + 1, rCount);
Array.Copy(rightInternal._prefixes.ToArray(), 0, leftInternal._prefixes.ToArray(), lCount + 1, rCount);
if (strategy.UsesPrefixes)
Array.Copy(rightInternal._prefixes, 0, leftInternal._prefixes, lCount + 1, rCount);
for (int i = 0; i <= rCount; i++)
{
@ -614,10 +836,12 @@ where TStrategy : IKeyStrategy<K>
// Remove Separator and Right Child from Parent
int pCount = parent.Header.Count;
Array.Copy(parent.Keys, separatorIndex + 1, parent.Keys, separatorIndex, pCount - separatorIndex - 1);
var pp = parent._prefixes;
for(int i=separatorIndex; i<pCount-1; i++) pp[i] = pp[i+1];
if (strategy.UsesPrefixes)
{
var pp = parent._prefixes;
for (int i = separatorIndex; i < pCount - 1; i++) pp[i] = pp[i + 1];
}
for(int i = separatorIndex + 2; i <= pCount; i++)
{
parent.Children[i - 1] = parent.Children[i];
@ -637,11 +861,12 @@ where TStrategy : IKeyStrategy<K>
// Move first of right to end of left
InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy);
RemoveFromLeaf(rightLeaf, 0);
RemoveFromLeaf(rightLeaf, 0, strategy);
// Update Parent Separator
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
parent._prefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
if (strategy.UsesPrefixes)
parent._prefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
}
else
{
@ -654,7 +879,8 @@ where TStrategy : IKeyStrategy<K>
// 2. Move Right[0] Key to Parent
parent.Keys[separatorIndex] = rightInternal.Keys[0];
parent._prefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]);
if (strategy.UsesPrefixes)
parent._prefixes[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.
@ -685,10 +911,11 @@ where TStrategy : IKeyStrategy<K>
int last = leftLeaf.Header.Count - 1;
InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy);
RemoveFromLeaf(leftLeaf, last);
RemoveFromLeaf(leftLeaf, last, strategy);
parent.Keys[separatorIndex] = rightLeaf.Keys[0];
parent._prefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
if (strategy.UsesPrefixes)
parent._prefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]);
}
else
{
@ -703,7 +930,8 @@ where TStrategy : IKeyStrategy<K>
// 2. Move Left[last] Key to Parent
parent.Keys[separatorIndex] = leftInternal.Keys[last];
parent._prefixes[separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
if (strategy.UsesPrefixes)
parent._prefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]);
// 3. Truncate Left
leftInternal.SetCount(last);

View file

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

View file

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

View file

@ -13,6 +13,8 @@ public interface IKeyStrategy<K>
{
int Compare(K x, K y);
long GetPrefix(K key);
bool UsesPrefixes => true;
}
@ -56,16 +58,47 @@ public struct IntStrategy : IKeyStrategy<int>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(int x, int y) => x.CompareTo(y);
public bool UsesPrefixes => false;
[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;
return 0;
}
}
public struct DoubleStrategy : IKeyStrategy<double>
{
// 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>
@ -74,6 +107,15 @@ 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);

View file

@ -92,7 +92,15 @@ public sealed class LeafNode<K, V> : Node<K>
{
Keys = new K[Capacity];
Values = new V[Capacity];
_prefixes = new long[Capacity];
if (typeof(K) == typeof(int)
|| typeof(K) == typeof(long))
{
_prefixes = null;
}
else
{
_prefixes = new long[Capacity];
}
}
// Copy Constructor for CoW
@ -105,7 +113,17 @@ public sealed class LeafNode<K, V> : Node<K>
// Allocate new arrays
Keys = new K[Capacity];
Values = new V[Capacity];
_prefixes = new long[Capacity];
if (typeof(K) == typeof(int)
|| typeof(K) == typeof(long))
{
_prefixes = null;
}
else
{
_prefixes = new long[Capacity];
}
// Copy data
Array.Copy(original.Keys, Keys, original.Header.Count);
@ -161,7 +179,16 @@ public sealed class InternalNode<K> : Node<K>
public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes)
{
Keys = new K[Capacity];
_prefixes = new long[Capacity];
if (typeof(K) == typeof(int)
|| typeof(K) == typeof(long))
{
_prefixes = null;
}
else
{
_prefixes = new long[Capacity];
}
// Children buffer is a struct, zero-initialized by default
}
@ -172,7 +199,17 @@ public sealed class InternalNode<K> : Node<K>
Header.Count = original.Header.Count;
Keys = new K[Capacity];
_prefixes = new long[Capacity];
if (typeof(K) == typeof(int)
|| typeof(K) == typeof(long))
{
_prefixes = null;
}
else
{
_prefixes = new long[Capacity];
}
// Copy Keys and Prefixes
Array.Copy(original.Keys, Keys, original.Header.Count);

89
PersistentMap/Readme.md Normal file
View file

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

View file

@ -7,7 +7,7 @@ public sealed class TransientMap<K, V, TStrategy> : BaseOrderedMap<K, V, TStrate
// This is mutable, but we treat it as readonly for the ID generation logic usually.
private OwnerId _transactionId;
internal TransientMap(Node<K> root, TStrategy strategy, int count)
public TransientMap(Node<K> root, TStrategy strategy, int count)
: base(root, strategy, count)
{
_transactionId = OwnerId.Next();

View file

@ -43,7 +43,12 @@ public class BTreeFuzzTests
if (isInsert)
{
// ACTION: INSERT
if (showOps)Console.WriteLine("insert");
if (showOps)Console.WriteLine($"insert: {key} : {val}");
if (key == 4436)
{
Console.WriteLine("BP");
}
reference[key] = val;
subject.Set(key, val);
}
@ -52,7 +57,7 @@ public class BTreeFuzzTests
// ACTION: REMOVE
if (reference.ContainsKey(key))
{
if (showOps)Console.WriteLine("remove");
if (showOps)Console.WriteLine($"remove ${key}");
reference.Remove(key);
subject.Remove(key);
}

View file

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

View file

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

View file

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