From 4d87e30b40e096e2680a507586fbcddd12992e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Thu, 16 Apr 2026 19:49:31 +0200 Subject: [PATCH] Add a standard key strategy for maps without prefixe Remove the stupid "next" field --- PersistentMap/KeyStrategies.cs | 55 ++++++++++++++++++++++++++-------- PersistentMap/Nodes.cs | 7 ++--- PersistentMap/Readme.org | 4 ++- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/PersistentMap/KeyStrategies.cs b/PersistentMap/KeyStrategies.cs index 6d5a30e..1a437a8 100644 --- a/PersistentMap/KeyStrategies.cs +++ b/PersistentMap/KeyStrategies.cs @@ -13,14 +13,45 @@ 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)] @@ -33,17 +64,17 @@ public struct UnicodeStrategy : IKeyStrategy // 1. Prepare Buffer (8 bytes) // stackalloc is virtually free (pointer bump) - Span utf8Bytes = stackalloc byte[8]; + 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 _, + 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 @@ -63,7 +94,7 @@ public struct IntStrategy : IKeyStrategy [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, @@ -92,13 +123,13 @@ public struct DoubleStrategy : IKeyStrategy // -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); } } @@ -120,7 +151,7 @@ public static class PrefixScanner //if (targetPrefix == long.MinValue) //{ // return 0; - //} + //} // Fallback for short arrays or unsupported hardware if (!Avx2.IsSupported || prefixes.Length < 4) diff --git a/PersistentMap/Nodes.cs b/PersistentMap/Nodes.cs index 548e07c..37e8533 100644 --- a/PersistentMap/Nodes.cs +++ b/PersistentMap/Nodes.cs @@ -91,7 +91,6 @@ public sealed class LeafNode : Node public K[]? Keys; public V[] Values; - public LeafNode? Next; internal long[]? _prefixes; @@ -111,9 +110,7 @@ public sealed class LeafNode : Node { Keys = new K[Capacity]; Values = new V[Capacity]; - Header.Count = original.Header.Count; - Next = original.Next; - _prefixes = new long[Capacity]; + Header.Count = original.Header.Count; _prefixes = new long[Capacity]; // Copy data Array.Copy(original.Keys, Keys, original.Header.Count); @@ -311,4 +308,4 @@ public readonly struct OwnerId(uint id, ushort gen) : IEquatable { return !left.Equals(right); } -} \ No newline at end of file +} diff --git a/PersistentMap/Readme.org b/PersistentMap/Readme.org index a67ee4a..46352d3 100644 --- a/PersistentMap/Readme.org +++ b/PersistentMap/Readme.org @@ -1,4 +1,4 @@ -* NiceBtree (PersistentMap) +* PersistentMap A high-performance, persistent (Copy-on-Write) B+ Tree implemented in C#. @@ -14,6 +14,8 @@ It is designed for zero-overhead reads, SIMD-accelerated key routing, and alloca ** 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