diff --git a/PersistentOrderedMap.sln b/NiceBtree.sln similarity index 77% rename from PersistentOrderedMap.sln rename to NiceBtree.sln index f313444..51a8511 100644 --- a/PersistentOrderedMap.sln +++ b/NiceBtree.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentOrderedMap", "PersistentOrderedMap\PersistentOrderedMap.csproj", "{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistentMap", "PersistentMap\PersistentMap.csproj", "{CA49AA3C-0CE6-4735-887F-FB3631D63CEE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{9E499000-5E37-42F8-89D2-E18A53F0EF0C}" EndProject @@ -10,7 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstImmutableDict", "benchmarks\AgainstImmutableDict\AgainstImmutableDict.csproj", "{13304F19-7ED3-4C40-9A08-46D539667D50}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyBenchMarks", "benchmarks\MyBenchMarks\MyBenchMarks.csproj", "{769E1CEA-7E01-405B-80A2-95CBF432A2BA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgainstLanguageExt", "benchmarks\AgainstLanguageExt\AgainstLanguageExt.csproj", "{6C16526B-5139-4EA3-BF74-E6320F467198}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,7 +20,7 @@ Global GlobalSection(NestedProjects) = preSolution {CA49AA3C-0CE6-4735-887F-FB3631D63CEE} = {B0432C7A-80E2-4EA6-8FAB-B8F23A8C39DE} {13304F19-7ED3-4C40-9A08-46D539667D50} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53} - {769E1CEA-7E01-405B-80A2-95CBF432A2BA} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53} + {6C16526B-5139-4EA3-BF74-E6320F467198} = {E38B3FCB-0D4D-401D-A2FC-EDF41B755E53} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CA49AA3C-0CE6-4735-887F-FB3631D63CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -35,9 +35,9 @@ Global {13304F19-7ED3-4C40-9A08-46D539667D50}.Debug|Any CPU.Build.0 = Debug|Any CPU {13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.ActiveCfg = Release|Any CPU {13304F19-7ED3-4C40-9A08-46D539667D50}.Release|Any CPU.Build.0 = Release|Any CPU - {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 + {6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C16526B-5139-4EA3-BF74-E6320F467198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C16526B-5139-4EA3-BF74-E6320F467198}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/PersistentMap/BTreeFunctions.cs b/PersistentMap/BTreeFunctions.cs new file mode 100644 index 0000000..9c44c18 --- /dev/null +++ b/PersistentMap/BTreeFunctions.cs @@ -0,0 +1,939 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace PersistentMap +{ + public static class BTreeFunctions + { + // --------------------------------------------------------- + // Public API + // --------------------------------------------------------- + + 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; + while (true) + { + 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) + { + value = leaf.Values[index]; + return true; + } + value = default!; + return false; + } + else + { + // FIX: Internal uses FindRoutingIndex (Upper Bound) + var internalNode = current.AsInternal(); + int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + current = internalNode.Children[index]!; + } + } + } + + // Public API + public static Node Set(Node root, K key, V value, IKeyStrategy strategy, OwnerId owner, out bool countChanged) + { + root = root.EnsureEditable(owner); + + var splitResult = InsertRecursive(root, key, value, strategy, owner, out countChanged); + + if (splitResult != null) + { + var newRoot = new InternalNode(owner); + newRoot.Children[0] = root; + newRoot.Keys[0] = splitResult.Separator; + newRoot.Children[1] = splitResult.NewNode; + newRoot.SetCount(1); + if (strategy.UsesPrefixes) + newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator); + + return newRoot; + } + + 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) + { + var leaf = node.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + 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. + return null; + } + + added = true; // New key. Count +1. + if (leaf.Header.Count < LeafNode.Capacity) + { + InsertIntoLeaf(leaf, index, key, value, strategy); + return null; + } + else + { + return SplitLeaf(leaf, index, key, value, strategy, owner); + } + } + else + { + var internalNode = node.AsInternal(); + int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + + var child = internalNode.Children[index]!.EnsureEditable(owner); + internalNode.Children[index] = child; + + var split = InsertRecursive(child, key, value, strategy, owner, out added); + + if (split != null) + { + if (internalNode.Header.Count < InternalNode.Capacity - 1) + { + InsertIntoInternal(internalNode, index, split.Separator, split.NewNode, strategy); + return null; + } + else + { + return SplitInternal(internalNode, index, split.Separator, split.NewNode, strategy, owner); + } + } + return null; + } + } + + // Public API + public static Node Remove(Node root, K key, TStrategy strategy, OwnerId owner, out bool countChanged) + where TStrategy : IKeyStrategy + { + root = root.EnsureEditable(owner); + + bool rebalanceNeeded = RemoveRecursive(root, key, strategy, owner, out countChanged); + + if (rebalanceNeeded) + { + if (!root.IsLeaf) + { + var internalRoot = root.AsInternal(); + if (internalRoot.Header.Count == 0) + { + return internalRoot.Children[0]!; + } + } + } + + return root; + } + + // Recursive Helper + private static bool RemoveRecursive(Node node, K key, TStrategy strategy, OwnerId owner, out bool removed) + where TStrategy : IKeyStrategy + { + // 1. Calculate ONCE + long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; + + if (node.IsLeaf) + { + var leaf = node.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + 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 = false; // Item not found. + return false; + } + else + { + var internalNode = node.AsInternal(); + int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + + var child = internalNode.Children[index]!.EnsureEditable(owner); + internalNode.Children[index] = child; + + bool childUnderflow = RemoveRecursive(child, key, strategy, owner, out removed); + + if (removed && childUnderflow) + { + return HandleUnderflow(internalNode, index, strategy, owner); + } + return false; + } + } + + // --------------------------------------------------------- + // 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 (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); + } + + [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); + } + + // Use the pre-calculated prefix here! + int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); + return RefineRouting(index, node.Keys, node.Header.Count, 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 + { + 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++; + } + return i; + } + + // --------------------------------------------------------- + // Insertion Logic + // --------------------------------------------------------- + + private class SplitResult + { + public Node NewNode; + public K 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 + { + 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.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.SetCount(count + 1); + } + private static SplitResult SplitLeaf(LeafNode left, int insertIndex, K key, V value, TStrategy strategy, OwnerId owner) + where TStrategy : IKeyStrategy + { + var right = new LeafNode(owner); + 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 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]; + } + + // 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); + } + + + // 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 + { + int count = node.Header.Count; + + // Shift Keys and Prefixes + if (index < count) + { + + int moveCount = count - index; + + // 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)); + } + } + + // 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.Children[i] = node.Children[i - 1]; + } + + 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 + { + var right = 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. + + K upKey = left.Keys[splitPoint]; + + // 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)); + + if (strategy.UsesPrefixes) + { + left.AllPrefixes.Slice(splitPoint + 1, moveCount).CopyTo(right.AllPrefixes.Slice(0)); + } + } + // 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); + } + else + { + InsertIntoInternal(right, insertIndex - (splitPoint + 1), separator, newChild, strategy); + } + + return new SplitResult(right, upKey); + } + + // --------------------------------------------------------- + // Removal Logic + // --------------------------------------------------------- + + // --------------------------------------------------------- + // Removal Logic (Fixed Type Inference & Casting) + // --------------------------------------------------------- + + + private static void RemoveFromLeaf(LeafNode leaf, int index, TStrategy strategy) + 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)); + } + } + + leaf.SetCount(count - 1); + } + + // FIX 3: Added to HandleUnderflow + private static bool HandleUnderflow(InternalNode parent, int childIndex, TStrategy strategy, OwnerId owner) + where TStrategy : IKeyStrategy + { + // Try to borrow from Right Sibling + if (childIndex < parent.Header.Count) + { + var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner); + parent.Children[childIndex + 1] = rightSibling; + var leftChild = parent.Children[childIndex]!; + + if (CanBorrow(rightSibling)) + { + RotateLeft(parent, childIndex, leftChild, rightSibling, strategy); + return false; + } + else + { + Merge(parent, childIndex, leftChild, rightSibling, strategy); + return parent.Header.Count < LeafNode.MergeThreshold; + } + } + // Try to borrow from Left Sibling + else if (childIndex > 0) + { + var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner); + parent.Children[childIndex - 1] = leftSibling; + var rightChild = parent.Children[childIndex]!; + + if (CanBorrow(leftSibling)) + { + RotateRight(parent, childIndex - 1, leftSibling, rightChild, strategy); + return false; + } + else + { + // Merge Left and Current. Note separator index is 'childIndex - 1' + Merge(parent, childIndex - 1, leftSibling, rightChild, strategy); + return parent.Header.Count < LeafNode.MergeThreshold; + } + } + + return true; + } + + 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 + { + // Case A: Merging Leaves + if (left.IsLeaf) + { + var leftLeaf = left.AsLeaf(); + var rightLeaf = right.AsLeaf(); + + 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)); + } + + 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)); + if (strategy.UsesPrefixes) + { + 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]; + } + + 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)); + + 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]; + } + + parent.SetCount(pCount - 1); + } + + private static void RotateLeft(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) + 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]; + if (rCount > 1) + { + // Fast Span memory moves (Replaces Array.Copy & manual loop) + rightInternal.Keys.AsSpan(1, rCount - 1).CopyTo(rightInternal.Keys.AsSpan(0)); + + if (strategy.UsesPrefixes) + { + rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes.Slice(0)); + } + } + + rightInternal.SetCount(rCount - 1); + } + } + + private static void RotateRight(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) + where TStrategy : IKeyStrategy + { + // Move one item from Left to Right + if (left.IsLeaf) + { + var leftLeaf = left.AsLeaf(); + var rightLeaf = right.AsLeaf(); + int last = leftLeaf.Header.Count - 1; + + InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys[last], leftLeaf.Values[last], strategy); + RemoveFromLeaf(leftLeaf, last, strategy); + + parent.Keys[separatorIndex] = rightLeaf.Keys[0]; + if (strategy.UsesPrefixes) + parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); + } + else + { + var leftInternal = (InternalNode)left; + var rightInternal = (InternalNode)right; + 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]); + + // 3. Truncate Left + leftInternal.SetCount(last); + } + } + + + + public static bool TryGetMin(Node root, out K key, out V value) + { + var current = root; + while (!current.IsLeaf) + { + current = current.AsInternal().Children[0]!; + } + + var leaf = current.AsLeaf(); + if (leaf.Header.Count == 0) + { + key = default!; + value = default!; + return false; + } + + key = leaf.Keys![0]; + value = leaf.Values[0]; + return true; + } + + public static bool TryGetMax(Node root, out K key, out V value) + { + var current = root; + while (!current.IsLeaf) + { + var internalNode = current.AsInternal(); + current = internalNode.Children[internalNode.Header.Count]!; + } + + var leaf = current.AsLeaf(); + if (leaf.Header.Count == 0) + { + key = default!; + value = default!; + return false; + } + + int last = leaf.Header.Count - 1; + key = leaf.Keys![last]; + value = leaf.Values[last]; + return true; + } + + public static bool TryGetSuccessor(Node root, K key, TStrategy strategy, out K nextKey, out V nextValue) + where TStrategy : IKeyStrategy + { + 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) + { + var internalNode = current.AsInternal(); + int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + path[depth] = internalNode; + indices[depth] = idx; + depth++; + current = internalNode.Children[idx]!; + } + + var leaf = current.AsLeaf(); + int index = FindIndex(leaf, key, keyPrefix, strategy); + + 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]; + 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) + { + current = current.AsInternal().Children[0]!; + } + + var targetLeaf = current.AsLeaf(); + nextKey = targetLeaf.Keys![0]; + nextValue = targetLeaf.Values[0]; + return true; + } + } + + nextKey = default!; + nextValue = default!; + return false; + } + + 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) + { + var internalNode = current.AsInternal(); + int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy); + path[depth] = internalNode; + indices[depth] = idx; + depth++; + current = internalNode.Children[idx]!; + } + + 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]; + 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) + { + var internalNode = current.AsInternal(); + current = internalNode.Children[internalNode.Header.Count]!; + } + + var targetLeaf = current.AsLeaf(); + int last = targetLeaf.Header.Count - 1; + prevKey = targetLeaf.Keys![last]; + prevValue = targetLeaf.Values[last]; + return true; + } + } + + prevKey = default!; + prevValue = default!; + return false; + } + } + + +} diff --git a/PersistentOrderedMap/BaseOrderedMap.cs b/PersistentMap/BaseOrderedMap.cs similarity index 51% rename from PersistentOrderedMap/BaseOrderedMap.cs rename to PersistentMap/BaseOrderedMap.cs index 393e2ee..ee99960 100644 --- a/PersistentOrderedMap/BaseOrderedMap.cs +++ b/PersistentMap/BaseOrderedMap.cs @@ -1,18 +1,18 @@ using System.Collections; -namespace PersistentOrderedMap; +namespace PersistentMap; -public abstract class BaseOrderedMap : IEnumerable> where TStrategy : IKeyStrategy +public abstract class BaseOrderedMap : IEnumerable> where TStrategy : IKeyStrategy { - internal Node Root; - internal readonly TStrategy Strategy; + internal Node _root; + internal readonly TStrategy _strategy; public int Count { get; protected set; } - protected BaseOrderedMap(Node root, TStrategy strategy, int count) + protected BaseOrderedMap(Node root, TStrategy strategy, int count) { - Root = root ?? throw new ArgumentNullException(nameof(root)); - Strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); + _root = root ?? throw new ArgumentNullException(nameof(root)); + _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); Count = count; } @@ -21,14 +21,14 @@ public abstract class BaseOrderedMap : IEnumerable(Root, key, Strategy, out _); + return BTreeFunctions.TryGetValue(_root, key, _strategy, out _); } @@ -37,26 +37,26 @@ public abstract class BaseOrderedMap : IEnumerable Create(TStrategy strategy) + public static PersistentMap Create(TStrategy strategy) { // Start with an empty leaf owned by None so the first write triggers CoW. - var emptyRoot = new LeafNode(OwnerId.None, strategy.UsesPrefixes); - return new PersistentOrderedMap(emptyRoot, strategy, 0); + var emptyRoot = new LeafNode(OwnerId.None); + return new PersistentMap(emptyRoot, strategy, 0); } - public static TransientOrderedMap CreateTransient(TStrategy strategy) + public static TransientMap CreateTransient(TStrategy strategy) { - var emptyRoot = new LeafNode(OwnerId.None, strategy.UsesPrefixes); - return new TransientOrderedMap(emptyRoot, strategy,0); + var emptyRoot = new LeafNode(OwnerId.None); + return new TransientMap(emptyRoot, strategy,0); } - public BTreeEnumerator GetEnumerator() + public BTreeEnumerator GetEnumerator() { return AsEnumerable().GetEnumerator(); } - IEnumerator> IEnumerable>.GetEnumerator() + IEnumerator> IEnumerable>.GetEnumerator() { return GetEnumerator(); } @@ -67,19 +67,19 @@ public abstract class BaseOrderedMap : IEnumerable AsEnumerable() - => new(Root, Strategy, false, default!, false, default!); + public BTreeEnumerable AsEnumerable() + => new(_root, _strategy, false, default, false, default); // 2. Exact Range - public BTreeEnumerable Range(TK min, TK max) - => new(Root, Strategy, true, min, true, max); + public BTreeEnumerable Range(K min, K max) + => new(_root, _strategy, true, min, true, max); // 3. Start From (Open Ended) - public BTreeEnumerable From(TK min) => new(Root, Strategy, true, min, false, default!); + public BTreeEnumerable From(K min) => new(_root, _strategy, true, min, false, default); // 4. Until (Start at beginning) - public BTreeEnumerable Until(TK max) - => new(Root, Strategy, false, default!, true, max); + public BTreeEnumerable Until(K max) + => new(_root, _strategy, false, default, true, max); @@ -87,19 +87,19 @@ public abstract class BaseOrderedMap : IEnumerable BTreeFunctions.TryGetMin(Root, out key, out value); +public bool TryGetMin(out K key, out V value) => BTreeFunctions.TryGetMin(_root, out key, out value); -public bool TryGetMax(out TK key, out TV value) => BTreeFunctions.TryGetMax(Root, out key, out value); +public bool TryGetMax(out K key, out V value) => BTreeFunctions.TryGetMax(_root, out key, out value); -public bool TryGetSuccessor(TK key, out TK nextKey, out TV nextValue) => BTreeFunctions.TryGetSuccessor(Root, key, Strategy, out nextKey, out nextValue); +public bool TryGetSuccessor(K key, out K nextKey, out V nextValue) => BTreeFunctions.TryGetSuccessor(_root, key, _strategy, out nextKey, out nextValue); -public bool TryGetPredecessor(TK key, out TK prevKey, out TV prevValue) => BTreeFunctions.TryGetPredecessor(Root, key, Strategy, out prevKey, out prevValue); +public bool TryGetPredecessor(K key, out K prevKey, out V prevValue) => BTreeFunctions.TryGetPredecessor(_root, key, _strategy, out prevKey, out prevValue); // --------------------------------------------------------- // Set Operations (Linear Merge O(N+M)) // --------------------------------------------------------- -public IEnumerable> Intersect(BaseOrderedMap other) +public IEnumerable> Intersect(BaseOrderedMap other) { using var enum1 = this.GetEnumerator(); using var enum2 = other.GetEnumerator(); @@ -109,7 +109,7 @@ public IEnumerable> Intersect(BaseOrderedMap> Intersect(BaseOrderedMap> Except(BaseOrderedMap other) +public IEnumerable> Except(BaseOrderedMap other) { using var enum1 = this.GetEnumerator(); using var enum2 = other.GetEnumerator(); @@ -131,7 +131,7 @@ public IEnumerable> Except(BaseOrderedMap> Except(BaseOrderedMap> SymmetricExcept(BaseOrderedMap other) +public IEnumerable> SymmetricExcept(BaseOrderedMap other) { using var enum1 = this.GetEnumerator(); using var enum2 = other.GetEnumerator(); @@ -165,7 +165,7 @@ public IEnumerable> SymmetricExcept(BaseOrderedMap : IEnumerable> -where TStrategy : IKeyStrategy +public struct BTreeEnumerable : IEnumerable> +where TStrategy : IKeyStrategy { - private readonly Node _root; + private readonly Node _root; private readonly TStrategy _strategy; - private readonly TK _min, _max; + private readonly K _min, _max; private readonly bool _hasMin, _hasMax; - public BTreeEnumerable(Node root, TStrategy strategy, bool hasMin, TK min, bool hasMax, TK max) + public BTreeEnumerable(Node root, TStrategy strategy, bool hasMin, K min, bool hasMax, K max) { _root = root; _strategy = strategy; _hasMin = hasMin; _min = min; _hasMax = hasMax; _max = max; } - public BTreeEnumerator GetEnumerator() + public BTreeEnumerator GetEnumerator() { - return new BTreeEnumerator(_root, _strategy, _hasMin, _min, _hasMax, _max); + return new BTreeEnumerator(_root, _strategy, _hasMin, _min, _hasMax, _max); } - IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } // Fixed-size buffer for the path. // Depth 16 * 32 (branching factor) = Exabytes of capacity. -// The B tree is, theoretically, not limited by the size of an int, like -// all int-indexed data structures (or bit partitioned ones). -// This should be enough for anyone. [InlineArray(16)] -internal struct IterNodeBuffer +internal struct IterNodeBuffer { - private Node _element0; + private Node _element0; } [InlineArray(16)] -internal struct IterIndexBuffer +internal struct IterIndexBuffer { private int _element0; } -public struct BTreeEnumerator : IEnumerator> - where TStrategy : IKeyStrategy +public struct BTreeEnumerator : IEnumerator> + where TStrategy : IKeyStrategy { private readonly TStrategy _strategy; - private readonly Node _root; + private readonly Node _root; // --- BOUNDS --- private readonly bool _hasMax; - private readonly TK _maxKey; + private readonly K _maxKey; private readonly bool _hasMin; - private readonly TK _minKey; + private readonly K _minKey; // --- INLINE STACK --- - private IterNodeBuffer _nodeStack; - private IterIndexBuffer _indexStack; + private IterNodeBuffer _nodeStack; + private IterIndexBuffer _indexStack; private int _depth; // --- STATE --- - private LeafNode? _currentLeaf; + private LeafNode? _currentLeaf; private int _currentLeafIndex; - private KeyValuePair _current; + private KeyValuePair _current; // Unified Constructor // We use boolean flags because 'K' might be a struct where 'null' is impossible. - public BTreeEnumerator(Node? root, TStrategy strategy, bool hasMin, TK minKey, bool hasMax, TK maxKey) + public BTreeEnumerator(Node root, TStrategy strategy, bool hasMin, K minKey, bool hasMax, K maxKey) { - _root = root!; + _root = root; _strategy = strategy; _hasMax = hasMax; _maxKey = maxKey; _hasMin = hasMin; _minKey = minKey; - _nodeStack = new IterNodeBuffer(); - _indexStack = new IterIndexBuffer(); // Explicit struct init + _nodeStack = new IterNodeBuffer(); + _indexStack = new IterIndexBuffer(); // Explicit struct init _depth = 0; _currentLeaf = null; _currentLeafIndex = -1; @@ -102,7 +99,7 @@ public struct BTreeEnumerator : IEnumerator node = _root; + Node node = _root; _depth = 0; while (!node.IsLeaf) @@ -114,14 +111,14 @@ public struct BTreeEnumerator : IEnumerator(); + _currentLeaf = node.AsLeaf(); _currentLeafIndex = -1; // Position before the first element (0) } // Logic 2: Bounded Start (Go to specific key) - private void Seek(TK key) + private void Seek(K key) { - Node node = _root; + Node node = _root; _depth = 0; long keyPrefix = _strategy.UsesPrefixes ? _strategy.GetPrefix(key) : 0; @@ -130,7 +127,7 @@ public struct BTreeEnumerator : IEnumerator(internalNode, key, keyPrefix, _strategy); _nodeStack[_depth] = internalNode; _indexStack[_depth] = idx; @@ -140,8 +137,8 @@ public struct BTreeEnumerator : IEnumerator(); - int index = BTreeFunctions.FindIndex(_currentLeaf, key, keyPrefix, _strategy); + _currentLeaf = node.AsLeaf(); + int index = BTreeFunctions.FindIndex(_currentLeaf, key, keyPrefix, _strategy); // Set position to (index - 1) so that the first MoveNext() lands on 'index' _currentLeafIndex = index - 1; @@ -158,14 +155,14 @@ public struct BTreeEnumerator : IEnumerator Max Key, we are done. - if (_strategy.Compare(_currentLeaf.Keys![_currentLeafIndex], _maxKey) > 0) + if (_strategy.Compare(_currentLeaf.Keys[_currentLeafIndex], _maxKey) > 0) { _currentLeaf = null; // Close iterator return false; } } - _current = new KeyValuePair(_currentLeaf.Keys![_currentLeafIndex], _currentLeaf.Values[_currentLeafIndex]); + _current = new KeyValuePair(_currentLeaf.Keys[_currentLeafIndex], _currentLeaf.Values[_currentLeafIndex]); return true; } @@ -176,14 +173,14 @@ public struct BTreeEnumerator : IEnumerator 0) + if (_strategy.Compare(_currentLeaf!.Keys[0], _maxKey) > 0) { _currentLeaf = null; return false; } } - _current = new KeyValuePair(_currentLeaf.Keys![0], _currentLeaf.Values[0]); + _current = new KeyValuePair(_currentLeaf.Keys[0], _currentLeaf.Values[0]); return true; } @@ -204,7 +201,7 @@ public struct BTreeEnumerator : IEnumerator node = internalNode.Children[nextIndex]!; + Node node = internalNode.Children[nextIndex]!; while (!node.IsLeaf) { _nodeStack[_depth] = node; @@ -213,7 +210,7 @@ public struct BTreeEnumerator : IEnumerator(); + _currentLeaf = node.AsLeaf(); _currentLeafIndex = 0; return true; } @@ -221,7 +218,7 @@ public struct BTreeEnumerator : IEnumerator Current => _current; + public KeyValuePair Current => _current; object IEnumerator.Current => _current; public void Reset() { diff --git a/PersistentMap/KeyStrategies.cs b/PersistentMap/KeyStrategies.cs new file mode 100644 index 0000000..1a437a8 --- /dev/null +++ b/PersistentMap/KeyStrategies.cs @@ -0,0 +1,241 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace PersistentMap; + +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +public interface IKeyStrategy +{ + int Compare(K x, K y); + long GetPrefix(K key); + + bool UsesPrefixes => true; + + // + bool IsLossless => 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 +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Compare(string? x, string? y) => string.CompareOrdinal(x, y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetPrefix(string key) + { + if (string.IsNullOrEmpty(key)) return long.MinValue; + + // 1. Prepare Buffer (8 bytes) + // stackalloc is virtually free (pointer bump) + Span utf8Bytes = stackalloc byte[8]; + + // 2. Transcode (The "Safe" Magic) + // This intrinsic handles ASCII efficiently and converts Surrogates/Chinese + // into bytes that maintain the correct "Magnitude" (Sort Order). + // Invalid surrogates become 0xEF (Replacement Char), which sorts > ASCII. + System.Text.Unicode.Utf8.FromUtf16( + key.AsSpan(0, Math.Min(key.Length, 8)), + utf8Bytes, + out _, + out _, + replaceInvalidSequences: true); // True ensures we get 0xEF for broken chars + + // 3. Load as Big Endian Long + long packed = BinaryPrimitives.ReadInt64BigEndian(utf8Bytes); + + // 4. Sign Toggle + // Maps the byte range 0x00..0xFF to the signed long range Min..Max + // Essential for the < and > operators to work correctly. + return packed ^ unchecked((long)0x8080808080808080); + } +} + +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 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/PersistentOrderedMap/Nodes.cs b/PersistentMap/Nodes.cs similarity index 63% rename from PersistentOrderedMap/Nodes.cs rename to PersistentMap/Nodes.cs index 20499ca..37e8533 100644 --- a/PersistentOrderedMap/Nodes.cs +++ b/PersistentMap/Nodes.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace PersistentOrderedMap; +namespace PersistentMap; [Flags] public enum NodeFlags : byte @@ -32,18 +32,12 @@ public struct NodeHeader } } -[InlineArray(32)] -public struct KeyBuffer -{ - private TK _element0; -} - // Constraint: Internal Nodes fixed at 32 children. // This removes the need for a separate array allocation for children references. [InlineArray(32)] -public struct NodeBuffer +public struct NodeBuffer { - private Node? _element0; + private Node? _element0; } [InlineArray(32)] @@ -52,7 +46,7 @@ internal struct InternalPrefixBuffer private long _element0; } -public abstract class Node +public abstract class Node { public NodeHeader Header; @@ -61,7 +55,7 @@ public abstract class Node Header = new NodeHeader(owner, 0, flags); } - public abstract Span GetKeys(); + public abstract Span GetKeys(); // Abstract access to prefixes regardless of storage backing public abstract Span AllPrefixes { get; } @@ -71,69 +65,61 @@ public abstract class Node public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0; - public abstract Node EnsureEditable(OwnerId transactionId); + public abstract Node EnsureEditable(OwnerId transactionId); public void SetCount(int newCount) => Header.Count = (byte)newCount; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public LeafNode AsLeaf() + public LeafNode AsLeaf() { // Zero-overhead cast. Assumes you checked IsLeaf or know logic flow. - return Unsafe.As>(this); + return Unsafe.As>(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public InternalNode AsInternal() + public InternalNode AsInternal() { // 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); + return Unsafe.As>(this); } } -public sealed class LeafNode : Node +public sealed class LeafNode : Node { public const int Capacity = 64; public const int MergeThreshold = 8; - public TK[]? Keys; - public TV[] Values; + public K[]? Keys; + public V[] Values; - private long[]? _prefixes; + internal long[]? _prefixes; public override Span AllPrefixes => _prefixes != null ? _prefixes : Span.Empty; - public LeafNode(OwnerId owner, bool usePrefixes) : base(owner, NodeFlags.IsLeaf | (usePrefixes ? NodeFlags.HasPrefixes : NodeFlags.None)) + public LeafNode(OwnerId owner) : base(owner, NodeFlags.IsLeaf | NodeFlags.HasPrefixes) { - Keys = new TK[Capacity]; - Values = new TV[Capacity]; - if (usePrefixes) - { - _prefixes = new long[Capacity]; - } + Keys = new K[Capacity]; + Values = new V[Capacity]; + _prefixes = new long[Capacity]; + } // Copy Constructor for CoW - private LeafNode(LeafNode original, OwnerId newOwner) + private LeafNode(LeafNode original, OwnerId newOwner) : base(newOwner, original.Header.Flags) { - Keys = new TK[Capacity]; - Values = new TV[Capacity]; + Keys = new K[Capacity]; + Values = new V[Capacity]; Header.Count = original.Header.Count; _prefixes = new long[Capacity]; // Copy data - Array.Copy(original.Keys!, Keys, original.Header.Count); + Array.Copy(original.Keys, Keys, original.Header.Count); Array.Copy(original.Values, Values, original.Header.Count); if (original._prefixes != null) Array.Copy(original._prefixes, _prefixes, original.Header.Count); } - public override Node EnsureEditable(OwnerId transactionId) + public override Node EnsureEditable(OwnerId transactionId) { // CASE 1: Persistent Mode (transactionId is None). // We MUST create a copy, because we cannot distinguish "Shared Immutable Node (0)" @@ -142,7 +128,7 @@ public sealed class LeafNode : Node // we won't copy the same fresh node twice. if (transactionId == OwnerId.None) { - return new LeafNode(this, OwnerId.None); + return new LeafNode(this, OwnerId.None); } // CASE 2: Transient Mode. @@ -153,89 +139,81 @@ public sealed class LeafNode : Node } // CASE 3: CoW needed (Ownership mismatch). - return new LeafNode(this, transactionId); + return new LeafNode(this, transactionId); } - public override Span GetKeys() + public override Span GetKeys() { return Keys.AsSpan(0, Header.Count); } - public Span GetValues() + public Span GetValues() { return Values.AsSpan(0, Header.Count); } } -public class InternalNode : Node +public sealed class InternalNode : Node { public const int Capacity = 32; - public KeyBuffer Keys; - public NodeBuffer Children; + // InlineArray storage + internal InternalPrefixBuffer _prefixBuffer; + public NodeBuffer Children; + + public K[]? Keys; + + public override Span AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity); - public override Span AllPrefixes => Span.Empty; - - public InternalNode(OwnerId owner, NodeFlags flags = NodeFlags.None) - : base(owner, flags) + public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes) { + Keys = new K[Capacity]; + // Children buffer is a struct, zero-initialized by default } - // Fixed CoW Constructor - protected InternalNode(InternalNode original, OwnerId newOwner, NodeFlags flags) - : base(newOwner, flags) + // Copy Constructor for CoW + private InternalNode(InternalNode original, OwnerId newOwner) + : base(newOwner, original.Header.Flags) { Header.Count = original.Header.Count; + Keys = new K[Capacity]; + Array.Copy(original.Keys, Keys, original.Header.Count); - // Fast struct blit for both Keys and Children. - // No loop required for InlineArrays! - this.Keys = original.Keys; - this.Children = original.Children; + // Fast struct blit for prefixes + this._prefixBuffer = original._prefixBuffer; + + 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() + public override Node EnsureEditable(OwnerId transactionId) { - // An internal node always has (Count + 1) children - return MemoryMarshal.CreateSpan(ref Children[0]!, Header.Count + 1); + 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); } - 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) + // Exposes the InlineArray as a Span + public Span?> GetChildren() { + return MemoryMarshal.CreateSpan?>(ref Children[0]!, Header.Count + 1); } - // CoW Constructor - private PrefixInternalNode(PrefixInternalNode original, OwnerId newOwner) - : base(original, newOwner, original.Header.Flags) + public void SetChild(int index, Node node) { - // 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 PrefixInternalNode(this, OwnerId.None); - if (Header.Owner == transactionId) return this; - return new PrefixInternalNode(this, transactionId); + Children[index] = node; } } diff --git a/PersistentMap/PersistentMap.cs b/PersistentMap/PersistentMap.cs new file mode 100644 index 0000000..5f6ed43 --- /dev/null +++ b/PersistentMap/PersistentMap.cs @@ -0,0 +1,44 @@ +using System.Collections; + +namespace PersistentMap; + +public sealed class PersistentMap : BaseOrderedMap, IEnumerable, IEnumerable> where TStrategy : IKeyStrategy +{ + internal PersistentMap(Node root, TStrategy strategy, int count) + : base(root, strategy, count) { } + + // --------------------------------------------------------- + // Immutable Write API (Returns new Map) + // --------------------------------------------------------- + public PersistentMap Set(K key, V value) + { + // OPTIMIZATION: Use OwnerId.None (0). + // This signals EnsureEditable to always copy the root path, + // producing a new tree of nodes that also have OwnerId.None. + var newRoot = BTreeFunctions.Set(_root, key, value, _strategy, OwnerId.None, out bool countChanged); + return new PersistentMap(newRoot, _strategy, countChanged ? Count + 1 : Count); + } + + public static PersistentMap Empty(TStrategy strategy) + { + // Create an empty Leaf Node. + // 'default(OwnerId)' (usually 0) marks this node as Immutable/Persistent. + // This ensures that any subsequent Set/Remove will clone this node + // instead of modifying it in place. + var emptyRoot = new LeafNode(default(OwnerId)); + + return new PersistentMap(emptyRoot, strategy, 0); + } + + public PersistentMap Remove(K key) + { + var newRoot = BTreeFunctions.Remove(_root, key, _strategy, OwnerId.None, out bool removed); + if (!removed) return this; + return new PersistentMap(newRoot, _strategy, Count - 1); + } + + public TransientMap ToTransient() + { + return new TransientMap(_root, _strategy, Count); + } +} diff --git a/PersistentOrderedMap/PersistentOrderedMap.csproj b/PersistentMap/PersistentMap.csproj similarity index 100% rename from PersistentOrderedMap/PersistentOrderedMap.csproj rename to PersistentMap/PersistentMap.csproj diff --git a/PersistentMap/Readme.md b/PersistentMap/Readme.md new file mode 100644 index 0000000..81517f1 --- /dev/null +++ b/PersistentMap/Readme.md @@ -0,0 +1,89 @@ +# PersistentMap + +A high-performance, persistent (immutable) B-Tree map implementation for .NET, designed for scenarios requiring efficient snapshots and transactional updates. + +## Features + +* **Persistent (Immutable) by Default:** Operations on `PersistentMap` return a new instance, sharing structure with the previous version. This makes it trivial to keep historical snapshots or implement undo/redo. +* **Transient (Mutable) Phase:** Supports a `TransientMap` for high-performance batch updates. This allows you to perform multiple mutations (Set/Remove) without the overhead of allocating new path nodes for every single operation, similar to Clojure's transients or Scala's builders. +* **Optimized B-Tree:** Uses a B-Tree structure optimized for modern CPU caches and SIMD instructions (AVX2/AVX512) for key prefix scanning. +* **Custom Key Strategies:** Flexible `IKeyStrategy` interface allows defining custom comparison and prefix generation logic (e.g., for strings, integers, or custom types). + +## Usage + +### When should I use this? + +Never, probably. This was just a fun little project. If you want a really fast immutable sorted map you should consider it. Despite this map being faster than LanguageExt.HashMap for some key types, you should definitely use that if you don't need a sorted collection. It is well tested and does not have any problems key collisions, which will slow this map down by a lot. + +It is also faster for just about every key that isn't a more-than-30-char-with-few-common-prefixes string. + + +### Basic Persistent Operations + +```csharp +using PersistentMap; + +// 1. Create an empty map with a strategy (e.g., for strings) +var map0 = PersistentMap.Empty(new UnicodeStrategy()); + +// 2. Add items (returns a new map) +var map1 = map0.Set("key1", "value1"); +var map2 = map1.Set("key2", "value2"); + +// map0 is still empty +// map1 has "key1" +// map2 has "key1" and "key2" + +// 3. Remove items +var map3 = map2.Remove("key1"); +// map3 has only "key2" +``` + +### Efficient Batch Updates (Transients) + +When you need to perform many updates at once (e.g., initial load, bulk import), use `ToTransient()` to switch to a mutable mode, and `ToPersistent()` to seal it back. + +```csharp +// 1. Start with a persistent map +var initialMap = PersistentMap.Empty(new IntStrategy()); + +// 2. Convert to transient (mutable) +var transientMap = initialMap.ToTransient(); + +// 3. Perform batch mutations (in-place, fast) +for (int i = 0; i < 10000; i++) +{ + transientMap.Set(i, $"Value {i}"); +} + +// 4. Convert back to persistent (immutable) +// This "seals" the current state. The transient map rolls its transaction ID, +// so subsequent writes to 'transientMap' won't affect 'finalMap'. +var finalMap = transientMap.ToPersistent(); +``` + +## Key Strategies + +The library uses `IKeyStrategy` to handle key comparisons and optimization. + +* **`UnicodeStrategy`**: Optimized for `string` keys. Uses SIMD to pack the first 8 bytes of the string into a `long` prefix for fast scanning. +* **`IntStrategy`**: Optimized for `int` keys. + +You can implement `IKeyStrategy` for your own types. + +## Performance Notes + +* **Structure Sharing:** `PersistentMap` shares unchanged nodes between versions, minimizing memory overhead. +* **Transients:** `TransientMap` uses an internal `OwnerId` (transaction ID) to track ownership. Nodes created within the same transaction are mutated in-place. `ToPersistent()` ensures that any future writes to the transient map will copy nodes instead of mutating the shared ones. This leads to very fast building times compared to using persistent updates. +* **SIMD:** The `PrefixScanner` uses AVX2/AVX512 (if available) to scan node keys efficiently. + +### Key strategies + +For string keys, the prefix optimization lets the library have really fast lookups. For mostly-ascii string keys, we are faster than most persistent hash maps once you pass a certain key size or collection size depending on implementation strategy. The B tree is shallow and has fewer cache misses, meaning it can be faster than either deep trees or hash maps despite doing linear searches. + +## Project Structure + +* `PersistentMap.cs`: The main immutable map implementation. +* `TransientMap.cs`: The mutable builder for batch operations. +* `Nodes.cs`: Internal B-Tree node definitions. +* `KeyStrategies.cs`: implementations of key comparison and prefixing. diff --git a/PersistentMap/Readme.org b/PersistentMap/Readme.org new file mode 100644 index 0000000..46352d3 --- /dev/null +++ b/PersistentMap/Readme.org @@ -0,0 +1,115 @@ +* PersistentMap + +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. +- *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. + +The general version of this, using =StandardStrategy= does not benefit from the prefix optimization. + +** Quick Start + +*** 1. Basic Immutable Usage +By default, the map is immutable. Every write operation returns a new, updated version of the map. + +#+begin_src csharp +// Create a map with a specific key strategy (e.g., Int, Unicode, Double) +var map1 = BaseOrderedMap.Create(new IntStrategy()); + +// Set returns a new tree instance. map1 remains empty. +var map2 = map1.Set(1, "Apple") + .Set(2, "Banana") + .Set(3, "Cherry"); + +if (map2.TryGetValue(2, out var value)) +{ + Console.WriteLine(value); // "Banana" +} +#+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. + +#+begin_src csharp +var transientMap = BaseOrderedMap.CreateTransient(new IntStrategy()); + +// Mutates in-place. No allocations for unchanged tree paths. +for (int i = 0; i < 10_000; i++) +{ + transientMap.Set(i, $"Value_{i}"); +} + +// O(1) freeze. Returns a thread-safe immutable PersistentMap. +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. + +#+begin_src csharp +var map = GetPopulatedMap(); + +// Iterate exact bounds +foreach (var kvp in map.Range(min: 10, max: 50)) +{ + Console.WriteLine($"{kvp.Key}: {kvp.Value}"); +} + +// Open-ended queries +var greaterThan100 = map.From(100); +var lessThan50 = map.Until(50); +var allElements = map.AsEnumerable(); +#+end_src + +*** 4. Tree Navigation +Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound. + +#+begin_src csharp +// Get extremes +map.TryGetMin(out int minKey, out string minVal); +map.TryGetMax(out int maxKey, out string maxVal); + +// Get the immediate next/previous element (works even if '42' doesn't exist) +if (map.TryGetSuccessor(42, out int nextKey, out string nextVal)) +{ + Console.WriteLine($"The key immediately after 42 is {nextKey}"); +} + +if (map.TryGetPredecessor(42, out int prevKey, out string prevVal)) +{ + Console.WriteLine($"The key immediately before 42 is {prevKey}"); +} +#+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. + +#+begin_src csharp +var mapA = CreateMap(1, 2, 3, 4); +var mapB = CreateMap(3, 4, 5, 6); + +// Returns { 3, 4 } +var common = mapA.Intersect(mapB); + +// Returns { 1, 2 } +var onlyInA = mapA.Except(mapB); + +// Returns { 1, 2, 5, 6 } +var symmetricDiff = mapA.SymmetricExcept(mapB); +#+end_src + +** 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. + +This means that it will be fast for integers and anything you can pack in 8 bytes. If you use stings with high prefix entropy, this will be /very/ performant. If you don't, it is just another b+tree. diff --git a/PersistentMap/TransientMap.cs b/PersistentMap/TransientMap.cs new file mode 100644 index 0000000..568b20d --- /dev/null +++ b/PersistentMap/TransientMap.cs @@ -0,0 +1,40 @@ +using System.Collections; + +namespace PersistentMap; + +public sealed class TransientMap : BaseOrderedMap where TStrategy : IKeyStrategy +{ + // This is mutable, but we treat it as readonly for the ID generation logic usually. + private OwnerId _transactionId; + + public TransientMap(Node root, TStrategy strategy, int count) + : base(root, strategy, count) + { + _transactionId = OwnerId.Next(); + } + + public void Set(K key, V value) + { + _root = BTreeFunctions.Set(_root, key, value, _strategy, _transactionId, out bool countChanged); + if (countChanged) Count++; + } + + public void Remove(K key) + { + _root = BTreeFunctions.Remove(_root, key, _strategy, _transactionId, out bool removed); + if (removed) Count--; + } + + public PersistentMap ToPersistent() + { + // 1. Create the snapshot by copying all relevant information + + var snapshot = new PersistentMap(_root, _strategy, Count); + + // 2. Protect the snapshot from THIS TransientMap by getting a new ownerId + // so that future edits will be done by CoW + _transactionId = OwnerId.Next(); + + return snapshot; + } +} \ No newline at end of file diff --git a/PersistentOrderedMap/BTreeFunctions.cs b/PersistentOrderedMap/BTreeFunctions.cs deleted file mode 100644 index 0d7a7af..0000000 --- a/PersistentOrderedMap/BTreeFunctions.cs +++ /dev/null @@ -1,852 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace PersistentOrderedMap -{ - public static class BTreeFunctions - { - // --------------------------------------------------------- - // Public API - // --------------------------------------------------------- - - /// TryGetValue tries to get the value at mapping key. If it finds the key it sets th - /// out var to value and returns true. - public static bool TryGetValue(Node root, TK key, TStrategy strategy, out TV value) - where TStrategy : IKeyStrategy - { - // We always get a strategy to avoid branching already here - long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; - - Node current = root; - while (true) - { - if (current.IsLeaf) - { - var leaf = current.AsLeaf(); - int index = FindIndex(leaf, key, keyPrefix, strategy); - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) - { - value = leaf.Values[index]; - return true; - } - value = default!; - return false; - } - else - { - var internalNode = current.AsInternal(); - int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); - current = internalNode.Children[index]!; - } - } - } - - public static Node Set(Node root, TK key, TV value, TStrategy strategy, OwnerId owner, out bool countChanged) - where TStrategy : IKeyStrategy - { - root = root.EnsureEditable(owner); - - // Todo, this should really be made a tuple return value to not stress the GC - var (newNode, sep) = InsertRecursive(root, key, value, strategy, owner, out countChanged); - - if (newNode != null) - { - var newRoot = strategy.UsesPrefixes - ? new PrefixInternalNode(owner) - : new InternalNode(owner); - - newRoot.Keys[0] = sep; - newRoot.Children[0] = root; - newRoot.Children[1] = newNode; - newRoot.SetCount(1); - - if (strategy.UsesPrefixes) - { - newRoot.AllPrefixes[0] = strategy.GetPrefix(sep); - } - - return newRoot; - } - - return root; - } - - private static (Node? newNode, TK separator ) InsertRecursive(Node node, TK key, TV value, IKeyStrategy strategy, OwnerId owner, out bool added) - { - long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; - - if (node.IsLeaf) - { - var leaf = node.AsLeaf(); - int index = FindIndex(leaf, key, keyPrefix, strategy); - - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) - { - leaf.Values[index] = value; - added = false; - return (null, default)!; - } - - added = true; - if (leaf.Header.Count < LeafNode.Capacity) - { - InsertIntoLeaf(leaf, index, key, value, strategy); - return (null, default)!; - } - else - { - return SplitLeaf(leaf, index, key, value, strategy, owner); - } - } - else - { - var internalNode = node.AsInternal(); - int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); - - var child = internalNode.Children[index]!.EnsureEditable(owner); - internalNode.Children[index] = child; - - var (newNode, sep) = InsertRecursive(child, key, value, strategy, owner, out added); - - if (newNode != null) - { - if (internalNode.Header.Count < InternalNode.Capacity - 1) - { - InsertIntoInternal(internalNode, index, sep, newNode, strategy); - return (null, default)!; - } - - return SplitInternal(internalNode, index, sep, newNode, strategy, owner); - - } - return (null, default)!; - } - } - - public static Node Remove(Node root, TK key, TStrategy strategy, OwnerId owner, out bool countChanged) - where TStrategy : IKeyStrategy - { - root = root.EnsureEditable(owner); - - bool rebalanceNeeded = RemoveRecursive(root, key, strategy, owner, out countChanged); - - if (rebalanceNeeded) - { - if (!root.IsLeaf) - { - var internalRoot = root.AsInternal(); - if (internalRoot.Header.Count == 0) - { - return internalRoot.Children[0]!; - } - } - } - - return root; - } - - private static bool RemoveRecursive(Node node, TK key, TStrategy strategy, OwnerId owner, out bool removed) - where TStrategy : IKeyStrategy - { - long keyPrefix = strategy.UsesPrefixes ? strategy.GetPrefix(key) : 0; - - if (node.IsLeaf) - { - var leaf = node.AsLeaf(); - int index = FindIndex(leaf, key, keyPrefix, strategy); - - if (index < leaf.Header.Count && strategy.Compare(leaf.Keys![index], key) == 0) - { - RemoveFromLeaf(leaf, index, strategy); - removed = true; - return leaf.Header.Count < LeafNode.MergeThreshold; - } - - removed = false; - return false; - } - else - { - var internalNode = node.AsInternal(); - int index = FindRoutingIndex(internalNode, key, keyPrefix, strategy); - - var child = internalNode.Children[index]!.EnsureEditable(owner); - internalNode.Children[index] = child; - - bool childUnderflow = RemoveRecursive(child, key, strategy, owner, out removed); - - if (removed && childUnderflow) - { - return HandleUnderflow(internalNode, index, strategy, owner); - } - return false; - } - } - - // --------------------------------------------------------- - // Internal Helpers: Search - // --------------------------------------------------------- - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int FindIndex(LeafNode node, TK key, long keyPrefix, TStrategy strategy) - where TStrategy : IKeyStrategy - { - if (typeof(TK) == typeof(int)) - { - Span keys = node.GetKeys(); - ref TK 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) - { - int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); - return RefineSearch(index, node.GetKeys(), key, strategy); - } - - return FallbackSearchKeys(node.GetKeys(), key, strategy); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int FindRoutingIndex(InternalNode node, TK key, long keyPrefix, TStrategy strategy) - where TStrategy : IKeyStrategy - { - - if (typeof(TK) == typeof(int)) - { - Span keys = node.GetKeys(); - ref TK 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.FindFirstGreater(intKeys, intKey); - } - if (!strategy.UsesPrefixes) - { - return FallbackRoutingKeys(node.GetKeys(), key, strategy); - } - - int index = PrefixScanner.FindFirstGreaterOrEqual(node.Prefixes, keyPrefix); - return RefineRouting(index, node.GetKeys(), key, strategy); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int RefineSearch(int startIndex, ReadOnlySpan keys, TK 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 RefineRouting(int startIndex, ReadOnlySpan keys, TK 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, TK 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, TK 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, TK 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, TK 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, TK key, TStrategy strategy) - where TStrategy : IKeyStrategy - { - int low = 0; - int high = keys.Length - 1; - ref TK keysRef = ref MemoryMarshal.GetReference(keys); - - while (low <= high) - { - int mid = low + ((high - low) >> 1); - TK midKey = Unsafe.Add(ref keysRef, mid); - int cmp = strategy.Compare(midKey, 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, TK key, TStrategy strategy) - where TStrategy : IKeyStrategy - { - int low = 0; - int high = keys.Length - 1; - ref TK keysRef = ref MemoryMarshal.GetReference(keys); - - while (low <= high) - { - int mid = low + ((high - low) >> 1); - TK midKey = Unsafe.Add(ref keysRef, mid); - int cmp = strategy.Compare(midKey, key); - - if (cmp <= 0) low = mid + 1; - else high = mid - 1; - } - return low; - } - - // --------------------------------------------------------- - // Insertion Logic - // --------------------------------------------------------- - - private static void InsertIntoLeaf(LeafNode leaf, int index, TK key, TV value, TStrategy strategy) - where TStrategy : IKeyStrategy - { - int count = leaf.Header.Count; - if (index < count) - { - int moveCount = count - index; - 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, moveCount).CopyTo(leaf.AllPrefixes.Slice(index + 1)); - } - } - - leaf.Keys![index] = key; - leaf.Values[index] = value; - - if (strategy.UsesPrefixes) - { - leaf.AllPrefixes[index] = strategy.GetPrefix(key); - } - - leaf.SetCount(count + 1); - } - - private static (Node, TK) SplitLeaf(LeafNode left, int insertIndex, TK key, TV value, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy - { - var right = new LeafNode(owner, strategy.UsesPrefixes); - int totalCount = left.Header.Count; - - int splitPoint = (insertIndex == totalCount) ? totalCount : (insertIndex == 0 ? 0 : totalCount / 2); - int moveCount = totalCount - splitPoint; - - if (moveCount > 0) - { - left.Keys.AsSpan(splitPoint, moveCount).CopyTo(right.Keys.AsSpan(0)); - left.Values.AsSpan(splitPoint, moveCount).CopyTo(right.Values.AsSpan(0)); - - if (strategy.UsesPrefixes) - { - left.AllPrefixes.Slice(splitPoint, moveCount).CopyTo(right.AllPrefixes); - } - } - - left.SetCount(splitPoint); - right.SetCount(moveCount); - - if (insertIndex < splitPoint || (splitPoint == 0 && insertIndex == 0)) - { - InsertIntoLeaf(left, insertIndex, key, value, strategy); - } - else - { - InsertIntoLeaf(right, insertIndex - splitPoint, key, value, strategy); - } - - return (right, right.Keys![0]); - } - - private static void InsertIntoInternal(InternalNode node, int index, TK separator, Node newChild, TStrategy strategy) - where TStrategy : IKeyStrategy - { - int count = node.Header.Count; - - 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)); - - if (strategy.UsesPrefixes) - { - node.AllPrefixes.Slice(index, moveCount).CopyTo(node.AllPrefixes.Slice(index + 1)); - } - } - - node.Keys[index] = separator; - node.Children[index + 1] = newChild; - - if (strategy.UsesPrefixes) - { - node.AllPrefixes[index] = strategy.GetPrefix(separator); - } - - node.SetCount(count + 1); - } - - private static (Node, TK) SplitInternal(InternalNode left, int insertIndex, TK separator, Node newChild, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy - { - var right = strategy.UsesPrefixes - ? new PrefixInternalNode(owner) - : new InternalNode(owner); - - int count = left.Header.Count; - int splitPoint = count / 2; - TK upKey = left.Keys[splitPoint]; - int moveCount = count - splitPoint - 1; - - if (moveCount > 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); - } - } - - left.SetCount(splitPoint); - right.SetCount(moveCount); - - if (insertIndex <= splitPoint) - { - InsertIntoInternal(left, insertIndex, separator, newChild, strategy); - } - else - { - InsertIntoInternal(right, insertIndex - (splitPoint + 1), separator, newChild, strategy); - } - - return (right, upKey); - } - - // --------------------------------------------------------- - // Removal Logic - // --------------------------------------------------------- - - private static void RemoveFromLeaf(LeafNode leaf, int index, TStrategy strategy) - where TStrategy : IKeyStrategy - { - int count = leaf.Header.Count; - int moveCount = count - index - 1; - - if (moveCount > 0) - { - 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) - { - leaf.AllPrefixes.Slice(index + 1, moveCount).CopyTo(leaf.AllPrefixes.Slice(index)); - } - } - - leaf.SetCount(count - 1); - } - - private static bool HandleUnderflow(InternalNode parent, int childIndex, TStrategy strategy, OwnerId owner) - where TStrategy : IKeyStrategy - { - if (childIndex < parent.Header.Count) - { - var rightSibling = parent.Children[childIndex + 1]!.EnsureEditable(owner); - parent.Children[childIndex + 1] = rightSibling; - var leftChild = parent.Children[childIndex]!; - - if (CanBorrow(rightSibling)) - { - RotateLeft(parent, childIndex, leftChild, rightSibling, strategy); - return false; - } - else - { - Merge(parent, childIndex, leftChild, rightSibling, strategy); - return parent.Header.Count < LeafNode.MergeThreshold; - } - } - else if (childIndex > 0) - { - var leftSibling = parent.Children[childIndex - 1]!.EnsureEditable(owner); - parent.Children[childIndex - 1] = leftSibling; - var rightChild = parent.Children[childIndex]!; - - if (CanBorrow(leftSibling)) - { - RotateRight(parent, childIndex - 1, leftSibling, rightChild, strategy); - return false; - } - else - { - Merge(parent, childIndex - 1, leftSibling, rightChild, strategy); - return parent.Header.Count < LeafNode.MergeThreshold; - } - } - - return true; - } - - private static bool CanBorrow(Node node) - { - return node.Header.Count > 8 + 1; - } - - private static void Merge(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) - where TStrategy : IKeyStrategy - { - if (left.IsLeaf) - { - var leftLeaf = left.AsLeaf(); - var rightLeaf = right.AsLeaf(); - - 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)); - } - - leftLeaf.SetCount(lCount + rCount); - } - else - { - var leftInternal = left.AsInternal(); - var rightInternal = right.AsInternal(); - - TK 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; - 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)); - } - - Span> rightChildren = rightInternal.Children!; - Span> leftChildren = leftInternal.Children!; - rightChildren.Slice(0, rCount + 1).CopyTo(leftChildren.Slice(lCount + 1)); - - leftInternal.SetCount(lCount + 1 + rCount); - } - - int pCount = parent.Header.Count; - int moveCount = pCount - separatorIndex - 1; - - if (moveCount > 0) - { - Span parentKeys = parent.Keys; - parentKeys.Slice(separatorIndex + 1, moveCount).CopyTo(parentKeys.Slice(separatorIndex)); - - if (strategy.UsesPrefixes) - { - parent.AllPrefixes.Slice(separatorIndex + 1, moveCount).CopyTo(parent.AllPrefixes.Slice(separatorIndex)); - } - - 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 - { - if (left.IsLeaf) - { - var leftLeaf = left.AsLeaf(); - var rightLeaf = right.AsLeaf(); - - InsertIntoLeaf(leftLeaf, leftLeaf.Header.Count, rightLeaf.Keys![0], rightLeaf.Values[0], strategy); - RemoveFromLeaf(rightLeaf, 0, strategy); - - 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(); - - TK sep = parent.Keys[separatorIndex]; - InsertIntoInternal(leftInternal, leftInternal.Header.Count, sep, rightInternal.Children[0]!, strategy); - - parent.Keys[separatorIndex] = rightInternal.Keys[0]; - if (strategy.UsesPrefixes) - { - parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]); - } - - int rCount = rightInternal.Header.Count; - - Span> rightChildren = rightInternal.Children!; - rightChildren.Slice(1, rCount).CopyTo(rightChildren); - - if (rCount > 1) - { - Span rightKeys = rightInternal.Keys; - rightKeys.Slice(1, rCount - 1).CopyTo(rightKeys); - - if (strategy.UsesPrefixes) - { - rightInternal.AllPrefixes.Slice(1, rCount - 1).CopyTo(rightInternal.AllPrefixes); - } - } - - rightInternal.SetCount(rCount - 1); - } - } - - private static void RotateRight(InternalNode parent, int separatorIndex, Node left, Node right, TStrategy strategy) - where TStrategy : IKeyStrategy - { - if (left.IsLeaf) - { - var leftLeaf = left.AsLeaf(); - var rightLeaf = right.AsLeaf(); - int last = leftLeaf.Header.Count - 1; - - InsertIntoLeaf(rightLeaf, 0, leftLeaf.Keys![last], leftLeaf.Values[last], strategy); - RemoveFromLeaf(leftLeaf, last, strategy); - - 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(); - int last = leftInternal.Header.Count - 1; - - TK sep = parent.Keys[separatorIndex]; - InsertIntoInternal(rightInternal, 0, sep, leftInternal.Children[last + 1]!, strategy); - - parent.Keys[separatorIndex] = leftInternal.Keys[last]; - if (strategy.UsesPrefixes) - { - parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); - } - - leftInternal.SetCount(last); - } - } - - public static bool TryGetMin(Node root, out TK key, out TV value) - { - var current = root; - while (!current.IsLeaf) - { - current = current.AsInternal().Children[0]!; - } - - var leaf = current.AsLeaf(); - if (leaf.Header.Count == 0) - { - key = default!; - value = default!; - return false; - } - - key = leaf.Keys![0]; - value = leaf.Values[0]; - return true; - } - - public static bool TryGetMax(Node root, out TK key, out TV value) - { - var current = root; - while (!current.IsLeaf) - { - var internalNode = current.AsInternal(); - current = internalNode.Children[internalNode.Header.Count]!; - } - - var leaf = current.AsLeaf(); - if (leaf.Header.Count == 0) - { - key = default!; - value = default!; - return false; - } - - int last = leaf.Header.Count - 1; - key = leaf.Keys![last]; - value = leaf.Values[last]; - return true; - } - - public static bool TryGetSuccessor(Node root, TK key, TStrategy strategy, out TK nextKey, out TV nextValue) - where TStrategy : IKeyStrategy - { - 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) - { - var internalNode = current.AsInternal(); - int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy); - path[depth] = internalNode; - indices[depth] = idx; - depth++; - current = internalNode.Children[idx]!; - } - - 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) - { - nextKey = leaf.Keys![index]; - nextValue = leaf.Values[index]; - return true; - } - - for (int i = depth - 1; i >= 0; i--) - { - if (indices[i] < path[i].Header.Count) - { - current = path[i].Children[indices[i] + 1]!; - while (!current.IsLeaf) - { - current = current.AsInternal().Children[0]!; - } - - var targetLeaf = current.AsLeaf(); - nextKey = targetLeaf.Keys![0]; - nextValue = targetLeaf.Values[0]; - return true; - } - } - - nextKey = default!; - nextValue = default!; - return false; - } - - public static bool TryGetPredecessor(Node root, TK key, TStrategy strategy, out TK prevKey, out TV prevValue) - where TStrategy : IKeyStrategy - { - 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) - { - var internalNode = current.AsInternal(); - int idx = FindRoutingIndex(internalNode, key, keyPrefix, strategy); - path[depth] = internalNode; - indices[depth] = idx; - depth++; - current = internalNode.Children[idx]!; - } - - var leaf = current.AsLeaf(); - int index = FindIndex(leaf, key, keyPrefix, strategy); - - if (index > 0) - { - prevKey = leaf.Keys![index - 1]; - prevValue = leaf.Values[index - 1]; - return true; - } - - for (int i = depth - 1; i >= 0; i--) - { - if (indices[i] > 0) - { - current = path[i].Children[indices[i] - 1]!; - while (!current.IsLeaf) - { - var internalNode = current.AsInternal(); - current = internalNode.Children[internalNode.Header.Count]!; - } - - var targetLeaf = current.AsLeaf(); - int last = targetLeaf.Header.Count - 1; - prevKey = targetLeaf.Keys![last]; - prevValue = targetLeaf.Values[last]; - return true; - } - } - - prevKey = default!; - prevValue = default!; - return false; - } - } -} diff --git a/PersistentOrderedMap/KeyStrategies.cs b/PersistentOrderedMap/KeyStrategies.cs deleted file mode 100644 index 9ef540d..0000000 --- a/PersistentOrderedMap/KeyStrategies.cs +++ /dev/null @@ -1,63 +0,0 @@ - -namespace PersistentOrderedMap; - -using System; -using System.Buffers.Binary; -using System.Runtime.CompilerServices; - -public interface IKeyStrategy -{ - int Compare(TK x, TK y); - long GetPrefix(TK key); - - bool UsesPrefixes => true; - - bool IsLossless => false; - bool UseBinarySearch => false; -} - - - - -public struct UnicodeStrategy : IKeyStrategy -{ - - public bool UsesPrefixes => true; - public bool UseBinarySearch => false; - public bool IsLossLess => false; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(string? x, string? y) => string.CompareOrdinal(x, y); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetPrefix(string key) - { - if (string.IsNullOrEmpty(key)) return long.MinValue; - - // 1. Prepare Buffer (8 bytes) - // stackalloc is virtually free (pointer bump) - Span utf8Bytes = stackalloc byte[8]; - - // 2. Transcode (The "Safe" Magic) - // This intrinsic handles ASCII efficiently and converts Surrogates/Chinese - // into bytes that maintain the correct "Magnitude" (Sort Order). - // Invalid surrogates become 0xEF (Replacement Char), which sorts > ASCII. - System.Text.Unicode.Utf8.FromUtf16( - key.AsSpan(0, Math.Min(key.Length, 8)), - utf8Bytes, - out _, - out _, - replaceInvalidSequences: true); // True ensures we get 0xEF for broken chars - - // 3. Load as Big Endian Long - long packed = BinaryPrimitives.ReadInt64BigEndian(utf8Bytes); - - // 4. Sign Toggle - // Maps the byte range 0x00..0xFF to the signed long range Min..Max - // Essential for the < and > operators to work correctly. - return packed ^ unchecked((long)0x8080808080808080); - } - -} - - - diff --git a/PersistentOrderedMap/KeyStrategies/ComparableStrategy.cs b/PersistentOrderedMap/KeyStrategies/ComparableStrategy.cs deleted file mode 100644 index c41acab..0000000 --- a/PersistentOrderedMap/KeyStrategies/ComparableStrategy.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace PersistentOrderedMap; - -using System.Runtime.CompilerServices; - -// This is a comparable strategy that may squeeze some extra time out of value types - -public readonly struct ComparableStrategy : IKeyStrategy where TK : IComparable -{ - public bool UsesPrefixes => false; - public bool UseBinarySearch => true; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetPrefix(TK key) => 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(TK x, TK y) => x.CompareTo(y); -} diff --git a/PersistentOrderedMap/KeyStrategies/DoubleStrategy.cs b/PersistentOrderedMap/KeyStrategies/DoubleStrategy.cs deleted file mode 100644 index 627c9a8..0000000 --- a/PersistentOrderedMap/KeyStrategies/DoubleStrategy.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace PersistentOrderedMap; -using System.Runtime.CompilerServices; - -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); - } -} - diff --git a/PersistentOrderedMap/KeyStrategies/IntScanner.cs b/PersistentOrderedMap/KeyStrategies/IntScanner.cs deleted file mode 100644 index 97afa3c..0000000 --- a/PersistentOrderedMap/KeyStrategies/IntScanner.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; - -namespace PersistentOrderedMap; - -public static class IntScanner -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int FindFirstGreaterOrEqual(ReadOnlySpan keys, int target) - { - // Fallback for short arrays or unsupported hardware. - // AVX2 processes 8 integers at a time. - if (!Avx2.IsSupported || keys.Length < 8) - return LinearScan(keys, target); - - return Avx512F.IsSupported - ? ScanAvx512(keys, target) - : ScanAvx2(keys, target); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int LinearScan(ReadOnlySpan keys, int target) - { - for (var i = 0; i < keys.Length; i++) - if (keys[i] >= target) - return i; - return keys.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int ScanAvx2(ReadOnlySpan keys, int target) - { - // AVX2 lacks a native GreaterOrEqual for 32-bit integers. - // We use GreaterThan(Data, target - 1). - var vTarget = Vector256.Create(target - 1); - var i = 0; - var len = keys.Length; - - for (; i <= len - 8; i += 8) - { - fixed (int* ptr = keys) - { - var vData = Avx2.LoadVector256(ptr + i); - var vResult = Avx2.CompareGreaterThan(vData, vTarget); - - // MoveMask creates a 32-bit integer from the most significant bit of each byte. - var mask = (uint)Avx2.MoveMask(vResult.AsByte()); - - if (mask != 0) - { - // Since an int is 4 bytes, MoveMask sets 4 bits per matching element. - // Dividing the trailing zero count by 4 maps the byte offset back to the integer index. - return i + (BitOperations.TrailingZeroCount(mask) / 4); - } - } - } - - return LinearScan(keys.Slice(i), target) + i; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int ScanAvx512(ReadOnlySpan keys, int target) - { - // AVX-512 processes 16 integers (512 bits) per instruction. - var vTarget = Vector512.Create(target); - var i = 0; - var len = keys.Length; - - for (; i <= len - 16; i += 16) - { - fixed (int* ptr = keys) - { - var vData = Avx512F.LoadVector512(ptr + i); - - // Vector512 API is used directly here to cleanly get the mask - var mask = Vector512.GreaterThanOrEqual(vData, vTarget); - - if (mask != Vector512.Zero) - { - uint m = (uint)mask.ExtractMostSignificantBits(); - return i + BitOperations.TrailingZeroCount(m); - } - } - } - - return LinearScan(keys.Slice(i), target) + i; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int FindFirstGreater(ReadOnlySpan keys, int target) - { - if (!Avx2.IsSupported || keys.Length < 8) - return LinearScanGreater(keys, target); - - return Avx512F.IsSupported - ? ScanAvx512Greater(keys, target) - : ScanAvx2Greater(keys, target); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int LinearScanGreater(ReadOnlySpan keys, int target) - { - for (var i = 0; i < keys.Length; i++) - if (keys[i] > target) - return i; - return keys.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int ScanAvx2Greater(ReadOnlySpan keys, int target) - { - // For > target, AVX2 CompareGreaterThan works directly without the (target - 1) offset - var vTarget = Vector256.Create(target); - var i = 0; - var len = keys.Length; - - for (; i <= len - 8; i += 8) - { - fixed (int* ptr = keys) - { - var vData = Avx2.LoadVector256(ptr + i); - var vResult = Avx2.CompareGreaterThan(vData, vTarget); - - var mask = (uint)Avx2.MoveMask(vResult.AsByte()); - - if (mask != 0) - { - return i + (BitOperations.TrailingZeroCount(mask) / 4); - } - } - } - - return LinearScanGreater(keys.Slice(i), target) + i; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int ScanAvx512Greater(ReadOnlySpan keys, int target) - { - var vTarget = Vector512.Create(target); - var i = 0; - var len = keys.Length; - - for (; i <= len - 16; i += 16) - { - fixed (int* ptr = keys) - { - var vData = Avx512F.LoadVector512(ptr + i); - - // Use GreaterThan instead of GreaterThanOrEqual - var mask = Vector512.GreaterThan(vData, vTarget); - - if (mask != Vector512.Zero) - { - uint m = (uint)mask.ExtractMostSignificantBits(); - return i + BitOperations.TrailingZeroCount(m); - } - } - } - - return LinearScanGreater(keys.Slice(i), target) + i; - } -} - - - - diff --git a/PersistentOrderedMap/KeyStrategies/IntStrategy.cs b/PersistentOrderedMap/KeyStrategies/IntStrategy.cs deleted file mode 100644 index 5e29db0..0000000 --- a/PersistentOrderedMap/KeyStrategies/IntStrategy.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PersistentOrderedMap; - -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/PersistentOrderedMap/KeyStrategies/PrefixScanner.cs b/PersistentOrderedMap/KeyStrategies/PrefixScanner.cs deleted file mode 100644 index 9364998..0000000 --- a/PersistentOrderedMap/KeyStrategies/PrefixScanner.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace PersistentOrderedMap; - -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; // For AVX2 -using System.Numerics; -/// -/// Helper for SIMD accelerated prefix scanning. -/// -public static class PrefixScanner -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int FindFirstGreaterOrEqual(ReadOnlySpan prefixes, long targetPrefix) - { - - // 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/PersistentOrderedMap/KeyStrategies/StandardStrategy.cs b/PersistentOrderedMap/KeyStrategies/StandardStrategy.cs deleted file mode 100644 index 996c808..0000000 --- a/PersistentOrderedMap/KeyStrategies/StandardStrategy.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace PersistentOrderedMap; - -using System.Runtime.CompilerServices; -/// -/// 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() - { - _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; - public bool UseBinarySearch => true; - // This will never be called because UsesPrefixes is false, - // but we must satisfy the interface. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetPrefix(TK key) => 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(TK x, TK y) - { - return _comparer.Compare(x, y); - } -} -public readonly struct StandardStrategy2 : IKeyStrategy - where TComparer : struct, IComparer - { - private readonly TComparer _comparer; - - public StandardStrategy2(TComparer comparer) => _comparer = comparer; - - public bool UsesPrefixes => false; - public bool UseBinarySearch => true; - -[MethodImpl(MethodImplOptions.AggressiveInlining)] -public int Compare(TK x, TK y) => _comparer.Compare(x, y); - - public long GetPrefix(TK key) => 0; - } - diff --git a/PersistentOrderedMap/PersistentOrderedMap.cs b/PersistentOrderedMap/PersistentOrderedMap.cs deleted file mode 100644 index 9731141..0000000 --- a/PersistentOrderedMap/PersistentOrderedMap.cs +++ /dev/null @@ -1,43 +0,0 @@ - -namespace PersistentOrderedMap; - -public sealed class PersistentOrderedMap : BaseOrderedMap where TStrategy : IKeyStrategy -{ - internal PersistentOrderedMap(Node root, TStrategy strategy, int count) - : base(root, strategy, count) { } - - // --------------------------------------------------------- - // Immutable Write API (Returns new Map) - // --------------------------------------------------------- - public PersistentOrderedMap Set(TK key, TV value) - { - // OPTIMIZATION: Use OwnerId.None (0). - // This signals EnsureEditable to always copy the root path, - // producing a new tree of nodes that also have OwnerId.None. - var newRoot = BTreeFunctions.Set(Root, key, value, Strategy, OwnerId.None, out bool countChanged); - return new PersistentOrderedMap(newRoot, Strategy, countChanged ? Count + 1 : Count); - } - - public static PersistentOrderedMap Empty(TStrategy strategy) - { - // Create an empty Leaf Node. - // 'default(OwnerId)' (usually 0) marks this node as Immutable/Persistent. - // This ensures that any subsequent Set/Remove will clone this node - // instead of modifying it in place. - var emptyRoot = new LeafNode(default(OwnerId), strategy.UsesPrefixes); - - return new PersistentOrderedMap(emptyRoot, strategy, 0); - } - - public PersistentOrderedMap Remove(TK key) - { - var newRoot = BTreeFunctions.Remove(Root, key, Strategy, OwnerId.None, out bool removed); - if (!removed) return this; - return new PersistentOrderedMap(newRoot, Strategy, Count - 1); - } - - public TransientOrderedMap ToTransient() - { - return new TransientOrderedMap(Root, Strategy, Count); - } -} diff --git a/PersistentOrderedMap/Readme.org b/PersistentOrderedMap/Readme.org deleted file mode 100644 index b79672c..0000000 --- a/PersistentOrderedMap/Readme.org +++ /dev/null @@ -1,623 +0,0 @@ -* PersistentMap - -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. The primary use case is for when editing operations are in bulk. Updating single elements many times will fail to distinguish this collection from other more mature collections. Bulk writes are very fast using transient interfaces, random reads are fastish depending on key type and entropy, Sequential reads and min/max queries are very fast. - -** Features -- *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 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, although might benefit from a usage of binary search in the future. - -** Quick Start - -*** 1. Basic Immutable Usage -By default, the map is immutable. Every write operation returns a new, updated version of the map. - -#+begin_src csharp -// Create a map with a specific key strategy (e.g., Int, Unicode, Double) -var map1 = BaseOrderedMap.Create(new IntStrategy()); - -// Set returns a new tree instance. map1 remains empty. -var map2 = map1.Set(1, "Apple") - .Set(2, "Banana") - .Set(3, "Cherry"); - -if (map2.TryGetValue(2, out var value)) -{ - Console.WriteLine(value); // "Banana" -} -#+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. 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()); - -// Mutates in-place. No allocations for unchanged tree paths. -for (int i = 0; i < 10_000; i++) -{ - transientMap.Set(i, $"Value_{i}"); -} - -// O(1) freeze. Returns a thread-safe immutable PersistentMap. -var persistentSnapshot = transientMap.ToPersistent(); -#+end_src - -*** 3. Range Queries and Iteration -Because it is a B+ tree range queries require zero allocations and simply walk the leaves. - -#+begin_src csharp -var map = GetPopulatedMap(); - -// Iterate exact bounds -foreach (var kvp in map.Range(min: 10, max: 50)) -{ - Console.WriteLine($"{kvp.Key}: {kvp.Value}"); -} - -// Open-ended queries -var greaterThan100 = map.From(100); -var lessThan50 = map.Until(50); -var allElements = map.AsEnumerable(); -#+end_sr - -*** 4. Tree Navigation -Find bounds and adjacent elements instantly. Missing keys will correctly resolve to the mathematical lower/upper bound. - -#+begin_src csharp -// Get extremes -map.TryGetMin(out int minKey, out string minVal); -map.TryGetMax(out int maxKey, out string maxVal); - -// Get the immediate next/previous element (works even if '42' doesn't exist) -if (map.TryGetSuccessor(42, out int nextKey, out string nextVal)) -{ - Console.WriteLine($"The key immediately after 42 is {nextKey}"); -} - -if (map.TryGetPredecessor(42, out int prevKey, out string prevVal)) -{ - Console.WriteLine($"The key immediately before 42 is {prevKey}"); -} -#+end_src - -*** 5. Set Operations -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); -var mapB = CreateMap(3, 4, 5, 6); - -// Returns { 3, 4 } -var common = mapA.Intersect(mapB); - -// Returns { 1, 2 } -var onlyInA = mapA.Except(mapB); - -// Returns { 1, 2, 5, 6 } -var symmetricDiff = mapA.SymmetricExcept(mapB); -#+end_src - -** Benchmarks -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. - -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. - -The update benchmarks updates a subset of the keys in random order. - -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_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 - -* 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 a binary search) 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 | StringLength | Mean | Gen0 | Gen1 | Gen2 | Allocated | -|----------------------------------|------------|--------------|---------------------:|-------------:|-------------:|---------:|--------------:| -| **Build_TransientMap_Standard** | **100** | **8** | **5,170.76 ns** | **0.7401** | **0.0076** | **-** | **6200 B** | -| Build_TransientMap_Unicode | 100 | 8 | 12,251.17 ns | 0.8850 | 0.0153 | - | 7528 B | -| Build_ImmDict | 100 | 8 | 13,926.78 ns | 5.3253 | 0.0610 | - | 44640 B | -| Build_ImmSortedDict | 100 | 8 | 21,789.13 ns | 4.2114 | 0.0305 | - | 35472 B | -| Build_ExtMap | 100 | 8 | 22,791.98 ns | 5.6152 | 0.0610 | - | 47104 B | -| Build_ExtHashMap | 100 | 8 | 9,993.13 ns | 4.0894 | 0.0305 | - | 34216 B | -| Retrieve_ImmDict | 100 | 8 | 78.83 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 100 | 8 | 105.97 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 100 | 8 | 149.07 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 100 | 8 | 1,203.47 ns | - | - | - | - | -| Retrieve_ExtMap | 100 | 8 | 1,297.20 ns | - | - | - | - | -| Retrieve_ExtHashMap | 100 | 8 | 189.50 ns | - | - | - | - | -| Update_ImmDict | 100 | 8 | 1,343.93 ns | 0.5207 | 0.0019 | - | 4368 B | -| Update_PersistentMap_Standard | 100 | 8 | 1,593.27 ns | 2.3994 | 0.0401 | - | 20080 B | -| Update_PersistentMap_Unicode | 100 | 8 | 1,913.07 ns | 2.7046 | 0.0496 | - | 22640 B | -| Update_TransientMap_Standard | 100 | 8 | 520.49 ns | 0.4339 | 0.0048 | - | 3632 B | -| Update_TransientMap_Unicode | 100 | 8 | 690.74 ns | 0.4644 | 0.0057 | - | 3888 B | -| Update_ImmSortedDict | 100 | 8 | 1,748.06 ns | 0.3662 | - | - | 3072 B | -| Update_ExtMap | 100 | 8 | 1,793.32 ns | 0.4120 | - | - | 3456 B | -| Update_ExtHashMap | 100 | 8 | 754.97 ns | 0.5264 | 0.0010 | - | 4408 B | -| UpdateSet_ImmDict | 100 | 8 | 1,361.32 ns | 0.5207 | 0.0019 | - | 4368 B | -| UpdateSet_PersistentMap_Standard | 100 | 8 | 1,838.15 ns | 2.3994 | 0.0420 | - | 20080 B | -| UpdateSet_PersistentMap_Unicode | 100 | 8 | 2,427.91 ns | 2.7046 | 0.0534 | - | 22640 B | -| UpdateSet_TransientMap_Standard | 100 | 8 | 711.17 ns | 0.4339 | 0.0048 | - | 3632 B | -| UpdateSet_TransientMap_Unicode | 100 | 8 | 1,247.32 ns | 0.4635 | 0.0057 | - | 3888 B | -| UpdateSet_ImmSortedDict | 100 | 8 | 2,399.76 ns | 0.4578 | - | - | 3840 B | -| UpdateSet_ExtMap | 100 | 8 | 2,202.70 ns | 0.4730 | - | - | 3960 B | -| UpdateSet_ExtHashMap | 100 | 8 | 773.22 ns | 0.5274 | 0.0019 | - | 4416 B | -| Iterate_ImmDict | 100 | 8 | 1,335.18 ns | - | - | - | - | -| Iterate_PersistentMap_Standard | 100 | 8 | 189.19 ns | - | - | - | - | -| Iterate_ImmSortedDict | 100 | 8 | 485.69 ns | - | - | - | - | -| Iterate_ExtMap | 100 | 8 | 327.67 ns | 0.0038 | - | - | 32 B | -| Iterate_ExtHashMap | 100 | 8 | 1,089.89 ns | 0.2480 | - | - | 2088 B | -| Iterate_PersistentMap_Unicode | 100 | 8 | 187.62 ns | - | - | - | - | -| Remove_ImmDict | 100 | 8 | 1,272.43 ns | 0.5131 | - | - | 4304 B | -| Remove_PersistentMap_Standard | 100 | 8 | 1,969.67 ns | 2.3689 | 0.0381 | - | 19840 B | -| Remove_PersistentMap_Unicode | 100 | 8 | 2,487.99 ns | 2.6779 | 0.0496 | - | 22400 B | -| Remove_TransientMap_Standard | 100 | 8 | 836.95 ns | 0.4101 | 0.0048 | - | 3432 B | -| Remove_TransientMap_Unicode | 100 | 8 | 1,333.99 ns | 0.4406 | 0.0057 | - | 3688 B | -| Remove_ImmSortedDict | 100 | 8 | 1,946.19 ns | 0.3777 | - | - | 3168 B | -| Remove_ExtMap | 100 | 8 | 2,221.31 ns | 0.5798 | - | - | 4856 B | -| Remove_ExtHashMap | 100 | 8 | 770.43 ns | 0.4721 | 0.0019 | - | 3952 B | -| **Build_TransientMap_Standard** | **100** | **50** | **5,216.44 ns** | **0.7401** | **0.0076** | **-** | **6200 B** | -| Build_TransientMap_Unicode | 100 | 50 | 11,944.76 ns | 0.8850 | 0.0153 | - | 7528 B | -| Build_ImmDict | 100 | 50 | 15,771.58 ns | 5.3101 | 0.0610 | - | 44576 B | -| Build_ImmSortedDict | 100 | 50 | 22,171.62 ns | 4.2114 | 0.0305 | - | 35232 B | -| Build_ExtMap | 100 | 50 | 27,705.49 ns | 5.6458 | 0.0610 | - | 47328 B | -| Build_ExtHashMap | 100 | 50 | 15,207.38 ns | 4.4861 | 0.0305 | - | 37672 B | -| Retrieve_ImmDict | 100 | 50 | 286.21 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 100 | 50 | 103.79 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 100 | 50 | 151.04 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 100 | 50 | 1,317.60 ns | - | - | - | - | -| Retrieve_ExtMap | 100 | 50 | 1,162.96 ns | - | - | - | - | -| Retrieve_ExtHashMap | 100 | 50 | 412.76 ns | - | - | - | - | -| Update_ImmDict | 100 | 50 | 1,359.95 ns | 0.4368 | - | - | 3664 B | -| Update_PersistentMap_Standard | 100 | 50 | 1,583.22 ns | 2.3994 | 0.0420 | - | 20080 B | -| Update_PersistentMap_Unicode | 100 | 50 | 1,861.53 ns | 2.7065 | 0.0515 | - | 22640 B | -| Update_TransientMap_Standard | 100 | 50 | 543.81 ns | 0.4339 | 0.0048 | - | 3632 B | -| Update_TransientMap_Unicode | 100 | 50 | 633.60 ns | 0.4644 | 0.0057 | - | 3888 B | -| Update_ImmSortedDict | 100 | 50 | 1,986.02 ns | 0.4120 | - | - | 3456 B | -| Update_ExtMap | 100 | 50 | 1,892.16 ns | 0.4253 | - | - | 3568 B | -| Update_ExtHashMap | 100 | 50 | 1,088.83 ns | 0.4997 | - | - | 4184 B | -| UpdateSet_ImmDict | 100 | 50 | 1,799.24 ns | 0.5817 | 0.0019 | - | 4880 B | -| UpdateSet_PersistentMap_Standard | 100 | 50 | 1,847.71 ns | 2.3994 | 0.0401 | - | 20080 B | -| UpdateSet_PersistentMap_Unicode | 100 | 50 | 2,510.91 ns | 2.7046 | 0.0496 | - | 22640 B | -| UpdateSet_TransientMap_Standard | 100 | 50 | 754.14 ns | 0.4339 | 0.0048 | - | 3632 B | -| UpdateSet_TransientMap_Unicode | 100 | 50 | 1,274.76 ns | 0.4635 | 0.0057 | - | 3888 B | -| UpdateSet_ImmSortedDict | 100 | 50 | 2,463.36 ns | 0.4730 | - | - | 3984 B | -| UpdateSet_ExtMap | 100 | 50 | 2,364.62 ns | 0.5913 | - | - | 4968 B | -| UpdateSet_ExtHashMap | 100 | 50 | 1,026.26 ns | 0.5074 | - | - | 4248 B | -| Iterate_ImmDict | 100 | 50 | 1,223.85 ns | - | - | - | - | -| Iterate_PersistentMap_Standard | 100 | 50 | 187.31 ns | - | - | - | - | -| Iterate_ImmSortedDict | 100 | 50 | 484.33 ns | - | - | - | - | -| Iterate_ExtMap | 100 | 50 | 358.22 ns | 0.0038 | - | - | 32 B | -| Iterate_ExtHashMap | 100 | 50 | 1,149.01 ns | 0.2575 | - | - | 2160 B | -| Iterate_PersistentMap_Unicode | 100 | 50 | 187.47 ns | - | - | - | - | -| Remove_ImmDict | 100 | 50 | 1,589.38 ns | 0.5283 | 0.0019 | - | 4432 B | -| Remove_PersistentMap_Standard | 100 | 50 | 1,976.63 ns | 2.3689 | 0.0381 | - | 19840 B | -| Remove_PersistentMap_Unicode | 100 | 50 | 2,568.69 ns | 2.6779 | 0.0458 | - | 22400 B | -| Remove_TransientMap_Standard | 100 | 50 | 839.46 ns | 0.4101 | 0.0048 | - | 3432 B | -| Remove_TransientMap_Unicode | 100 | 50 | 1,399.50 ns | 0.4406 | 0.0057 | - | 3688 B | -| Remove_ImmSortedDict | 100 | 50 | 2,069.17 ns | 0.3891 | - | - | 3264 B | -| Remove_ExtMap | 100 | 50 | 2,124.59 ns | 0.4387 | - | - | 3680 B | -| Remove_ExtHashMap | 100 | 50 | 1,029.42 ns | 0.5112 | - | - | 4288 B | -| **Build_TransientMap_Standard** | **1000** | **8** | **102,292.92 ns** | **5.7373** | **0.3662** | **-** | **48592 B** | -| Build_TransientMap_Unicode | 1000 | 8 | 172,854.88 ns | 7.3242 | 0.7324 | - | 62248 B | -| Build_ImmDict | 1000 | 8 | 247,732.93 ns | 79.1016 | 8.3008 | - | 662016 B | -| Build_ImmSortedDict | 1000 | 8 | 429,391.31 ns | 61.0352 | 4.8828 | - | 513312 B | -| Build_ExtMap | 1000 | 8 | 416,823.27 ns | 79.1016 | 7.3242 | - | 662448 B | -| Build_ExtHashMap | 1000 | 8 | 154,417.86 ns | 70.8008 | 4.6387 | - | 592920 B | -| Retrieve_ImmDict | 1000 | 8 | 1,043.45 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 1000 | 8 | 1,783.56 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 1000 | 8 | 2,026.44 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 1000 | 8 | 21,724.33 ns | - | - | - | - | -| Retrieve_ExtMap | 1000 | 8 | 24,401.52 ns | - | - | - | - | -| Retrieve_ExtHashMap | 1000 | 8 | 2,472.28 ns | - | - | - | - | -| Update_ImmDict | 1000 | 8 | 19,547.84 ns | 7.2937 | 0.2747 | - | 61024 B | -| Update_PersistentMap_Standard | 1000 | 8 | 19,846.20 ns | 23.9868 | 2.2278 | - | 200800 B | -| Update_PersistentMap_Unicode | 1000 | 8 | 27,309.69 ns | 27.0386 | 2.5330 | - | 226400 B | -| Update_TransientMap_Standard | 1000 | 8 | 6,888.79 ns | 4.3945 | 0.4501 | - | 36768 B | -| Update_TransientMap_Unicode | 1000 | 8 | 7,801.38 ns | 4.4250 | 0.4425 | - | 37024 B | -| Update_ImmSortedDict | 1000 | 8 | 32,192.51 ns | 5.7373 | 0.1221 | - | 48288 B | -| Update_ExtMap | 1000 | 8 | 31,972.71 ns | 6.4087 | 0.1831 | - | 53824 B | -| Update_ExtHashMap | 1000 | 8 | 10,476.16 ns | 7.2021 | 0.2289 | - | 60272 B | -| UpdateSet_ImmDict | 1000 | 8 | 22,622.37 ns | 8.2703 | 0.3967 | - | 69280 B | -| UpdateSet_PersistentMap_Standard | 1000 | 8 | 22,274.40 ns | 23.9868 | 2.3499 | - | 200800 B | -| UpdateSet_PersistentMap_Unicode | 1000 | 8 | 32,897.61 ns | 27.0386 | 2.6855 | - | 226400 B | -| UpdateSet_TransientMap_Standard | 1000 | 8 | 8,218.75 ns | 4.5624 | 0.4272 | - | 38176 B | -| UpdateSet_TransientMap_Unicode | 1000 | 8 | 12,847.07 ns | 4.5929 | 0.4883 | - | 38432 B | -| UpdateSet_ImmSortedDict | 1000 | 8 | 37,683.95 ns | 6.3477 | 0.2441 | - | 53232 B | -| UpdateSet_ExtMap | 1000 | 8 | 38,195.16 ns | 7.6904 | 0.3052 | - | 64576 B | -| UpdateSet_ExtHashMap | 1000 | 8 | 14,327.00 ns | 7.7057 | 0.2747 | - | 64480 B | -| Iterate_ImmDict | 1000 | 8 | 12,971.77 ns | - | - | - | - | -| Iterate_PersistentMap_Standard | 1000 | 8 | 1,615.06 ns | - | - | - | - | -| Iterate_ImmSortedDict | 1000 | 8 | 4,905.20 ns | - | - | - | - | -| Iterate_ExtMap | 1000 | 8 | 3,282.20 ns | 0.0038 | - | - | 32 B | -| Iterate_ExtHashMap | 1000 | 8 | 14,214.03 ns | 2.6093 | - | - | 21888 B | -| Iterate_PersistentMap_Unicode | 1000 | 8 | 1,644.30 ns | - | - | - | - | -| Remove_ImmDict | 1000 | 8 | 19,504.24 ns | 7.5989 | 0.3052 | - | 63648 B | -| Remove_PersistentMap_Standard | 1000 | 8 | 24,227.34 ns | 23.7122 | 2.0752 | - | 198400 B | -| Remove_PersistentMap_Unicode | 1000 | 8 | 29,622.70 ns | 26.7639 | 2.4414 | - | 224000 B | -| Remove_TransientMap_Standard | 1000 | 8 | 9,476.68 ns | 3.7689 | 0.3510 | - | 31592 B | -| Remove_TransientMap_Unicode | 1000 | 8 | 14,237.22 ns | 3.7994 | 0.3662 | - | 31848 B | -| Remove_ImmSortedDict | 1000 | 8 | 33,197.78 ns | 5.6763 | 0.1221 | - | 47616 B | -| Remove_ExtMap | 1000 | 8 | 39,241.19 ns | 6.8970 | 0.1831 | - | 57744 B | -| Remove_ExtHashMap | 1000 | 8 | 11,374.77 ns | 7.5378 | 0.2136 | - | 63056 B | -| **Build_TransientMap_Standard** | **1000** | **50** | **104,538.75 ns** | **5.4932** | **0.3662** | **-** | **46784 B** | -| Build_TransientMap_Unicode | 1000 | 50 | 175,661.70 ns | 7.0801 | 0.4883 | - | 59368 B | -| Build_ImmDict | 1000 | 50 | 278,290.76 ns | 79.5898 | 8.7891 | - | 669696 B | -| Build_ImmSortedDict | 1000 | 50 | 417,827.13 ns | 61.0352 | 5.3711 | - | 511344 B | -| Build_ExtMap | 1000 | 50 | 409,994.29 ns | 78.1250 | 7.3242 | - | 656288 B | -| Build_ExtHashMap | 1000 | 50 | 181,354.06 ns | 70.3125 | 4.6387 | - | 588328 B | -| Retrieve_ImmDict | 1000 | 50 | 3,132.97 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 1000 | 50 | 1,794.95 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 1000 | 50 | 2,065.82 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 1000 | 50 | 21,834.49 ns | - | - | - | - | -| Retrieve_ExtMap | 1000 | 50 | 23,236.99 ns | - | - | - | - | -| Retrieve_ExtHashMap | 1000 | 50 | 4,931.32 ns | - | - | - | - | -| Update_ImmDict | 1000 | 50 | 22,234.72 ns | 7.5073 | 0.3052 | - | 62816 B | -| Update_PersistentMap_Standard | 1000 | 50 | 20,313.66 ns | 23.9868 | 2.0142 | - | 200800 B | -| Update_PersistentMap_Unicode | 1000 | 50 | 27,829.75 ns | 27.0386 | 2.2888 | - | 226400 B | -| Update_TransientMap_Standard | 1000 | 50 | 6,827.30 ns | 4.0588 | 0.4044 | - | 33952 B | -| Update_TransientMap_Unicode | 1000 | 50 | 7,654.44 ns | 4.0894 | 0.3967 | - | 34208 B | -| Update_ImmSortedDict | 1000 | 50 | 32,637.97 ns | 5.6763 | 0.1221 | - | 47952 B | -| Update_ExtMap | 1000 | 50 | 32,523.61 ns | 6.5308 | 0.1831 | - | 54720 B | -| Update_ExtHashMap | 1000 | 50 | 12,993.79 ns | 7.2479 | 0.2441 | - | 60720 B | -| UpdateSet_ImmDict | 1000 | 50 | 25,835.15 ns | 8.3313 | 0.3967 | - | 69728 B | -| UpdateSet_PersistentMap_Standard | 1000 | 50 | 22,970.95 ns | 24.1089 | 2.2278 | - | 201704 B | -| UpdateSet_PersistentMap_Unicode | 1000 | 50 | 34,226.66 ns | 27.2217 | 2.6245 | - | 227840 B | -| UpdateSet_TransientMap_Standard | 1000 | 50 | 7,701.61 ns | 4.3335 | 0.4578 | - | 36264 B | -| UpdateSet_TransientMap_Unicode | 1000 | 50 | 13,115.82 ns | 4.4250 | 0.4578 | - | 37056 B | -| UpdateSet_ImmSortedDict | 1000 | 50 | 37,636.40 ns | 6.2866 | 0.1831 | - | 53088 B | -| UpdateSet_ExtMap | 1000 | 50 | 38,985.88 ns | 7.8735 | 0.3052 | - | 66200 B | -| UpdateSet_ExtHashMap | 1000 | 50 | 17,008.89 ns | 7.6294 | 0.2441 | - | 63936 B | -| Iterate_ImmDict | 1000 | 50 | 13,396.48 ns | - | - | - | - | -| Iterate_PersistentMap_Standard | 1000 | 50 | 1,626.01 ns | - | - | - | - | -| Iterate_ImmSortedDict | 1000 | 50 | 4,912.18 ns | - | - | - | - | -| Iterate_ExtMap | 1000 | 50 | 3,126.26 ns | 0.0038 | - | - | 32 B | -| Iterate_ExtHashMap | 1000 | 50 | 14,857.12 ns | 2.7924 | - | - | 23472 B | -| Iterate_PersistentMap_Unicode | 1000 | 50 | 1,654.97 ns | - | - | - | - | -| Remove_ImmDict | 1000 | 50 | 22,385.84 ns | 7.6904 | 0.3052 | - | 64352 B | -| Remove_PersistentMap_Standard | 1000 | 50 | 25,269.13 ns | 23.7122 | 1.9531 | - | 198400 B | -| Remove_PersistentMap_Unicode | 1000 | 50 | 30,307.99 ns | 26.7639 | 2.2583 | - | 224000 B | -| Remove_TransientMap_Standard | 1000 | 50 | 9,482.18 ns | 3.2654 | 0.2747 | - | 27368 B | -| Remove_TransientMap_Unicode | 1000 | 50 | 13,754.52 ns | 3.2959 | 0.2899 | - | 27624 B | -| Remove_ImmSortedDict | 1000 | 50 | 32,695.64 ns | 5.6763 | 0.1221 | - | 47664 B | -| Remove_ExtMap | 1000 | 50 | 37,495.23 ns | 6.9580 | 0.2441 | - | 58640 B | -| Remove_ExtHashMap | 1000 | 50 | 14,713.28 ns | 7.9193 | 0.2594 | - | 66264 B | -| **Build_TransientMap_Standard** | **10000** | **8** | **1,680,964.28 ns** | **52.7344** | **15.6250** | **-** | **452352 B** | -| Build_TransientMap_Unicode | 10000 | 8 | 2,275,404.91 ns | 66.4063 | 19.5313 | - | 576584 B | -| Build_ImmDict | 10000 | 8 | 4,364,880.86 ns | 1046.8750 | 507.8125 | - | 8766016 B | -| Build_ImmSortedDict | 10000 | 8 | 6,551,472.85 ns | 804.6875 | 281.2500 | - | 6767232 B | -| Build_ExtMap | 10000 | 8 | 6,411,766.35 ns | 1015.6250 | 437.5000 | - | 8542480 B | -| Build_ExtHashMap | 10000 | 8 | 1,913,707.20 ns | 945.3125 | 320.3125 | - | 7912992 B | -| Retrieve_ImmDict | 10000 | 8 | 23,538.66 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 10000 | 8 | 44,522.29 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 10000 | 8 | 33,878.51 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 10000 | 8 | 385,003.57 ns | - | - | - | - | -| Retrieve_ExtMap | 10000 | 8 | 415,526.12 ns | - | - | - | - | -| Retrieve_ExtHashMap | 10000 | 8 | 38,484.21 ns | - | - | - | - | -| Update_ImmDict | 10000 | 8 | 395,683.89 ns | 101.5625 | 26.3672 | - | 849792 B | -| Update_PersistentMap_Standard | 10000 | 8 | 348,111.33 ns | 304.6875 | 131.3477 | - | 2552000 B | -| Update_PersistentMap_Unicode | 10000 | 8 | 415,082.82 ns | 366.2109 | 160.1563 | - | 3064000 B | -| Update_TransientMap_Standard | 10000 | 8 | 138,925.69 ns | 40.5273 | 13.6719 | - | 339232 B | -| Update_TransientMap_Unicode | 10000 | 8 | 155,075.00 ns | 40.7715 | 16.6016 | - | 341792 B | -| Update_ImmSortedDict | 10000 | 8 | 576,796.03 ns | 76.1719 | 15.6250 | - | 640368 B | -| Update_ExtMap | 10000 | 8 | 566,570.69 ns | 87.8906 | 22.4609 | - | 735640 B | -| Update_ExtHashMap | 10000 | 8 | 169,597.22 ns | 103.0273 | 22.7051 | - | 862144 B | -| UpdateSet_ImmDict | 10000 | 8 | 430,876.44 ns | 108.3984 | 28.3203 | - | 907136 B | -| UpdateSet_PersistentMap_Standard | 10000 | 8 | 390,932.56 ns | 306.6406 | 136.7188 | - | 2566136 B | -| UpdateSet_PersistentMap_Unicode | 10000 | 8 | 485,184.77 ns | 368.6523 | 154.2969 | - | 3086432 B | -| UpdateSet_TransientMap_Standard | 10000 | 8 | 183,267.74 ns | 42.2363 | 16.3574 | - | 354776 B | -| UpdateSet_TransientMap_Unicode | 10000 | 8 | 222,767.01 ns | 43.7012 | 16.3574 | - | 365632 B | -| UpdateSet_ImmSortedDict | 10000 | 8 | 648,381.31 ns | 82.0313 | 19.5313 | - | 687648 B | -| UpdateSet_ExtMap | 10000 | 8 | 637,483.97 ns | 99.6094 | 25.3906 | - | 833752 B | -| UpdateSet_ExtHashMap | 10000 | 8 | 189,210.57 ns | 104.9805 | 23.6816 | - | 878600 B | -| Iterate_ImmDict | 10000 | 8 | 176,031.53 ns | - | - | - | - | -| Iterate_PersistentMap_Standard | 10000 | 8 | 17,218.51 ns | - | - | - | - | -| Iterate_ImmSortedDict | 10000 | 8 | 56,553.28 ns | - | - | - | - | -| Iterate_ExtMap | 10000 | 8 | 66,099.89 ns | - | - | - | 32 B | -| Iterate_ExtHashMap | 10000 | 8 | 175,854.49 ns | 20.0195 | - | - | 168696 B | -| Iterate_PersistentMap_Unicode | 10000 | 8 | 17,469.24 ns | - | - | - | - | -| Remove_ImmDict | 10000 | 8 | 395,433.28 ns | 102.5391 | 26.3672 | - | 858560 B | -| Remove_PersistentMap_Standard | 10000 | 8 | 417,267.98 ns | 302.2461 | 112.7930 | - | 2529408 B | -| Remove_PersistentMap_Unicode | 10000 | 8 | 489,859.89 ns | 363.2813 | 180.6641 | - | 3041408 B | -| Remove_TransientMap_Standard | 10000 | 8 | 198,908.02 ns | 37.8418 | 13.6719 | - | 316680 B | -| Remove_TransientMap_Unicode | 10000 | 8 | 223,427.67 ns | 38.0859 | 15.1367 | - | 319240 B | -| Remove_ImmSortedDict | 10000 | 8 | 602,358.94 ns | 77.1484 | 15.6250 | - | 652944 B | -| Remove_ExtMap | 10000 | 8 | 614,493.62 ns | 91.7969 | 20.5078 | - | 774000 B | -| Remove_ExtHashMap | 10000 | 8 | 179,222.20 ns | 104.0039 | 20.2637 | - | 870432 B | -| **Build_TransientMap_Standard** | **10000** | **50** | **1,799,942.31 ns** | **52.7344** | **13.6719** | **-** | **446352 B** | -| Build_TransientMap_Unicode | 10000 | 50 | 2,336,360.10 ns | 66.4063 | 23.4375 | - | 567112 B | -| Build_ImmDict | 10000 | 50 | 4,618,106.24 ns | 1046.8750 | 515.6250 | - | 8772288 B | -| Build_ImmSortedDict | 10000 | 50 | 6,614,766.71 ns | 804.6875 | 265.6250 | - | 6751584 B | -| Build_ExtMap | 10000 | 50 | 6,524,244.50 ns | 1015.6250 | 429.6875 | - | 8544720 B | -| Build_ExtHashMap | 10000 | 50 | 2,195,902.51 ns | 945.3125 | 312.5000 | - | 7921128 B | -| Retrieve_ImmDict | 10000 | 50 | 53,644.62 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 10000 | 50 | 51,772.04 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 10000 | 50 | 35,183.58 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 10000 | 50 | 384,954.19 ns | - | - | - | - | -| Retrieve_ExtMap | 10000 | 50 | 419,295.20 ns | - | - | - | - | -| Retrieve_ExtHashMap | 10000 | 50 | 68,364.13 ns | - | - | - | - | -| Update_ImmDict | 10000 | 50 | 416,499.71 ns | 100.5859 | 24.9023 | - | 842880 B | -| Update_PersistentMap_Standard | 10000 | 50 | 352,007.58 ns | 304.6875 | 131.3477 | - | 2552000 B | -| Update_PersistentMap_Unicode | 10000 | 50 | 419,485.01 ns | 366.2109 | 158.2031 | - | 3064000 B | -| Update_TransientMap_Standard | 10000 | 50 | 145,767.49 ns | 39.3066 | 13.6719 | - | 328832 B | -| Update_TransientMap_Unicode | 10000 | 50 | 152,884.18 ns | 39.5508 | 13.9160 | - | 331136 B | -| Update_ImmSortedDict | 10000 | 50 | 581,461.95 ns | 76.1719 | 14.6484 | - | 641616 B | -| Update_ExtMap | 10000 | 50 | 562,336.86 ns | 86.9141 | 20.5078 | - | 732896 B | -| Update_ExtHashMap | 10000 | 50 | 189,986.29 ns | 103.0273 | 22.2168 | - | 863280 B | -| UpdateSet_ImmDict | 10000 | 50 | 457,611.78 ns | 108.8867 | 30.7617 | - | 912128 B | -| UpdateSet_PersistentMap_Standard | 10000 | 50 | 400,214.07 ns | 306.6406 | 129.8828 | - | 2565560 B | -| UpdateSet_PersistentMap_Unicode | 10000 | 50 | 497,552.64 ns | 368.1641 | 152.3438 | - | 3085600 B | -| UpdateSet_TransientMap_Standard | 10000 | 50 | 191,918.16 ns | 41.0156 | 13.6719 | - | 343800 B | -| UpdateSet_TransientMap_Unicode | 10000 | 50 | 222,651.45 ns | 42.2363 | 15.8691 | - | 354144 B | -| UpdateSet_ImmSortedDict | 10000 | 50 | 656,130.18 ns | 82.0313 | 19.5313 | - | 691728 B | -| UpdateSet_ExtMap | 10000 | 50 | 644,220.45 ns | 98.6328 | 23.4375 | - | 829552 B | -| UpdateSet_ExtHashMap | 10000 | 50 | 213,717.48 ns | 104.7363 | 23.6816 | - | 876664 B | -| Iterate_ImmDict | 10000 | 50 | 172,329.11 ns | - | - | - | - | -| Iterate_PersistentMap_Standard | 10000 | 50 | 17,538.70 ns | - | - | - | - | -| Iterate_ImmSortedDict | 10000 | 50 | 52,626.12 ns | - | - | - | - | -| Iterate_ExtMap | 10000 | 50 | 75,440.14 ns | - | - | - | 32 B | -| Iterate_ExtHashMap | 10000 | 50 | 176,467.34 ns | 20.0195 | - | - | 168192 B | -| Iterate_PersistentMap_Unicode | 10000 | 50 | 17,931.81 ns | - | - | - | - | -| Remove_ImmDict | 10000 | 50 | 420,385.25 ns | 102.5391 | 26.3672 | - | 858432 B | -| Remove_PersistentMap_Standard | 10000 | 50 | 417,831.54 ns | 302.2461 | 113.7695 | - | 2530816 B | -| Remove_PersistentMap_Unicode | 10000 | 50 | 488,933.92 ns | 363.2813 | 158.2031 | - | 3042816 B | -| Remove_TransientMap_Standard | 10000 | 50 | 215,079.21 ns | 37.1094 | 13.4277 | - | 310504 B | -| Remove_TransientMap_Unicode | 10000 | 50 | 221,947.80 ns | 37.3535 | 13.1836 | - | 312808 B | -| Remove_ImmSortedDict | 10000 | 50 | 607,856.65 ns | 77.1484 | 15.6250 | - | 651840 B | -| Remove_ExtMap | 10000 | 50 | 628,754.81 ns | 91.7969 | 21.4844 | - | 771256 B | -| Remove_ExtHashMap | 10000 | 50 | 203,488.31 ns | 104.2480 | 21.4844 | - | 873560 B | -| **Build_TransientMap_Standard** | **100000** | **8** | **27,358,697.21 ns** | **531.2500** | **406.2500** | **-** | **4540216 B** | -| Build_TransientMap_Unicode | 100000 | 8 | 30,530,320.71 ns | 687.5000 | 593.7500 | - | 5798728 B | -| Build_ImmDict | 100000 | 8 | 90,877,970.56 ns | 13166.6667 | 4500.0000 | 166.6667 | 109369973 B | -| Build_ImmSortedDict | 100000 | 8 | 116,689,394.37 ns | 10000.0000 | 3800.0000 | - | 83946432 B | -| Build_ExtMap | 100000 | 8 | 117,813,711.80 ns | 12400.0000 | 5600.0000 | - | 104660688 B | -| Build_ExtHashMap | 100000 | 8 | 44,164,471.41 ns | 12000.0000 | 2583.3333 | 83.3333 | 99774201 B | -| Retrieve_ImmDict | 100000 | 8 | 1,475,338.26 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 100000 | 8 | 1,774,506.52 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 100000 | 8 | 1,336,091.68 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 100000 | 8 | 6,136,773.68 ns | - | - | - | - | -| Retrieve_ExtMap | 100000 | 8 | 6,552,400.29 ns | - | - | - | - | -| Retrieve_ExtHashMap | 100000 | 8 | 1,008,726.75 ns | - | - | - | - | -| Update_ImmDict | 100000 | 8 | 7,725,041.14 ns | 1265.6250 | 1070.3125 | - | 10622656 B | -| Update_PersistentMap_Standard | 100000 | 8 | 9,751,403.12 ns | 3734.3750 | 2984.3750 | 46.8750 | 30960042 B | -| Update_PersistentMap_Unicode | 100000 | 8 | 11,106,373.87 ns | 4671.8750 | 2468.7500 | 62.5000 | 38640073 B | -| Update_TransientMap_Standard | 100000 | 8 | 3,072,594.67 ns | 410.1563 | 347.6563 | - | 3458240 B | -| Update_TransientMap_Unicode | 100000 | 8 | 2,678,497.53 ns | 414.0625 | 351.5625 | - | 3487168 B | -| Update_ImmSortedDict | 100000 | 8 | 9,958,696.31 ns | 953.1250 | 781.2500 | - | 8020080 B | -| Update_ExtMap | 100000 | 8 | 10,091,372.46 ns | 1093.7500 | 937.5000 | - | 9246400 B | -| Update_ExtHashMap | 100000 | 8 | 3,713,620.94 ns | 1269.5313 | 996.0938 | - | 10625016 B | -| UpdateSet_ImmDict | 100000 | 8 | 8,447,925.20 ns | 1343.7500 | 1062.5000 | - | 11239232 B | -| UpdateSet_PersistentMap_Standard | 100000 | 8 | 10,256,666.84 ns | 3750.0000 | 2609.3750 | 46.8750 | 31066796 B | -| UpdateSet_PersistentMap_Unicode | 100000 | 8 | 12,170,123.49 ns | 4687.5000 | 2250.0000 | 62.5000 | 38809425 B | -| UpdateSet_TransientMap_Standard | 100000 | 8 | 3,437,362.03 ns | 421.8750 | 351.5625 | - | 3545288 B | -| UpdateSet_TransientMap_Unicode | 100000 | 8 | 3,401,903.92 ns | 433.5938 | 386.7188 | - | 3636832 B | -| UpdateSet_ImmSortedDict | 100000 | 8 | 11,287,098.80 ns | 1015.6250 | 875.0000 | - | 8495856 B | -| UpdateSet_ExtMap | 100000 | 8 | 11,577,275.47 ns | 1218.7500 | 1015.6250 | - | 10287328 B | -| UpdateSet_ExtHashMap | 100000 | 8 | 3,795,803.13 ns | 1285.1563 | 996.0938 | - | 10752160 B | -| Iterate_ImmDict | 100000 | 8 | 2,176,936.15 ns | - | - | - | 192 B | -| Iterate_PersistentMap_Standard | 100000 | 8 | 240,376.55 ns | - | - | - | - | -| Iterate_ImmSortedDict | 100000 | 8 | 713,292.36 ns | - | - | - | - | -| Iterate_ExtMap | 100000 | 8 | 1,041,016.32 ns | - | - | - | 32 B | -| Iterate_ExtHashMap | 100000 | 8 | 2,609,178.31 ns | 277.3438 | - | - | 2321200 B | -| Iterate_PersistentMap_Unicode | 100000 | 8 | 228,758.17 ns | - | - | - | - | -| Remove_ImmDict | 100000 | 8 | 7,633,500.34 ns | 1281.2500 | 1070.3125 | - | 10733056 B | -| Remove_PersistentMap_Standard | 100000 | 8 | 10,634,178.02 ns | 3718.7500 | 3093.7500 | 46.8750 | 30732714 B | -| Remove_PersistentMap_Unicode | 100000 | 8 | 11,815,412.68 ns | 4640.6250 | 2875.0000 | 62.5000 | 38412734 B | -| Remove_TransientMap_Standard | 100000 | 8 | 3,501,181.09 ns | 378.9063 | 312.5000 | - | 3197160 B | -| Remove_TransientMap_Unicode | 100000 | 8 | 3,113,607.62 ns | 382.8125 | 324.2188 | - | 3226088 B | -| Remove_ImmSortedDict | 100000 | 8 | 10,204,663.55 ns | 953.1250 | 781.2500 | - | 8031888 B | -| Remove_ExtMap | 100000 | 8 | 11,026,369.80 ns | 1140.6250 | 937.5000 | - | 9630448 B | -| Remove_ExtHashMap | 100000 | 8 | 3,801,747.29 ns | 1265.6250 | 937.5000 | - | 10608872 B | -| **Build_TransientMap_Standard** | **100000** | **50** | **31,414,873.58 ns** | **500.0000** | **312.5000** | **-** | **4518440 B** | -| Build_TransientMap_Unicode | 100000 | 50 | 34,272,872.95 ns | 666.6667 | 466.6667 | - | 5763784 B | -| Build_ImmDict | 100000 | 50 | 89,991,315.99 ns | 13166.6667 | 4833.3333 | 166.6667 | 109418176 B | -| Build_ImmSortedDict | 100000 | 50 | 131,306,574.64 ns | 10000.0000 | 4000.0000 | - | 83731632 B | -| Build_ExtMap | 100000 | 50 | 128,889,148.68 ns | 12250.0000 | 5250.0000 | - | 104371560 B | -| Build_ExtHashMap | 100000 | 50 | 48,924,428.69 ns | 11909.0909 | 2272.7273 | - | 99798944 B | -| Retrieve_ImmDict | 100000 | 50 | 1,789,773.89 ns | - | - | - | - | -| Retrieve_PersistentMap_Standard | 100000 | 50 | 2,280,540.33 ns | - | - | - | - | -| Retrieve_PersistentMap_Unicode | 100000 | 50 | 1,447,748.76 ns | - | - | - | - | -| Retrieve_ImmSortedDict | 100000 | 50 | 7,097,150.11 ns | - | - | - | - | -| Retrieve_ExtMap | 100000 | 50 | 7,013,962.09 ns | - | - | - | - | -| Retrieve_ExtHashMap | 100000 | 50 | 1,327,343.45 ns | - | - | - | - | -| Update_ImmDict | 100000 | 50 | 8,075,323.36 ns | 1265.6250 | 1062.5000 | - | 10593088 B | -| Update_PersistentMap_Standard | 100000 | 50 | 11,441,201.31 ns | 3734.3750 | 3421.8750 | 46.8750 | 30960051 B | -| Update_PersistentMap_Unicode | 100000 | 50 | 11,646,138.25 ns | 4656.2500 | 2765.6250 | 46.8750 | 38640052 B | -| Update_TransientMap_Standard | 100000 | 50 | 3,785,331.39 ns | 406.2500 | 347.6563 | - | 3410592 B | -| Update_TransientMap_Unicode | 100000 | 50 | 2,931,528.16 ns | 410.1563 | 343.7500 | - | 3440288 B | -| Update_ImmSortedDict | 100000 | 50 | 11,055,325.40 ns | 953.1250 | 781.2500 | - | 8012928 B | -| Update_ExtMap | 100000 | 50 | 10,703,171.07 ns | 1093.7500 | 906.2500 | - | 9233632 B | -| Update_ExtHashMap | 100000 | 50 | 4,100,682.76 ns | 1265.6250 | 976.5625 | - | 10623184 B | -| UpdateSet_ImmDict | 100000 | 50 | 8,848,378.05 ns | 1343.7500 | 1046.8750 | - | 11255552 B | -| UpdateSet_PersistentMap_Standard | 100000 | 50 | 12,223,495.13 ns | 3734.3750 | 2640.6250 | 31.2500 | 31083384 B | -| UpdateSet_PersistentMap_Unicode | 100000 | 50 | 12,917,217.90 ns | 4687.5000 | 2437.5000 | 46.8750 | 38835954 B | -| UpdateSet_TransientMap_Standard | 100000 | 50 | 4,490,436.88 ns | 414.0625 | 343.7500 | - | 3528320 B | -| UpdateSet_TransientMap_Unicode | 100000 | 50 | 3,756,285.03 ns | 433.5938 | 367.1875 | - | 3630560 B | -| UpdateSet_ImmSortedDict | 100000 | 50 | 12,490,770.48 ns | 1015.6250 | 890.6250 | - | 8501136 B | -| UpdateSet_ExtMap | 100000 | 50 | 12,674,840.15 ns | 1218.7500 | 1015.6250 | - | 10265544 B | -| UpdateSet_ExtHashMap | 100000 | 50 | 4,395,939.08 ns | 1281.2500 | 992.1875 | - | 10756792 B | -| Iterate_ImmDict | 100000 | 50 | 2,416,834.88 ns | - | - | - | 96 B | -| Iterate_PersistentMap_Standard | 100000 | 50 | 214,203.41 ns | - | - | - | - | -| Iterate_ImmSortedDict | 100000 | 50 | 712,519.08 ns | - | - | - | - | -| Iterate_ExtMap | 100000 | 50 | 1,091,987.30 ns | - | - | - | 32 B | -| Iterate_ExtHashMap | 100000 | 50 | 2,669,431.61 ns | 273.4375 | - | - | 2314072 B | -| Iterate_PersistentMap_Unicode | 100000 | 50 | 211,247.16 ns | - | - | - | - | -| Remove_ImmDict | 100000 | 50 | 8,089,556.02 ns | 1281.2500 | 1031.2500 | - | 10743040 B | -| Remove_PersistentMap_Standard | 100000 | 50 | 11,874,296.78 ns | 3703.1250 | 2750.0000 | 31.2500 | 30722852 B | -| Remove_PersistentMap_Unicode | 100000 | 50 | 11,948,106.52 ns | 4625.0000 | 2859.3750 | 46.8750 | 38402877 B | -| Remove_TransientMap_Standard | 100000 | 50 | 4,337,489.41 ns | 375.0000 | 304.6875 | - | 3176264 B | -| Remove_TransientMap_Unicode | 100000 | 50 | 3,278,674.84 ns | 382.8125 | 312.5000 | - | 3205960 B | -| Remove_ImmSortedDict | 100000 | 50 | 11,338,022.46 ns | 953.1250 | 781.2500 | - | 8032656 B | -| Remove_ExtMap | 100000 | 50 | 11,912,777.70 ns | 1140.6250 | 921.8750 | - | 9617232 B | -| Remove_ExtHashMap | 100000 | 50 | 4,223,945.68 ns | 1265.6250 | 937.5000 | - | 10621824 B |#+end_src - 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. - -This means that it will be fast for integers and anything you can pack in 8 bytes. If you use stings with high prefix entropy, this will be /very/ performant. If you don't, it is just another b+tree. diff --git a/PersistentOrderedMap/TransientOrderedMap.cs b/PersistentOrderedMap/TransientOrderedMap.cs deleted file mode 100644 index 93786f8..0000000 --- a/PersistentOrderedMap/TransientOrderedMap.cs +++ /dev/null @@ -1,39 +0,0 @@ - -namespace PersistentOrderedMap; - -public sealed class TransientOrderedMap : BaseOrderedMap where TStrategy : IKeyStrategy -{ - // This is mutable, but we treat it as readonly for the ID generation logic usually. - private OwnerId _transactionId; - - public TransientOrderedMap(Node root, TStrategy strategy, int count) - : base(root, strategy, count) - { - _transactionId = OwnerId.Next(); - } - - public void Set(TK key, TV value) - { - Root = BTreeFunctions.Set(Root, key, value, Strategy, _transactionId, out bool countChanged); - if (countChanged) Count++; - } - - public void Remove(TK key) - { - Root = BTreeFunctions.Remove(Root, key, Strategy, _transactionId, out bool removed); - if (removed) Count--; - } - - public PersistentOrderedMap ToPersistent() - { - // 1. Create the snapshot by copying all relevant information - - var snapshot = new PersistentOrderedMap(Root, Strategy, Count); - - // 2. Protect the snapshot from THIS TransientOrderedMap by getting a new ownerId - // so that future edits will be done by CoW - _transactionId = OwnerId.Next(); - - return snapshot; - } -} \ No newline at end of file diff --git a/TestProject1/FuzzTest.cs b/TestProject1/FuzzTest.cs index bc4a081..cded93b 100644 --- a/TestProject1/FuzzTest.cs +++ b/TestProject1/FuzzTest.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using Xunit; using Xunit.Abstractions; -using PersistentOrderedMap; +using PersistentMap; public class BTreeFuzzTests { @@ -19,25 +19,25 @@ public class BTreeFuzzTests public void Fuzz_Insert_And_Remove_consistency() { // CONFIGURATION - const int iterations = 100_000; // High enough to trigger all splits/merges - const int keyRange = 5000; // Small enough to cause frequent collisions + const int Iterations = 100_000; // High enough to trigger all splits/merges + const int KeyRange = 5000; // Small enough to cause frequent collisions const bool showOps = false; - int seed = 2135974; // Environment.TickCount; + int Seed = 2135974; // Environment.TickCount; // ORACLES var reference = new SortedDictionary(); var subject = BaseOrderedMap.CreateTransient(_strategy); - var random = new Random(seed); - _output.WriteLine($"Starting Fuzz Test with Seed: {seed}"); + var random = new Random(Seed); + _output.WriteLine($"Starting Fuzz Test with Seed: {Seed}"); try { - for (int i = 0; i < iterations; i++) + for (int i = 0; i < Iterations; i++) { // 1. Pick an Action: 70% Insert/Update, 30% Remove bool isInsert = random.NextDouble() < 0.7; - int key = random.Next(keyRange); + int key = random.Next(KeyRange); int val = key * 100; if (isInsert) @@ -82,7 +82,7 @@ public class BTreeFuzzTests } catch (Exception) { - _output.WriteLine($"FAILED at iteration with SEED: {seed}"); + _output.WriteLine($"FAILED at iteration with SEED: {Seed}"); throw; // Re-throw to fail the test } } @@ -91,28 +91,28 @@ public class BTreeFuzzTests public void Fuzz_Range_Queries() { // Validates that your Range Enumerator matches LINQ on the reference - const int iterations = 1000; - const int keyRange = 2000; - int seed = Environment.TickCount; + const int Iterations = 1000; + const int KeyRange = 2000; + int Seed = Environment.TickCount; var reference = new SortedDictionary(); var subject = BaseOrderedMap.CreateTransient(_strategy); - var random = new Random(seed); + var random = new Random(Seed); // Fill Data - for(int i=0; i= min + int min = random.Next(KeyRange); + int max = min + random.Next(KeyRange - min); // Ensure max >= min // 1. Reference Result (LINQ) // Note: SortedDictionary doesn't have a direct Range query, so we filter memory. @@ -137,7 +137,7 @@ public class BTreeFuzzTests } } - private void AssertConsistency(SortedDictionary expected, TransientOrderedMap actual) + private void AssertConsistency(SortedDictionary expected, TransientMap actual) { // 1. Count if (expected.Count != actual.Count) diff --git a/TestProject1/FuzzTestStandardStrategy.cs b/TestProject1/FuzzTestStandardStrategy.cs deleted file mode 100644 index 5c5ee61..0000000 --- a/TestProject1/FuzzTestStandardStrategy.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Xunit; -using Xunit.Abstractions; -using PersistentOrderedMap; - -public class BTreeFuzzTestStandardStrategy -{ - private readonly ITestOutputHelper _output; - private readonly StandardStrategy _strategy = new(); - - public BTreeFuzzTestStandardStrategy(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void Fuzz_Insert_And_Remove_consistency() - { - // CONFIGURATION - const int iterations = 100_000; // High enough to trigger all splits/merges - const int keyRange = 5000; // Small enough to cause frequent collisions - const bool showOps = false; - int seed = 2135974; // Environment.TickCount; - - // ORACLES - var reference = new SortedDictionary(); - var subject = BaseOrderedMap>.CreateTransient(_strategy); - - var random = new Random(seed); - _output.WriteLine($"Starting Fuzz Test with Seed: {seed}"); - - try - { - for (int i = 0; i < iterations; i++) - { - // 1. Pick an Action: 70% Insert/Update, 30% Remove - bool isInsert = random.NextDouble() < 0.7; - int key = random.Next(keyRange); - int val = key * 100; - - if (isInsert) - { - // ACTION: INSERT - if (showOps)Console.WriteLine($"insert: {key} : {val}"); - if (key == 4436) - { - Console.WriteLine("BP"); - } - - reference[key] = val; - subject.Set(key, val); - } - else - { - // ACTION: REMOVE - if (reference.ContainsKey(key)) - { - if (showOps)Console.WriteLine($"remove ${key}"); - reference.Remove(key); - subject.Remove(key); - } - else - { - // Try removing non-existent key (should be safe) - subject.Remove(key); - } - } - - // 2. VERIFY CONSISTENCY (Expensive but necessary) - // We check consistency every 1000 ops or if the tree is small, - // to keep the test fast enough. - //if (i % 1000 == 0 || reference.Count < 100) - //{ - AssertConsistency(reference, subject); - //} - } - - // Final check - AssertConsistency(reference, subject); - } - catch (Exception) - { - _output.WriteLine($"FAILED at iteration with SEED: {seed}"); - throw; // Re-throw to fail the test - } - } - - [Fact] - public void Fuzz_Range_Queries() - { - // Validates that your Range Enumerator matches LINQ on the reference - const int iterations = 1000; - const int keyRange = 2000; - int seed = Environment.TickCount; - - var reference = new SortedDictionary(); - var subject = BaseOrderedMap>.CreateTransient(_strategy); - var random = new Random(seed); - - // Fill Data - for(int i=0; i= min - - // 1. Reference Result (LINQ) - // Note: SortedDictionary doesn't have a direct Range query, so we filter memory. - var expected = reference - .Where(kv => kv.Key >= min && kv.Key <= max) - .Select(kv => kv.Key) - .ToList(); - - // 2. Subject Result - var actual = persistent.Range(min, max) - .Select(kv => kv.Key) - .ToList(); - - // 3. Compare - if (!expected.SequenceEqual(actual)) - { - _output.WriteLine($"Range Mismatch! Range: [{min}, {max}]"); - _output.WriteLine($"Expected: {string.Join(",", expected)}"); - _output.WriteLine($"Actual: {string.Join(",", actual)}"); - Assert.Fail("Range query results differ."); - } - } - } - - private void AssertConsistency(SortedDictionary expected, TransientOrderedMap> actual) - { - // 1. Count - if (expected.Count != actual.Count) - { - Console.WriteLine("BP"); - throw new Exception($"Count Mismatch! Expected {expected.Count}, Got {actual.Count}"); - } - - // 2. Full Scan Verification - using var enumerator = actual.GetEnumerator(); - foreach (var kvp in expected) - { - if (!enumerator.MoveNext()) - { - throw new Exception("Enumerator ended too early!"); - } - - if (enumerator.Current.Key != kvp.Key || enumerator.Current.Value != kvp.Value) - { - Console.WriteLine("BP"); - throw new Exception($"Content Mismatch! Expected [{kvp.Key}:{kvp.Value}], Got [{enumerator.Current.Key}:{enumerator.Current.Value}]"); - } - } - - if (enumerator.MoveNext()) - { - throw new Exception("Enumerator has extra items!"); - } - } -} diff --git a/TestProject1/IteratorTests.cs b/TestProject1/IteratorTests.cs index 453e1af..acd07e3 100644 --- a/TestProject1/IteratorTests.cs +++ b/TestProject1/IteratorTests.cs @@ -1,5 +1,5 @@ using Xunit; -using PersistentOrderedMap; +using PersistentMap; using System.Linq; using System.Collections.Generic; @@ -8,8 +8,8 @@ public class EnumeratorTests // Use IntStrategy for simple numeric testing private readonly IntStrategy _strategy = new(); - // Helper to create a populated PersistentOrderedMap quickly - private PersistentOrderedMap CreateMap(params int[] keys) + // Helper to create a populated PersistentMap quickly + private PersistentMap CreateMap(params int[] keys) { var map = BaseOrderedMap.CreateTransient(_strategy); foreach (var k in keys) @@ -22,7 +22,7 @@ public class EnumeratorTests [Fact] public void EmptyMap_EnumeratesNothing() { - var map = PersistentOrderedMap.Empty(_strategy); + var map = PersistentMap.Empty(_strategy); var list = new List(); foreach(var kv in map) list.Add(kv.Key); diff --git a/TestProject1/OrderedQueriesTest.cs b/TestProject1/OrderedQueriesTest.cs index ac24d20..b9b996a 100644 --- a/TestProject1/OrderedQueriesTest.cs +++ b/TestProject1/OrderedQueriesTest.cs @@ -1,13 +1,13 @@ using System.Linq; using Xunit; -using PersistentOrderedMap; +using PersistentMap; namespace PersistentMap.Tests { public class BTreeExtendedOperationsTests { // Helper to quickly spin up a populated map - private TransientOrderedMap CreateMap(params int[] keys) + private TransientMap CreateMap(params int[] keys) { var map = BaseOrderedMap.CreateTransient(new IntStrategy()); foreach (var key in keys) diff --git a/TestProject1/PersistenceTests.cs b/TestProject1/PersistenceTests.cs index 78a163e..713df90 100644 --- a/TestProject1/PersistenceTests.cs +++ b/TestProject1/PersistenceTests.cs @@ -1,5 +1,5 @@ namespace TestProject1; -using PersistentOrderedMap; +using PersistentMap; public class PersistenceTests { private readonly UnicodeStrategy _strategy = new UnicodeStrategy(); diff --git a/TestProject1/StandardStrategy.cs b/TestProject1/StandardStrategy.cs deleted file mode 100644 index 94cc484..0000000 --- a/TestProject1/StandardStrategy.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace PersistentMap.Tests; -using PersistentOrderedMap; -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/TestProject1/StressTest.cs b/TestProject1/StressTest.cs index adcd6a7..ef086c3 100644 --- a/TestProject1/StressTest.cs +++ b/TestProject1/StressTest.cs @@ -1,6 +1,6 @@ namespace TestProject1; -using PersistentOrderedMap; +using PersistentMap; public class StressTests { diff --git a/TestProject1/TestProject1.csproj b/TestProject1/TestProject1.csproj index 0aea003..e36bb42 100644 --- a/TestProject1/TestProject1.csproj +++ b/TestProject1/TestProject1.csproj @@ -19,7 +19,7 @@ - + \ No newline at end of file diff --git a/TestProject1/UnitTest1.cs b/TestProject1/UnitTest1.cs index 25f698a..a9a9d77 100644 --- a/TestProject1/UnitTest1.cs +++ b/TestProject1/UnitTest1.cs @@ -1,7 +1,7 @@ namespace TestProject1; using Xunit; -using PersistentOrderedMap; +using PersistentMap; public class BasicTests { diff --git a/benchmarks/AgainstImmutableDict/AgainstImmutable.cs b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs new file mode 100644 index 0000000..269355f --- /dev/null +++ b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using PersistentMap; + +// Ensure your PersistentMap namespace is included here +// using MyProject.Collections; + + +[MemoryDiagnoser] +public class ImmutableBenchmark +{ + [Params(10, 100)] + public int N { get; set; } + + [Params(10000)] + public int CollectionSize { get; set; } + + private ImmutableDictionary _immutableDict; + private ImmutableSortedDictionary _immutableSortedDict; + + // 1. Add field for your map + private PersistentMap _persistentMap; + + private string[] _searchKeys; + + [GlobalSetup] + public void Setup() + { + var random = new Random(42); + var data = new Dictionary(); + + while (data.Count < CollectionSize) + { + string key = GenerateRandomString(random, N); + if (!data.ContainsKey(key)) + { + data[key] = "value"; + } + } + + _immutableDict = data.ToImmutableDictionary(); + _immutableSortedDict = data.ToImmutableSortedDictionary(); + + // 2. Initialize your map. + // ASSUMPTION: Standard immutable pattern (Add returns new instance). + // Adjust if you have a bulk loader like .ToPersistentMap() or a constructor. + _persistentMap = PersistentMap.Empty(new UnicodeStrategy()); + foreach (var kvp in data) + { + _persistentMap = _persistentMap.Set(kvp.Key, kvp.Value); + } + + _searchKeys = data.Keys.ToArray(); + } + + [Benchmark(Baseline = true)] + public string ImmutableDict_Lookup() + { + var key = _searchKeys[CollectionSize / 2]; + _immutableDict.TryGetValue(key, out var value); + return value; + } + + [Benchmark] + public string ImmutableSortedDict_Lookup() + { + var key = _searchKeys[CollectionSize / 2]; + _immutableSortedDict.TryGetValue(key, out var value); + return value; + } + + // 3. Add the benchmark case + [Benchmark] + public string PersistentMap_Lookup() + { + var key = _searchKeys[CollectionSize / 2]; + // Adjust API call if your map uses a different method (e.g. Find, Get, indexer) + _persistentMap.TryGetValue(key, out var value); + return value; + } + + private string GenerateRandomString(Random rng, int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var buffer = new char[length]; + for (int i = 0; i < length; i++) + { + buffer[i] = chars[rng.Next(chars.Length)]; + } + return new string(buffer); + } +} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs b/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs new file mode 100644 index 0000000..3a221ca --- /dev/null +++ b/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs @@ -0,0 +1,198 @@ +using System.Collections.Immutable; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using LanguageExt; +using PersistentMap; + +namespace AgainstLanguageExt; +// Mocking your library types for the sake of the example to ensure compilation. +// Replace these with your actual namespace imports. + + +[MemoryDiagnoser] +[HideColumns("Ratio", "RatioSD", "Alloc Ratio")] +public class MapBenchmarks +{ + [Params(10, 100, 1000)] + public int KeyLength { get; set; } // Key length + + [Params(1000, 100000, 1000000)] + public int CollectionSize { get; set; } + + // Data Source + private KeyValuePair[] _data; + private string[] _searchKeys; + + private int _index = 0; + private int _mask; + + // Comparison Targets (for Lookup) + private ImmutableDictionary _sysDict; + private ImmutableSortedDictionary _sysSorted; + private LanguageExt.HashMap _langExtHash; + private LanguageExt.Map _langExtSorted; // Map is Sorted in LangExt + private PersistentMap _persistentMap; + + [GlobalSetup] + public void Setup() + { + var random = new Random(42); + var dict = new Dictionary(); + + // 1. Generate Data + while (dict.Count < CollectionSize) + { + string key = GenerateRandomString(random, KeyLength); + if (!dict.ContainsKey(key)) + { + dict[key] = random.Next(); + } + } + _data = dict.ToArray(); + _searchKeys = dict.Keys.ToArray(); + + // 2. Pre-build maps for the Lookup benchmarks + _sysDict = dict.ToImmutableDictionary(); + _sysSorted = dict.ToImmutableSortedDictionary(); + + _langExtHash = Prelude.toHashMap(dict); + _langExtSorted = Prelude.toMap(dict); + + // Build PersistentMap for lookup test + var trans = PersistentMap.Empty(new UnicodeStrategy()).ToTransient(); + foreach (var kvp in _data) + { + trans.Set(kvp.Key, kvp.Value); + } + _persistentMap = trans.ToPersistent(); + } + + // ========================================== + // BUILD BENCHMARKS + // ========================================== + + [Benchmark(Description = "Lookup: PersistentMap (Cyclic)")] + public int Lookup_Persistent_Cyclic() + { + // Fast wrap-around + var key = _searchKeys[_index++ & _mask]; + _persistentMap.TryGetValue(key, out var value); + return value; + } + + [Benchmark(Description = "Lookup: Sys.Sorted (Cyclic)")] + public int Lookup_SysSorted_Cyclic() + { + var key = _searchKeys[_index++ & _mask]; + _sysSorted.TryGetValue(key, out var value); + return value; + } + + [Benchmark(Description = "Build: Sys.ImmutableDict")] + public ImmutableDictionary Build_SysImmutable() + { + // Using CreateRange/ToImmutable is usually the standard 'bulk' build + return _data.ToImmutableDictionary(); + } + + [Benchmark(Description = "Build: LangExt.HashMap")] + public LanguageExt.HashMap Build_LangExtHash() + { + return Prelude.toHashMap(_data); + } + + [Benchmark(Description = "Build: LangExt.SortedMap")] + public LanguageExt.Map Build_LangExtSorted() + { + return Prelude.toMap(_data); + } + + [Benchmark(Description = "Build: PersistentMap (Iterative)")] + public PersistentMap Build_Persistent_Iterative() + { + // Simulating naive immutable building (O(n log n) or worse due to copying) + var map = PersistentMap.Empty(new UnicodeStrategy()); + foreach (var item in _data) + { + map = map.Set(item.Key, item.Value); + } + return map; + } + + [Benchmark(Description = "Build: PersistentMap (Transient)")] + public PersistentMap Build_Persistent_Transient() + { + // Simulating efficient mutable build -> freeze + var trans = PersistentMap.Empty(new UnicodeStrategy()).ToTransient(); + foreach (var item in _data) + { + trans.Set(item.Key, item.Value); + } + return trans.ToPersistent(); + } + + // ========================================== + // LOOKUP BENCHMARKS + // ========================================== + + [Benchmark(Baseline = true, Description = "Lookup: Sys.ImmutableDict")] + public int Lookup_SysImmutable() + { + var key = _searchKeys[CollectionSize / 2]; + _sysDict.TryGetValue(key, out var value); + return value; + } + + [Benchmark(Description = "Lookup: Sys.SortedDict")] + public int Lookup_SysSorted() + { + var key = _searchKeys[CollectionSize / 2]; + _sysSorted.TryGetValue(key, out var value); + return value; + } + + [Benchmark(Description = "Lookup: LangExt.HashMap")] + public int Lookup_LangExtHash() + { + var key = _searchKeys[CollectionSize / 2]; + // LanguageExt often uses Find which returns Option, or [] operator + // Assuming TryGetValue-like behavior or using match for fairness + return _langExtHash.Find(key).IfNone(0); + } + + [Benchmark(Description = "Lookup: LangExt.SortedMap")] + public int Lookup_LangExtSorted() + { + var key = _searchKeys[CollectionSize / 2]; + return _langExtSorted.Find(key).IfNone(0); + } + + [Benchmark(Description = "Lookup: PersistentMap")] + public int Lookup_PersistentMap() + { + var key = _searchKeys[CollectionSize / 2]; + _persistentMap.TryGetValue(key, out var value); + return value; + } + + // Helper + private string GenerateRandomString(Random rng, int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var buffer = new char[length]; + for (int i = 0; i < length; i++) + { + buffer[i] = chars[rng.Next(chars.Length)]; + } + return new string(buffer); + } +} + +public class Program +{ + public static void Main(string[] args) + { + // This scans the assembly and lets the command line (args) decide what to run + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } +} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/Cycicmap.cs b/benchmarks/AgainstLanguageExt/Cycicmap.cs new file mode 100644 index 0000000..0cf32e4 --- /dev/null +++ b/benchmarks/AgainstLanguageExt/Cycicmap.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using LanguageExt; +using static LanguageExt.Prelude; +using PersistentMap; + + +[MemoryDiagnoser] +[HideColumns("Ratio", "RatioSD", "Alloc Ratio")] +public class CyclicMapBenchmarks +{ + // Powers of 2 for fast bitwise masking + [Params(1024, 131072)] + public int CollectionSize { get; set; } + + [Params(10, 100, 1000)] + public int N { get; set; } + + // Collections + private PersistentMap _persistentMap; + private ImmutableSortedDictionary _sysSorted; + private LanguageExt.HashMap _langExtHash; + private LanguageExt.Map _langExtSorted; + + // Lookup scaffolding + private string[] _searchKeys; + private int _index = 0; + private int _mask; + + [GlobalSetup] + public void Setup() + { + _mask = CollectionSize - 1; + var random = new Random(42); + var data = new Dictionary(); + + // 1. Generate Data + while (data.Count < CollectionSize) + { + string key = GenerateRandomString(random, N); + if (!data.ContainsKey(key)) + { + data[key] = random.Next(); + } + } + + // 2. Build Collections + // PersistentMap + var builder = PersistentMap.Empty(new UnicodeStrategy()).ToTransient(); + foreach (var kvp in data) builder.Set(kvp.Key, kvp.Value); + _persistentMap = builder.ToPersistent(); + + // System + _sysSorted = data.ToImmutableSortedDictionary(); + + // LanguageExt + _langExtHash = toHashMap(data); + _langExtSorted = toMap(data); + + // 3. Setup Cyclic Keys + _searchKeys = data.Keys.ToArray(); + // Shuffle to defeat branch prediction / cache pre-fetching + Random.Shared.Shuffle(_searchKeys); + } + + [Benchmark(Description = "Cyclic: PersistentMap")] + public int Lookup_Persistent() + { + var key = _searchKeys[_index++ & _mask]; + _persistentMap.TryGetValue(key, out var value); + return value; + } + + [Benchmark(Description = "Cyclic: Sys.Sorted")] + public int Lookup_SysSorted() + { + var key = _searchKeys[_index++ & _mask]; + _sysSorted.TryGetValue(key, out var value); + return value; + } + + [Benchmark(Description = "Cyclic: LangExt.HashMap")] + public int Lookup_LangExtHash() + { + var key = _searchKeys[_index++ & _mask]; + // Option struct return, overhead is minimal but present + return _langExtHash.Find(key).IfNone(0); + } + + [Benchmark(Description = "Cyclic: LangExt.Sorted")] + public int Lookup_LangExtSorted() + { + var key = _searchKeys[_index++ & _mask]; + // AVL Tree traversal + return _langExtSorted.Find(key).IfNone(0); + } + + private string GenerateRandomString(Random rng, int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var buffer = new char[length]; + for (int i = 0; i < length; i++) + { + buffer[i] = chars[rng.Next(chars.Length)]; + } + return new string(buffer); + } +} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/integerBenchmarks.cs b/benchmarks/AgainstLanguageExt/integerBenchmarks.cs new file mode 100644 index 0000000..2b43cb9 --- /dev/null +++ b/benchmarks/AgainstLanguageExt/integerBenchmarks.cs @@ -0,0 +1,219 @@ +using System; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using LanguageExt; // Your Namespace +using LanguageExt; +using LanguageExt; +using PersistentMap; // NuGet: LanguageExt.Core + +[MemoryDiagnoser] +public class ImmutableCollectionBenchmarks +{ + [Params(100, 1000, 100_000)] + public int N; + + private int[] _keys; + private int[] _values; + + // --- 1. Your Collections --- + private PersistentMap _niceMap; + private IntStrategy _strategy; + + // --- 2. Microsoft Collections --- + private System.Collections.Immutable.ImmutableSortedDictionary _msSortedMap; + private System.Collections.Immutable.ImmutableDictionary _msHashMap; + + // --- 3. LanguageExt Collections --- + private LanguageExt.Map _leMap; // AVL Tree (Sorted) + private LanguageExt.HashMap _leHashMap; // Hash Array Mapped Trie (Unsorted) + + [GlobalSetup] + public void Setup() + { + // Generate Data + var rand = new Random(42); + _keys = Enumerable.Range(0, N).Select(x => x * 2).ToArray(); + rand.Shuffle(_keys); + _values = _keys.Select(k => k * 100).ToArray(); + _strategy = new IntStrategy(); + + // 1. Setup NiceBTree + var transient = BaseOrderedMap.CreateTransient(_strategy); + for (int i = 0; i < N; i++) transient.Set(_keys[i], _values[i]); + _niceMap = transient.ToPersistent(); + + // 2. Setup MS Immutable + var msBuilder = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder(); + for (int i = 0; i < N; i++) msBuilder.Add(_keys[i], _values[i]); + _msSortedMap = msBuilder.ToImmutable(); + + _msHashMap = System.Collections.Immutable.ImmutableDictionary.CreateRange( + _keys.Zip(_values, (k, v) => new System.Collections.Generic.KeyValuePair(k, v))); + + // 3. Setup LanguageExt + // Note: LanguageExt performs best when bulk-loaded from tuples + var tuples = _keys.Zip(_values, (k, v) => (k, v)); + _leMap = new LanguageExt.Map(tuples); + _leHashMap = new HashMap(tuples); + } + + // ========================================================= + // 1. BUILD (Item by Item) + // Note: LanguageExt has no "Mutable Builder", so this tests + // the cost of pure immutable inserts vs your Transient/Builder. + // ========================================================= + + [Benchmark(Description = "Build: NiceBTree (Transient)")] + public int Build_NiceBTree() + { + var t = BaseOrderedMap.CreateTransient(_strategy); + for (int i = 0; i < N; i++) t.Set(_keys[i], _values[i]); + return t.Count; + } + + [Benchmark(Description = "Build: MS Sorted (Builder)")] + public int Build_MsSorted() + { + var b = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder(); + for (int i = 0; i < N; i++) b.Add(_keys[i], _values[i]); + return b.Count; + } + + [Benchmark(Description = "Build: LanguageExt Map (AVL)")] + public int Build_LanguageExt_Map() + { + var map = LanguageExt.Map.Empty; + for (int i = 0; i < N; i++) + { + // Pure immutable add + map = map.Add(_keys[i], _values[i]); + } + return map.Count; + } + + [Benchmark(Description = "Build: LanguageExt HashMap")] + public int Build_LanguageExt_HashMap() + { + var map = LanguageExt.HashMap.Empty; + for (int i = 0; i < N; i++) + { + map = map.Add(_keys[i], _values[i]); + } + return map.Count; + } + + // ========================================================= + // 2. READ (Lookup) + // ========================================================= + + [Benchmark(Description = "Read: NiceBTree")] + public int Read_NiceBTree() + { + int found = 0; + for (int i = 0; i < N; i++) + { + if (_niceMap.TryGetValue(_keys[i], out _)) found++; + } + return found; + } + + [Benchmark(Description = "Read: MS Sorted")] + public int Read_MsSorted() + { + int found = 0; + for (int i = 0; i < N; i++) + { + if (_msSortedMap.ContainsKey(_keys[i])) found++; + } + return found; + } + + [Benchmark(Description = "Read: LanguageExt Map")] + public int Read_LanguageExt_Map() + { + int found = 0; + for (int i = 0; i < N; i++) + { + // Find returns Option, IsSome checks if it exists + if (_leMap.Find(_keys[i]).IsSome) found++; + } + return found; + } + + [Benchmark(Description = "Read: LanguageExt HashMap")] + public int Read_LanguageExt_HashMap() + { + int found = 0; + for (int i = 0; i < N; i++) + { + if (_leHashMap.Find(_keys[i]).IsSome) found++; + } + return found; + } + + // ========================================================= + // 3. ITERATE (Foreach) + // ========================================================= + + [Benchmark(Description = "Iterate: NiceBTree")] + public int Iterate_NiceBTree() + { + int sum = 0; + foreach (var kvp in _niceMap) sum += kvp.Key; + return sum; + } + + [Benchmark(Description = "Iterate: MS Sorted")] + public int Iterate_MsSorted() + { + int sum = 0; + foreach (var kvp in _msSortedMap) sum += kvp.Key; + return sum; + } + + [Benchmark(Description = "Iterate: LanguageExt Map")] + public int Iterate_LanguageExt_Map() + { + int sum = 0; + // LanguageExt Map is IEnumerable<(Key, Value)> + foreach (var item in _leMap) sum += item.Key; + return sum; + } + + [Benchmark(Description = "Iterate: LanguageExt HashMap")] + public int Iterate_LanguageExt_HashMap() + { + int sum = 0; + foreach (var item in _leHashMap) sum += item.Key; + return sum; + } + + // ========================================================= + // 4. SET (Persistent / Immutable Update) + // ========================================================= + + [Benchmark(Description = "Set: NiceBTree")] + public PersistentMap Set_NiceBTree() + { + return _niceMap.Set(_keys[N / 2], -1); + } + + [Benchmark(Description = "Set: MS Sorted")] + public System.Collections.Immutable.ImmutableSortedDictionary Set_MsSorted() + { + return _msSortedMap.SetItem(_keys[N / 2], -1); + } + + [Benchmark(Description = "Set: LanguageExt Map")] + public LanguageExt.Map Set_LanguageExt_Map() + { + return _leMap.SetItem(_keys[N / 2], -1); + } + + [Benchmark(Description = "Set: LanguageExt HashMap")] + public LanguageExt.HashMap Set_LanguageExt_HashMap() + { + return _leHashMap.SetItem(_keys[N / 2], -1); + } +} \ No newline at end of file diff --git a/benchmarks/MyBenchMarks/MyBenchMarks.csproj b/benchmarks/MyBenchMarks/MyBenchMarks.csproj deleted file mode 100644 index 3e30a8f..0000000 --- a/benchmarks/MyBenchMarks/MyBenchMarks.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - - - - - - diff --git a/benchmarks/MyBenchMarks/Program.cs b/benchmarks/MyBenchMarks/Program.cs deleted file mode 100644 index a7acb3c..0000000 --- a/benchmarks/MyBenchMarks/Program.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using LanguageExt; -using PersistentOrderedMap; - -namespace MapBenchmarks; - -[MemoryDiagnoser] -public class IntMapBenchmarks -{ - [Params(100, 1000, 10000, 100000)] - public int N { get; set; } - - private int[] _allKeys; - private int[] _retrieveKeys; - private int[] _updateKeys; - private int[] _removeKeys; - private int[] _mixedKeys; // Half existing, half new - - // Pre-built collections for read/update/remove tests - private ImmutableDictionary _immDict; - private ImmutableSortedDictionary _immSortedDict; - private LanguageExt.Map _extMap; - private LanguageExt.HashMap _extHashMap; - private PersistentOrderedMap _persistentOrderedMap; - - private readonly IntStrategy _intStrategy = new IntStrategy(); - - [GlobalSetup] - public void Setup() - { - var rnd = new Random(42); - - // Build integer keys (inserted sorted) - _allKeys = Enumerable.Range(0, N).ToArray(); - - int subsetSize = Math.Max(1, N / 10); - - // Subsets for different operations - 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(); - - // Mixed keys: half existing, half completely new (N + 1 to N + subsetSize/2) - var existingHalf = shuffled.Skip(subsetSize * 3).Take(subsetSize / 2).ToArray(); - var newHalf = Enumerable.Range(N + 1, subsetSize - (subsetSize / 2)).ToArray(); - _mixedKeys = existingHalf.Concat(newHalf).OrderBy(x => rnd.Next()).ToArray(); - - // Pre-build collections - _immDict = ImmutableDictionary.CreateRange(_allKeys.Select(k => new KeyValuePair(k, k))); - _immSortedDict = ImmutableSortedDictionary.CreateRange(_allKeys.Select(k => new KeyValuePair(k, k))); - - _extMap = LanguageExt.Map.empty(); - _extHashMap = LanguageExt.HashMap.empty(); - foreach (var k in _allKeys) - { - _extMap = _extMap.AddOrUpdate(k, k); - _extHashMap = _extHashMap.AddOrUpdate(k, k); - } - - var transient = BaseOrderedMap.CreateTransient(_intStrategy); - foreach (var k in _allKeys) transient.Set(k, k); - _persistentOrderedMap = transient.ToPersistent(); - } - - // --- 1. BUILD --- - - [Benchmark] - public ImmutableDictionary Build_ImmDict() - { - var map = ImmutableDictionary.Empty; - foreach (var k in _allKeys) map = map.Add(k, k); - return map; - } - - [Benchmark] - public ImmutableSortedDictionary Build_ImmSortedDict() - { - var map = ImmutableSortedDictionary.Empty; - foreach (var k in _allKeys) map = map.Add(k, k); - return map; - } - - [Benchmark] - public LanguageExt.Map Build_ExtMap() - { - var map = LanguageExt.Map.empty(); - foreach (var k in _allKeys) map = map.AddOrUpdate(k, k); - return map; - } - - [Benchmark] - public LanguageExt.HashMap Build_ExtHashMap() - { - var map = LanguageExt.HashMap.empty(); - foreach (var k in _allKeys) map = map.AddOrUpdate(k, k); - return map; - } - - [Benchmark] - public PersistentOrderedMap Build_PersistentMap() - { - var map = PersistentOrderedMap.Empty(_intStrategy); - foreach (var k in _allKeys) map = map.Set(k, k); - return map; - } - - [Benchmark] - public PersistentOrderedMap Build_TransientMap() - { - var map = BaseOrderedMap.CreateTransient(_intStrategy); - foreach (var k in _allKeys) map.Set(k, k); - return map.ToPersistent(); - } - - // --- 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_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; - } - - [Benchmark] - public int Retrieve_PersistentMap() - { - int count = 0; - foreach (var k in _retrieveKeys) - if (_persistentOrderedMap.TryGetValue(k, out _)) 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 PersistentOrderedMap Update_PersistentMap() - { - var map = _persistentOrderedMap; - foreach (var k in _updateKeys) map = map.Set(k, 999); - return map; - } - - [Benchmark] - public PersistentOrderedMap Update_TransientMap() - { - var transient = _persistentOrderedMap.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 PersistentOrderedMap UpdateSet_PersistentMap() - { - var map = _persistentOrderedMap; - foreach (var k in _mixedKeys) map = map.Set(k, 999); - return map; - } - - [Benchmark] - public PersistentOrderedMap UpdateSet_TransientMap() - { - var transient = _persistentOrderedMap.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() - { - int sum = 0; - foreach (var kvp in _persistentOrderedMap) 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; - } - - - // --- 6. REMOVAL --- - - [Benchmark] - public ImmutableDictionary Remove_ImmDict() - { - var map = _immDict; - foreach (var k in _removeKeys) map = map.Remove(k); - return map; - } - -[Benchmark] - public PersistentOrderedMap Remove_PersistentMap() - { - var map = _persistentOrderedMap; - foreach (var k in _removeKeys) map = map.Remove(k); - return map; - } - [Benchmark] - public PersistentOrderedMap Remove_TransientMap() - { - var transient = _persistentOrderedMap.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; - } - - - - public static void Main(string[] args) - { - BenchmarkSwitcher - .FromAssembly(typeof(IntMapBenchmarks).Assembly) - .Run(args); - } -} diff --git a/benchmarks/MyBenchMarks/StringBenchmarks.cs b/benchmarks/MyBenchMarks/StringBenchmarks.cs deleted file mode 100644 index f7caa42..0000000 --- a/benchmarks/MyBenchMarks/StringBenchmarks.cs +++ /dev/null @@ -1,451 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using BenchmarkDotNet.Attributes; -using LanguageExt; -using PersistentOrderedMap; -using System.Runtime.CompilerServices; -namespace MapBenchmarks; - -public readonly struct OrdinalComparer : IComparer -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Compare(string? x, string? y) => string.CompareOrdinal(x, y); -} - -[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 PersistentOrderedMap> _persistentOrderedMapStandard; - private PersistentOrderedMap _persistentOrderedMapUnicode; - - private readonly StandardStrategy2 _stdStrategy = new StandardStrategy2(new OrdinalComparer()); - 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); - } - _persistentOrderedMapStandard = transStd.ToPersistent(); - _persistentOrderedMapUnicode = 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 PersistentOrderedMap> 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 PersistentOrderedMap 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 (_persistentOrderedMapStandard.TryGetValue(k, out _)) count++; - return count; - } - - [Benchmark] - public int Retrieve_PersistentMap_Unicode() - { - int count = 0; - foreach (var k in _retrieveKeys) - if (_persistentOrderedMapUnicode.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 PersistentOrderedMap> Update_PersistentMap_Standard() - { - var map = _persistentOrderedMapStandard; - foreach (var k in _updateKeys) map = map.Set(k, 999); - return map; - } - [Benchmark] - public PersistentOrderedMap Update_PersistentMap_Unicode() - { - var map = _persistentOrderedMapUnicode; - foreach (var k in _updateKeys) map = map.Set(k, 999); - return map; - } - - [Benchmark] - public PersistentOrderedMap> Update_TransientMap_Standard() - { - var transient = _persistentOrderedMapStandard.ToTransient(); - foreach (var k in _updateKeys) transient.Set(k, 999); - return transient.ToPersistent(); - } - -[Benchmark] - public PersistentOrderedMap Update_TransientMap_Unicode() - { - var transient = _persistentOrderedMapUnicode.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 PersistentOrderedMap> UpdateSet_PersistentMap_Standard() - { - var map = _persistentOrderedMapStandard; - foreach (var k in _mixedKeys) map = map.Set(k, 999); - return map; - } - -[Benchmark] - public PersistentOrderedMap UpdateSet_PersistentMap_Unicode() - { - var map = _persistentOrderedMapUnicode; - foreach (var k in _mixedKeys) map = map.Set(k, 999); - return map; - } - - [Benchmark] - public PersistentOrderedMap> UpdateSet_TransientMap_Standard() - { - var transient = _persistentOrderedMapStandard.ToTransient(); - foreach (var k in _mixedKeys) transient.Set(k, 999); - return transient.ToPersistent(); - } - -[Benchmark] - public PersistentOrderedMap UpdateSet_TransientMap_Unicode() - { - var transient = _persistentOrderedMapUnicode.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 _persistentOrderedMapStandard) 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 _persistentOrderedMapUnicode) 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 PersistentOrderedMap> Remove_PersistentMap_Standard() - { - var map = _persistentOrderedMapStandard; - foreach (var k in _removeKeys) map = map.Remove(k); - return map; - } - - [Benchmark] - public PersistentOrderedMap Remove_PersistentMap_Unicode() - { - var map = _persistentOrderedMapUnicode; - foreach (var k in _removeKeys) map = map.Remove(k); - return map; - } - - [Benchmark] - public PersistentOrderedMap> Remove_TransientMap_Standard() - { - var transient = _persistentOrderedMapStandard.ToTransient(); - foreach (var k in _removeKeys) transient.Remove(k); - return transient.ToPersistent(); - } - -[Benchmark] - public PersistentOrderedMap Remove_TransientMap_Unicode() - { - var transient = _persistentOrderedMapUnicode.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; - } -}