fix prefixless search
This commit is contained in:
parent
280177a9cb
commit
a9b6ed9161
12 changed files with 1054 additions and 44 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|----------------------------------- |---------- |--------------- |--------------------:|------------------:|------------------:|------------:|------------:|----------:|-------------:|
|
||||
| **'Build: Sys.ImmutableDict'** | **10** | **1000** | **80,197.68 ns** | **628.656 ns** | **557.288 ns** | **3.7842** | **0.6104** | **-** | **64072 B** |
|
||||
| 'Build: LangExt.HashMap' | 10 | 1000 | 101,031.57 ns | 842.539 ns | 703.558 ns | 16.8457 | 2.3193 | - | 282816 B |
|
||||
| 'Build: LangExt.SortedMap' | 10 | 1000 | 231,169.60 ns | 4,303.018 ns | 3,814.513 ns | 3.1738 | 0.4883 | - | 56088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 10 | 1000 | 247,805.45 ns | 4,848.561 ns | 7,106.959 ns | 138.1836 | 15.1367 | - | 2314008 B |
|
||||
| 'Build: PersistentMap (Transient)' | 10 | 1000 | 85,655.04 ns | 826.536 ns | 690.195 ns | 3.5400 | 0.3662 | - | 59624 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 10 | 1000 | 11.26 ns | 0.040 ns | 0.035 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 10 | 1000 | 132.93 ns | 1.110 ns | 0.984 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 10 | 1000 | 24.35 ns | 0.220 ns | 0.206 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 10 | 1000 | 191.15 ns | 0.908 ns | 0.758 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 10 | 1000 | 25.57 ns | 0.185 ns | 0.173 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **10** | **100000** | **26,453,597.57 ns** | **369,042.136 ns** | **345,202.243 ns** | **375.0000** | **281.2500** | **-** | **6400128 B** |
|
||||
| 'Build: LangExt.HashMap' | 10 | 100000 | 30,466,067.88 ns | 607,708.598 ns | 568,451.000 ns | 2125.0000 | 1125.0000 | 125.0000 | 33872183 B |
|
||||
| 'Build: LangExt.SortedMap' | 10 | 100000 | 57,320,977.00 ns | 294,859.779 ns | 246,221.270 ns | 333.3333 | 222.2222 | - | 5600088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 10 | 100000 | 61,269,640.88 ns | 944,908.467 ns | 883,867.966 ns | 22375.0000 | 15375.0000 | 125.0000 | 373286500 B |
|
||||
| 'Build: PersistentMap (Transient)' | 10 | 100000 | 17,053,104.34 ns | 75,075.340 ns | 66,552.333 ns | 343.7500 | 218.7500 | - | 5764192 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 10 | 100000 | 14.23 ns | 0.048 ns | 0.043 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 10 | 100000 | 317.13 ns | 1.974 ns | 1.750 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 10 | 100000 | 30.39 ns | 0.115 ns | 0.102 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 10 | 100000 | 367.82 ns | 1.219 ns | 1.080 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 10 | 100000 | 44.58 ns | 0.191 ns | 0.178 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **10** | **1000000** | **565,648,044.07 ns** | **7,804,426.380 ns** | **7,300,265.281 ns** | **3000.0000** | **2000.0000** | **-** | **64006400 B** |
|
||||
| 'Build: LangExt.HashMap' | 10 | 1000000 | 705,337,758.36 ns | 8,815,356.221 ns | 7,814,583.679 ns | 22000.0000 | 20000.0000 | - | 371442296 B |
|
||||
| 'Build: LangExt.SortedMap' | 10 | 1000000 | 1,090,945,766.47 ns | 15,976,157.606 ns | 14,944,107.744 ns | 3000.0000 | 2000.0000 | - | 56000088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 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 |
|
||||
| 'Build: PersistentMap (Transient)' | 10 | 1000000 | 327,151,058.39 ns | 6,192,298.711 ns | 6,625,690.250 ns | 3000.0000 | 2500.0000 | - | 57911880 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 10 | 1000000 | 16.76 ns | 0.329 ns | 0.323 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 10 | 1000000 | 229.91 ns | 0.557 ns | 0.521 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 10 | 1000000 | 30.30 ns | 0.095 ns | 0.089 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 10 | 1000000 | 439.20 ns | 3.921 ns | 3.668 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 10 | 1000000 | 57.93 ns | 0.262 ns | 0.219 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **100** | **1000** | **152,297.84 ns** | **1,453.902 ns** | **1,359.981 ns** | **3.6621** | **0.4883** | **-** | **64072 B** |
|
||||
| 'Build: LangExt.HashMap' | 100 | 1000 | 162,130.87 ns | 1,572.067 ns | 1,470.513 ns | 17.0898 | 2.1973 | - | 286248 B |
|
||||
| 'Build: LangExt.SortedMap' | 100 | 1000 | 227,901.82 ns | 3,599.541 ns | 3,190.900 ns | 3.1738 | 0.4883 | - | 56088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 100 | 1000 | 246,870.77 ns | 4,663.995 ns | 4,362.704 ns | 138.4277 | 15.1367 | - | 2316904 B |
|
||||
| 'Build: PersistentMap (Transient)' | 100 | 1000 | 87,058.89 ns | 995.531 ns | 831.314 ns | 3.6621 | 0.3662 | - | 62520 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 100 | 1000 | 51.85 ns | 0.344 ns | 0.287 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 100 | 1000 | 126.84 ns | 0.807 ns | 0.755 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 100 | 1000 | 59.05 ns | 0.193 ns | 0.161 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 100 | 1000 | 172.41 ns | 1.498 ns | 1.401 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 100 | 1000 | 26.71 ns | 0.094 ns | 0.088 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **100** | **100000** | **33,894,492.92 ns** | **510,741.665 ns** | **477,748.070 ns** | **333.3333** | **266.6667** | **-** | **6400184 B** |
|
||||
| 'Build: LangExt.HashMap' | 100 | 100000 | 42,503,012.72 ns | 844,933.727 ns | 1,854,650.153 ns | 2066.6667 | 1066.6667 | 66.6667 | 33877456 B |
|
||||
| 'Build: LangExt.SortedMap' | 100 | 100000 | 58,627,288.83 ns | 659,710.519 ns | 550,888.162 ns | 333.3333 | 222.2222 | - | 5600088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 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 |
|
||||
| 'Build: PersistentMap (Transient)' | 100 | 100000 | 19,221,436.72 ns | 376,157.061 ns | 628,473.997 ns | 343.7500 | 281.2500 | - | 5791360 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 100 | 100000 | 55.38 ns | 0.349 ns | 0.309 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 100 | 100000 | 298.94 ns | 3.088 ns | 2.888 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 100 | 100000 | 81.90 ns | 0.693 ns | 0.649 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 100 | 100000 | 401.93 ns | 2.570 ns | 2.278 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 100 | 100000 | 49.00 ns | 0.523 ns | 0.490 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **100** | **1000000** | **679,242,633.13 ns** | **13,320,688.340 ns** | **12,460,180.143 ns** | **3000.0000** | **2000.0000** | **-** | **64006792 B** |
|
||||
| 'Build: LangExt.HashMap' | 100 | 1000000 | 905,145,222.68 ns | 16,013,156.606 ns | 19,665,596.105 ns | 22000.0000 | 20000.0000 | - | 371420160 B |
|
||||
| 'Build: LangExt.SortedMap' | 100 | 1000000 | 1,311,013,675.64 ns | 21,946,824.587 ns | 19,455,288.355 ns | 3000.0000 | 2000.0000 | - | 56000088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 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 |
|
||||
| 'Build: PersistentMap (Transient)' | 100 | 1000000 | 378,070,334.14 ns | 7,546,360.889 ns | 19,747,512.605 ns | 3000.0000 | 2000.0000 | - | 57879232 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 100 | 1000000 | 56.94 ns | 0.391 ns | 0.366 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 100 | 1000000 | 299.78 ns | 4.283 ns | 4.006 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 100 | 1000000 | 81.32 ns | 0.714 ns | 0.633 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 100 | 1000000 | 371.55 ns | 1.288 ns | 1.142 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 100 | 1000000 | 57.35 ns | 0.203 ns | 0.180 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **1000** | **1000** | **862,166.32 ns** | **6,173.897 ns** | **5,775.067 ns** | **2.9297** | **-** | **-** | **64072 B** |
|
||||
| 'Build: LangExt.HashMap' | 1000 | 1000 | 715,350.87 ns | 5,832.760 ns | 5,455.967 ns | 16.6016 | 1.9531 | - | 288824 B |
|
||||
| 'Build: LangExt.SortedMap' | 1000 | 1000 | 231,953.07 ns | 4,633.161 ns | 4,957.431 ns | 3.1738 | 0.4883 | - | 56088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 1000 | 1000 | 244,027.59 ns | 4,381.202 ns | 3,883.821 ns | 138.1836 | 14.6484 | - | 2312560 B |
|
||||
| 'Build: PersistentMap (Transient)' | 1000 | 1000 | 85,141.37 ns | 997.399 ns | 832.873 ns | 3.4180 | 0.2441 | - | 58176 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 1000 | 1000 | 463.86 ns | 2.281 ns | 1.905 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 1000 | 1000 | 141.16 ns | 1.373 ns | 1.285 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 1000 | 1000 | 482.30 ns | 2.993 ns | 2.499 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 1000 | 1000 | 151.26 ns | 0.522 ns | 0.463 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 1000 | 1000 | 25.14 ns | 0.100 ns | 0.089 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **1000** | **100000** | **110,681,036.36 ns** | **2,111,588.232 ns** | **2,073,861.990 ns** | **285.7143** | **142.8571** | **-** | **6400072 B** |
|
||||
| 'Build: LangExt.HashMap' | 1000 | 100000 | 97,557,350.10 ns | 551,991.120 ns | 516,332.837 ns | 2000.0000 | 1000.0000 | - | 33861848 B |
|
||||
| 'Build: LangExt.SortedMap' | 1000 | 100000 | 64,549,795.52 ns | 1,182,362.259 ns | 1,105,982.391 ns | 250.0000 | 125.0000 | - | 5600088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 1000 | 100000 | 67,619,915.03 ns | 537,449.664 ns | 502,730.749 ns | 22500.0000 | 15500.0000 | - | 377887656 B |
|
||||
| 'Build: PersistentMap (Transient)' | 1000 | 100000 | 18,935,946.67 ns | 148,299.648 ns | 138,719.583 ns | 343.7500 | 250.0000 | - | 5797496 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 1000 | 100000 | 468.15 ns | 4.581 ns | 4.285 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 1000 | 100000 | 314.21 ns | 0.812 ns | 0.720 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 1000 | 100000 | 485.12 ns | 3.011 ns | 2.817 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 1000 | 100000 | 312.35 ns | 1.905 ns | 1.782 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 1000 | 100000 | 45.20 ns | 0.272 ns | 0.254 ns | - | - | - | - |
|
||||
| | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **1000** | **1000000** | **1,243,324,259.58 ns** | **23,435,444.565 ns** | **26,048,434.545 ns** | **3000.0000** | **2000.0000** | **-** | **64006232 B** |
|
||||
| 'Build: LangExt.HashMap' | 1000 | 1000000 | 1,622,230,008.71 ns | 12,545,963.874 ns | 11,121,670.194 ns | 22000.0000 | 20000.0000 | - | 371481912 B |
|
||||
| 'Build: LangExt.SortedMap' | 1000 | 1000000 | 1,405,640,022.27 ns | 20,672,528.624 ns | 19,337,096.110 ns | 3000.0000 | 2000.0000 | - | 56000088 B |
|
||||
| 'Build: PersistentMap (Iterative)' | 1000 | 1000000 | 1,576,250,726.60 ns | 21,132,287.760 ns | 19,767,155.091 ns | 250000.0000 | 134000.0000 | - | 4196398744 B |
|
||||
| 'Build: PersistentMap (Transient)' | 1000 | 1000000 | 399,073,585.07 ns | 7,329,442.854 ns | 6,855,965.396 ns | 3000.0000 | 2000.0000 | - | 57944840 B |
|
||||
| 'Lookup: Sys.ImmutableDict' | 1000 | 1000000 | 469.34 ns | 2.373 ns | 2.104 ns | - | - | - | - |
|
||||
| 'Lookup: Sys.SortedDict' | 1000 | 1000000 | 328.66 ns | 1.561 ns | 1.384 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.HashMap' | 1000 | 1000000 | 484.32 ns | 3.425 ns | 3.204 ns | - | - | - | - |
|
||||
| 'Lookup: LangExt.SortedMap' | 1000 | 1000000 | 400.19 ns | 2.649 ns | 2.349 ns | - | - | - | - |
|
||||
| 'Lookup: PersistentMap' | 1000 | 1000000 | 56.25 ns | 0.147 ns | 0.130 ns | - | - | - | - |
|
||||
78
PersistentMap/Benchmarks/gh.benchmarks.md
Normal file
78
PersistentMap/Benchmarks/gh.benchmarks.md
Normal 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 |
|
||||
|----------------------------------- |----------|--------------- |--------------:|-------------:|-------------:|--------------:|--------:|---------:|--------:|----------:|------------:|
|
||||
| **'Build: Sys.ImmutableDict'** | **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** |
|
||||
| 'Build: LangExt.HashMap' | 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 |
|
||||
| 'Build: LangExt.SortedMap' | 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 |
|
||||
| 'Build: PersistentMap (Iterative)' | 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 |
|
||||
| 'Build: PersistentMap (Transient)' | 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 |
|
||||
| 'Lookup: Sys.ImmutableDict' | 10 | 1000 | 11.88 ns | 0.130 ns | 0.122 ns | 1.00 | 0.01 | - | - | - | NA |
|
||||
| 'Lookup: Sys.SortedDict' | 10 | 1000 | 129.81 ns | 1.686 ns | 1.494 ns | 10.93 | 0.16 | - | - | - | NA |
|
||||
| 'Lookup: LangExt.HashMap' | 10 | 1000 | 24.52 ns | 0.293 ns | 0.274 ns | 2.07 | 0.03 | - | - | - | NA |
|
||||
| 'Lookup: LangExt.SortedMap' | 10 | 1000 | 202.85 ns | 0.770 ns | 0.682 ns | 17.08 | 0.18 | - | - | - | NA |
|
||||
| 'Lookup: PersistentMap' | 10 | 1000 | 25.24 ns | 0.159 ns | 0.148 ns | 2.13 | 0.02 | - | - | - | NA |
|
||||
| | | | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **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** |
|
||||
| 'Build: LangExt.HashMap' | 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 |
|
||||
| 'Build: LangExt.SortedMap' | 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 |
|
||||
| 'Build: PersistentMap (Iterative)' | 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 |
|
||||
| 'Build: PersistentMap (Transient)' | 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 |
|
||||
| 'Lookup: Sys.ImmutableDict' | 100 | 1000 | 51.60 ns | 0.263 ns | 0.205 ns | 1.00 | 0.01 | - | - | - | NA |
|
||||
| 'Lookup: Sys.SortedDict' | 100 | 1000 | 126.22 ns | 0.747 ns | 0.662 ns | 2.45 | 0.02 | - | - | - | NA |
|
||||
| 'Lookup: LangExt.HashMap' | 100 | 1000 | 65.88 ns | 0.353 ns | 0.295 ns | 1.28 | 0.01 | - | - | - | NA |
|
||||
| 'Lookup: LangExt.SortedMap' | 100 | 1000 | 168.63 ns | 0.768 ns | 0.718 ns | 3.27 | 0.02 | - | - | - | NA |
|
||||
| 'Lookup: PersistentMap' | 100 | 1000 | 26.67 ns | 0.149 ns | 0.132 ns | 0.52 | 0.00 | - | - | - | NA |
|
||||
| | | | | | | | | | | | |
|
||||
| **'Build: Sys.ImmutableDict'** | **1000** | **1000** | **858,405.87 ns** | **3,122.202 ns** | **2,437.610 ns** | **1,837.79** | **11.46** | **2.9297** | **-** | **64072 B** | **NA** |
|
||||
| 'Build: LangExt.HashMap' | 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 |
|
||||
| 'Build: LangExt.SortedMap' | 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 |
|
||||
| 'Build: PersistentMap (Iterative)' | 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 |
|
||||
| 'Build: PersistentMap (Transient)' | 1000 | 1000 | 85,297.82 ns | 1,186.402 ns | 990.700 ns | 182.62 | 2.29 | 3.4180 | 0.2441 | 58176 B | NA |
|
||||
| 'Lookup: Sys.ImmutableDict' | 1000 | 1000 | 467.10 ns | 3.062 ns | 2.714 ns | 1.00 | 0.01 | - | - | - | NA |
|
||||
| 'Lookup: Sys.SortedDict' | 1000 | 1000 | 131.87 ns | 1.521 ns | 1.348 ns | 0.28 | 0.00 | - | - | - | NA |
|
||||
| 'Lookup: LangExt.HashMap' | 1000 | 1000 | 478.53 ns | 2.786 ns | 2.606 ns | 1.02 | 0.01 | - | - | - | NA |
|
||||
| 'Lookup: LangExt.SortedMap' | 1000 | 1000 | 151.49 ns | 1.183 ns | 1.049 ns | 0.32 | 0.00 | - | - | - | NA |
|
||||
| 'Lookup: PersistentMap' | 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 | - |
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
89
PersistentMap/Readme.md
Normal 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.
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
96
benchmarks/AgainstImmutableDict/AgainstImmutable.cs
Normal file
96
benchmarks/AgainstImmutableDict/AgainstImmutable.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
198
benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs
Normal file
198
benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
112
benchmarks/AgainstLanguageExt/Cycicmap.cs
Normal file
112
benchmarks/AgainstLanguageExt/Cycicmap.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue