From f1488881d3d31159da8f7475b3b2249fadbc9142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Thu, 12 Feb 2026 10:34:01 +0100 Subject: [PATCH] working --- NiceBtree.sln | 7 - PersistentMap/BTreeFunctions.cs | 42 ---- PersistentMap/Benchmarks/intkeys.md | 65 ++++++ PersistentMap/Nodes.cs | 7 + .../AgainstLanguageExt/integerBenchmarks.cs | 219 ++++++++++++++++++ 5 files changed, 291 insertions(+), 49 deletions(-) create mode 100644 PersistentMap/Benchmarks/intkeys.md create mode 100644 benchmarks/AgainstLanguageExt/integerBenchmarks.cs diff --git a/NiceBtree.sln b/NiceBtree.sln index 5e80df9..51a8511 100644 --- a/NiceBtree.sln +++ b/NiceBtree.sln @@ -12,8 +12,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstImmutableDict", "ben 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 @@ -23,7 +21,6 @@ Global {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 @@ -42,9 +39,5 @@ Global {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 dad9c1a..80f3fe5 100644 --- a/PersistentMap/BTreeFunctions.cs +++ b/PersistentMap/BTreeFunctions.cs @@ -675,48 +675,6 @@ where TStrategy : IKeyStrategy // Removal Logic (Fixed Type Inference & Casting) // --------------------------------------------------------- - private static bool RemoveRecursive(Node node, K key, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy - { - - if (node.IsLeaf) - { - var leaf = node.AsLeaf(); - int index = FindIndex(leaf, key, strategy); - // Exact match check - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) - { - - RemoveFromLeaf(leaf, index, strategy); - return leaf.Header.Count < LeafNode.MergeThreshold; - } - return false; // Key not found - } - else - { - - // Internal Node - var internalNode = node.AsInternal(); - - int index = FindRoutingIndex(internalNode, key, strategy); - - // Descend - var child = internalNode.Children[index]!.EnsureEditable(owner); - internalNode.Children[index] = child; - - // FIX 1: Explicitly specify here. - // The compiler cannot infer 'V' because it is not in the arguments. - bool childUnderflow = RemoveRecursive(child, key, strategy, owner); - - if (childUnderflow) - { - // FIX 2: HandleUnderflow also needs to know to perform Leaf casts correctly - return HandleUnderflow(internalNode, index, strategy, owner); - } - - return false; - } - } private static void RemoveFromLeaf(LeafNode leaf, int index, TStrategy strategy) where TStrategy : IKeyStrategy diff --git a/PersistentMap/Benchmarks/intkeys.md b/PersistentMap/Benchmarks/intkeys.md new file mode 100644 index 0000000..e99f19b --- /dev/null +++ b/PersistentMap/Benchmarks/intkeys.md @@ -0,0 +1,65 @@ +This uses integer keys, and thus there are no hashing penalties. This is more or less raw overhead of the data structure. + + +``` +BenchmarkDotNet v0.15.8, Linux openSUSE Tumbleweed-Slowroll +AMD Ryzen 9 7900 3.02GHz, 1 CPU, 24 logical and 12 physical cores +.NET SDK 10.0.100 +[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 +ShortRun : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 +``` + +| Method | N | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------------------------- |------- |-----------------:|-----------------:|---------------:|----------:|----------:|--------:|-----------:| +| 'Build: NiceBTree (Transient)' | 100 | 3,056.96 ns | 404.797 ns | 22.188 ns | 0.2861 | - | - | 4800 B | +| 'Build: MS Sorted (Builder)' | 100 | 3,099.28 ns | 551.017 ns | 30.203 ns | 0.2899 | 0.0038 | - | 4864 B | +| 'Build: LanguageExt Map (AVL)' | 100 | 6,376.44 ns | 2,154.744 ns | 118.109 ns | 2.2736 | 0.0229 | - | 38144 B | +| 'Build: LanguageExt HashMap' | 100 | 4,577.22 ns | 1,143.256 ns | 62.666 ns | 1.9684 | 0.0076 | - | 33024 B | +| 'Read: NiceBTree' | 100 | 1,281.41 ns | 326.069 ns | 17.873 ns | - | - | - | - | +| 'Read: MS Sorted' | 100 | 484.55 ns | 176.528 ns | 9.676 ns | - | - | - | - | +| 'Read: LanguageExt Map' | 100 | 1,284.80 ns | 770.679 ns | 42.244 ns | - | - | - | - | +| 'Read: LanguageExt HashMap' | 100 | 641.44 ns | 33.663 ns | 1.845 ns | - | - | - | - | +| 'Iterate: NiceBTree' | 100 | 136.77 ns | 14.772 ns | 0.810 ns | - | - | - | - | +| 'Iterate: MS Sorted' | 100 | 425.40 ns | 78.190 ns | 4.286 ns | - | - | - | - | +| 'Iterate: LanguageExt Map' | 100 | 291.45 ns | 113.262 ns | 6.208 ns | 0.0019 | - | - | 32 B | +| 'Iterate: LanguageExt HashMap' | 100 | 763.45 ns | 72.014 ns | 3.947 ns | 0.0648 | - | - | 1088 B | +| 'Set: NiceBTree' | 100 | 60.85 ns | 11.258 ns | 0.617 ns | 0.0678 | 0.0002 | - | 1136 B | +| 'Set: MS Sorted' | 100 | 73.15 ns | 23.712 ns | 1.300 ns | 0.0229 | - | - | 384 B | +| 'Set: LanguageExt Map' | 100 | 58.56 ns | 1.750 ns | 0.096 ns | 0.0219 | - | - | 368 B | +| 'Set: LanguageExt HashMap' | 100 | 36.32 ns | 14.171 ns | 0.777 ns | 0.0206 | - | - | 344 B | +| 'Build: NiceBTree (Transient)' | 1000 | 42,256.55 ns | 4,394.007 ns | 240.850 ns | 2.3804 | 0.1221 | - | 40176 B | +| 'Build: MS Sorted (Builder)' | 1000 | 49,147.19 ns | 3,136.972 ns | 171.948 ns | 2.8687 | 0.4272 | - | 48064 B | +| 'Build: LanguageExt Map (AVL)' | 1000 | 103,207.86 ns | 38,017.513 ns | 2,083.868 ns | 34.6680 | 3.1738 | - | 580688 B | +| 'Build: LanguageExt HashMap' | 1000 | 118,382.76 ns | 8,091.969 ns | 443.548 ns | 45.4102 | 3.2959 | - | 760096 B | +| 'Read: NiceBTree' | 1000 | 13,839.35 ns | 680.579 ns | 37.305 ns | - | - | - | - | +| 'Read: MS Sorted' | 1000 | 8,663.56 ns | 1,007.067 ns | 55.201 ns | - | - | - | - | +| 'Read: LanguageExt Map' | 1000 | 22,507.35 ns | 2,405.937 ns | 131.878 ns | - | - | - | - | +| 'Read: LanguageExt HashMap' | 1000 | 9,727.15 ns | 1,226.266 ns | 67.216 ns | - | - | - | - | +| 'Iterate: NiceBTree' | 1000 | 1,216.36 ns | 264.964 ns | 14.524 ns | - | - | - | - | +| 'Iterate: MS Sorted' | 1000 | 3,870.96 ns | 280.519 ns | 15.376 ns | - | - | - | - | +| 'Iterate: LanguageExt Map' | 1000 | 2,571.58 ns | 422.239 ns | 23.144 ns | - | - | - | 32 B | +| 'Iterate: LanguageExt HashMap' | 1000 | 12,252.69 ns | 2,654.186 ns | 145.485 ns | 1.9226 | - | - | 32320 B | +| 'Set: NiceBTree' | 1000 | 122.89 ns | 31.121 ns | 1.706 ns | 0.0677 | 0.0002 | - | 1136 B | +| 'Set: MS Sorted' | 1000 | 94.78 ns | 68.248 ns | 3.741 ns | 0.0315 | - | - | 528 B | +| 'Set: LanguageExt Map' | 1000 | 81.47 ns | 39.825 ns | 2.183 ns | 0.0305 | - | - | 512 B | +| 'Set: LanguageExt HashMap' | 1000 | 58.48 ns | 22.323 ns | 1.224 ns | 0.0368 | 0.0001 | - | 616 B | +| 'Build: NiceBTree (Transient)' | 100000 | 8,648,357.47 ns | 528,464.456 ns | 28,966.920 ns | 234.3750 | 125.0000 | - | 3952352 B | +| 'Build: MS Sorted (Builder)' | 100000 | 16,518,142.94 ns | 754,623.461 ns | 41,363.458 ns | 281.2500 | 250.0000 | - | 4800064 B | +| 'Build: LanguageExt Map (AVL)' | 100000 | 41,025,632.97 ns | 4,549,124.512 ns | 249,352.866 ns | 5333.3333 | 3333.3333 | - | 89959040 B | +| 'Build: LanguageExt HashMap' | 100000 | 21,273,736.10 ns | 1,167,411.035 ns | 63,989.738 ns | 5781.2500 | 2937.5000 | 31.2500 | 96555424 B | +| 'Read: NiceBTree' | 100000 | 5,469,322.19 ns | 351,350.121 ns | 19,258.686 ns | - | - | - | - | +| 'Read: MS Sorted' | 100000 | 8,066,906.24 ns | 709,413.540 ns | 38,885.350 ns | - | - | - | - | +| 'Read: LanguageExt Map' | 100000 | 10,104,086.04 ns | 1,534,486.048 ns | 84,110.359 ns | - | - | - | - | +| 'Read: LanguageExt HashMap' | 100000 | 1,932,000.56 ns | 247,929.366 ns | 13,589.845 ns | - | - | - | - | +| 'Iterate: NiceBTree' | 100000 | 144,689.84 ns | 57,439.701 ns | 3,148.464 ns | - | - | - | - | +| 'Iterate: MS Sorted' | 100000 | 1,087,465.02 ns | 240,535.824 ns | 13,184.580 ns | - | - | - | - | +| 'Iterate: LanguageExt Map' | 100000 | 774,299.86 ns | 55,922.321 ns | 3,065.291 ns | - | - | - | 32 B | +| 'Iterate: LanguageExt HashMap' | 100000 | 1,230,437.83 ns | 47,039.229 ns | 2,578.379 ns | 64.4531 | - | - | 1082432 B | +| 'Set: NiceBTree' | 100000 | 201.88 ns | 431.379 ns | 23.645 ns | 0.1223 | 0.0007 | - | 2048 B | +| 'Set: MS Sorted' | 100000 | 144.35 ns | 75.135 ns | 4.118 ns | 0.0458 | - | - | 768 B | +| 'Set: LanguageExt Map' | 100000 | 121.02 ns | 31.124 ns | 1.706 ns | 0.0448 | - | - | 752 B | +| 'Set: LanguageExt HashMap' | 100000 | 84.70 ns | 56.667 ns | 3.106 ns | 0.0583 | - | - | 976 B | + diff --git a/PersistentMap/Nodes.cs b/PersistentMap/Nodes.cs index 707083b..1ff7e2b 100644 --- a/PersistentMap/Nodes.cs +++ b/PersistentMap/Nodes.cs @@ -40,6 +40,12 @@ public struct NodeBuffer private Node? _element0; } +[InlineArray(32)] +internal struct InternalPrefixBuffer +{ + private long _element0; +} + public abstract class Node { public NodeHeader Header; @@ -85,6 +91,7 @@ public sealed class LeafNode : Node // Leaf stores Keys and Values public K[] Keys; + public LeafNode? Next; // For range scans public V[] Values; diff --git a/benchmarks/AgainstLanguageExt/integerBenchmarks.cs b/benchmarks/AgainstLanguageExt/integerBenchmarks.cs new file mode 100644 index 0000000..2b43cb9 --- /dev/null +++ b/benchmarks/AgainstLanguageExt/integerBenchmarks.cs @@ -0,0 +1,219 @@ +using System; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using LanguageExt; // Your Namespace +using LanguageExt; +using LanguageExt; +using PersistentMap; // NuGet: LanguageExt.Core + +[MemoryDiagnoser] +public class ImmutableCollectionBenchmarks +{ + [Params(100, 1000, 100_000)] + public int N; + + private int[] _keys; + private int[] _values; + + // --- 1. Your Collections --- + private PersistentMap _niceMap; + private IntStrategy _strategy; + + // --- 2. Microsoft Collections --- + private System.Collections.Immutable.ImmutableSortedDictionary _msSortedMap; + private System.Collections.Immutable.ImmutableDictionary _msHashMap; + + // --- 3. LanguageExt Collections --- + private LanguageExt.Map _leMap; // AVL Tree (Sorted) + private LanguageExt.HashMap _leHashMap; // Hash Array Mapped Trie (Unsorted) + + [GlobalSetup] + public void Setup() + { + // Generate Data + var rand = new Random(42); + _keys = Enumerable.Range(0, N).Select(x => x * 2).ToArray(); + rand.Shuffle(_keys); + _values = _keys.Select(k => k * 100).ToArray(); + _strategy = new IntStrategy(); + + // 1. Setup NiceBTree + var transient = BaseOrderedMap.CreateTransient(_strategy); + for (int i = 0; i < N; i++) transient.Set(_keys[i], _values[i]); + _niceMap = transient.ToPersistent(); + + // 2. Setup MS Immutable + var msBuilder = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder(); + for (int i = 0; i < N; i++) msBuilder.Add(_keys[i], _values[i]); + _msSortedMap = msBuilder.ToImmutable(); + + _msHashMap = System.Collections.Immutable.ImmutableDictionary.CreateRange( + _keys.Zip(_values, (k, v) => new System.Collections.Generic.KeyValuePair(k, v))); + + // 3. Setup LanguageExt + // Note: LanguageExt performs best when bulk-loaded from tuples + var tuples = _keys.Zip(_values, (k, v) => (k, v)); + _leMap = new LanguageExt.Map(tuples); + _leHashMap = new HashMap(tuples); + } + + // ========================================================= + // 1. BUILD (Item by Item) + // Note: LanguageExt has no "Mutable Builder", so this tests + // the cost of pure immutable inserts vs your Transient/Builder. + // ========================================================= + + [Benchmark(Description = "Build: NiceBTree (Transient)")] + public int Build_NiceBTree() + { + var t = BaseOrderedMap.CreateTransient(_strategy); + for (int i = 0; i < N; i++) t.Set(_keys[i], _values[i]); + return t.Count; + } + + [Benchmark(Description = "Build: MS Sorted (Builder)")] + public int Build_MsSorted() + { + var b = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder(); + for (int i = 0; i < N; i++) b.Add(_keys[i], _values[i]); + return b.Count; + } + + [Benchmark(Description = "Build: LanguageExt Map (AVL)")] + public int Build_LanguageExt_Map() + { + var map = LanguageExt.Map.Empty; + for (int i = 0; i < N; i++) + { + // Pure immutable add + map = map.Add(_keys[i], _values[i]); + } + return map.Count; + } + + [Benchmark(Description = "Build: LanguageExt HashMap")] + public int Build_LanguageExt_HashMap() + { + var map = LanguageExt.HashMap.Empty; + for (int i = 0; i < N; i++) + { + map = map.Add(_keys[i], _values[i]); + } + return map.Count; + } + + // ========================================================= + // 2. READ (Lookup) + // ========================================================= + + [Benchmark(Description = "Read: NiceBTree")] + public int Read_NiceBTree() + { + int found = 0; + for (int i = 0; i < N; i++) + { + if (_niceMap.TryGetValue(_keys[i], out _)) found++; + } + return found; + } + + [Benchmark(Description = "Read: MS Sorted")] + public int Read_MsSorted() + { + int found = 0; + for (int i = 0; i < N; i++) + { + if (_msSortedMap.ContainsKey(_keys[i])) found++; + } + return found; + } + + [Benchmark(Description = "Read: LanguageExt Map")] + public int Read_LanguageExt_Map() + { + int found = 0; + for (int i = 0; i < N; i++) + { + // Find returns Option, IsSome checks if it exists + if (_leMap.Find(_keys[i]).IsSome) found++; + } + return found; + } + + [Benchmark(Description = "Read: LanguageExt HashMap")] + public int Read_LanguageExt_HashMap() + { + int found = 0; + for (int i = 0; i < N; i++) + { + if (_leHashMap.Find(_keys[i]).IsSome) found++; + } + return found; + } + + // ========================================================= + // 3. ITERATE (Foreach) + // ========================================================= + + [Benchmark(Description = "Iterate: NiceBTree")] + public int Iterate_NiceBTree() + { + int sum = 0; + foreach (var kvp in _niceMap) sum += kvp.Key; + return sum; + } + + [Benchmark(Description = "Iterate: MS Sorted")] + public int Iterate_MsSorted() + { + int sum = 0; + foreach (var kvp in _msSortedMap) sum += kvp.Key; + return sum; + } + + [Benchmark(Description = "Iterate: LanguageExt Map")] + public int Iterate_LanguageExt_Map() + { + int sum = 0; + // LanguageExt Map is IEnumerable<(Key, Value)> + foreach (var item in _leMap) sum += item.Key; + return sum; + } + + [Benchmark(Description = "Iterate: LanguageExt HashMap")] + public int Iterate_LanguageExt_HashMap() + { + int sum = 0; + foreach (var item in _leHashMap) sum += item.Key; + return sum; + } + + // ========================================================= + // 4. SET (Persistent / Immutable Update) + // ========================================================= + + [Benchmark(Description = "Set: NiceBTree")] + public PersistentMap Set_NiceBTree() + { + return _niceMap.Set(_keys[N / 2], -1); + } + + [Benchmark(Description = "Set: MS Sorted")] + public System.Collections.Immutable.ImmutableSortedDictionary Set_MsSorted() + { + return _msSortedMap.SetItem(_keys[N / 2], -1); + } + + [Benchmark(Description = "Set: LanguageExt Map")] + public LanguageExt.Map Set_LanguageExt_Map() + { + return _leMap.SetItem(_keys[N / 2], -1); + } + + [Benchmark(Description = "Set: LanguageExt HashMap")] + public LanguageExt.HashMap Set_LanguageExt_HashMap() + { + return _leHashMap.SetItem(_keys[N / 2], -1); + } +} \ No newline at end of file