From 9242c1c751e586f25508d83724335442f3850820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Wed, 22 Apr 2026 15:55:33 +0200 Subject: [PATCH] perf: Optimize non-prefix key strategies and memory usage - Conditionally allocate prefix buffers in `LeafNode` and introduce `PrefixInternalNode` to reduce memory overhead when prefixes are disabled. - Bypass prefix calculation and logic entirely when `UsesPrefixes` is false. - Add a binary search fallback for key scanning. - Implement a dedicated `int` scanning fast-path, removing SIMD prefix usage from `IntStrategy`. - Reorganize key strategies into separate files. - Add a new benchmark project specifically for string keys. --- NiceBtree.sln | 7 + PersistentMap/BTreeFunctions.cs | 519 ++++++--------- PersistentMap/BaseOrderedMap.cs | 4 +- PersistentMap/KeyStrategies.cs | 184 +----- PersistentMap/KeyStrategies/DoubleStrategy.cs | 2 + PersistentMap/KeyStrategies/IntStrategy.cs | 16 + PersistentMap/KeyStrategies/PrefixScanner.cs | 6 + .../KeyStrategies/StandardStrategy.cs | 11 +- PersistentMap/Nodes.cs | 118 ++-- PersistentMap/PersistentMap.cs | 2 +- PersistentMap/Readme.org | 622 ++++++++++++++---- TestProject1/StandardStrategy.cs | 38 ++ benchmarks/MyBenchMarks/StringBenchmarks.cs | 445 +++++++++++++ 13 files changed, 1297 insertions(+), 677 deletions(-) create mode 100644 TestProject1/StandardStrategy.cs create mode 100644 benchmarks/MyBenchMarks/StringBenchmarks.cs diff --git a/NiceBtree.sln b/NiceBtree.sln index 51a8511..4da8777 100644 --- a/NiceBtree.sln +++ b/NiceBtree.sln @@ -12,6 +12,8 @@ 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}") = "MyBenchMarks", "benchmarks\MyBenchMarks\MyBenchMarks.csproj", "{769E1CEA-7E01-405B-80A2-95CBF432A2BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,7 @@ 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} + {769E1CEA-7E01-405B-80A2-95CBF432A2BA} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -39,5 +42,9 @@ Global {6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.Build.0 = Debug|Any CPU {6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.ActiveCfg = Release|Any CPU {6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.Build.0 = Release|Any CPU + {769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {769E1CEA-7E01-405B-80A2-95CBF432A2BA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/PersistentMap/BTreeFunctions.cs b/PersistentMap/BTreeFunctions.cs index 9c44c18..a039e75 100644 --- a/PersistentMap/BTreeFunctions.cs +++ b/PersistentMap/BTreeFunctions.cs @@ -13,7 +13,6 @@ namespace PersistentMap public static bool TryGetValue(Node root, K key, TStrategy strategy, out V value) where TStrategy : IKeyStrategy { - // 1. Calculate ONCE long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; Node current = root; @@ -22,7 +21,6 @@ namespace PersistentMap if (current.IsLeaf) { var leaf = current.AsLeaf(); - // Leaf uses standard FindIndex (Lower Bound) to find exact match int index = FindIndex(leaf, key, keyPrefix, strategy); if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) { @@ -34,7 +32,6 @@ namespace PersistentMap } else { - // FIX: Internal uses FindRoutingIndex (Upper Bound) var internalNode = current.AsInternal(); int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); current = internalNode.Children[index]!; @@ -42,7 +39,6 @@ namespace PersistentMap } } - // Public API public static Node Set(Node root, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool countChanged) { root = root.EnsureEditable(owner); @@ -51,13 +47,19 @@ namespace PersistentMap if (splitResult != null) { - var newRoot = new InternalNode(owner); - newRoot.Children[0] = root; + var newRoot = strategy.UsesPrefixes + ? new PrefixInternalNode(owner) + : new InternalNode(owner); + newRoot.Keys[0] = splitResult.Separator; + newRoot.Children[0] = root; newRoot.Children[1] = splitResult.NewNode; newRoot.SetCount(1); + if (strategy.UsesPrefixes) + { newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator); + } return newRoot; } @@ -65,10 +67,8 @@ namespace PersistentMap return root; } - // Recursive Helper private static SplitResult? InsertRecursive(Node node, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool added) { - // 1. Calculate ONCE long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; if (node.IsLeaf) @@ -79,11 +79,11 @@ namespace PersistentMap if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) { leaf.Values[index] = value; - added = false; // Key existed, value updated. Count does not change. + added = false; return null; } - added = true; // New key. Count +1. + added = true; if (leaf.Header.Count < LeafNode.Capacity) { InsertIntoLeaf(leaf, index, key, value, strategy); @@ -120,9 +120,8 @@ namespace PersistentMap } } - // Public API public static Node Remove(Node root, K key, TStrategy strategy, OwnerId owner, out bool countChanged) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { root = root.EnsureEditable(owner); @@ -143,11 +142,9 @@ namespace PersistentMap return root; } - // Recursive Helper private static bool RemoveRecursive(Node node, K key, TStrategy strategy, OwnerId owner, out bool removed) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - // 1. Calculate ONCE long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; if (node.IsLeaf) @@ -158,11 +155,11 @@ namespace PersistentMap if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) { RemoveFromLeaf(leaf, index, strategy); - removed = true; // Item removed. Count -1. - return leaf.Header.Count .MergeThreshold; + removed = true; + return leaf.Header.Count < LeafNode.MergeThreshold; } - removed = false; // Item not found. + removed = false; return false; } else @@ -187,101 +184,129 @@ namespace PersistentMap // Internal Helpers: Search // --------------------------------------------------------- - // Used by Leaf Nodes: Finds the first key >= searchKey (Lower Bound) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int FindIndex(Node node, K key, long keyPrefix, TStrategy strategy) where TStrategy : IKeyStrategy { + if (typeof(K) == typeof(int)) + { + Span keys = node.GetKeys(); + ref K firstKeyRef = ref MemoryMarshal.GetReference(keys); + ref int firstIntRef = ref Unsafe.As(ref firstKeyRef); + ReadOnlySpan intKeys = MemoryMarshal.CreateReadOnlySpan(ref firstIntRef, keys.Length); + int intKey = Unsafe.As(ref key); + return IntScanner.FindFirstGreaterOrEqual(intKeys, intKey); + } + if (strategy.UsesPrefixes) { - // Use the pre-calculated prefix here! int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); return RefineSearch(index, node.GetKeys(), key, strategy); } - return LinearSearchKeys(node.GetKeys(), key, strategy); + return FallbackSearchKeys(node.GetKeys(), key, strategy); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int LinearSearchKeys(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)] - private static int RefineSearch(int startIndex, Span keys, K key, TStrategy strategy) - where TStrategy : IKeyStrategy - { - int i = startIndex; - // JIT can now inline 'strategy.Compare' here! - while (i < keys.Length && strategy.Compare(keys[i], key) < 0) - { - i++; - } - return i; - } - - // Used by Internal Nodes: Finds the child index to descend into. - // If Key == Separator, we must go RIGHT (index + 1), so we need (Upper Bound). - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int FindRoutingIndex(InternalNode node, K key, long keyPrefix, TStrategy strategy) where TStrategy : IKeyStrategy { if (!strategy.UsesPrefixes) { - return LinearSearchRouting(node.GetKeys(), key, strategy); + return FallbackRoutingKeys(node.GetKeys(), key, strategy); } - // Use the pre-calculated prefix here! int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); - return RefineRouting(index, node.Keys, node.Header.Count, key, strategy); + return RefineRouting(index, node.GetKeys(), key, strategy); } [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 + private static int RefineSearch(int startIndex, ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy { int i = startIndex; - // DIFFERENCE: We continue past valid matches. - // We want the first key STRICTLY GREATER than target. - // If keys[i] == key, we increment (go to right child). - while (i < count && strategy.Compare(keys[i], key) <= 0) - { - i++; - } + while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++; return i; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RefineRouting(int startIndex, ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + int i = startIndex; + while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++; + return i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FallbackSearchKeys(ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + return strategy.UseBinarySearch + ? BinarySearchKeys(keys, key, strategy) + : LinearSearchKeys(keys, key, strategy); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FallbackRoutingKeys(ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + return strategy.UseBinarySearch + ? BinaryRoutingKeys(keys, key, strategy) + : LinearRoutingKeys(keys, key, strategy); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LinearSearchKeys(ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + int i = 0; + while (i < keys.Length && strategy.Compare(keys[i], key) < 0) i++; + return i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LinearRoutingKeys(ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + int i = 0; + while (i < keys.Length && strategy.Compare(keys[i], key) <= 0) i++; + return i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BinarySearchKeys(ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + int low = 0; + int high = keys.Length - 1; + while (low <= high) + { + int mid = low + ((high - low) >> 1); + int cmp = strategy.Compare(keys[mid], key); + if (cmp == 0) return mid; + if (cmp < 0) low = mid + 1; + else high = mid - 1; + } + return low; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BinaryRoutingKeys(ReadOnlySpan keys, K key, TStrategy strategy) + where TStrategy : IKeyStrategy + { + int low = 0; + int high = keys.Length - 1; + while (low <= high) + { + int mid = low + ((high - low) >> 1); + int cmp = strategy.Compare(keys[mid], key); + if (cmp <= 0) low = mid + 1; + else high = mid - 1; + } + return low; + } + // --------------------------------------------------------- // Insertion Logic // --------------------------------------------------------- @@ -290,226 +315,137 @@ namespace PersistentMap { public Node NewNode; public K Separator; - public SplitResult(Node newNode, K separator) { NewNode = newNode; Separator = separator; } + public SplitResult(Node newNode, K separator) + { + NewNode = newNode; + Separator = separator; + } } - private static SplitResult? InsertRecur2sive(Node node, K key, V value, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy - { - - // 1. Calculate ONCE - long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; - - - // --- LEAF CASE --- - if (node.IsLeaf) - { - var leaf = node.AsLeaf(); - // Leaf uses FindIndex - int index = FindIndex(leaf, key, keyPrefix, strategy); - - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) - { - leaf.Values[index] = value; - return null; - } - - if (leaf.Header.Count < LeafNode.Capacity) - { - InsertIntoLeaf(leaf, index, key, value, strategy); - return null; - } - else - { - return SplitLeaf(leaf, index, key, value, strategy, owner); - } - } - - // --- INTERNAL CASE --- - var internalNode = node.AsInternal(); - - // FIX: Internal uses FindRoutingIndex - int childIndex = FindRoutingIndex(internalNode, key, keyPrefix, strategy); - - var child = internalNode.Children[childIndex]!.EnsureEditable(owner); - internalNode.Children[childIndex] = child; - - var split = InsertRecursive(child, key, value, strategy, owner, out _); - - if (split != null) - { - // ... checks ... - // Use childIndex here - if (internalNode.Header.Count < InternalNode.Capacity - 1) - { - InsertIntoInternal(internalNode, childIndex, split.Separator, split.NewNode, strategy); - return null; - } - else - { - return SplitInternal(internalNode, childIndex, split.Separator, split.NewNode, strategy, owner); - } - } - return null; - } private static void InsertIntoLeaf(LeafNode leaf, int index, K key, V value, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { int count = leaf.Header.Count; if (index < count) { int moveCount = count - index; - // Fast Span memory moves leaf.Keys.AsSpan(index, moveCount).CopyTo(leaf.Keys.AsSpan(index + 1)); leaf.Values.AsSpan(index, moveCount).CopyTo(leaf.Values.AsSpan(index + 1)); if (strategy.UsesPrefixes) { - leaf.AllPrefixes.Slice(index, count - index) - .CopyTo(leaf.AllPrefixes.Slice(index + 1)); + leaf.AllPrefixes.Slice(index, moveCount).CopyTo(leaf.AllPrefixes.Slice(index + 1)); } } leaf.Keys[index] = key; - - // This fails if leaf.Values is a Span of length 'count' leaf.Values[index] = value; + if (strategy.UsesPrefixes) - leaf.AllPrefixes![index] = strategy.GetPrefix(key); + { + leaf.AllPrefixes[index] = strategy.GetPrefix(key); + } leaf.SetCount(count + 1); } + private static SplitResult SplitLeaf(LeafNode left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - var right = new LeafNode(owner); + var right = new LeafNode(owner, strategy.UsesPrefixes); int totalCount = left.Header.Count; - // Heuristics - int splitPoint; - if (insertIndex == totalCount) splitPoint = totalCount; // Append: Keep all in Left (90/10 logic effectively) - else if (insertIndex == 0) splitPoint = 0; // Prepend: Right gets all - else splitPoint = totalCount / 2; - - // Move items to Right + int splitPoint = (insertIndex == totalCount) ? totalCount : (insertIndex == 0 ? 0 : totalCount / 2); int moveCount = totalCount - splitPoint; + if (moveCount > 0) { - // Fast Span memory moves left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0)); left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0)); - // Manually copy prefixes if needed or re-calculate + if (strategy.UsesPrefixes) - for (int i = 0; i < moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + i]; + { + left.AllPrefixes.Slice(splitPoint, moveCount).CopyTo(right.AllPrefixes); + } } - // Update Counts left.SetCount(splitPoint); right.SetCount(moveCount); - // Insert the New Item into the correct node if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0)) - { - InsertIntoLeaf(left, insertIndex, key, value, strategy); - } - else - { - InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy); - } +{ + InsertIntoLeaf(left, insertIndex, key, value, strategy); +} +else +{ + InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy); +} - - // In B+ Tree, the separator is the first key of the right node return new SplitResult(right, right.Keys[0]); } private static void InsertIntoInternal(InternalNode node, int index, K separator, Node newChild, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { int count = node.Header.Count; - // Shift Keys and Prefixes if (index < count) { - int moveCount = count - index; + Span keysSpan = node.Keys; + keysSpan.Slice(index, moveCount).CopyTo(keysSpan.Slice(index + 1)); + + Span> childrenSpan = node.Children; + childrenSpan.Slice(index + 1, moveCount).CopyTo(childrenSpan.Slice(index + 2)); - // Fast Span memory moves - node.Keys.AsSpan(index, moveCount).CopyTo(node.Keys.AsSpan(index + 1)); - - // FIX: Shift raw prefix array if (strategy.UsesPrefixes) { - node.AllPrefixes.Slice(index, count - index) - .CopyTo(node.AllPrefixes.Slice(index + 1)); + node.AllPrefixes.Slice(index, moveCount).CopyTo(node.AllPrefixes.Slice(index + 1)); } } - // Shift Children - // Children buffer is indexable like an array but requires manual loop or Unsafe copy - // if we don't want to use unsafe pointers. - // Since it's a small struct buffer (size 33), a loop is fine/fast. - for (int i = count + 1; i > index + 1; i--) + node.Keys[index] = separator; + node.Children[index + 1] = newChild; + + if (strategy.UsesPrefixes) { - node.Children[i] = node.Children[i - 1]; + node.AllPrefixes[index] = strategy.GetPrefix(separator); } - node.Keys[index] = separator; - - // FIX: Write to raw array - if (strategy.UsesPrefixes) - node.AllPrefixes![index] = strategy.GetPrefix(separator); - - node.Children[index + 1] = newChild; node.SetCount(count + 1); } private static SplitResult SplitInternal(InternalNode left, int insertIndex, K separator, Node newChild, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - var right = new InternalNode(owner); + var right = strategy.UsesPrefixes + ? new PrefixInternalNode(owner) + : new InternalNode(owner); + int count = left.Header.Count; - int splitPoint = count / 2; // Internal nodes usually split 50/50 to keep tree fat - - // The key at splitPoint moves UP to become the separator. - // Keys > splitPoint move to Right. - + int splitPoint = count / 2; K upKey = left.Keys[splitPoint]; + int moveCount = count - splitPoint - 1; - // Move Keys/Prefixes to Right - int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up - // Fast Span memory moves if (moveCount > 0) { - left.Keys.AsSpan(splitPoint + 1, moveCount).CopyTo(right.Keys.AsSpan(0)); + Span leftKeys = left.Keys; + Span rightKeys = right.Keys; + leftKeys.Slice(splitPoint + 1, moveCount).CopyTo(rightKeys); + + Span> leftChildren = left.Children; + Span> rightChildren = right.Children; + leftChildren.Slice(splitPoint + 1, moveCount + 1).CopyTo(rightChildren); if (strategy.UsesPrefixes) { - left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0)); + left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes); } } - // Left has children 0..splitPoint. Right has children splitPoint+1..End - for (int i = 0; i <= moveCount; i++) - { - right.Children[i] = left.Children[splitPoint + 1 + i]; - } left.SetCount(splitPoint); right.SetCount(moveCount); - // Determine where to insert the new Separator/Child - // Note: We extracted 'upKey' from the original array. - // We now have to compare the *incoming* separator with 'upKey' - // to see if it goes Left or Right. - - if (insertIndex == splitPoint) - { - // Special case: The new key is exactly the one pushing up? - // Usually easier to insert into temp buffer and split, - // but here we can branch: - // If insertIndex <= splitPoint, insert left. Else right. - } - - // Simplified insertion into split nodes: if (insertIndex <= splitPoint) { InsertIntoInternal(left, insertIndex, separator, newChild, strategy); @@ -526,26 +462,19 @@ namespace PersistentMap // Removal Logic // --------------------------------------------------------- - // --------------------------------------------------------- - // Removal Logic (Fixed Type Inference & Casting) - // --------------------------------------------------------- - - private static void RemoveFromLeaf(LeafNode leaf, int index, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { int count = leaf.Header.Count; int moveCount = count - index - 1; if (moveCount > 0) { - // Fast Span memory moves leaf.Keys.AsSpan(index + 1, moveCount).CopyTo(leaf.Keys.AsSpan(index)); leaf.Values.AsSpan(index + 1, moveCount).CopyTo(leaf.Values.AsSpan(index)); if (strategy.UsesPrefixes) { - // Replaced manual 'for' loop with native slice copy leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index)); } } @@ -553,11 +482,9 @@ namespace PersistentMap leaf.SetCount(count - 1); } - // FIX 3: Added to HandleUnderflow private static bool HandleUnderflow(InternalNode parent, int childIndex, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - // Try to borrow from Right Sibling if (childIndex < parent.Header.Count) { var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner); @@ -575,7 +502,6 @@ namespace PersistentMap return parent.Header.Count < LeafNode.MergeThreshold; } } - // Try to borrow from Left Sibling else if (childIndex > 0) { var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner); @@ -589,7 +515,6 @@ namespace PersistentMap } else { - // Merge Left and Current. Note separator index is 'childIndex - 1' Merge(parent, childIndex - 1, leftSibling, rightChild, strategy); return parent.Header.Count < LeafNode.MergeThreshold; } @@ -600,15 +525,12 @@ namespace PersistentMap private static bool CanBorrow(Node node) { - // Note: LeafNode.MergeThreshold is constant 8, so we can access it statically or via 8 return node.Header.Count > 8 + 1; } - // FIX 4: Added to Merge/Rotate so we can cast to LeafNode successfully. private static void Merge(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - // Case A: Merging Leaves if (left.IsLeaf) { var leftLeaf = left.AsLeaf(); @@ -616,117 +538,113 @@ namespace PersistentMap int lCount = leftLeaf.Header.Count; int rCount = rightLeaf.Header.Count; + rightLeaf.Keys.AsSpan(0, rCount).CopyTo(leftLeaf.Keys.AsSpan(lCount)); rightLeaf.Values.AsSpan(0, rCount).CopyTo(leftLeaf.Values.AsSpan(lCount)); + if (strategy.UsesPrefixes) { - rightLeaf.AllPrefixes.Slice(0, rCount) - .CopyTo(leftLeaf.AllPrefixes.Slice(lCount)); + rightLeaf.AllPrefixes.Slice(0, rCount).CopyTo(leftLeaf.AllPrefixes.Slice(lCount)); } leftLeaf.SetCount(lCount + rCount); } - // Case B: Merging Internal Nodes else { var leftInternal = left.AsInternal(); var rightInternal = right.AsInternal(); - // Pull separator from parent K separator = parent.Keys[separatorIndex]; int lCount = leftInternal.Header.Count; leftInternal.Keys[lCount] = separator; + if (strategy.UsesPrefixes) + { leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator); + } int rCount = rightInternal.Header.Count; - rightInternal.Keys.AsSpan(0, rCount).CopyTo(leftInternal.Keys.AsSpan(lCount + 1)); + Span rightKeys = rightInternal.Keys; + Span leftKeys = leftInternal.Keys; + rightKeys.Slice(0, rCount).CopyTo(leftKeys.Slice(lCount + 1)); + if (strategy.UsesPrefixes) { - rightInternal.AllPrefixes.Slice(0, rCount) - .CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1)); + rightInternal.AllPrefixes.Slice(0, rCount).CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1)); } - for (int i = 0; i <= rCount; i++) - { - leftInternal.Children[lCount + 1 + i] = rightInternal.Children[i]; - } + Span> rightChildren = rightInternal.Children; + Span> leftChildren = leftInternal.Children; + rightChildren.Slice(0, rCount + 1).CopyTo(leftChildren.Slice(lCount + 1)); leftInternal.SetCount(lCount + 1 + rCount); } - // Remove Separator and Right Child from Parent int pCount = parent.Header.Count; int moveCount = pCount - separatorIndex - 1; if (moveCount > 0) { - parent.Keys.AsSpan(separatorIndex + 1, moveCount).CopyTo(parent.Keys.AsSpan(separatorIndex)); + Span parentKeys = parent.Keys; + parentKeys.Slice(separatorIndex + 1, moveCount).CopyTo(parentKeys.Slice(separatorIndex)); if (strategy.UsesPrefixes) { - // Replaced manual 'for' loop with native slice copy parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex)); } - } - for (int i = separatorIndex + 2; i <= pCount; i++) - { - parent.Children[i - 1] = parent.Children[i]; + Span> parentChildren = parent.Children; + parentChildren.Slice(separatorIndex + 2, moveCount).CopyTo(parentChildren.Slice(separatorIndex + 1)); } parent.SetCount(pCount - 1); } private static void RotateLeft(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - // Move one item from Right to Left if (left.IsLeaf) { var leftLeaf = left.AsLeaf(); var rightLeaf = right.AsLeaf(); - // Move first of right to end of left InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys[0], rightLeaf.Values[0], strategy); RemoveFromLeaf(rightLeaf, 0, strategy); - // Update Parent Separator parent.Keys[separatorIndex] = rightLeaf.Keys[0]; if (strategy.UsesPrefixes) + { parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); + } } else { var leftInternal = left.AsInternal(); var rightInternal = right.AsInternal(); - // 1. Move Parent Separator to Left End K sep = parent.Keys[separatorIndex]; InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy); - // 2. Move Right[0] Key to Parent parent.Keys[separatorIndex] = rightInternal.Keys[0]; if (strategy.UsesPrefixes) + { parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]); + } - // 3. Fix Right (Remove key 0 and shift child 0 out) - // We basically remove key at 0. Child 0 was moved to left. Child 1 becomes Child 0. - // Re-using Remove logic implies shifts. - // Manual shift for performance: int rCount = rightInternal.Header.Count; - // Shift children - for (int i = 0; i < rCount; i++) rightInternal.Children[i] = rightInternal.Children[i + 1]; + Span> rightChildren = rightInternal.Children; + rightChildren.Slice(1, rCount).CopyTo(rightChildren); + if (rCount > 1) { - // Fast Span memory moves (Replaces Array.Copy & manual loop) - rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0)); + Span rightKeys = rightInternal.Keys; + rightKeys.Slice(1, rCount - 1).CopyTo(rightKeys); if (strategy.UsesPrefixes) { - rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0)); + rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes); } } @@ -735,9 +653,8 @@ namespace PersistentMap } private static void RotateRight(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) - where TStrategy : IKeyStrategy + where TStrategy : IKeyStrategy { - // Move one item from Left to Right if (left.IsLeaf) { var leftLeaf = left.AsLeaf(); @@ -749,31 +666,29 @@ namespace PersistentMap parent.Keys[separatorIndex] = rightLeaf.Keys[0]; if (strategy.UsesPrefixes) - parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); + { + parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); + } } else { - var leftInternal = (InternalNode)left; - var rightInternal = (InternalNode)right; + var leftInternal = left.AsInternal(); + var rightInternal = right.AsInternal(); int last = leftInternal.Header.Count - 1; - // 1. Move Parent Separator to Right Start K sep = parent.Keys[separatorIndex]; - // The child moving to right is the *last* child of left (index count) InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy); - // 2. Move Left[last] Key to Parent parent.Keys[separatorIndex] = leftInternal.Keys[last]; if (strategy.UsesPrefixes) - parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); + { + parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); + } - // 3. Truncate Left leftInternal.SetCount(last); } } - - public static bool TryGetMin(Node root, out K key, out V value) { var current = root; @@ -790,7 +705,7 @@ namespace PersistentMap return false; } - key = leaf.Keys![0]; + key = leaf.Keys[0]; value = leaf.Values[0]; return true; } @@ -813,7 +728,7 @@ namespace PersistentMap } int last = leaf.Header.Count - 1; - key = leaf.Keys![last]; + key = leaf.Keys[last]; value = leaf.Values[last]; return true; } @@ -826,7 +741,6 @@ namespace PersistentMap int depth = 0; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; - var current = root; while (!current.IsLeaf) { @@ -841,23 +755,19 @@ namespace PersistentMap var leaf = current.AsLeaf(); int index = FindIndex(leaf, key, keyPrefix, strategy); - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) index++; + if (index < leaf.Header.Count && strategy.Compare(leaf.Keys[index], key) == 0) index++; - // 1. Successor is in the same leaf if (index < leaf.Header.Count) { - nextKey = leaf.Keys![index]; + nextKey = leaf.Keys[index]; nextValue = leaf.Values[index]; return true; } - // 2. Successor is in the next leaf (We must backtrack up the tree!) for (int i = depth - 1; i >= 0; i--) { - // If we haven't reached the right-most child of this parent if (indices[i] < path[i].Header.Count) { - // Take one step right, then go absolute left all the way down current = path[i].Children[indices[i] + 1]!; while (!current.IsLeaf) { @@ -865,7 +775,7 @@ namespace PersistentMap } var targetLeaf = current.AsLeaf(); - nextKey = targetLeaf.Keys![0]; + nextKey = targetLeaf.Keys[0]; nextValue = targetLeaf.Values[0]; return true; } @@ -879,13 +789,11 @@ namespace PersistentMap public static bool TryGetPredecessor(Node root, K key, TStrategy strategy, out K prevKey, out V prevValue) where TStrategy : IKeyStrategy { - // Max depth of a B-Tree is small, preallocate a small array to track the descent path. InternalNode[] path = new InternalNode[32]; int[] indices = new int[32]; int depth = 0; long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; - var current = root; while (!current.IsLeaf) { @@ -900,20 +808,17 @@ namespace PersistentMap var leaf = current.AsLeaf(); int index = FindIndex(leaf, key, keyPrefix, strategy); - // Easy case: Predecessor is in the same leaf if (index > 0) { - prevKey = leaf.Keys![index - 1]; + prevKey = leaf.Keys[index - 1]; prevValue = leaf.Values[index - 1]; return true; } - // Hard case: We need to backtrack to find the first left branch we ignored for (int i = depth - 1; i >= 0; i--) { if (indices[i] > 0) { - // Jump to the left sibling branch, then take the absolute right-most path down current = path[i].Children[indices[i] - 1]!; while (!current.IsLeaf) { @@ -923,7 +828,7 @@ namespace PersistentMap var targetLeaf = current.AsLeaf(); int last = targetLeaf.Header.Count - 1; - prevKey = targetLeaf.Keys![last]; + prevKey = targetLeaf.Keys[last]; prevValue = targetLeaf.Values[last]; return true; } @@ -934,6 +839,4 @@ namespace PersistentMap return false; } } - - } diff --git a/PersistentMap/BaseOrderedMap.cs b/PersistentMap/BaseOrderedMap.cs index ee99960..613bca0 100644 --- a/PersistentMap/BaseOrderedMap.cs +++ b/PersistentMap/BaseOrderedMap.cs @@ -40,13 +40,13 @@ public abstract class BaseOrderedMap : IEnumerable Create(TStrategy strategy) { // Start with an empty leaf owned by None so the first write triggers CoW. - var emptyRoot = new LeafNode(OwnerId.None); + var emptyRoot = new LeafNode(OwnerId.None, strategy.UsesPrefixes); return new PersistentMap(emptyRoot, strategy, 0); } public static TransientMap CreateTransient(TStrategy strategy) { - var emptyRoot = new LeafNode(OwnerId.None); + var emptyRoot = new LeafNode(OwnerId.None, strategy.UsesPrefixes); return new TransientMap(emptyRoot, strategy,0); } diff --git a/PersistentMap/KeyStrategies.cs b/PersistentMap/KeyStrategies.cs index 2efb4ac..2bc14a6 100644 --- a/PersistentMap/KeyStrategies.cs +++ b/PersistentMap/KeyStrategies.cs @@ -18,38 +18,10 @@ public interface IKeyStrategy // bool IsLossless => false; + bool UseBinarySearch => false; } -/// -/// A universal key strategy for any type that relies on standard comparisons -/// (IComparable, IComparer, or custom StringComparers) without SIMD prefixes. -/// -public readonly struct StandardStrategy : IKeyStrategy -{ - private readonly IComparer _comparer; - - // If no comparer is provided, it defaults to Comparer.Default - // which automatically uses IComparable if the type implements it. - public StandardStrategy(IComparer? comparer = null) - { - _comparer = comparer ?? Comparer.Default; - } - - // Tell the B-Tree to skip SIMD routing and just use LinearSearch - public bool UsesPrefixes => false; - - // This will never be called because UsesPrefixes is false, - // but we must satisfy the interface. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetPrefix(K key) => 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(K x, K y) - { - return _comparer.Compare(x, y); - } -} public struct UnicodeStrategy : IKeyStrategy @@ -89,159 +61,5 @@ public struct UnicodeStrategy : IKeyStrategy public bool UsesPrefixes => true; } -public struct IntStrategy : IKeyStrategy -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(int x, int y) => x.CompareTo(y); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetPrefix(int key) - { - // Pack the 32-bit int into the high 32-bits of the long. - // This preserves sorting order when scanning the long array. - // Cast to uint first to prevent sign extension confusion during the shift, - // though standard int shifting usually works fine for direct mapping. - return (long)key << 32; - } - - public bool UsesPrefixes => true; - - public bool IsLossless => true; -} - -public struct DoubleStrategy : IKeyStrategy -{ - public bool IsLossless => true; - // Use the standard comparison for the fallback/refine step - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(double x, double y) => x.CompareTo(y); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetPrefix(double key) - { - // 1. Bit Cast to Long (0 cost) - long bits = Unsafe.As(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. -/// -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); - - return Avx512F.IsSupported - ? ScanAvx512(prefixes, targetPrefix) - : ScanAvx2(prefixes, targetPrefix); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int LinearScan(ReadOnlySpan prefixes, long target) - { - for (var i = 0; i < prefixes.Length; i++) - if (prefixes[i] >= target) - return i; - return prefixes.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int ScanAvx2(ReadOnlySpan prefixes, long target) - { - // Create a vector where every element is the target prefix - var vTarget = Vector256.Create(target); - var i = 0; - var len = prefixes.Length; - - // Process 4 longs at a time (256 bits) - for (; i <= len - 4; i += 4) - fixed (long* ptr = prefixes) - { - var vData = Avx2.LoadVector256(ptr + i); - - // Compare: result is -1 (all 1s) if true, 0 if false - // We want Data >= Target. - // AVX2 CompareGreaterThan is for signed. Longs should be treated carefully, - // but for text prefixes (positive), signed compare is usually sufficient. - // Effectively: !(Data < Target) could be safer if signs vary, - // but here we assume prefixes are derived from unsigned chars. - // Standard AVX2 hack for CompareGreaterOrEqual (Signed): - // No native _mm256_cmpge_epi64 in AVX2. - // Use CompareGreaterThan(Data, Target - 1) - var vResult = Avx2.CompareGreaterThan(vData, Vector256.Create(target - 1)); - - var mask = Avx2.MoveMask(vResult.AsByte()); - - if (mask != 0) - { - // Identify the first set bit corresponding to a 64-bit element - // MoveMask returns 32 bits (1 per byte). Each long is 8 bytes. - // We check bits 0, 8, 16, 24. - if ((mask & 0xFF) != 0) return i + 0; - if ((mask & 0xFF00) != 0) return i + 1; - if ((mask & 0xFF0000) != 0) return i + 2; - return i + 3; - } - } - - return LinearScan(prefixes.Slice(i), target) + i; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int ScanAvx512(ReadOnlySpan prefixes, long target) - { - var vTarget = Vector512.Create(target); - var i = 0; - var len = prefixes.Length; - - for (; i <= len - 8; i += 8) - fixed (long* ptr = prefixes) - { - var vData = Avx512F.LoadVector512(ptr + i); - // AVX512 has dedicated Compare Greater Than or Equal Long - var mask = Avx512F.CompareGreaterThanOrEqual(vData, vTarget); - - if (mask != Vector512.Zero) - { - // Extract most significant bit mask - var m = mask.ExtractMostSignificantBits(); - // Count trailing zeros to find the index - return i + BitOperations.TrailingZeroCount(m); - } - } - - return LinearScan(prefixes.Slice(i), target) + i; - } -} diff --git a/PersistentMap/KeyStrategies/DoubleStrategy.cs b/PersistentMap/KeyStrategies/DoubleStrategy.cs index e41425e..b62090e 100644 --- a/PersistentMap/KeyStrategies/DoubleStrategy.cs +++ b/PersistentMap/KeyStrategies/DoubleStrategy.cs @@ -1,3 +1,5 @@ +namespace PersistentMap; +using System.Runtime.CompilerServices; public struct DoubleStrategy : IKeyStrategy { diff --git a/PersistentMap/KeyStrategies/IntStrategy.cs b/PersistentMap/KeyStrategies/IntStrategy.cs index e69de29..f6133e2 100644 --- a/PersistentMap/KeyStrategies/IntStrategy.cs +++ b/PersistentMap/KeyStrategies/IntStrategy.cs @@ -0,0 +1,16 @@ +namespace PersistentMap; + +using System.Runtime.CompilerServices; + +public struct IntStrategy : IKeyStrategy +{ + public bool UsesPrefixes => false; + public bool IsLossless => true; + public bool UseBinarySearch => false; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Compare(int x, int y) => x.CompareTo(y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetPrefix(int key) => 0; // Unused +} diff --git a/PersistentMap/KeyStrategies/PrefixScanner.cs b/PersistentMap/KeyStrategies/PrefixScanner.cs index 7ad9de1..9f6d907 100644 --- a/PersistentMap/KeyStrategies/PrefixScanner.cs +++ b/PersistentMap/KeyStrategies/PrefixScanner.cs @@ -1,3 +1,9 @@ +namespace PersistentMap; + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; // For AVX2 +using System.Numerics; /// /// Helper for SIMD accelerated prefix scanning. /// diff --git a/PersistentMap/KeyStrategies/StandardStrategy.cs b/PersistentMap/KeyStrategies/StandardStrategy.cs index 835b661..23ebfdd 100644 --- a/PersistentMap/KeyStrategies/StandardStrategy.cs +++ b/PersistentMap/KeyStrategies/StandardStrategy.cs @@ -1,4 +1,6 @@ +namespace PersistentMap; +using System.Runtime.CompilerServices; /// /// A universal key strategy for any type that relies on standard comparisons /// (IComparable, IComparer, or custom StringComparers) without SIMD prefixes. @@ -9,11 +11,16 @@ public readonly struct StandardStrategy : IKeyStrategy // If no comparer is provided, it defaults to Comparer.Default // which automatically uses IComparable if the type implements it. - public StandardStrategy(IComparer? comparer = null) + + public StandardStrategy() + { + _comparer = Comparer.Default; + } + + public StandardStrategy(IComparer? comparer) { _comparer = comparer ?? Comparer.Default; } - // Tell the B-Tree to skip SIMD routing and just use LinearSearch public bool UsesPrefixes => false; diff --git a/PersistentMap/Nodes.cs b/PersistentMap/Nodes.cs index 37e8533..1e1d6ba 100644 --- a/PersistentMap/Nodes.cs +++ b/PersistentMap/Nodes.cs @@ -32,6 +32,12 @@ public struct NodeHeader } } +[InlineArray(32)] +public struct KeyBuffer +{ + private K _element0; +} + // Constraint: Internal Nodes fixed at 32 children. // This removes the need for a separate array allocation for children references. [InlineArray(32)] @@ -82,6 +88,12 @@ public abstract class Node // Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow. return Unsafe.As>(this); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +public PrefixInternalNode AsPrefixInternal() +{ + return Unsafe.As>(this); +} } public sealed class LeafNode : Node @@ -96,12 +108,14 @@ public sealed class LeafNode : Node public override Span AllPrefixes => _prefixes != null ? _prefixes : Span.Empty; - public LeafNode(OwnerId owner) : base(owner, NodeFlags.IsLeaf | NodeFlags.HasPrefixes) + public LeafNode(OwnerId owner, bool usePrefixes) : base(owner, NodeFlags.IsLeaf | (usePrefixes ? NodeFlags.HasPrefixes : NodeFlags.None)) { Keys = new K[Capacity]; Values = new V[Capacity]; - _prefixes = new long[Capacity]; - + if (usePrefixes) + { + _prefixes = new long[Capacity]; + } } // Copy Constructor for CoW @@ -153,67 +167,75 @@ public sealed class LeafNode : Node } } -public sealed class InternalNode : Node +public class InternalNode : Node { public const int Capacity = 32; - // InlineArray storage - internal InternalPrefixBuffer _prefixBuffer; + public KeyBuffer Keys; public NodeBuffer Children; - - public K[]? Keys; - - public override Span AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity); - public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes) + public override Span AllPrefixes => Span.Empty; + + public InternalNode(OwnerId owner, NodeFlags flags = NodeFlags.None) + : base(owner, flags) { - Keys = new K[Capacity]; - // Children buffer is a struct, zero-initialized by default } - // Copy Constructor for CoW - private InternalNode(InternalNode original, OwnerId newOwner) - : base(newOwner, original.Header.Flags) + // Fixed CoW Constructor + protected InternalNode(InternalNode original, OwnerId newOwner, NodeFlags flags) + : base(newOwner, flags) { Header.Count = original.Header.Count; - Keys = new K[Capacity]; - Array.Copy(original.Keys, Keys, original.Header.Count); - // Fast struct blit for prefixes - this._prefixBuffer = original._prefixBuffer; + // Fast struct blit for both Keys and Children. + // No loop required for InlineArrays! + this.Keys = original.Keys; + this.Children = original.Children; + } - var srcChildren = original.GetChildren(); - for (var i = 0; i < srcChildren.Length; i++) Children[i] = srcChildren[i]; + // The missing method needed by BTreeFunctions for routing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span> GetChildren() + { + // An internal node always has (Count + 1) children + return MemoryMarshal.CreateSpan(ref Children[0], Header.Count + 1); + } + + public override Span GetKeys() => MemoryMarshal.CreateSpan(ref Keys[0], Header.Count); + + public override Node EnsureEditable(OwnerId transactionId) + { + if (transactionId == OwnerId.None) return new InternalNode(this, OwnerId.None, Header.Flags); + if (Header.Owner == transactionId) return this; + return new InternalNode(this, transactionId, Header.Flags); + } +} + + +public sealed class PrefixInternalNode : InternalNode +{ + internal InternalPrefixBuffer _prefixBuffer; + + public override Span AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity); + + public PrefixInternalNode(OwnerId owner) + : base(owner, NodeFlags.HasPrefixes) + { + } + + // CoW Constructor + private PrefixInternalNode(PrefixInternalNode original, OwnerId newOwner) + : base(original, newOwner, original.Header.Flags) + { + // Copy the base Keys and Children, then blit the prefix buffer + this._prefixBuffer = original._prefixBuffer; } public override Node EnsureEditable(OwnerId transactionId) { - if (transactionId == OwnerId.None) - { - return new InternalNode(this, OwnerId.None); - } - - if (Header.Owner == transactionId) - { - return this; - } - - return new InternalNode(this, transactionId); - } - public override Span GetKeys() - { - return Keys.AsSpan(0, Header.Count); - } - - // Exposes the InlineArray as a Span - public Span?> GetChildren() - { - return MemoryMarshal.CreateSpan?>(ref Children[0]!, Header.Count + 1); - } - - public void SetChild(int index, Node node) - { - Children[index] = node; + if (transactionId == OwnerId.None) return new PrefixInternalNode(this, OwnerId.None); + if (Header.Owner == transactionId) return this; + return new PrefixInternalNode(this, transactionId); } } diff --git a/PersistentMap/PersistentMap.cs b/PersistentMap/PersistentMap.cs index 5f6ed43..c691918 100644 --- a/PersistentMap/PersistentMap.cs +++ b/PersistentMap/PersistentMap.cs @@ -25,7 +25,7 @@ public sealed class PersistentMap : BaseOrderedMap(default(OwnerId)); + var emptyRoot = new LeafNode(default(OwnerId), strategy.UsesPrefixes); return new PersistentMap(emptyRoot, strategy, 0); } diff --git a/PersistentMap/Readme.org b/PersistentMap/Readme.org index f608d5f..af2ae28 100644 --- a/PersistentMap/Readme.org +++ b/PersistentMap/Readme.org @@ -5,16 +5,16 @@ A high-performance, persistent (Copy-on-Write) B+ Tree implemented in C#. It is designed for zero-overhead reads, SIMD-accelerated key routing, and allocation-free range queries. It supports both fully immutable usage and "Transient" mode for high-throughput bulk mutations. ** Features -- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tree yields a new version while sharing unmodified nodes. +- *Copy-on-Write Semantics*: Thread-safe, immutable tree states. Modifying the tre yields a new version while sharing unmodified nodes. - *Transient Mode*: Perform bulk mutations in-place with standard mutable performance, then freeze it into a =PersistentMap= in $O(1)$ time. - *SIMD Prefix Scanning*: Uses AVX2/AVX512 to vectorize B+ tree routing and binary searches via =long= key-prefixes. - *Linear Time Set Operations*: Sort-merge based =Intersect=, =Except=, and =SymmetricExcept= execute in $O(N+M)$ time using lazy evaluation. ** When should I use this? -Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does not have any problems key collisions, which will slow this map down by a lot. +Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does have a very nice API, and it also has a performance model that is easy to understand. If you look at the performance characteristics of it below, it scales much more linear with collection size. For example: I don't know why PersistentMap lookups suddenly becomes so much slower when we reach 100000 integer keys. There might be a gazillion things like that, that make PersistentMap much slower for real world usage. -The general version of this, using =StandardStrategy= does not benefit from the prefix optimization. +The general version of this, using =StandardStrategy= does not benefit from the prefix optimization, although might benefit from a usage of binary search in the future. ** Quick Start @@ -36,8 +36,8 @@ if (map2.TryGetValue(2, out var value)) } #+end_src -*** 2. Transient Mode (Bulk Mutations) -If you need to insert thousands of elements, creating a new persistent tree on every insert is too slow. Use a =TransientMap= to mutate the tree in-place, then lock it into a persistent snapshot. +*** 2. Transient Mode (Bulk Mutations +If you need to insert thousands of elements, creating a new persistent tree on every insert is too slow. Use a =TransientMap= to mutate the tree in-place, then lock it into a persistent snapshot. This does not edit an existing map, but will make bulk operations a lot faster on nodes "owned" by the current map. #+begin_src csharp var transientMap = BaseOrderedMap.CreateTransient(new IntStrategy()); @@ -53,7 +53,7 @@ var persistentSnapshot = transientMap.ToPersistent(); #+end_src *** 3. Range Queries and Iteration -Because it is a B+ tree, leaf nodes are linked. Range queries require zero allocations and simply walk the leaves. +Because it is a B+ tree range queries require zero allocations and simply walk the leaves. #+begin_src csharp var map = GetPopulatedMap(); @@ -68,7 +68,7 @@ foreach (var kvp in map.Range(min: 10, max: 50)) var greaterThan100 = map.From(100); var lessThan50 = map.Until(50); var allElements = map.AsEnumerable(); -#+end_src +#+end_sr *** 4. Tree Navigation Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound. @@ -91,7 +91,7 @@ if (map.TryGetPredecessor(42, out int prevKey, out string prevVal)) #+end_src *** 5. Set Operations -Set operations take advantage of the tree's underlying sorted linked-list structure to merge trees in linear $O(N+M)$ time. +Set operations take advantage of the tree's underlying sorted structure to merge trees in linear $O(N+M)$ time. #+begin_src csharp var mapA = CreateMap(1, 2, 3, 4); @@ -108,146 +108,502 @@ var symmetricDiff = mapA.SymmetricExcept(mapB); #+end_src ** Benchmarks -This is going to be all over the place, but here is a small comparison to other immutable sequences. Due to how the prefix optimization works, this persistent map will be the absolutely most performant when there is high entropy in the first 8 bytes of the key. The following is pretty much the best scenario we can have since we probably only look at the first 8 characters (this is for reading a value). +These benchmarks tries a variety of operations. The Int benchmarks (the first ones) use avx for lookups. On a computer without avx this is bound to be slower. In benchmarks where there are many writes to the tree, the transient version is used, since this is the workload this datastructure is optimized for. -#+begin_src -| Method | CollectionSize | KeySize | Mean | Gen0 | Allocated | -|-----------------|----------------|----------|--------------:|-----------:|----------:| -| PersistentMap | **1024** | **10** | **25.61 ns** | **0.0043** | **72 B** | -| Sys.Sorted | 1024 | 10 | 153.18 ns | - | - | -| LangExt.HashMap | 1024 | 10 | 24.80 ns | - | - | -| LangExtSorted | 1024 | 10 | 176.90 ns | - | - | -| PersistentMap | **1024** | **100** | **26.43 ns** | **0.0043** | **72 B** | -| SysSorted | 1024 | 100 | 154.77 ns | - | - | -| LangExt.HashMap | 1024 | 100 | 66.30 ns | - | - | -| LangExtSorted | 1024 | 100 | 177.28 ns | - | - | -| PersistentMap | **1024** | **1000** | **26.17 ns** | **0.0043** | **72 B** | -| SysSorted | 1024 | 1000 | 155.68 ns | - | - | -| LangExt.HashMap | 1024 | 1000 | 491.97 ns | - | - | -| LangExtSorted | 1024 | 1000 | 181.58 ns | - | - | -| PersistentMap | **131072** | **10** | **109.34 ns** | **0.0072** | **120 B** | -| SysSorted | 131072 | 10 | 460.22 ns | - | - | -| LangExt.HashMap | 131072 | 10 | 60.35 ns | - | - | -| LangExtSorted | 131072 | 10 | 555.17 ns | - | - | -| PersistentMap | **131072** | **100** | **147.30 ns** | **0.0072** | **120 B** | -| SysSorted | 131072 | 100 | 556.39 ns | - | - | -| LangExt.HashMap | 131072 | 100 | 162.81 ns | - | - | -| LangExtSorted | 131072 | 100 | 605.15 ns | - | - | -| PersistentMap | **131072** | **1000** | **170.16 ns** | **0.0072** | **120 B** | -| SysSorted | 131072 | 1000 | 625.78 ns | - | - | -| LangExt.HashMap | 131072 | 1000 | 763.75 ns | - | - | -| LangExtSorted | 131072 | 1000 | 692.92 ns | - | - | +Build: builds a map of size N. I did not benchmark the builders used by the built in collections, but they are almost certainly at least as fast as the transients used by this library. For integers, the map was sorted (triggering a small optimization in PersistentMap). For the string benchmark it was random. +The retrieval benchmarks reads a subset of the keys in random order. -#+end_src +The update benchmarks updates a subset of the keys in random order. -To look at pure overhead, here is a benchmark using integers as keys. This is also a good fit for this BTree, since it can utilize a key strategy that compares integers using AVX. The hash based alternatives are going to have a huge advantage here. As you can see, reading a single value isn't great, and setting a single value after building the btree is also pretty awful (setting many should probably be done using transients). Iterating a building (using transients. The only valid comparison here is probably to MS SortedDict) is however plenty fast. +The update and set benchmarks updates and sets keys in random order. Half of the keys are new. + +The iteration benchmarks iterate from start to finish. The ordered collections are of course in order. + +The removal benchmarks removes a subset of the keys in random order. + +*** Integer keys +This is pretty much the best case scenario for everyone. Key comparisons are fast, hashing is minimal. For b+trees this means we can do a lot of key comparisons at once using avx. The machine this is done on is an amd 5900x, which supports some kind of bastardized avx512. It is not really a gain over avx256 in this benchmark though on that processor. With regards to building, the microsoft collections have builders, and they are about as fast as the TransientMap, but a little little slower on my computer. LanguageExt lacks transients, and thus these comparisons are _not_ fair. + +ImmDict is System.Collections.Immutable dictionary. ImmSortedDict is it's sorted sibling. ExtMap is the sorted map from LanguageExt. ExtHashMap is the unsorted HashMap from LanguageExt. #+begin_src -| Method | N | Mean | Gen0 | Gen1 | Gen2 | Allocated | -|-------------------------------------|--------|-----------------:|----------:|----------:|--------:|------------:| -| 'Build: PersistentMap (Transient)' | 100 | 3,764.63 ns | 0.3929 | 0.0038 | - | 6632 B | -| 'Build: MS Sorted (Builder)' | 100 | 3,096.11 ns | 0.2899 | 0.0038 | - | 4864 B | -| 'Build: LanguageExt Map (AVL)' | 100 | 6,967.02 ns | 2.2736 | 0.0229 | - | 38144 B | -| 'Build: LanguageExt HashMap' | 100 | 4,594.07 ns | 1.9684 | 0.0076 | - | 33024 B | -| 'Read: PersistentMap' | 100 | 1,596.68 ns | 0.4292 | - | - | 7200 B | -| 'Read: MS Sorted' | 100 | 474.54 ns | - | - | - | - | -| 'Read: LanguageExt Map' | 100 | 1,311.31 ns | - | - | - | - | -| 'Read: LanguageExt HashMap' | 100 | 641.22 ns | - | - | - | - | -| 'Iterate: PersistentMap' | 100 | 135.41 ns | - | - | - | - | -| 'Iterate: MS Sorted' | 100 | 372.31 ns | - | - | - | - | -| 'Iterate: LanguageExt Map' | 100 | 287.33 ns | 0.0019 | - | - | 32 B | -| 'Iterate: LanguageExt HashMap' | 100 | 781.56 ns | 0.0648 | - | - | 1088 B | -| 'Set: PersistentMap' | 100 | 85.68 ns | 0.1142 | 0.0007 | - | 1912 B | -| 'Set: MS Sorted' | 100 | 66.44 ns | 0.0229 | - | - | 384 B | -| 'Set: LanguageExt Map' | 100 | 60.04 ns | 0.0219 | - | - | 368 B | -| 'Set: LanguageExt HashMap' | 100 | 36.62 ns | 0.0206 | - | - | 344 B | -| 'Build: PersistentMap (Transient)' | 1000 | 49,445.56 ns | 3.1738 | 0.2441 | - | 53096 B | -| 'Build: MS Sorted (Builder)' | 1000 | 50,163.19 ns | 2.8687 | 0.4272 | - | 48064 B | -| 'Build: LanguageExt Map (AVL)' | 1000 | 103,877.98 ns | 34.6680 | 3.1738 | - | 580688 B | -| 'Build: LanguageExt HashMap' | 1000 | 124,339.17 ns | 45.4102 | 3.2959 | - | 760096 B | -| 'Read: PersistentMap' | 1000 | 17,671.71 ns | 4.3030 | - | - | 72000 B | -| 'Read: MS Sorted' | 1000 | 7,911.72 ns | - | - | - | - | -| 'Read: LanguageExt Map' | 1000 | 20,187.52 ns | - | - | - | - | -| 'Read: LanguageExt HashMap' | 1000 | 9,740.28 ns | - | - | - | - | -| 'Iterate: PersistentMap' | 1000 | 1,217.47 ns | - | - | - | - | -| 'Iterate: MS Sorted' | 1000 | 3,875.47 ns | - | - | - | - | -| 'Iterate: LanguageExt Map' | 1000 | 2,862.82 ns | - | - | - | 32 B | -| 'Iterate: LanguageExt HashMap' | 1000 | 11,974.93 ns | 1.9226 | - | - | 32320 B | -| 'Set: PersistentMap' | 1000 | 121.01 ns | 0.1142 | 0.0007 | - | 1912 B | -| 'Set: MS Sorted' | 1000 | 91.62 ns | 0.0315 | - | - | 528 B | -| 'Set: LanguageExt Map' | 1000 | 82.26 ns | 0.0305 | - | - | 512 B | -| 'Set: LanguageExt HashMap' | 1000 | 57.02 ns | 0.0367 | - | - | 616 B | -| 'Build: PersistentMap (Transient)' | 100000 | 10,808,233.62 ns | 296.8750 | 218.7500 | - | 5185832 B | -| 'Build: MS Sorted (Builder)' | 100000 | 16,655,882.43 ns | 281.2500 | 250.0000 | - | 4800064 B | -| 'Build: LanguageExt Map (AVL)' | 100000 | 39,932,734.83 ns | 5333.3333 | 3333.3333 | - | 89959040 B | -| 'Build: LanguageExt HashMap' | 100000 | 21,220,179.10 ns | 5781.2500 | 2968.7500 | 31.2500 | 96555422 B | -| 'Read: PersistentMap' | 100000 | 7,359,807.97 ns | 710.9375 | - | - | 12000000 B | -| 'Read: MS Sorted' | 100000 | 8,428,009.48 ns | - | - | - | - | -| 'Read: LanguageExt Map' | 100000 | 10,268,884.43 ns | - | - | - | - | -| 'Read: LanguageExt HashMap' | 100000 | 1,936,555.07 ns | - | - | - | - | -| 'Iterate: PersistentMap' | 100000 | 151,028.79 ns | - | - | - | - | -| 'Iterate: MS Sorted' | 100000 | 1,068,072.16 ns | - | - | - | - | -| 'Iterate: LanguageExt Map' | 100000 | 837,677.39 ns | - | - | - | 32 B | -| 'Iterate: LanguageExt HashMap' | 100000 | 1,226,773.82 ns | 64.4531 | - | - | 1082432 B | -| 'Set: PersistentMap' | 100000 | 208.61 ns | 0.1984 | 0.0024 | - | 3320 B | -| 'Set: MS Sorted' | 100000 | 138.82 ns | 0.0458 | - | - | 768 B | -| 'Set: LanguageExt Map' | 100000 | 128.28 ns | 0.0448 | - | - | 752 B | -| 'Set: LanguageExt HashMap' | 100000 | 84.33 ns | 0.0583 | - | - | 976 B | - +| Method | N | Mean | Gen0 | Gen1 | Gen2 | Allocated | +|-------------------------|--------|-----------------:|-----------:|----------:|--------:|------------:| +| Build_ImmDict | 100 | 11,307.04 ns | 4.9744 | 0.0458 | - | 41688 B | +| Build_ImmSortedDict | 100 | 8,493.79 ns | 4.4250 | 0.0458 | - | 37104 B | +| Build_ExtMap | 100 | 8,519.63 ns | 5.3101 | 0.0458 | - | 44432 B | +| Build_ExtHashMap | 100 | 9,855.33 ns | 7.5378 | 0.0458 | - | 63104 B | +| Build_PersistentMap | 100 | 8,698.33 ns | 16.3879 | 0.1526 | - | 137072 B | +| Build_TransientMap | 100 | 1,665.90 ns | 0.6332 | 0.0038 | - | 5304 B | +| Retrieve_ImmDict | 100 | 39.19 ns | - | - | - | - | +| Retrieve_ImmSortedDict | 100 | 64.32 ns | - | - | - | - | +| Retrieve_ExtMap | 100 | 117.61 ns | - | - | - | - | +| Retrieve_ExtHashMap | 100 | 84.19 ns | - | - | - | - | +| Retrieve_PersistentMap | 100 | 47.25 ns | - | - | - | - | +| Update_ImmDict | 100 | 1,145.75 ns | 0.4616 | 0.0019 | - | 3872 B | +| Update_PersistentMap | 100 | 1.107 μs | 1.9398 | 0.0248 | - | 15.86 KB | +| Update_TransientMap | 100 | 347.49 ns | 0.3576 | 0.0033 | - | 2992 B | +| Update_ImmSortedDict | 100 | 849.39 ns | 0.3958 | 0.0010 | - | 3312 B | +| Update_ExtMap | 100 | 642.50 ns | 0.3939 | 0.0010 | - | 3296 B | +| Update_ExtHashMap | 100 | 541.84 ns | 0.5283 | 0.0010 | - | 4424 B | +| UpdateSet_ImmDict | 100 | 1,236.82 ns | 0.5226 | 0.0019 | - | 4376 B | +| UpdateSet_PersistentMap | 100 | 1189 ns | 1.9398 | 0.0248 | - | 15.86 KB | +| UpdateSet_TransientMap | 100 | 380.70 ns | 0.3576 | 0.0033 | - | 2992 B | +| UpdateSet_ImmSortedDict | 100 | 887.07 ns | 0.4587 | 0.0010 | - | 3840 B | +| UpdateSet_ExtMap | 100 | 856.76 ns | 0.4797 | 0.0010 | - | 4016 B | +| UpdateSet_ExtHashMap | 100 | 582.31 ns | 0.5312 | 0.0019 | - | 4448 B | +| Iterate_ImmDict | 100 | 1,324.41 ns | - | - | - | - | +| Iterate_PersistentMap | 100 | 175.98 ns | - | - | - | - | +| Iterate_ImmSortedDict | 100 | 488.69 ns | - | - | - | - | +| Iterate_ExtMap | 100 | 337.40 ns | 0.0038 | - | - | 32 B | +| Iterate_ExtHashMap | 100 | 1,209.77 ns | 0.2518 | - | - | 2112 B | +| Remove_ImmDict | 100 | 899.57 ns | 0.4425 | 0.0010 | - | 3704 B | +| Remove_TransientMap | 100 | 433.52 ns | 0.3290 | 0.0029 | - | 2752 B | +| Remove_ImmSortedDict | 100 | 728.77 ns | 0.3786 | 0.0010 | - | 3168 B | +| Remove_ExtMap | 100 | 653.72 ns | 0.3767 | 0.0010 | - | 3152 B | +| Remove_ExtHashMap | 100 | 589.03 ns | 0.5178 | - | - | 4336 B | +| Build_ImmDict | 1000 | 168,692.47 ns | 71.5332 | 6.8359 | - | 598712 B | +| Build_ImmSortedDict | 1000 | 125,591.01 ns | 62.9883 | 5.1270 | - | 526896 B | +| Build_ExtMap | 1000 | 117,763.81 ns | 72.3877 | 6.1035 | - | 605936 B | +| Build_ExtHashMap | 1000 | 64,443.19 ns | 67.5049 | 1.5869 | - | 564864 B | +| Build_PersistentMap | 1000 | 133,156.05 ns | 192.1387 | 7.8125 | - | 1607744 B | +| Build_TransientMap | 1000 | 25,945.72 ns | 4.2725 | 0.1526 | - | 35976 B | +| Retrieve_ImmDict | 1000 | 686.48 ns | - | - | - | - | +| Retrieve_ImmSortedDict | 1000 | 1,145.83 ns | - | - | - | - | +| Retrieve_ExtMap | 1000 | 2,276.07 ns | - | - | - | - | +| Retrieve_ExtHashMap | 1000 | 808.53 ns | - | - | - | - | +| Retrieve_PersistentMap | 1000 | 680.50 ns | - | - | - | - | +| Update_ImmDict | 1000 | 16,863.81 ns | 6.5613 | 0.2136 | - | 54960 B | +| Update_PersistentMap | 1000 | 13,617.12 ns | 19.4092 | 1.1597 | - | 158.59 KB | +| Update_TransientMap | 1000 | 3,611.03 ns | 2.5406 | 0.1564 | - | 21280 B | +| Update_ImmSortedDict | 1000 | 12,428.90 ns | 5.5542 | 0.1526 | - | 46464 B | +| Update_ExtMap | 1000 | 10,091.51 ns | 5.6000 | 0.1678 | - | 46880 B | +| Update_ExtHashMap | 1000 | 6,758.96 ns | 7.9575 | 0.2136 | - | 66616 B | +| UpdateSet_ImmDict | 1000 | 21,489.70 ns | 7.0496 | 0.1831 | - | 59160 B | +| UpdateSet_PersistentMap | 1000 | 14,890.21 ns | 19.4855 | 1.0529 | - | 159.23 KB | +| UpdateSet_TransientMap | 1000 | 5,063.11 ns | 2.4796 | 0.1450 | - | 20776 B | +| UpdateSet_ImmSortedDict | 1000 | 13,333.61 ns | 6.3782 | 0.1526 | - | 53472 B | +| UpdateSet_ExtMap | 1000 | 11,221.39 ns | 6.5918 | 0.1526 | - | 55184 B | +| UpdateSet_ExtHashMap | 1000 | 15,967.43 ns | 13.1836 | 0.4578 | - | 110440 B | +| Iterate_ImmDict | 1000 | 15,325.93 ns | - | - | - | - | +| Iterate_PersistentMap | 1000 | 1,574.20 ns | - | - | - | - | +| Iterate_ImmSortedDict | 1000 | 5,110.07 ns | - | - | - | - | +| Iterate_ExtMap | 1000 | 3,432.88 ns | 0.0038 | - | - | 32 B | +| Iterate_ExtHashMap | 1000 | 8,207.75 ns | 0.2441 | - | - | 2112 B | +| Remove_ImmDict | 1000 | 15,205.95 ns | 6.4392 | 0.2136 | - | 54064 B | +| Remove_TransientMap | 1000 | 4,036.18 ns | 2.2507 | 0.1373 | - | 18880 B | +| Remove_ImmSortedDict | 1000 | 10,664.14 ns | 5.7068 | 0.1678 | - | 47760 B | +| Remove_ExtMap | 1000 | 9,993.90 ns | 5.5084 | 0.1526 | - | 46160 B | +| Remove_ExtHashMap | 1000 | 7,475.24 ns | 7.7209 | 0.1907 | - | 64608 B | +| Build_ImmDict | 10000 | 2,571,753.12 ns | 41.4063 | 390.6250 | - | 7882552 B | +| Build_ImmSortedDict | 10000 | 1,975,364.14 ns | 820.3125 | 296.8750 | - | 6893616 B | +| Build_ExtMap | 10000 | 1,866,221.83 ns | 917.9688 | 320.3125 | - | 7692272 B | +| Build_ExtHashMap | 10000 | 1,215,103.58 ns | 1009.7656 | 240.2344 | - | 8446080 B | +| Build_PersistentMap | 10000 | 1,930,457.96 ns | 2345.7031 | 494.1406 | - | 19626728 B | +| Build_TransientMap | 10000 | 640,413.08 ns | 41.0156 | 8.7891 | - | 347344 B | +| Retrieve_ImmDict | 10000 | 14,880.32 ns | - | - | - | - | +| Retrieve_ImmSortedDict | 10000 | 15,595.68 ns | - | - | - | - | +| Retrieve_ExtMap | 10000 | 36,225.60 ns | - | - | - | - | +| Retrieve_ExtHashMap | 10000 | 11,987.70 ns | - | - | - | - | +| Retrieve_PersistentMap | 10000 | 10,227.73 ns | - | - | - | - | +| Update_ImmDict | 10000 | 318,905.00 ns | 86.9141 | 23.4375 | - | 730200 B | +| Update_PersistentMap | 10000 | 202,244.42 ns | 243.6523 | 73.2422 | - | 1992.19 KB | +| Update_TransientMap | 10000 | 73,203.50 ns | 24.9023 | 7.3242 | - | 209056 B | +| Update_ImmSortedDict | 10000 | 216,638.87 ns | 77.1484 | 16.1133 | - | 645360 B | +| Update_ExtMap | 10000 | 176,737.24 ns | 74.4629 | 17.5781 | - | 623600 B | +| Update_ExtHashMap | 10000 | 105,445.84 ns | 97.2900 | 17.3340 | - | 814376 B | +| UpdateSet_ImmDict | 10000 | 333,260.72 ns | 92.2852 | 19.0430 | - | 775784 B | +| UpdateSet_PersistentMap | 10000 | 221,958.91 ns | 244.6289 | 71.2891 | - | 1998.95 KB | +| UpdateSet_TransientMap | 10000 | 93,484.07 ns | 24.9023 | 7.3242 | - | 209072 B | +| UpdateSet_ImmSortedDict | 10000 | 224,214.31 ns | 83.2520 | 14.6484 | - | 697920 B | +| UpdateSet_ExtMap | 10000 | 186,761.55 ns | 83.7402 | 14.4043 | - | 700880 B | +| UpdateSet_ExtHashMap | 10000 | 112,371.27 ns | 97.7783 | 20.2637 | - | 818240 B | +| Iterate_ImmDict | 10000 | 152,686.50 ns | - | - | - | - | +| Iterate_PersistentMap | 10000 | 14,841.56 ns | - | - | - | - | +| Iterate_ImmSortedDict | 10000 | 53,372.05 ns | - | - | - | - | +| Iterate_ExtMap | 10000 | 38,673.93 ns | - | - | - | 32 B | +| Iterate_ExtHashMap | 10000 | 111,676.15 ns | 8.0566 | - | - | 67648 B | +| Remove_ImmDict | 10000 | 303,798.22 ns | 86.4258 | 19.5313 | - | 726560 B | +| Remove_TransientMap | 10000 | 58,890.93 ns | 22.0947 | 6.5308 | - | 185056 B | +| Remove_ImmSortedDict | 10000 | 219,974.63 ns | 77.8809 | 15.1367 | - | 653184 B | +| Remove_ExtMap | 10000 | 188,713.80 ns | 74.2188 | 14.4043 | - | 621248 B | +| Remove_ExtHashMap | 10000 | 120,113.97 ns | 95.9473 | 15.9912 | - | 802944 B | +| Build_ImmDict | 100000 | 38,394,437.95 ns | 11714.2857 | 1071.4286 | 71.4286 | 97460075 B | +| Build_ImmSortedDict | 100000 | 30,860,676.12 ns | 10187.5000 | 906.2500 | 62.5000 | 84908636 B | +| Build_ExtMap | 100000 | 28,415,796.22 ns | 11156.2500 | 937.5000 | 62.5000 | 92907004 B | +| Build_ExtHashMap | 100000 | 29,149,824.12 ns | 15375.0000 | 2750.0000 | 62.5000 | 128198060 B | +| Build_PersistentMap | 100000 | 24,745,757.59 ns | 27687.5000 | 375.0000 | - | 231722008 B | +| Build_TransientMap | 100000 | 9,137,195.74 ns | 406.2500 | 234.3750 | - | 3460512 B | +| Retrieve_ImmDict | 100000 | 1,259,618.31 ns | - | - | - | - | +| Retrieve_ImmSortedDict | 100000 | 975,518.14 ns | - | - | - | - | +| Retrieve_ExtMap | 100000 | 1,535,487.85 ns | - | - | - | - | +| Retrieve_ExtHashMap | 100000 | 284,590.55 ns | - | - | - | - | +| Retrieve_PersistentMap | 100000 | 429,001.27 ns | - | - | - | - | +| Update_ImmDict | 100000 | 5,705,786.31 ns | 1093.7500 | 906.2500 | - | 9183488 B | +| Update_PersistentMap | 100000 | 4,056,612.12 ns | 2945.3125 | 2781.2500 | 15.6250 | 23984.39 KB | +| Update_TransientMap | 100000 | 1,145,551.13 ns | 248.0469 | 199.2188 | - | 2081568 B | +| Update_ImmSortedDict | 100000 | 4,433,611.11 ns | 953.1250 | 796.8750 | - | 8021136 B | +| Update_ExtMap | 100000 | 3,901,065.86 ns | 937.5000 | 789.0625 | - | 7848704 B | +| Update_ExtHashMap | 100000 | 2,696,228.39 ns | 1289.0625 | 960.9375 | - | 10805952 B | +| UpdateSet_ImmDict | 100000 | 5,340,382.88 ns | 1109.3750 | 867.1875 | - | 9318896 B | +| UpdateSet_PersistentMap | 100000 | 4,629,564.21 ns | 2984.3750 | 1906.2500 | 39.0625 | 24060.44 KB | +| UpdateSet_TransientMap | 100000 | 1,332,859.76 ns | 250.0000 | 208.9844 | - | 2099520 B | +| UpdateSet_ImmSortedDict | 100000 | 4,418,076.49 ns | 1000.0000 | 992.1875 | - | 8396544 B | +| UpdateSet_ExtMap | 100000 | 3,107,339.72 ns | 996.0938 | 507.8125 | - | 8349248 B | +| UpdateSet_ExtHashMap | 100000 | 2,630,473.81 ns | 1292.9688 | 976.5625 | - | 10845480 B | +| Iterate_ImmDict | 100000 | 1,550,040.28 ns | - | - | - | - | +| Iterate_PersistentMap | 100000 | 149,743.16 ns | - | - | - | - | +| Iterate_ImmSortedDict | 100000 | 723,978.27 ns | - | - | - | - | +| Iterate_ExtMap | 100000 | 504,204.91 ns | - | - | - | 32 B | +| Iterate_ExtHashMap | 100000 | 1,936,574.10 ns | 257.8125 | - | - | 2164800 B | +| Remove_ImmDict | 100000 | 5,419,879.00 ns | 1093.7500 | 914.0625 | - | 9149160 B | +| Remove_TransientMap | 100000 | 951,332.63 ns | 219.7266 | 155.2734 | - | 1839264 B | +| Remove_ImmSortedDict | 100000 | 4,203,794.51 ns | 953.1250 | 781.2500 | - | 8028144 B | +| Remove_ExtMap | 100000 | 3,896,109.04 ns | 929.6875 | 789.0625 | - | 7824560 B | +| Remove_ExtHashMap | 100000 | 2,816,957.99 ns | 1277.3438 | 914.0625 | - | 10709360 B | #+end_src -Lastly, here is a comparison of how things look compared to itself for when the prefixes are turned off for strings. This relies on regular linear string searches. This is however STILL a pretty good benchmark for all ordered dicts, since the strings are random, meaning the string comparison can stop almost immediately. For real world keys, all hash based dicts will be better, with everything regarding getting or setting a single key. +* String keys + +These benchmarks act like above, but do not insert keys in a specific order. Sorting them before will yield a speed boost. One uses the standardkeystrategy (does linear string comparisons) and one uses the unicodstrategy which encodes the first 8 bytes as a long and uses avx to search for keys. #+begin_src -| Method | N | KeyLength | Mean | Gen0 | Gen1 | Allocated | -|--------------------------- |------ |---------- |----------------:|---------:|---------:|----------:| -| 'Build: NiceBTree' | 10000 | 10 | 2,037,851.45 ns | 35.1563 | 15.6250 | 644600 B | -| 'Build: MS HashDict' | 10000 | 10 | 1,647,876.61 ns | 37.1094 | 15.6250 | 640096 B | -| 'Build: MS SortedDict' | 10000 | 10 | 3,853,709.48 ns | 31.2500 | 11.7188 | 560112 B | -| 'Build: LangExt HashMap' | 10000 | 10 | 1,612,117.07 ns | 472.6563 | 154.2969 | 7919328 B | -| 'Build: LangExt Map' | 10000 | 10 | 5,363,298.26 ns | 507.8125 | 203.1250 | 8594784 B | -| 'Read: NiceBTree' | 10000 | 10 | 36.30 ns | - | - | - | -| 'Read: MS HashDict' | 10000 | 10 | 12.66 ns | - | - | - | -| 'Read: MS SortedDict' | 10000 | 10 | 233.59 ns | - | - | - | -| 'Read: LangExt HashMap' | 10000 | 10 | 28.61 ns | - | - | - | -| 'Read: LangExt Map' | 10000 | 10 | 268.13 ns | - | - | - | -| 'Iterate: NiceBTree' | 10000 | 10 | 12,630.95 ns | - | - | - | -| 'Iterate: MS HashDict' | 10000 | 10 | 151,314.44 ns | - | - | - | -| 'Iterate: MS SortedDict' | 10000 | 10 | 57,402.20 ns | - | - | - | -| 'Iterate: LangExt HashMap' | 10000 | 10 | 148,980.47 ns | 10.0098 | - | 170712 B | -| 'Iterate: LangExt Map' | 10000 | 10 | 34,428.07 ns | - | - | 32 B | -| 'Update: NiceBTree' | 10000 | 10 | 303.01 ns | 0.2027 | 0.0024 | 3392 B | -| 'Update: MS HashDict' | 10000 | 10 | 48.36 ns | 0.0100 | - | 168 B | -| 'Update: MS SortedDict' | 10000 | 10 | 137.47 ns | 0.0196 | - | 328 B | -| 'Update: LangExt HashMap' | 10000 | 10 | 102.57 ns | 0.0502 | 0.0001 | 840 B | -| 'Update: LangExt Map' | 10000 | 10 | 122.54 ns | 0.0186 | - | 312 B | -| 'Build: NiceBTree' | 10000 | 50 | 2,020,984.87 ns | 35.1563 | 11.7188 | 624248 B | -| 'Build: MS HashDict' | 10000 | 50 | 1,811,186.24 ns | 37.1094 | 15.6250 | 640096 B | -| 'Build: MS SortedDict' | 10000 | 50 | 3,883,214.25 ns | 31.2500 | 15.6250 | 560112 B | -| 'Build: LangExt HashMap' | 10000 | 50 | 1,784,616.64 ns | 472.6563 | 154.2969 | 7926712 B | -| 'Build: LangExt Map' | 10000 | 50 | 5,248,030.22 ns | 507.8125 | 203.1250 | 8544720 B | -| 'Read: NiceBTree' | 10000 | 50 | 40.64 ns | - | - | - | -| 'Read: MS HashDict' | 10000 | 50 | 29.91 ns | - | - | - | -| 'Read: MS SortedDict' | 10000 | 50 | 255.55 ns | - | - | - | -| 'Read: LangExt HashMap' | 10000 | 50 | 47.61 ns | - | - | - | -| 'Read: LangExt Map' | 10000 | 50 | 255.68 ns | - | - | - | -| 'Iterate: NiceBTree' | 10000 | 50 | 12,718.71 ns | - | - | - | -| 'Iterate: MS HashDict' | 10000 | 50 | 170,815.59 ns | - | - | - | -| 'Iterate: MS SortedDict' | 10000 | 50 | 68,982.58 ns | - | - | - | -| 'Iterate: LangExt HashMap' | 10000 | 50 | 144,442.27 ns | 9.7656 | - | 165600 B | -| 'Iterate: LangExt Map' | 10000 | 50 | 35,082.49 ns | - | - | 32 B | -| 'Update: NiceBTree' | 10000 | 50 | 393.56 ns | 0.2027 | 0.0024 | 3392 B | -| 'Update: MS HashDict' | 10000 | 50 | 114.57 ns | 0.0215 | - | 360 B | -| 'Update: MS SortedDict' | 10000 | 50 | 65.51 ns | 0.0129 | - | 216 B | -| 'Update: LangExt HashMap' | 10000 | 50 | 103.28 ns | 0.0535 | - | 896 B | -| 'Update: LangExt Map' | 10000 | 50 | 67.62 ns | 0.0119 | - | 200 B | +``` + +BenchmarkDotNet v0.15.8, Linux Fedora Linux 43 (Container Image) +AMD Ryzen 9 5900X 3.69GHz, 1 CPU, 24 logical and 12 physical cores +.NET SDK 10.0.104 + [Host] : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3 + ShortRun : .NET 10.0.4 (10.0.4, 10.0.426.12010), X64 RyuJIT x86-64-v3 + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 + +``` +| Method | N | StringLength | Mean | Gen0 | Gen1 | Gen2 | Allocated | +|--------------------------------- |------- |------------- |------------------:|-----------:|----------:|---------:|------------:| +| Build_TransientMap_Standard | 100 | 8 | 42,942.85 ns | 0.7324 | - | - | 6216 B | +| Build_TransientMap_Unicode | 100 | 8 | 12,289.81 ns | 0.8850 | 0.0153 | - | 7528 B | +| Build_ImmDict | 100 | 8 | 14,166.14 ns | 5.4016 | 0.0610 | - | 45280 B | +| Build_ImmSortedDict | 100 | 8 | 22,037.60 ns | 4.2114 | 0.0305 | - | 35472 B | +| Build_ExtMap | 100 | 8 | 22,837.07 ns | 5.6152 | 0.0610 | - | 47104 B | +| Build_ExtHashMap | 100 | 8 | 8,907.91 ns | 3.9673 | 0.0305 | - | 33240 B | +| Retrieve_ImmDict | 100 | 8 | 80.81 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 100 | 8 | 4,956.86 ns | 0.0534 | - | - | 480 B | +| Retrieve_PersistentMap_Unicode | 100 | 8 | 148.61 ns | - | - | - | - | +| Update_ImmDict | 100 | 8 | 1,089.03 ns | 0.4215 | - | - | 3536 B | +| Update_PersistentMap_Standard | 100 | 8 | 8,182.15 ns | 2.3956 | 0.0305 | - | 20160 B | +| Update_PersistentMap_Unicode | 100 | 8 | 1,954.07 ns | 2.7046 | 0.0496 | - | 22640 B | +| Update_TransientMap_Standard | 100 | 8 | 7,007.11 ns | 0.4349 | - | - | 3640 B | +| Update_TransientMap_Unicode | 100 | 8 | 667.55 ns | 0.4644 | 0.0057 | - | 3888 B | +| Update_ImmSortedDict | 100 | 8 | 1,741.49 ns | 0.3662 | - | - | 3072 B | +| Update_ExtMap | 100 | 8 | 1,812.71 ns | 0.4120 | - | - | 3456 B | +| Update_ExtHashMap | 100 | 8 | 767.69 ns | 0.5360 | 0.0010 | - | 4488 B | +| UpdateSet_ImmDict | 100 | 8 | 1,508.45 ns | 0.5589 | 0.0019 | - | 4688 B | +| UpdateSet_PersistentMap_Standard | 100 | 8 | 6,877.98 ns | 2.4033 | 0.0381 | - | 20160 B | +| UpdateSet_PersistentMap_Unicode | 100 | 8 | 2,660.42 ns | 2.7046 | 0.0534 | - | 22640 B | +| UpdateSet_TransientMap_Standard | 100 | 8 | 5,415.27 ns | 0.4349 | - | - | 3640 B | +| UpdateSet_TransientMap_Unicode | 100 | 8 | 1,212.47 ns | 0.4635 | 0.0057 | - | 3888 B | +| UpdateSet_ImmSortedDict | 100 | 8 | 2,424.14 ns | 0.4578 | - | - | 3840 B | +| UpdateSet_ExtMap | 100 | 8 | 2,111.13 ns | 0.4730 | - | - | 3960 B | +| UpdateSet_ExtHashMap | 100 | 8 | 876.06 ns | 0.5646 | 0.0019 | - | 4728 B | +| Iterate_ImmDict | 100 | 8 | 1,272.01 ns | - | - | - | - | +| Iterate_PersistentMap_Standard | 100 | 8 | 191.98 ns | - | - | - | - | +| Iterate_ImmSortedDict | 100 | 8 | 475.35 ns | - | - | - | - | +| Iterate_ExtMap | 100 | 8 | 322.36 ns | 0.0038 | - | - | 32 B | +| Iterate_ExtHashMap | 100 | 8 | 1,179.25 ns | 0.2747 | - | - | 2304 B | +| Iterate_PersistentMap_Unicode | 100 | 8 | 191.41 ns | - | - | - | - | +| Remove_ImmDict | 100 | 8 | 1,420.61 ns | 0.5360 | 0.0019 | - | 4496 B | +| Remove_PersistentMap_Standard | 100 | 8 | 8,294.72 ns | 2.4261 | 0.0305 | - | 20400 B | +| Remove_PersistentMap_Unicode | 100 | 8 | 2,597.23 ns | 2.6779 | 0.0496 | - | 22400 B | +| Remove_TransientMap_Standard | 100 | 8 | 7,178.94 ns | 0.4654 | - | - | 3928 B | +| Remove_TransientMap_Unicode | 100 | 8 | 1,446.12 ns | 0.4406 | 0.0057 | - | 3688 B | +| Remove_ImmSortedDict | 100 | 8 | 2,031.56 ns | 0.3777 | - | - | 3168 B | +| Remove_ExtMap | 100 | 8 | 2,274.33 ns | 0.5798 | - | - | 4856 B | +| Remove_ExtHashMap | 100 | 8 | 817.02 ns | 0.5102 | 0.0019 | - | 4272 B | +| Build_TransientMap_Standard | 100 | 50 | 43,518.39 ns | 0.7324 | - | - | 6216 B | +| Build_TransientMap_Unicode | 100 | 50 | 12,061.37 ns | 0.8850 | 0.0153 | - | 7528 B | +| Build_ImmDict | 100 | 50 | 16,497.93 ns | 5.4016 | 0.0610 | - | 45216 B | +| Build_ImmSortedDict | 100 | 50 | 22,820.20 ns | 4.2114 | 0.0305 | - | 35232 B | +| Build_ExtMap | 100 | 50 | 22,925.57 ns | 5.6458 | 0.0610 | - | 47328 B | +| Build_ExtHashMap | 100 | 50 | 13,019.93 ns | 4.3945 | 0.0305 | - | 36776 B | +| Retrieve_ImmDict | 100 | 50 | 286.77 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 100 | 50 | 4,620.16 ns | 0.0534 | - | - | 480 B | +| Retrieve_PersistentMap_Unicode | 100 | 50 | 158.67 ns | - | - | - | - | +| Update_ImmDict | 100 | 50 | 1,490.11 ns | 0.4902 | 0.0019 | - | 4112 B | +| Update_PersistentMap_Standard | 100 | 50 | 7,007.05 ns | 2.4033 | 0.0381 | - | 20160 B | +| Update_PersistentMap_Unicode | 100 | 50 | 1,976.44 ns | 2.7046 | 0.0534 | - | 22640 B | +| Update_TransientMap_Standard | 100 | 50 | 6,208.54 ns | 0.4349 | - | - | 3640 B | +| Update_TransientMap_Unicode | 100 | 50 | 665.76 ns | 0.4644 | 0.0057 | - | 3888 B | +| Update_ImmSortedDict | 100 | 50 | 2,133.72 ns | 0.4120 | - | - | 3456 B | +| Update_ExtMap | 100 | 50 | 1,842.17 ns | 0.4253 | - | - | 3568 B | +| Update_ExtHashMap | 100 | 50 | 971.20 ns | 0.5474 | 0.0019 | - | 4592 B | +| UpdateSet_ImmDict | 100 | 50 | 1,816.66 ns | 0.6046 | 0.0019 | - | 5072 B | +| UpdateSet_PersistentMap_Standard | 100 | 50 | 6,616.38 ns | 2.4033 | 0.0381 | - | 20160 B | +| UpdateSet_PersistentMap_Unicode | 100 | 50 | 2,517.24 ns | 2.7046 | 0.0496 | - | 22640 B | +| UpdateSet_TransientMap_Standard | 100 | 50 | 5,282.92 ns | 0.4349 | - | - | 3640 B | +| UpdateSet_TransientMap_Unicode | 100 | 50 | 1,214.38 ns | 0.4635 | 0.0057 | - | 3888 B | +| UpdateSet_ImmSortedDict | 100 | 50 | 2,531.36 ns | 0.4730 | - | - | 3984 B | +| UpdateSet_ExtMap | 100 | 50 | 2,399.90 ns | 0.5913 | - | - | 4968 B | +| UpdateSet_ExtHashMap | 100 | 50 | 1,041.57 ns | 0.5512 | 0.0019 | - | 4624 B | +| Iterate_ImmDict | 100 | 50 | 1,265.60 ns | - | - | - | - | +| Iterate_PersistentMap_Standard | 100 | 50 | 183.51 ns | - | - | - | - | +| Iterate_ImmSortedDict | 100 | 50 | 487.68 ns | - | - | - | - | +| Iterate_ExtMap | 100 | 50 | 313.58 ns | 0.0038 | - | - | 32 B | +| Iterate_ExtHashMap | 100 | 50 | 1,174.73 ns | 0.2747 | - | - | 2304 B | +| Iterate_PersistentMap_Unicode | 100 | 50 | 190.36 ns | - | - | - | - | +| Remove_ImmDict | 100 | 50 | 1,566.27 ns | 0.5283 | 0.0019 | - | 4432 B | +| Remove_PersistentMap_Standard | 100 | 50 | 7,539.09 ns | 2.4338 | 0.0381 | - | 20400 B | +| Remove_PersistentMap_Unicode | 100 | 50 | 2,572.60 ns | 2.6779 | 0.0458 | - | 22400 B | +| Remove_TransientMap_Standard | 100 | 50 | 6,450.13 ns | 0.4654 | - | - | 3928 B | +| Remove_TransientMap_Unicode | 100 | 50 | 1,408.78 ns | 0.4406 | 0.0057 | - | 3688 B | +| Remove_ImmSortedDict | 100 | 50 | 2,241.88 ns | 0.3891 | - | - | 3264 B | +| Remove_ExtMap | 100 | 50 | 2,156.69 ns | 0.4387 | - | - | 3680 B | +| Remove_ExtHashMap | 100 | 50 | 927.47 ns | 0.4807 | 0.0010 | - | 4024 B | +| Build_TransientMap_Standard | 1000 | 8 | 741,046.07 ns | 5.8594 | - | - | 49512 B | +| Build_TransientMap_Unicode | 1000 | 8 | 176,282.35 ns | 7.3242 | 0.7324 | - | 62248 B | +| Build_ImmDict | 1000 | 8 | 251,462.15 ns | 79.1016 | 8.3008 | - | 663488 B | +| Build_ImmSortedDict | 1000 | 8 | 420,903.44 ns | 61.0352 | 4.8828 | - | 513312 B | +| Build_ExtMap | 1000 | 8 | 426,942.00 ns | 79.1016 | 7.3242 | - | 662448 B | +| Build_ExtHashMap | 1000 | 8 | 154,509.50 ns | 70.3125 | 4.8828 | - | 589264 B | +| Retrieve_ImmDict | 1000 | 8 | 1,057.94 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 1000 | 8 | 71,081.84 ns | 0.4883 | - | - | 4800 B | +| Retrieve_PersistentMap_Unicode | 1000 | 8 | 2,056.30 ns | - | - | - | - | +| Update_ImmDict | 1000 | 8 | 20,613.44 ns | 7.4768 | 0.2747 | - | 62688 B | +| Update_PersistentMap_Standard | 1000 | 8 | 91,445.80 ns | 24.0479 | 2.3193 | - | 201600 B | +| Update_PersistentMap_Unicode | 1000 | 8 | 29,005.34 ns | 27.0386 | 2.5330 | - | 226400 B | +| Update_TransientMap_Standard | 1000 | 8 | 85,352.16 ns | 4.5166 | 0.4883 | - | 38184 B | +| Update_TransientMap_Unicode | 1000 | 8 | 7,968.40 ns | 4.4250 | 0.4425 | - | 37024 B | +| Update_ImmSortedDict | 1000 | 8 | 32,334.14 ns | 5.7373 | 0.1221 | - | 48288 B | +| Update_ExtMap | 1000 | 8 | 34,280.75 ns | 6.4087 | 0.1831 | - | 53824 B | +| Update_ExtHashMap | 1000 | 8 | 11,163.54 ns | 7.1716 | 0.2289 | - | 60016 B | +| UpdateSet_ImmDict | 1000 | 8 | 23,570.66 ns | 8.3618 | 0.3967 | - | 69984 B | +| UpdateSet_PersistentMap_Standard | 1000 | 8 | 103,655.77 ns | 24.0479 | 2.1973 | - | 201600 B | +| UpdateSet_PersistentMap_Unicode | 1000 | 8 | 33,858.71 ns | 27.0386 | 2.6245 | - | 226400 B | +| UpdateSet_TransientMap_Standard | 1000 | 8 | 86,914.93 ns | 4.1504 | 0.3662 | - | 35368 B | +| UpdateSet_TransientMap_Unicode | 1000 | 8 | 12,939.69 ns | 4.5929 | 0.4883 | - | 38432 B | +| UpdateSet_ImmSortedDict | 1000 | 8 | 37,789.08 ns | 6.3477 | 0.2441 | - | 53232 B | +| UpdateSet_ExtMap | 1000 | 8 | 38,304.46 ns | 7.6904 | 0.3052 | - | 64576 B | +| UpdateSet_ExtHashMap | 1000 | 8 | 14,695.01 ns | 7.7057 | 0.2747 | - | 64464 B | +| Iterate_ImmDict | 1000 | 8 | 13,240.07 ns | - | - | - | - | +| Iterate_PersistentMap_Standard | 1000 | 8 | 1,628.88 ns | - | - | - | - | +| Iterate_ImmSortedDict | 1000 | 8 | 4,933.29 ns | - | - | - | - | +| Iterate_ExtMap | 1000 | 8 | 3,191.58 ns | 0.0038 | - | - | 32 B | +| Iterate_ExtHashMap | 1000 | 8 | 14,380.64 ns | 2.6550 | - | - | 22320 B | +| Iterate_PersistentMap_Unicode | 1000 | 8 | 1,647.93 ns | - | - | - | - | +| Remove_ImmDict | 1000 | 8 | 20,527.32 ns | 7.6599 | 0.3052 | - | 64160 B | +| Remove_PersistentMap_Standard | 1000 | 8 | 106,140.73 ns | 24.2920 | 1.5869 | - | 204000 B | +| Remove_PersistentMap_Unicode | 1000 | 8 | 30,242.16 ns | 26.7639 | 2.4414 | - | 224000 B | +| Remove_TransientMap_Standard | 1000 | 8 | 91,057.33 ns | 4.6387 | 0.4883 | - | 39224 B | +| Remove_TransientMap_Unicode | 1000 | 8 | 14,218.28 ns | 3.7994 | 0.3662 | - | 31848 B | +| Remove_ImmSortedDict | 1000 | 8 | 33,413.24 ns | 5.6763 | 0.1221 | - | 47616 B | +| Remove_ExtMap | 1000 | 8 | 37,472.01 ns | 6.8970 | 0.1831 | - | 57744 B | +| Remove_ExtHashMap | 1000 | 8 | 12,185.93 ns | 7.9803 | 0.2441 | - | 66864 B | +| Build_TransientMap_Standard | 1000 | 50 | 727,350.80 ns | 4.8828 | - | - | 44992 B | +| Build_TransientMap_Unicode | 1000 | 50 | 176,948.61 ns | 7.0801 | 0.4883 | - | 59368 B | +| Build_ImmDict | 1000 | 50 | 285,938.59 ns | 78.6133 | 8.3008 | - | 660096 B | +| Build_ImmSortedDict | 1000 | 50 | 433,882.40 ns | 61.0352 | 5.3711 | - | 511344 B | +| Build_ExtMap | 1000 | 50 | 412,922.58 ns | 78.1250 | 7.3242 | - | 656288 B | +| Build_ExtHashMap | 1000 | 50 | 185,309.94 ns | 70.3125 | 4.8828 | - | 589128 B | +| Retrieve_ImmDict | 1000 | 50 | 3,151.77 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 1000 | 50 | 76,705.78 ns | 0.4883 | - | - | 4800 B | +| Retrieve_PersistentMap_Unicode | 1000 | 50 | 2,077.37 ns | - | - | - | - | +| Update_ImmDict | 1000 | 50 | 23,312.29 ns | 7.5073 | 0.3052 | - | 62880 B | +| Update_PersistentMap_Standard | 1000 | 50 | 93,945.90 ns | 24.0479 | 2.0752 | - | 201600 B | +| Update_PersistentMap_Unicode | 1000 | 50 | 29,787.05 ns | 27.0386 | 2.2888 | - | 226400 B | +| Update_TransientMap_Standard | 1000 | 50 | 85,349.42 ns | 3.7842 | 0.2441 | - | 32552 B | +| Update_TransientMap_Unicode | 1000 | 50 | 7,349.19 ns | 4.0894 | 0.4044 | - | 34208 B | +| Update_ImmSortedDict | 1000 | 50 | 32,705.50 ns | 5.6763 | 0.1221 | - | 47952 B | +| Update_ExtMap | 1000 | 50 | 33,742.93 ns | 6.5308 | 0.1831 | - | 54720 B | +| Update_ExtHashMap | 1000 | 50 | 13,720.77 ns | 7.2479 | 0.2289 | - | 60688 B | +| UpdateSet_ImmDict | 1000 | 50 | 24,725.01 ns | 8.0566 | 0.3967 | - | 67424 B | +| UpdateSet_PersistentMap_Standard | 1000 | 50 | 106,965.60 ns | 24.1699 | 1.9531 | - | 202504 B | +| UpdateSet_PersistentMap_Unicode | 1000 | 50 | 34,398.72 ns | 27.2217 | 2.6245 | - | 227840 B | +| UpdateSet_TransientMap_Standard | 1000 | 50 | 89,942.92 ns | 3.9063 | 0.3662 | - | 33456 B | +| UpdateSet_TransientMap_Unicode | 1000 | 50 | 13,555.52 ns | 4.4250 | 0.4578 | - | 37056 B | +| UpdateSet_ImmSortedDict | 1000 | 50 | 37,035.94 ns | 6.2866 | 0.1831 | - | 53088 B | +| UpdateSet_ExtMap | 1000 | 50 | 39,025.94 ns | 7.8735 | 0.3052 | - | 66200 B | +| UpdateSet_ExtHashMap | 1000 | 50 | 15,654.51 ns | 7.5989 | 0.2441 | - | 63608 B | +| Iterate_ImmDict | 1000 | 50 | 12,858.73 ns | - | - | - | - | +| Iterate_PersistentMap_Standard | 1000 | 50 | 1,674.98 ns | - | - | - | - | +| Iterate_ImmSortedDict | 1000 | 50 | 4,882.38 ns | - | - | - | - | +| Iterate_ExtMap | 1000 | 50 | 3,498.40 ns | 0.0038 | - | - | 32 B | +| Iterate_ExtHashMap | 1000 | 50 | 14,585.65 ns | 2.7008 | - | - | 22608 B | +| Iterate_PersistentMap_Unicode | 1000 | 50 | 1,654.17 ns | - | - | - | - | +| Remove_ImmDict | 1000 | 50 | 24,001.50 ns | 7.6904 | 0.3052 | - | 64480 B | +| Remove_PersistentMap_Standard | 1000 | 50 | 115,583.75 ns | 24.2920 | 1.7090 | - | 204000 B | +| Remove_PersistentMap_Unicode | 1000 | 50 | 30,177.90 ns | 26.7639 | 2.2583 | - | 224000 B | +| Remove_TransientMap_Standard | 1000 | 50 | 104,629.87 ns | 3.9063 | 0.2441 | - | 33592 B | +| Remove_TransientMap_Unicode | 1000 | 50 | 13,859.15 ns | 3.2959 | 0.2899 | - | 27624 B | +| Remove_ImmSortedDict | 1000 | 50 | 34,305.81 ns | 5.6763 | 0.1221 | - | 47664 B | +| Remove_ExtMap | 1000 | 50 | 36,962.41 ns | 6.9580 | 0.2441 | - | 58640 B | +| Remove_ExtHashMap | 1000 | 50 | 14,045.01 ns | 7.8583 | 0.1984 | - | 65800 B | +| Build_TransientMap_Standard | 10000 | 8 | 10,481,091.44 ns | 46.8750 | 15.6250 | - | 453272 B | +| Build_TransientMap_Unicode | 10000 | 8 | 2,288,777.17 ns | 66.4063 | 19.5313 | - | 576584 B | +| Build_ImmDict | 10000 | 8 | 4,451,716.58 ns | 1046.8750 | 500.0000 | - | 8790528 B | +| Build_ImmSortedDict | 10000 | 8 | 6,589,276.41 ns | 804.6875 | 281.2500 | - | 6767232 B | +| Build_ExtMap | 10000 | 8 | 6,466,222.77 ns | 1015.6250 | 437.5000 | - | 8542480 B | +| Build_ExtHashMap | 10000 | 8 | 1,987,067.22 ns | 945.3125 | 312.5000 | - | 7921664 B | +| Retrieve_ImmDict | 10000 | 8 | 23,653.66 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 10000 | 8 | 1,067,006.38 ns | 7.8125 | - | - | 72000 B | +| Retrieve_PersistentMap_Unicode | 10000 | 8 | 33,794.37 ns | - | - | - | - | +| Update_ImmDict | 10000 | 8 | 393,904.16 ns | 100.5859 | 25.3906 | - | 844992 B | +| Update_PersistentMap_Standard | 10000 | 8 | 1,373,414.67 ns | 304.6875 | 132.8125 | - | 2560000 B | +| Update_PersistentMap_Unicode | 10000 | 8 | 416,266.85 ns | 366.2109 | 160.1563 | - | 3064000 B | +| Update_TransientMap_Standard | 10000 | 8 | 1,160,717.93 ns | 41.0156 | 13.6719 | - | 346280 B | +| Update_TransientMap_Unicode | 10000 | 8 | 154,306.58 ns | 40.7715 | 16.6016 | - | 341792 B | +| Update_ImmSortedDict | 10000 | 8 | 583,839.85 ns | 76.1719 | 15.6250 | - | 640368 B | +| Update_ExtMap | 10000 | 8 | 571,924.12 ns | 87.8906 | 22.4609 | - | 735640 B | +| Update_ExtHashMap | 10000 | 8 | 164,331.80 ns | 102.7832 | 22.9492 | - | 860152 B | +| UpdateSet_ImmDict | 10000 | 8 | 433,669.48 ns | 108.3984 | 29.2969 | - | 907072 B | +| UpdateSet_PersistentMap_Standard | 10000 | 8 | 1,426,063.43 ns | 306.6406 | 130.8594 | - | 2572328 B | +| UpdateSet_PersistentMap_Unicode | 10000 | 8 | 502,233.19 ns | 368.1641 | 154.2969 | - | 3086432 B | +| UpdateSet_TransientMap_Standard | 10000 | 8 | 1,183,758.86 ns | 41.0156 | 15.6250 | - | 357200 B | +| UpdateSet_TransientMap_Unicode | 10000 | 8 | 224,362.69 ns | 43.7012 | 16.3574 | - | 365632 B | +| UpdateSet_ImmSortedDict | 10000 | 8 | 648,222.74 ns | 82.0313 | 19.5313 | - | 687648 B | +| UpdateSet_ExtMap | 10000 | 8 | 638,998.68 ns | 99.6094 | 25.3906 | - | 833752 B | +| UpdateSet_ExtHashMap | 10000 | 8 | 188,297.97 ns | 104.9805 | 23.9258 | - | 878640 B | +| Iterate_ImmDict | 10000 | 8 | 180,912.56 ns | - | - | - | - | +| Iterate_PersistentMap_Standard | 10000 | 8 | 17,134.49 ns | - | - | - | - | +| Iterate_ImmSortedDict | 10000 | 8 | 55,355.92 ns | - | - | - | - | +| Iterate_ExtMap | 10000 | 8 | 66,171.51 ns | - | - | - | 32 B | +| Iterate_ExtHashMap | 10000 | 8 | 173,270.08 ns | 20.2637 | - | - | 171360 B | +| Iterate_PersistentMap_Unicode | 10000 | 8 | 17,291.12 ns | - | - | - | - | +| Remove_ImmDict | 10000 | 8 | 398,833.85 ns | 102.0508 | 24.9023 | - | 857216 B | +| Remove_PersistentMap_Standard | 10000 | 8 | 1,488,081.64 ns | 310.5469 | 123.0469 | - | 2608000 B | +| Remove_PersistentMap_Unicode | 10000 | 8 | 492,391.59 ns | 363.2813 | 180.6641 | - | 3041408 B | +| Remove_TransientMap_Standard | 10000 | 8 | 1,334,741.70 ns | 44.9219 | 15.6250 | - | 388696 B | +| Remove_TransientMap_Unicode | 10000 | 8 | 224,694.52 ns | 38.0859 | 15.1367 | - | 319240 B | +| Remove_ImmSortedDict | 10000 | 8 | 624,219.05 ns | 77.1484 | 15.6250 | - | 652944 B | +| Remove_ExtMap | 10000 | 8 | 648,812.07 ns | 91.7969 | 20.5078 | - | 774000 B | +| Remove_ExtHashMap | 10000 | 8 | 187,697.63 ns | 104.2480 | 21.4844 | - | 873600 B | +| Build_TransientMap_Standard | 10000 | 50 | 11,121,008.03 ns | 46.8750 | 15.6250 | - | 453272 B | +| Build_TransientMap_Unicode | 10000 | 50 | 2,345,447.27 ns | 66.4063 | 23.4375 | - | 567112 B | +| Build_ImmDict | 10000 | 50 | 4,711,900.43 ns | 1039.0625 | 507.8125 | - | 8746752 B | +| Build_ImmSortedDict | 10000 | 50 | 6,774,329.88 ns | 804.6875 | 265.6250 | - | 6751584 B | +| Build_ExtMap | 10000 | 50 | 6,748,429.60 ns | 1015.6250 | 429.6875 | - | 8544720 B | +| Build_ExtHashMap | 10000 | 50 | 2,211,770.12 ns | 941.4063 | 332.0313 | - | 7894224 B | +| Retrieve_ImmDict | 10000 | 50 | 53,674.39 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 10000 | 50 | 1,099,621.23 ns | 7.8125 | - | - | 72000 B | +| Retrieve_PersistentMap_Unicode | 10000 | 50 | 34,282.21 ns | - | - | - | - | +| Update_ImmDict | 10000 | 50 | 416,110.39 ns | 101.0742 | 24.9023 | - | 845696 B | +| Update_PersistentMap_Standard | 10000 | 50 | 1,432,050.69 ns | 304.6875 | 125.0000 | - | 2560000 B | +| Update_PersistentMap_Unicode | 10000 | 50 | 482,376.79 ns | 366.2109 | 158.2031 | - | 3064000 B | +| Update_TransientMap_Standard | 10000 | 50 | 1,185,346.37 ns | 39.0625 | 11.7188 | - | 339240 B | +| Update_TransientMap_Unicode | 10000 | 50 | 153,479.49 ns | 39.5508 | 13.9160 | - | 331136 B | +| Update_ImmSortedDict | 10000 | 50 | 578,927.03 ns | 76.1719 | 14.6484 | - | 641616 B | +| Update_ExtMap | 10000 | 50 | 582,689.89 ns | 86.9141 | 20.5078 | - | 732896 B | +| Update_ExtHashMap | 10000 | 50 | 195,769.80 ns | 102.5391 | 20.5078 | - | 859328 B | +| UpdateSet_ImmDict | 10000 | 50 | 460,644.35 ns | 109.3750 | 30.2734 | - | 915264 B | +| UpdateSet_PersistentMap_Standard | 10000 | 50 | 1,488,123.25 ns | 306.6406 | 132.8125 | - | 2575040 B | +| UpdateSet_PersistentMap_Unicode | 10000 | 50 | 516,710.59 ns | 368.1641 | 152.3438 | - | 3085600 B | +| UpdateSet_TransientMap_Standard | 10000 | 50 | 1,257,184.91 ns | 41.0156 | 13.6719 | - | 355688 B | +| UpdateSet_TransientMap_Unicode | 10000 | 50 | 222,602.17 ns | 42.2363 | 15.8691 | - | 354144 B | +| UpdateSet_ImmSortedDict | 10000 | 50 | 659,407.65 ns | 82.0313 | 19.5313 | - | 691728 B | +| UpdateSet_ExtMap | 10000 | 50 | 652,772.00 ns | 98.6328 | 23.4375 | - | 829552 B | +| UpdateSet_ExtHashMap | 10000 | 50 | 216,637.22 ns | 104.9805 | 24.1699 | - | 878840 B | +| Iterate_ImmDict | 10000 | 50 | 172,211.95 ns | - | - | - | - | +| Iterate_PersistentMap_Standard | 10000 | 50 | 17,227.48 ns | - | - | - | - | +| Iterate_ImmSortedDict | 10000 | 50 | 56,105.18 ns | - | - | - | - | +| Iterate_ExtMap | 10000 | 50 | 70,322.76 ns | - | - | - | 32 B | +| Iterate_ExtHashMap | 10000 | 50 | 176,803.08 ns | 20.0195 | - | - | 168264 B | +| Iterate_PersistentMap_Unicode | 10000 | 50 | 17,473.52 ns | - | - | - | - | +| Remove_ImmDict | 10000 | 50 | 429,520.72 ns | 102.5391 | 28.3203 | - | 860032 B | +| Remove_PersistentMap_Standard | 10000 | 50 | 1,558,056.72 ns | 310.5469 | 125.0000 | - | 2610816 B | +| Remove_PersistentMap_Unicode | 10000 | 50 | 522,677.95 ns | 363.2813 | 158.2031 | - | 3042816 B | +| Remove_TransientMap_Standard | 10000 | 50 | 1,333,105.09 ns | 44.9219 | 15.6250 | - | 391512 B | +| Remove_TransientMap_Unicode | 10000 | 50 | 223,026.23 ns | 37.3535 | 13.1836 | - | 312808 B | +| Remove_ImmSortedDict | 10000 | 50 | 611,137.62 ns | 77.1484 | 15.6250 | - | 651840 B | +| Remove_ExtMap | 10000 | 50 | 630,117.55 ns | 91.7969 | 21.4844 | - | 771256 B | +| Remove_ExtHashMap | 10000 | 50 | 208,051.51 ns | 104.7363 | 20.7520 | - | 877216 B | +| Build_TransientMap_Standard | 100000 | 8 | 158,074,169.00 ns | 500.0000 | 250.0000 | - | 4524208 B | +| Build_TransientMap_Unicode | 100000 | 8 | 40,354,289.36 ns | 692.3077 | 538.4615 | - | 5798728 B | +| Build_ImmDict | 100000 | 8 | 95,840,918.80 ns | 13200.0000 | 4600.0000 | 200.0000 | 109352965 B | +| Build_ImmSortedDict | 100000 | 8 | 145,451,097.25 ns | 10000.0000 | 3750.0000 | - | 83946432 B | +| Build_ExtMap | 100000 | 8 | 121,026,729.25 ns | 12500.0000 | 5750.0000 | - | 104660688 B | +| Build_ExtHashMap | 100000 | 8 | 45,582,232.06 ns | 12000.0000 | 2583.3333 | 83.3333 | 99799303 B | +| Retrieve_ImmDict | 100000 | 8 | 1,518,946.53 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 100000 | 8 | 15,954,066.23 ns | 93.7500 | - | - | 960000 B | +| Retrieve_PersistentMap_Unicode | 100000 | 8 | 1,344,373.24 ns | - | - | - | - | +| Update_ImmDict | 100000 | 8 | 9,168,537.50 ns | 1265.6250 | 1062.5000 | - | 10588288 B | +| Update_PersistentMap_Standard | 100000 | 8 | 24,537,720.52 ns | 3718.7500 | 2906.2500 | 31.2500 | 31040034 B | +| Update_PersistentMap_Unicode | 100000 | 8 | 11,821,730.59 ns | 4671.8750 | 2500.0000 | 62.5000 | 38640073 B | +| Update_TransientMap_Standard | 100000 | 8 | 18,410,364.31 ns | 406.2500 | 312.5000 | - | 3415688 B | +| Update_TransientMap_Unicode | 100000 | 8 | 3,724,912.85 ns | 414.0625 | 351.5625 | - | 3487208 B | +| Update_ImmSortedDict | 100000 | 8 | 11,002,667.72 ns | 953.1250 | 781.2500 | - | 8020080 B | +| Update_ExtMap | 100000 | 8 | 11,289,391.77 ns | 1093.7500 | 937.5000 | - | 9246400 B | +| Update_ExtHashMap | 100000 | 8 | 3,795,553.44 ns | 1265.6250 | 968.7500 | - | 10624032 B | +| UpdateSet_ImmDict | 100000 | 8 | 9,324,688.19 ns | 1343.7500 | 1046.8750 | - | 11264256 B | +| UpdateSet_PersistentMap_Standard | 100000 | 8 | 28,227,498.72 ns | 3750.0000 | 2593.7500 | 31.2500 | 31143252 B | +| UpdateSet_PersistentMap_Unicode | 100000 | 8 | 12,980,484.49 ns | 4703.1250 | 2250.0000 | 62.5000 | 38809434 B | +| UpdateSet_TransientMap_Standard | 100000 | 8 | 18,455,887.50 ns | 406.2500 | 312.5000 | - | 3514688 B | +| UpdateSet_TransientMap_Unicode | 100000 | 8 | 4,599,394.95 ns | 429.6875 | 382.8125 | - | 3636872 B | +| UpdateSet_ImmSortedDict | 100000 | 8 | 11,611,425.71 ns | 1015.6250 | 875.0000 | - | 8495856 B | +| UpdateSet_ExtMap | 100000 | 8 | 13,256,339.44 ns | 1218.7500 | 1015.6250 | - | 10287328 B | +| UpdateSet_ExtHashMap | 100000 | 8 | 4,056,833.05 ns | 1281.2500 | 992.1875 | - | 10752664 B | +| Iterate_ImmDict | 100000 | 8 | 2,245,167.14 ns | - | - | - | 96 B | +| Iterate_PersistentMap_Standard | 100000 | 8 | 226,078.34 ns | - | - | - | - | +| Iterate_ImmSortedDict | 100000 | 8 | 752,117.01 ns | - | - | - | - | +| Iterate_ExtMap | 100000 | 8 | 1,153,701.70 ns | - | - | - | 32 B | +| Iterate_ExtHashMap | 100000 | 8 | 2,747,144.18 ns | 273.4375 | - | - | 2318432 B | +| Iterate_PersistentMap_Unicode | 100000 | 8 | 241,814.65 ns | - | - | - | - | +| Remove_ImmDict | 100000 | 8 | 8,922,500.09 ns | 1281.2500 | 1031.2500 | - | 10744896 B | +| Remove_PersistentMap_Standard | 100000 | 8 | 28,366,797.02 ns | 3812.5000 | 2750.0000 | 31.2500 | 31781143 B | +| Remove_PersistentMap_Unicode | 100000 | 8 | 12,499,542.97 ns | 4640.6250 | 2875.0000 | 62.5000 | 38412730 B | +| Remove_TransientMap_Standard | 100000 | 8 | 21,085,953.04 ns | 468.7500 | 343.7500 | - | 4134328 B | +| Remove_TransientMap_Unicode | 100000 | 8 | 3,227,138.07 ns | 382.8125 | 324.2188 | - | 3226088 B | +| Remove_ImmSortedDict | 100000 | 8 | 10,475,303.83 ns | 953.1250 | 781.2500 | - | 8031888 B | +| Remove_ExtMap | 100000 | 8 | 11,473,612.68 ns | 1140.6250 | 937.5000 | - | 9630448 B | +| Remove_ExtHashMap | 100000 | 8 | 3,920,078.08 ns | 1265.6250 | 914.0625 | - | 10624344 B | +| Build_TransientMap_Standard | 100000 | 50 | 186,991,900.50 ns | 500.0000 | - | - | 4511632 B | +| Build_TransientMap_Unicode | 100000 | 50 | 42,267,081.83 ns | 666.6667 | 500.0000 | - | 5763784 B | +| Build_ImmDict | 100000 | 50 | 91,363,990.94 ns | 13166.6667 | 4833.3333 | 166.6667 | 109457840 B | +| Build_ImmSortedDict | 100000 | 50 | 134,058,261.08 ns | 10000.0000 | 4000.0000 | - | 83731632 B | +| Build_ExtMap | 100000 | 50 | 130,083,384.42 ns | 12250.0000 | 5250.0000 | - | 104371560 B | +| Build_ExtHashMap | 100000 | 50 | 50,176,040.06 ns | 11909.0909 | 2545.4545 | - | 99721040 B | +| Retrieve_ImmDict | 100000 | 50 | 1,878,376.76 ns | - | - | - | - | +| Retrieve_PersistentMap_Standard | 100000 | 50 | 22,662,880.57 ns | 93.7500 | - | - | 960000 B | +| Retrieve_PersistentMap_Unicode | 100000 | 50 | 1,421,803.13 ns | - | - | - | - | +| Update_ImmDict | 100000 | 50 | 8,957,341.32 ns | 1265.6250 | 1062.5000 | - | 10646144 B | +| Update_PersistentMap_Standard | 100000 | 50 | 37,471,664.44 ns | 3692.3077 | 2769.2308 | - | 31040000 B | +| Update_PersistentMap_Unicode | 100000 | 50 | 12,856,950.49 ns | 4656.2500 | 2781.2500 | 46.8750 | 38640047 B | +| Update_TransientMap_Standard | 100000 | 50 | 26,563,266.79 ns | 406.2500 | 312.5000 | - | 3414056 B | +| Update_TransientMap_Unicode | 100000 | 50 | 4,173,539.70 ns | 406.2500 | 335.9375 | - | 3440328 B | +| Update_ImmSortedDict | 100000 | 50 | 11,812,577.59 ns | 953.1250 | 781.2500 | - | 8012928 B | +| Update_ExtMap | 100000 | 50 | 11,842,757.42 ns | 1093.7500 | 906.2500 | - | 9233632 B | +| Update_ExtHashMap | 100000 | 50 | 4,189,884.01 ns | 1265.6250 | 984.3750 | - | 10632736 B | +| UpdateSet_ImmDict | 100000 | 50 | 10,459,268.37 ns | 1343.7500 | 1031.2500 | - | 11256960 B | +| UpdateSet_PersistentMap_Standard | 100000 | 50 | 41,029,434.25 ns | 3666.6667 | 2500.0000 | - | 31141992 B | +| UpdateSet_PersistentMap_Unicode | 100000 | 50 | 14,028,512.79 ns | 4687.5000 | 2421.8750 | 46.8750 | 38835953 B | +| UpdateSet_TransientMap_Standard | 100000 | 50 | 28,314,403.39 ns | 406.2500 | 312.5000 | - | 3509008 B | +| UpdateSet_TransientMap_Unicode | 100000 | 50 | 5,267,848.18 ns | 429.6875 | 359.3750 | - | 3630600 B | +| UpdateSet_ImmSortedDict | 100000 | 50 | 14,089,168.54 ns | 1015.6250 | 890.6250 | - | 8501136 B | +| UpdateSet_ExtMap | 100000 | 50 | 12,785,268.31 ns | 1218.7500 | 1015.6250 | - | 10265544 B | +| UpdateSet_ExtHashMap | 100000 | 50 | 4,418,089.19 ns | 1281.2500 | 992.1875 | - | 10750000 B | +| Iterate_ImmDict | 100000 | 50 | 2,338,660.26 ns | - | - | - | 384 B | +| Iterate_PersistentMap_Standard | 100000 | 50 | 211,359.08 ns | - | - | - | - | +| Iterate_ImmSortedDict | 100000 | 50 | 715,600.57 ns | - | - | - | - | +| Iterate_ExtMap | 100000 | 50 | 1,117,575.94 ns | - | - | - | 32 B | +| Iterate_ExtHashMap | 100000 | 50 | 2,975,826.72 ns | 277.3438 | - | - | 2319904 B | +| Iterate_PersistentMap_Unicode | 100000 | 50 | 217,594.04 ns | - | - | - | - | +| Remove_ImmDict | 100000 | 50 | 9,190,906.43 ns | 1281.2500 | 1046.8750 | - | 10741824 B | +| Remove_PersistentMap_Standard | 100000 | 50 | 42,259,057.45 ns | 3727.2727 | 2636.3636 | - | 31774080 B | +| Remove_PersistentMap_Unicode | 100000 | 50 | 11,946,176.86 ns | 4625.0000 | 2859.3750 | 46.8750 | 38402868 B | +| Remove_TransientMap_Standard | 100000 | 50 | 26,120,804.07 ns | 468.7500 | 343.7500 | - | 4127064 B | +| Remove_TransientMap_Unicode | 100000 | 50 | 3,583,417.50 ns | 382.8125 | 312.5000 | - | 3205960 B | +| Remove_ImmSortedDict | 100000 | 50 | 14,164,094.39 ns | 953.1250 | 781.2500 | - | 8032656 B | +| Remove_ExtMap | 100000 | 50 | 13,861,593.95 ns | 1140.6250 | 921.8750 | - | 9617232 B | +| Remove_ExtHashMap | 100000 | 50 | 4,797,368.31 ns | 1265.6250 | 937.5000 | - | 10628320 B | + #+end_src -** Architecture Notes: Key Strategies + Architecture Notes: Key Strategies + + NiceBtree uses =IKeyStrategy= to map generic keys (like =string= or =double=) into sortable =long= prefixes. This achieves two things: 1. Enables AVX512/AVX2 vector instructions to search internal nodes simultaneously. 2. Avoids expensive =IComparable= interface calls or =string.Compare= during the initial descent of the tree, only falling back to exact comparisons when refining the search within a leaf. diff --git a/TestProject1/StandardStrategy.cs b/TestProject1/StandardStrategy.cs new file mode 100644 index 0000000..912c7a7 --- /dev/null +++ b/TestProject1/StandardStrategy.cs @@ -0,0 +1,38 @@ +namespace PersistentMap.Tests; +using PersistentMap; +using System.Linq; +using Xunit; +public class StandardStrategy +{ + private static string GenerateRandomString(int length, Random rnd) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[rnd.Next(s.Length)]).ToArray()); + } + + [Fact] + public void Setup() + { + var N = 1000; + var _stdStrategy = new StandardStrategy(); + var _uniStrategy = new UnicodeStrategy(); + var rnd = new Random(42); + var StringLength = 10; + // Build random strings + var _allKeys = Enumerable.Range(0, N).Select(_ => GenerateRandomString(StringLength, rnd)).Distinct().ToArray(); + + // Regenerate if Distinct() reduced array size (highly unlikely with length 8/50, but safe) + while (_allKeys.Length < N) + { + _allKeys = _allKeys.Concat(new[] { GenerateRandomString(StringLength, rnd) }).Distinct().ToArray(); + } + + var transStd = BaseOrderedMap>.CreateTransient(_stdStrategy); + var transUni = BaseOrderedMap.CreateTransient(_uniStrategy); + for (int i = 0; i < _allKeys.Length; i++) + { + transStd.Set(_allKeys[i], i); + transUni.Set(_allKeys[i], i); + } + } +} \ No newline at end of file diff --git a/benchmarks/MyBenchMarks/StringBenchmarks.cs b/benchmarks/MyBenchMarks/StringBenchmarks.cs new file mode 100644 index 0000000..302e5d1 --- /dev/null +++ b/benchmarks/MyBenchMarks/StringBenchmarks.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Attributes; +using LanguageExt; +using PersistentMap; + +namespace MapBenchmarks; + +[MemoryDiagnoser] +public class StringMapBenchmarks +{ + [Params(100, 1000, 10000, 100000)] + public int N { get; set; } + + [Params(8, 50)] + public int StringLength { get; set; } + + private string[] _allKeys; + private string[] _retrieveKeys; + private string[] _updateKeys; + private string[] _removeKeys; + private string[] _mixedKeys; + + private ImmutableDictionary _immDict; + private ImmutableSortedDictionary _immSortedDict; + private LanguageExt.Map _extMap; + private LanguageExt.HashMap _extHashMap; + + private PersistentMap> _persistentMapStandard; + private PersistentMap _persistentMapUnicode; + + private readonly StandardStrategy _stdStrategy = new StandardStrategy(); + private readonly UnicodeStrategy _uniStrategy = new UnicodeStrategy(); + + [GlobalSetup] + public void Setup() + { + var rnd = new Random(42); + + // Build random strings + _allKeys = Enumerable.Range(0, N).Select(_ => GenerateRandomString(StringLength, rnd)).Distinct().ToArray(); + + // Regenerate if Distinct() reduced array size (highly unlikely with length 8/50, but safe) + while (_allKeys.Length < N) + { + _allKeys = _allKeys.Concat(new[] { GenerateRandomString(StringLength, rnd) }).Distinct().ToArray(); + } + + int subsetSize = Math.Max(1, N / 10); + + var shuffled = _allKeys.OrderBy(x => rnd.Next()).ToArray(); + _retrieveKeys = shuffled.Take(subsetSize).ToArray(); + _updateKeys = shuffled.Skip(subsetSize).Take(subsetSize).ToArray(); + _removeKeys = shuffled.Skip(subsetSize * 2).Take(subsetSize).ToArray(); + + var existingHalf = shuffled.Skip(subsetSize * 3).Take(subsetSize / 2).ToArray(); + var newHalf = Enumerable.Range(0, subsetSize - (subsetSize / 2)).Select(_ => GenerateRandomString(StringLength, rnd)).ToArray(); + _mixedKeys = existingHalf.Concat(newHalf).OrderBy(x => rnd.Next()).ToArray(); + + // Pre-build collections + _immDict = ImmutableDictionary.CreateRange(_allKeys.Select((k, i) => new KeyValuePair(k, i))); + _immSortedDict = ImmutableSortedDictionary.CreateRange(_allKeys.Select((k, i) => new KeyValuePair(k, i))); + + _extMap = LanguageExt.Map.empty(); + _extHashMap = LanguageExt.HashMap.empty(); + for (int i = 0; i < _allKeys.Length; i++) + { + _extMap = _extMap.AddOrUpdate(_allKeys[i], i); + _extHashMap = _extHashMap.AddOrUpdate(_allKeys[i], i); + } + + var transStd = BaseOrderedMap>.CreateTransient(_stdStrategy); + var transUni = BaseOrderedMap.CreateTransient(_uniStrategy); + for (int i = 0; i < _allKeys.Length; i++) + { + transStd.Set(_allKeys[i], i); + transUni.Set(_allKeys[i], i); + } + _persistentMapStandard = transStd.ToPersistent(); + _persistentMapUnicode = transUni.ToPersistent(); + } + + private static string GenerateRandomString(int length, Random rnd) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[rnd.Next(s.Length)]).ToArray()); + } + + // --- 1. BUILD --- + + [Benchmark] + public PersistentMap> Build_TransientMap_Standard() + { + var map = BaseOrderedMap>.CreateTransient(_stdStrategy); + for (int i = 0; i < _allKeys.Length; i++) map.Set(_allKeys[i], i); + return map.ToPersistent(); + } + + [Benchmark] + public PersistentMap Build_TransientMap_Unicode() + { + var map = BaseOrderedMap.CreateTransient(_uniStrategy); + for (int i = 0; i < _allKeys.Length; i++) map.Set(_allKeys[i], i); + return map.ToPersistent(); + } + + [Benchmark] + public ImmutableDictionary Build_ImmDict() + { + var map = ImmutableDictionary.Empty; + for (int i = 0; i < _allKeys.Length; i++) map = map.Add(_allKeys[i], i); + return map; + } + + // --- 1. BUILD (Missing) --- + + [Benchmark] + public ImmutableSortedDictionary Build_ImmSortedDict() + { + var map = ImmutableSortedDictionary.Empty; + for (int i = 0; i < _allKeys.Length; i++) map = map.Add(_allKeys[i], i); + return map; + } + + [Benchmark] + public LanguageExt.Map Build_ExtMap() + { + var map = LanguageExt.Map.empty(); + for (int i = 0; i < _allKeys.Length; i++) map = map.AddOrUpdate(_allKeys[i], i); + return map; + } + + [Benchmark] + public LanguageExt.HashMap Build_ExtHashMap() + { + var map = LanguageExt.HashMap.empty(); + for (int i = 0; i < _allKeys.Length; i++) map = map.AddOrUpdate(_allKeys[i], i); + return map; + } + + // --- 2. RETRIEVAL --- + + [Benchmark] + public int Retrieve_ImmDict() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_immDict.TryGetValue(k, out _)) count++; + return count; + } + + [Benchmark] + public int Retrieve_PersistentMap_Standard() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_persistentMapStandard.TryGetValue(k, out _)) count++; + return count; + } + + [Benchmark] + public int Retrieve_PersistentMap_Unicode() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_persistentMapUnicode.TryGetValue(k, out _)) count++; + return count; + } +[Benchmark] + public int Retrieve_ImmSortedDict() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_immSortedDict.TryGetValue(k, out _)) count++; + return count; + } + + [Benchmark] + public int Retrieve_ExtMap() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_extMap.Find(k).IsSome) count++; + return count; + } + + [Benchmark] + public int Retrieve_ExtHashMap() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_extHashMap.Find(k).IsSome) count++; + return count; + } + + + // --- 3. UPDATING --- + + [Benchmark] + public ImmutableDictionary Update_ImmDict() + { + var map = _immDict; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public PersistentMap> Update_PersistentMap_Standard() + { + var map = _persistentMapStandard; + foreach (var k in _updateKeys) map = map.Set(k, 999); + return map; + } + [Benchmark] + public PersistentMap Update_PersistentMap_Unicode() + { + var map = _persistentMapUnicode; + foreach (var k in _updateKeys) map = map.Set(k, 999); + return map; + } + + [Benchmark] + public PersistentMap> Update_TransientMap_Standard() + { + var transient = _persistentMapStandard.ToTransient(); + foreach (var k in _updateKeys) transient.Set(k, 999); + return transient.ToPersistent(); + } + +[Benchmark] + public PersistentMap Update_TransientMap_Unicode() + { + var transient = _persistentMapUnicode.ToTransient(); + foreach (var k in _updateKeys) transient.Set(k, 999); + return transient.ToPersistent(); + } + + [Benchmark] + public ImmutableSortedDictionary Update_ImmSortedDict() + { + var map = _immSortedDict; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.Map Update_ExtMap() + { + var map = _extMap; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.HashMap Update_ExtHashMap() + { + var map = _extHashMap; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + // --- 4. UPDATE & SET (MIXED) --- + + [Benchmark] + public ImmutableDictionary UpdateSet_ImmDict() + { + var map = _immDict; + foreach (var k in _mixedKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public PersistentMap> UpdateSet_PersistentMap_Standard() + { + var map = _persistentMapStandard; + foreach (var k in _mixedKeys) map = map.Set(k, 999); + return map; + } + +[Benchmark] + public PersistentMap UpdateSet_PersistentMap_Unicode() + { + var map = _persistentMapUnicode; + foreach (var k in _mixedKeys) map = map.Set(k, 999); + return map; + } + + [Benchmark] + public PersistentMap> UpdateSet_TransientMap_Standard() + { + var transient = _persistentMapStandard.ToTransient(); + foreach (var k in _mixedKeys) transient.Set(k, 999); + return transient.ToPersistent(); + } + +[Benchmark] + public PersistentMap UpdateSet_TransientMap_Unicode() + { + var transient = _persistentMapUnicode.ToTransient(); + foreach (var k in _mixedKeys) transient.Set(k, 999); + return transient.ToPersistent(); + } + + [Benchmark] + public ImmutableSortedDictionary UpdateSet_ImmSortedDict() + { + var map = _immSortedDict; + foreach (var k in _mixedKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.Map UpdateSet_ExtMap() + { + var map = _extMap; + foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.HashMap UpdateSet_ExtHashMap() + { + var map = _extHashMap; + foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999); + return map; + } + + // --- 5. ITERATION --- + + [Benchmark] + public int Iterate_ImmDict() + { + int sum = 0; + foreach (var kvp in _immDict) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_PersistentMap_Standard() + { + int sum = 0; + foreach (var kvp in _persistentMapStandard) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_ImmSortedDict() + { + int sum = 0; + foreach (var kvp in _immSortedDict) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_ExtMap() + { + int sum = 0; + foreach (var kvp in _extMap) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_ExtHashMap() + { + int sum = 0; + foreach (var kvp in _extHashMap) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_PersistentMap_Unicode() + { + int sum = 0; + foreach (var kvp in _persistentMapUnicode) sum += kvp.Value; + return sum; + } + + // --- 6. REMOVAL --- + + [Benchmark] + public ImmutableDictionary Remove_ImmDict() + { + var map = _immDict; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public PersistentMap> Remove_PersistentMap_Standard() + { + var map = _persistentMapStandard; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public PersistentMap Remove_PersistentMap_Unicode() + { + var map = _persistentMapUnicode; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public PersistentMap> Remove_TransientMap_Standard() + { + var transient = _persistentMapStandard.ToTransient(); + foreach (var k in _removeKeys) transient.Remove(k); + return transient.ToPersistent(); + } + +[Benchmark] + public PersistentMap Remove_TransientMap_Unicode() + { + var transient = _persistentMapUnicode.ToTransient(); + foreach (var k in _removeKeys) transient.Remove(k); + return transient.ToPersistent(); + } + + [Benchmark] + public ImmutableSortedDictionary Remove_ImmSortedDict() + { + var map = _immSortedDict; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public LanguageExt.Map Remove_ExtMap() + { + var map = _extMap; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public LanguageExt.HashMap Remove_ExtHashMap() + { + var map = _extHashMap; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } +}