From 570a736606ce0bc703772f66e9f1100cee453dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Tue, 21 Apr 2026 08:41:59 +0200 Subject: [PATCH] Refactor key strategies --- PersistentMap/KeyStrategies/DoubleStrategy.cs | 32 +++++++ PersistentMap/KeyStrategies/IntStrategy.cs | 0 PersistentMap/KeyStrategies/PrefixScanner.cs | 95 +++++++++++++++++++ .../KeyStrategies/StandardStrategy.cs | 30 ++++++ 4 files changed, 157 insertions(+) create mode 100644 PersistentMap/KeyStrategies/DoubleStrategy.cs create mode 100644 PersistentMap/KeyStrategies/IntStrategy.cs create mode 100644 PersistentMap/KeyStrategies/PrefixScanner.cs create mode 100644 PersistentMap/KeyStrategies/StandardStrategy.cs diff --git a/PersistentMap/KeyStrategies/DoubleStrategy.cs b/PersistentMap/KeyStrategies/DoubleStrategy.cs new file mode 100644 index 0000000..e41425e --- /dev/null +++ b/PersistentMap/KeyStrategies/DoubleStrategy.cs @@ -0,0 +1,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); + } +} + diff --git a/PersistentMap/KeyStrategies/IntStrategy.cs b/PersistentMap/KeyStrategies/IntStrategy.cs new file mode 100644 index 0000000..e69de29 diff --git a/PersistentMap/KeyStrategies/PrefixScanner.cs b/PersistentMap/KeyStrategies/PrefixScanner.cs new file mode 100644 index 0000000..7ad9de1 --- /dev/null +++ b/PersistentMap/KeyStrategies/PrefixScanner.cs @@ -0,0 +1,95 @@ +/// +/// 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/PersistentMap/KeyStrategies/StandardStrategy.cs b/PersistentMap/KeyStrategies/StandardStrategy.cs new file mode 100644 index 0000000..835b661 --- /dev/null +++ b/PersistentMap/KeyStrategies/StandardStrategy.cs @@ -0,0 +1,30 @@ + +/// +/// 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); + } +}