From 978d0873dc266f9a2b69d86321f86dd5df3ad421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Thu, 16 Apr 2026 10:20:24 +0200 Subject: [PATCH] Updated benchmarks fixed prefix logic --- PersistentMap/BTreeFunctions.cs | 205 +++--------------- PersistentMap/KeyStrategies.cs | 14 +- PersistentMap/Nodes.cs | 90 +++----- .../AgainstImmutableDict/AgainstImmutable.cs | 4 +- 4 files changed, 67 insertions(+), 246 deletions(-) diff --git a/PersistentMap/BTreeFunctions.cs b/PersistentMap/BTreeFunctions.cs index 80f3fe5..ed4da06 100644 --- a/PersistentMap/BTreeFunctions.cs +++ b/PersistentMap/BTreeFunctions.cs @@ -54,7 +54,7 @@ public static Node Set(Node root, K key, V value, IKeyStrategy st newRoot.Children[1] = splitResult.NewNode; newRoot.SetCount(1); if (strategy.UsesPrefixes) - newRoot._prefixes![0] = strategy.GetPrefix(splitResult.Separator); + newRoot.AllPrefixes[0] = strategy.GetPrefix(splitResult.Separator); return newRoot; } @@ -194,32 +194,6 @@ where TStrategy : IKeyStrategy } Span keys = node.GetKeys(); - - // --------------------------------------------------------- - // COMPILE-TIME DISPATCH (INT) - // --------------------------------------------------------- - if (typeof(K) == typeof(int)) - { - // 1. Get pointer to start of keys - ref K startK = ref MemoryMarshal.GetReference(keys); - - // 2. Cast pointer to int (Bypasses 'struct' constraint) - ref int startInt = ref Unsafe.As(ref startK); - - // 3. Create new Span manually - var intKeys = MemoryMarshal.CreateSpan(ref startInt, keys.Length); - - // 4. Run SIMD Search - return SearchNumericKeysSIMD(intKeys, Unsafe.As(ref key)); - } - else if (typeof(K) == typeof(long)) - { - ref K startK = ref MemoryMarshal.GetReference(keys); - ref long startLong = ref Unsafe.As(ref startK); - var longKeys = MemoryMarshal.CreateSpan(ref startLong, keys.Length); - - return SearchNumericKeysSIMD(longKeys, Unsafe.As(ref key)); - } return LinearSearchKeys(node.GetKeys(), key, strategy); @@ -259,25 +233,6 @@ where TStrategy : IKeyStrategy { if (!strategy.UsesPrefixes) { - // A. Optimize for INT - if (typeof(K) == typeof(int)) - { - ref K startK = ref MemoryMarshal.GetReference(node.GetKeys()); - ref int startInt = ref Unsafe.As(ref startK); - var intKeys = MemoryMarshal.CreateSpan(ref startInt, node.GetKeys().Length); - - return SearchNumericRoutingSIMD(intKeys, Unsafe.As(ref key)); - } - // B. Optimize for LONG (or Double via bit-casting) - else if (typeof(K) == typeof(long)) - { - ref K startK = ref MemoryMarshal.GetReference(node.GetKeys()); - ref long startLong = ref Unsafe.As(ref startK); - var longKeys = MemoryMarshal.CreateSpan(ref startLong, node.GetKeys().Length); - - return SearchNumericRoutingSIMD(longKeys, Unsafe.As(ref key)); - } - // C. Fallback return LinearSearchRouting(node.GetKeys(), key, strategy); } @@ -292,118 +247,6 @@ where TStrategy : IKeyStrategy return RefineRouting(index, node.Keys, node.Header.Count, key, strategy); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int SearchNumericKeysSIMD(Span keys, T key) - where T : struct, INumber // Constraints ensure we deal with values - { - // 1. Vector Setup - int len = keys.Length; - int vectorSize = Vector.Count; - int i = 0; - - // Create a vector of [key, key, key...] - Vector vKey = new Vector(key); - - // 2. Main SIMD Loop - ref T start = ref MemoryMarshal.GetReference(keys); - - while (i <= len - vectorSize) - { - // Load data - Vector vData = Unsafe.ReadUnaligned>( - ref Unsafe.As(ref Unsafe.Add(ref start, i)) - ); - - // Compare: GreaterThanOrEqual is not directly supported by Vector on all hardware, - // but LessThan IS. So we invert: !(Data < Key) - // Wait! We want First GreaterOrEqual. - // Sorted array: [10, 20, 30, 40]. Search 25. - // 10 < 25 (True), 20 < 25 (True), 30 < 25 (False), 40 < 25 (False). - // The first "False" is our target. - - Vector vLessThan = Vector.LessThan(vData, vKey); - - // If NOT all are less than key (i.e., some are >= key), we found the block. - if (vLessThan != Vector.One) // Vector.One is all bits set (True) - { - // Iterate this small block to find the exact index - // (There are fancier bit-twiddling ways, but a tight loop over 4-8 items is instant) - for (int j = 0; j < vectorSize; j++) - { - // Re-check locally - if (Comparer.Default.Compare(Unsafe.Add(ref start, i + j), key) >= 0) - { - return i + j; - } - } - } - - i += vectorSize; - } - - // 3. Scalar Cleanup (Tail) - while (i < len) - { - if (Comparer.Default.Compare(Unsafe.Add(ref start, i), key) >= 0) - { - return i; - } - i++; - } - - return i; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int SearchNumericRoutingSIMD(Span keys, T key) - where T : struct, INumber - { - if (!Vector.IsSupported) return LinearSearchRouting(keys, key); - - int len = keys.Length; - int vectorSize = Vector.Count; - int i = 0; - Vector vKey = new Vector(key); - - ref T start = ref MemoryMarshal.GetReference(keys); - - while (i <= len - vectorSize) - { - Vector vData = Unsafe.ReadUnaligned>( - ref Unsafe.As(ref Unsafe.Add(ref start, i)) - ); - - // ROUTING LOGIC: We want STRICTLY GREATER (>). - // Vector.GreaterThan returns -1 (All 1s) for True, 0 for False. - Vector vGreater = Vector.GreaterThan(vData, vKey); - - if (vGreater != Vector.Zero) - { - // Found a block with values > key. Find the exact one. - for (int j = 0; j < vectorSize; j++) - { - if (Comparer.Default.Compare(Unsafe.Add(ref start, i + j), key) > 0) - { - return i + j; - } - } - } - i += vectorSize; - } - - - - // Tail cleanup - while (i < len) - { - if (Comparer.Default.Compare(Unsafe.Add(ref start, i), key) > 0) - { - return i; - } - i++; - } - return i; - } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int LinearSearchRouting(Span keys, K key, TStrategy strategy) where TStrategy : IKeyStrategy @@ -520,9 +363,12 @@ where TStrategy : IKeyStrategy // This fails if leaf.Values is a Span of length 'count' Array.Copy(leaf.Values, index, leaf.Values, index + 1, count - index); - + if (strategy.UsesPrefixes) - Array.Copy(leaf._prefixes!, index, leaf._prefixes!, index + 1, count - index); + { + leaf.AllPrefixes.Slice(index, count-index) + .CopyTo(leaf.AllPrefixes.Slice(index+1)); + } } leaf.Keys[index] = key; @@ -530,7 +376,7 @@ where TStrategy : IKeyStrategy // This fails if leaf.Values is a Span of length 'count' leaf.Values[index] = value; if (strategy.UsesPrefixes) - leaf._prefixes![index] = strategy.GetPrefix(key); + leaf.AllPrefixes![index] = strategy.GetPrefix(key); leaf.SetCount(count + 1); } @@ -554,7 +400,7 @@ where TStrategy : IKeyStrategy Array.Copy(left.Values, splitPoint, right.Values, 0, moveCount); // Manually copy prefixes if needed or re-calculate if (strategy.UsesPrefixes) - for(int i=0; i // FIX: Shift raw prefix array if (strategy.UsesPrefixes) - Array.Copy(node._prefixes!, index, node._prefixes!, index + 1, count - index); + { + node.AllPrefixes.Slice(index, count-index) + .CopyTo(node.AllPrefixes.Slice(index +1)); + } } // Shift Children @@ -607,7 +456,7 @@ where TStrategy : IKeyStrategy // FIX: Write to raw array if (strategy.UsesPrefixes) - node._prefixes![index] = strategy.GetPrefix(separator); + node.AllPrefixes![index] = strategy.GetPrefix(separator); node.Children[index + 1] = newChild; node.SetCount(count + 1); @@ -629,7 +478,7 @@ where TStrategy : IKeyStrategy int moveCount = count - splitPoint - 1; // -1 because splitPoint key goes up Array.Copy(left.Keys, splitPoint + 1, right.Keys, 0, moveCount); if (strategy.UsesPrefixes) - for(int i=0; i if (strategy.UsesPrefixes) { - var p = leaf._prefixes; + var p = leaf.AllPrefixes; for (int i = index; i < count - 1; i++) p[i] = p[i + 1]; } @@ -759,7 +608,10 @@ where TStrategy : IKeyStrategy Array.Copy(rightLeaf.Keys, 0, leftLeaf.Keys, lCount, rCount); Array.Copy(rightLeaf.Values, 0, leftLeaf.Values, lCount, rCount); if (strategy.UsesPrefixes) - Array.Copy(rightLeaf._prefixes, 0, leftLeaf._prefixes, lCount, rCount); + { + rightLeaf.AllPrefixes.Slice(0, rCount) + .CopyTo(leftLeaf.AllPrefixes.Slice(lCount)); + } leftLeaf.SetCount(lCount + rCount); leftLeaf.Next = rightLeaf.Next; @@ -776,12 +628,15 @@ where TStrategy : IKeyStrategy int lCount = leftInternal.Header.Count; leftInternal.Keys[lCount] = separator; if (strategy.UsesPrefixes) - leftInternal._prefixes[lCount] = strategy.GetPrefix(separator); + leftInternal.AllPrefixes[lCount] = strategy.GetPrefix(separator); int rCount = rightInternal.Header.Count; Array.Copy(rightInternal.Keys, 0, leftInternal.Keys, lCount + 1, rCount); if (strategy.UsesPrefixes) - Array.Copy(rightInternal._prefixes, 0, leftInternal._prefixes, lCount + 1, rCount); + { + rightInternal.AllPrefixes.Slice(0, rCount) + .CopyTo(leftInternal.AllPrefixes.Slice(lCount + 1)); + } for (int i = 0; i <= rCount; i++) { @@ -796,7 +651,7 @@ where TStrategy : IKeyStrategy Array.Copy(parent.Keys, separatorIndex + 1, parent.Keys, separatorIndex, pCount - separatorIndex - 1); if (strategy.UsesPrefixes) { - var pp = parent._prefixes; + var pp = parent.AllPrefixes; for (int i = separatorIndex; i < pCount - 1; i++) pp[i] = pp[i + 1]; } @@ -824,7 +679,7 @@ where TStrategy : IKeyStrategy // Update Parent Separator parent.Keys[separatorIndex] = rightLeaf.Keys[0]; if (strategy.UsesPrefixes) - parent._prefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); + parent.AllPrefixes[separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); } else { @@ -838,7 +693,7 @@ where TStrategy : IKeyStrategy // 2. Move Right[0] Key to Parent parent.Keys[separatorIndex] = rightInternal.Keys[0]; if (strategy.UsesPrefixes) - parent._prefixes[separatorIndex] = strategy.GetPrefix(rightInternal.Keys[0]); + 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. @@ -851,7 +706,7 @@ where TStrategy : IKeyStrategy // Shift keys Array.Copy(rightInternal.Keys, 1, rightInternal.Keys, 0, rCount - 1); - var rp = rightInternal._prefixes; + var rp = rightInternal.AllPrefixes; for(int i=0; i parent.Keys[separatorIndex] = rightLeaf.Keys[0]; if (strategy.UsesPrefixes) - parent._prefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); + parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(rightLeaf.Keys[0]); } else { @@ -889,7 +744,7 @@ where TStrategy : IKeyStrategy // 2. Move Left[last] Key to Parent parent.Keys[separatorIndex] = leftInternal.Keys[last]; if (strategy.UsesPrefixes) - parent._prefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); + parent.AllPrefixes![separatorIndex] = strategy.GetPrefix(leftInternal.Keys[last]); // 3. Truncate Left leftInternal.SetCount(last); diff --git a/PersistentMap/KeyStrategies.cs b/PersistentMap/KeyStrategies.cs index 3c4e173..e0af3a2 100644 --- a/PersistentMap/KeyStrategies.cs +++ b/PersistentMap/KeyStrategies.cs @@ -15,6 +15,9 @@ public interface IKeyStrategy long GetPrefix(K key); bool UsesPrefixes => true; + + // + bool IsLossless => false; } @@ -58,17 +61,20 @@ public struct IntStrategy : IKeyStrategy [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Compare(int x, int y) => x.CompareTo(y); - public bool UsesPrefixes => false; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public long GetPrefix(int key) - { - return 0; + { + // 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); diff --git a/PersistentMap/Nodes.cs b/PersistentMap/Nodes.cs index 1ff7e2b..548e07c 100644 --- a/PersistentMap/Nodes.cs +++ b/PersistentMap/Nodes.cs @@ -49,9 +49,6 @@ internal struct InternalPrefixBuffer public abstract class Node { public NodeHeader Header; - - // FIX: Change to 'internal' so BTreeFunctions can shift the array directly. - internal long[]? _prefixes; protected Node(OwnerId owner, NodeFlags flags) { @@ -59,9 +56,12 @@ public abstract class Node } public abstract Span GetKeys(); + + // Abstract access to prefixes regardless of storage backing + public abstract Span AllPrefixes { get; } + + public Span Prefixes => AllPrefixes.Slice(0, Header.Count); - // Keep this for Search (Read-Only): it limits the view to valid items only. - public Span Prefixes => _prefixes.AsSpan(0, Header.Count); public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0; @@ -88,49 +88,32 @@ public sealed class LeafNode : Node { public const int Capacity = 64; public const int MergeThreshold = 8; - - // Leaf stores Keys and Values - public K[] Keys; - public LeafNode? Next; // For range scans + public K[]? Keys; public V[] Values; + public LeafNode? Next; + internal long[]? _prefixes; + + public override Span AllPrefixes => _prefixes != null ? _prefixes : Span.Empty; + public LeafNode(OwnerId owner) : base(owner, NodeFlags.IsLeaf | NodeFlags.HasPrefixes) { Keys = new K[Capacity]; Values = new V[Capacity]; - if (typeof(K) == typeof(int) - || typeof(K) == typeof(long)) - { - _prefixes = null; - } - else - { - _prefixes = new long[Capacity]; - } + _prefixes = new long[Capacity]; + } // Copy Constructor for CoW private LeafNode(LeafNode original, OwnerId newOwner) : base(newOwner, original.Header.Flags) { - Header.Count = original.Header.Count; - Next = original.Next; - - // Allocate new arrays Keys = new K[Capacity]; Values = new V[Capacity]; - - if (typeof(K) == typeof(int) - || typeof(K) == typeof(long)) - { - _prefixes = null; - } - else - { - - _prefixes = new long[Capacity]; - } + Header.Count = original.Header.Count; + Next = original.Next; + _prefixes = new long[Capacity]; // Copy data Array.Copy(original.Keys, Keys, original.Header.Count); @@ -177,25 +160,17 @@ public sealed class InternalNode : Node { public const int Capacity = 32; - // Inline buffer for children (no array object overhead) + // InlineArray storage + internal InternalPrefixBuffer _prefixBuffer; public NodeBuffer Children; - - // Internal stores Keys (separators) and Children - public K[] Keys; + + public K[]? Keys; + + public override Span AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity); public InternalNode(OwnerId owner) : base(owner, NodeFlags.HasPrefixes) { Keys = new K[Capacity]; - if (typeof(K) == typeof(int) - || typeof(K) == typeof(long)) - { - _prefixes = null; - } - else - { - - _prefixes = new long[Capacity]; - } // Children buffer is a struct, zero-initialized by default } @@ -204,27 +179,12 @@ public sealed class InternalNode : Node : base(newOwner, original.Header.Flags) { Header.Count = original.Header.Count; - Keys = new K[Capacity]; - if (typeof(K) == typeof(int) - || typeof(K) == typeof(long)) - { - - _prefixes = null; - } - else - { - - _prefixes = new long[Capacity]; - } - - // Copy Keys and Prefixes Array.Copy(original.Keys, Keys, original.Header.Count); - if (original._prefixes != null) - Array.Copy(original._prefixes, _prefixes, original.Header.Count); + + // Fast struct blit for prefixes + this._prefixBuffer = original._prefixBuffer; - // Copy Children (Manual loop required for InlineArray in generic context usually, - // but here we can iterate the span) var srcChildren = original.GetChildren(); for (var i = 0; i < srcChildren.Length; i++) Children[i] = srcChildren[i]; } diff --git a/benchmarks/AgainstImmutableDict/AgainstImmutable.cs b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs index 007ee37..269355f 100644 --- a/benchmarks/AgainstImmutableDict/AgainstImmutable.cs +++ b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs @@ -13,10 +13,10 @@ using PersistentMap; [MemoryDiagnoser] public class ImmutableBenchmark { - [Params(10, 100, 1000)] + [Params(10, 100)] public int N { get; set; } - [Params(1000)] + [Params(10000)] public int CollectionSize { get; set; } private ImmutableDictionary _immutableDict;