Refactor key strategies

This commit is contained in:
Linus Björnstam 2026-04-21 08:41:59 +02:00
parent ae3e8ae8c4
commit 570a736606
4 changed files with 157 additions and 0 deletions

View file

@ -0,0 +1,32 @@
public struct DoubleStrategy : IKeyStrategy<double>
{
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<double, long>(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);
}
}

View file

@ -0,0 +1,95 @@
/// <summary>
/// Helper for SIMD accelerated prefix scanning.
/// </summary>
public static class PrefixScanner
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int FindFirstGreaterOrEqual(ReadOnlySpan<long> 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<long> 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<long> 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<long> 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<long>.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;
}
}

View file

@ -0,0 +1,30 @@
/// <summary>
/// A universal key strategy for any type that relies on standard comparisons
/// (IComparable, IComparer, or custom StringComparers) without SIMD prefixes.
/// </summary>
public readonly struct StandardStrategy<K> : IKeyStrategy<K>
{
private readonly IComparer<K> _comparer;
// If no comparer is provided, it defaults to Comparer<K>.Default
// which automatically uses IComparable<K> if the type implements it.
public StandardStrategy(IComparer<K>? comparer = null)
{
_comparer = comparer ?? Comparer<K>.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);
}
}