From a9b6ed9161a4d3220089da6259d491664fada313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Wed, 11 Feb 2026 20:59:55 +0100 Subject: [PATCH] fix prefixless search --- NiceBtree.sln | 14 + PersistentMap/BTreeFunctions.cs | 292 ++++++++++++++++-- ...LanguageExt.MapBenchmarks-report-github.md | 111 +++++++ PersistentMap/Benchmarks/gh.benchmarks.md | 78 +++++ PersistentMap/KeyStrategies.cs | 52 +++- PersistentMap/Nodes.cs | 45 ++- PersistentMap/Readme.md | 89 ++++++ PersistentMap/TransientMap.cs | 2 +- TestProject1/FuzzTest.cs | 9 +- .../AgainstImmutableDict/AgainstImmutable.cs | 96 ++++++ .../AgainstLanguageExt/AgainstLanguageExt.cs | 198 ++++++++++++ benchmarks/AgainstLanguageExt/Cycicmap.cs | 112 +++++++ 12 files changed, 1054 insertions(+), 44 deletions(-) create mode 100644 PersistentMap/Benchmarks/AgainstLanguageExt.MapBenchmarks-report-github.md create mode 100644 PersistentMap/Benchmarks/gh.benchmarks.md create mode 100644 PersistentMap/Readme.md create mode 100644 benchmarks/AgainstImmutableDict/AgainstImmutable.cs create mode 100644 benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs create mode 100644 benchmarks/AgainstLanguageExt/Cycicmap.cs diff --git a/NiceBtree.sln b/NiceBtree.sln index 12c6d02..5e80df9 100644 --- a/NiceBtree.sln +++ b/NiceBtree.sln @@ -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 diff --git a/PersistentMap/BTreeFunctions.cs b/PersistentMap/BTreeFunctions.cs index 312dfaf..dad9c1a 100644 --- a/PersistentMap/BTreeFunctions.cs +++ b/PersistentMap/BTreeFunctions.cs @@ -1,4 +1,6 @@ +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace PersistentMap { @@ -51,7 +53,8 @@ public static Node Set(Node root, K key, V value, IKeyStrategy 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 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.MergeThreshold; } @@ -181,11 +184,58 @@ where TStrategy : IKeyStrategy internal static int FindIndex(Node node, K key, TStrategy strategy) where TStrategy : IKeyStrategy { - 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 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(ref startK); + + // 3. Create new Span manually + var intKeys = MemoryMarshal.CreateSpan(ref startInt, keys.Length); + + // 4. Run SIMD Search + return SearchNumericKeysSIMD(intKeys, Unsafe.As(ref key)); + } + else if (typeof(K) == typeof(long)) + { + ref K startK = ref MemoryMarshal.GetReference(keys); + ref long startLong = ref Unsafe.As(ref startK); + var longKeys = MemoryMarshal.CreateSpan(ref startLong, keys.Length); + + return SearchNumericKeysSIMD(longKeys, Unsafe.As(ref key)); + } + + + return LinearSearchKeys(node.GetKeys(), key, strategy); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LinearSearchKeys(Span keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + 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 internal static int FindRoutingIndex(InternalNode node, K key, TStrategy strategy) where TStrategy : IKeyStrategy { + 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(ref startK); + var intKeys = MemoryMarshal.CreateSpan(ref startInt, node.GetKeys().Length); + + return SearchNumericRoutingSIMD(intKeys, Unsafe.As(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(ref startK); + var longKeys = MemoryMarshal.CreateSpan(ref startLong, node.GetKeys().Length); + + return SearchNumericRoutingSIMD(longKeys, Unsafe.As(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 // 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(Span keys, T key) + where T : struct, INumber // Constraints ensure we deal with values + { + // 1. Vector Setup + int len = keys.Length; + int vectorSize = Vector.Count; + int i = 0; + // Create a vector of [key, key, key...] + Vector vKey = new Vector(key); + + // 2. Main SIMD Loop + ref T start = ref MemoryMarshal.GetReference(keys); + + while (i <= len - vectorSize) + { + // Load data + Vector vData = Unsafe.ReadUnaligned>( + ref Unsafe.As(ref Unsafe.Add(ref start, i)) + ); + + // Compare: GreaterThanOrEqual is not directly supported by Vector 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 vLessThan = Vector.LessThan(vData, vKey); + + // If NOT all are less than key (i.e., some are >= key), we found the block. + if (vLessThan != Vector.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.Default.Compare(Unsafe.Add(ref start, i + j), key) >= 0) + { + return i + j; + } + } + } + + i += vectorSize; + } + + // 3. Scalar Cleanup (Tail) + while (i < len) + { + if (Comparer.Default.Compare(Unsafe.Add(ref start, i), key) >= 0) + { + return i; + } + i++; + } + + return i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SearchNumericRoutingSIMD(Span keys, T key) + where T : struct, INumber + { + if (!Vector.IsSupported) return LinearSearchRouting(keys, key); + + int len = keys.Length; + int vectorSize = Vector.Count; + int i = 0; + Vector vKey = new Vector(key); + + ref T start = ref MemoryMarshal.GetReference(keys); + + while (i <= len - vectorSize) + { + Vector vData = Unsafe.ReadUnaligned>( + ref Unsafe.As(ref Unsafe.Add(ref start, i)) + ); + + // ROUTING LOGIC: We want STRICTLY GREATER (>). + // Vector.GreaterThan returns -1 (All 1s) for True, 0 for False. + Vector vGreater = Vector.GreaterThan(vData, vKey); + + if (vGreater != Vector.Zero) + { + // Found a block with values > key. Find the exact one. + for (int j = 0; j < vectorSize; j++) + { + if (Comparer.Default.Compare(Unsafe.Add(ref start, i + j), key) > 0) + { + return i + j; + } + } + } + i += vectorSize; + } + + + + // Tail cleanup + while (i < len) + { + if (Comparer.Default.Compare(Unsafe.Add(ref start, i), key) > 0) + { + return i; + } + i++; + } + return i; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LinearSearchRouting(Span keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + 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(Span keys, T key) where T : struct, IComparable + { + int i = 0; + while (i < keys.Length && keys[i].CompareTo(key) <= 0) i++; + return i; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RefineRouting(int startIndex, K[] keys, int count, K key, TStrategy strategy) where TStrategy : IKeyStrategy @@ -311,15 +521,16 @@ where TStrategy : IKeyStrategy // This fails if leaf.Values is a Span 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 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 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 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 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 // 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 if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) { - RemoveFromLeaf(leaf, index); + RemoveFromLeaf(leaf, index, strategy); return leaf.Header.Count < LeafNode.MergeThreshold; } return false; // Key not found @@ -503,15 +718,19 @@ where TStrategy : IKeyStrategy } } - private static void RemoveFromLeaf(LeafNode leaf, int index) + private static void RemoveFromLeaf(LeafNode leaf, int index, TStrategy strategy) + where TStrategy : IKeyStrategy { 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 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 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 // 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 // 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 // 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 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 // 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); diff --git a/PersistentMap/Benchmarks/AgainstLanguageExt.MapBenchmarks-report-github.md b/PersistentMap/Benchmarks/AgainstLanguageExt.MapBenchmarks-report-github.md new file mode 100644 index 0000000..a34f42e --- /dev/null +++ b/PersistentMap/Benchmarks/AgainstLanguageExt.MapBenchmarks-report-github.md @@ -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 | - | - | - | - | diff --git a/PersistentMap/Benchmarks/gh.benchmarks.md b/PersistentMap/Benchmarks/gh.benchmarks.md new file mode 100644 index 0000000..aaafdfe --- /dev/null +++ b/PersistentMap/Benchmarks/gh.benchmarks.md @@ -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 | - | diff --git a/PersistentMap/KeyStrategies.cs b/PersistentMap/KeyStrategies.cs index f179f31..3c4e173 100644 --- a/PersistentMap/KeyStrategies.cs +++ b/PersistentMap/KeyStrategies.cs @@ -13,6 +13,8 @@ public interface IKeyStrategy { int Compare(K x, K y); long GetPrefix(K key); + + bool UsesPrefixes => true; } @@ -56,16 +58,47 @@ public struct IntStrategy : IKeyStrategy [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 +{ + // 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(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); + } +} + + + /// /// Helper for SIMD accelerated prefix scanning. /// @@ -74,6 +107,15 @@ public static class PrefixScanner [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int FindFirstGreaterOrEqual(ReadOnlySpan 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); diff --git a/PersistentMap/Nodes.cs b/PersistentMap/Nodes.cs index b784229..707083b 100644 --- a/PersistentMap/Nodes.cs +++ b/PersistentMap/Nodes.cs @@ -92,7 +92,15 @@ public sealed class LeafNode : Node { 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 : Node // 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 : Node 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 : Node 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); diff --git a/PersistentMap/Readme.md b/PersistentMap/Readme.md new file mode 100644 index 0000000..81517f1 --- /dev/null +++ b/PersistentMap/Readme.md @@ -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` 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.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.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` 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` 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. diff --git a/PersistentMap/TransientMap.cs b/PersistentMap/TransientMap.cs index 030ef0b..f5f282d 100644 --- a/PersistentMap/TransientMap.cs +++ b/PersistentMap/TransientMap.cs @@ -7,7 +7,7 @@ public sealed class TransientMap : BaseOrderedMap root, TStrategy strategy, int count) + public TransientMap(Node root, TStrategy strategy, int count) : base(root, strategy, count) { _transactionId = OwnerId.Next(); diff --git a/TestProject1/FuzzTest.cs b/TestProject1/FuzzTest.cs index 6362dee..cde5ee8 100644 --- a/TestProject1/FuzzTest.cs +++ b/TestProject1/FuzzTest.cs @@ -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); } diff --git a/benchmarks/AgainstImmutableDict/AgainstImmutable.cs b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs new file mode 100644 index 0000000..007ee37 --- /dev/null +++ b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs @@ -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 _immutableDict; + private ImmutableSortedDictionary _immutableSortedDict; + + // 1. Add field for your map + private PersistentMap _persistentMap; + + private string[] _searchKeys; + + [GlobalSetup] + public void Setup() + { + var random = new Random(42); + var data = new Dictionary(); + + 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.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); + } +} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs b/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs new file mode 100644 index 0000000..3a221ca --- /dev/null +++ b/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs @@ -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[] _data; + private string[] _searchKeys; + + private int _index = 0; + private int _mask; + + // Comparison Targets (for Lookup) + private ImmutableDictionary _sysDict; + private ImmutableSortedDictionary _sysSorted; + private LanguageExt.HashMap _langExtHash; + private LanguageExt.Map _langExtSorted; // Map is Sorted in LangExt + private PersistentMap _persistentMap; + + [GlobalSetup] + public void Setup() + { + var random = new Random(42); + var dict = new Dictionary(); + + // 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.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 Build_SysImmutable() + { + // Using CreateRange/ToImmutable is usually the standard 'bulk' build + return _data.ToImmutableDictionary(); + } + + [Benchmark(Description = "Build: LangExt.HashMap")] + public LanguageExt.HashMap Build_LangExtHash() + { + return Prelude.toHashMap(_data); + } + + [Benchmark(Description = "Build: LangExt.SortedMap")] + public LanguageExt.Map Build_LangExtSorted() + { + return Prelude.toMap(_data); + } + + [Benchmark(Description = "Build: PersistentMap (Iterative)")] + public PersistentMap Build_Persistent_Iterative() + { + // Simulating naive immutable building (O(n log n) or worse due to copying) + var map = PersistentMap.Empty(new UnicodeStrategy()); + foreach (var item in _data) + { + map = map.Set(item.Key, item.Value); + } + return map; + } + + [Benchmark(Description = "Build: PersistentMap (Transient)")] + public PersistentMap Build_Persistent_Transient() + { + // Simulating efficient mutable build -> freeze + var trans = PersistentMap.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); + } +} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/Cycicmap.cs b/benchmarks/AgainstLanguageExt/Cycicmap.cs new file mode 100644 index 0000000..0cf32e4 --- /dev/null +++ b/benchmarks/AgainstLanguageExt/Cycicmap.cs @@ -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 _persistentMap; + private ImmutableSortedDictionary _sysSorted; + private LanguageExt.HashMap _langExtHash; + private LanguageExt.Map _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(); + + // 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.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 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); + } +} \ No newline at end of file