Updated benchmarks

fixed prefix logic
This commit is contained in:
Linus Björnstam 2026-04-16 10:20:24 +02:00
parent f1488881d3
commit 978d0873dc
4 changed files with 67 additions and 246 deletions

View file

@ -54,7 +54,7 @@ public static Node<K> Set<K, V>(Node<K> root, K key, V value, IKeyStrategy<K> 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<K>
}
Span<K> 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<K, int>(ref startK);
// 3. Create new Span<int> manually
var intKeys = MemoryMarshal.CreateSpan(ref startInt, keys.Length);
// 4. Run SIMD Search
return SearchNumericKeysSIMD(intKeys, Unsafe.As<K, int>(ref key));
}
else if (typeof(K) == typeof(long))
{
ref K startK = ref MemoryMarshal.GetReference(keys);
ref long startLong = ref Unsafe.As<K, long>(ref startK);
var longKeys = MemoryMarshal.CreateSpan(ref startLong, keys.Length);
return SearchNumericKeysSIMD(longKeys, Unsafe.As<K, long>(ref key));
}
return LinearSearchKeys(node.GetKeys(), key, strategy);
@ -259,25 +233,6 @@ where TStrategy : IKeyStrategy<K>
{
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<K, int>(ref startK);
var intKeys = MemoryMarshal.CreateSpan(ref startInt, node.GetKeys().Length);
return SearchNumericRoutingSIMD(intKeys, Unsafe.As<K, int>(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<K, long>(ref startK);
var longKeys = MemoryMarshal.CreateSpan(ref startLong, node.GetKeys().Length);
return SearchNumericRoutingSIMD(longKeys, Unsafe.As<K, long>(ref key));
}
// C. Fallback
return LinearSearchRouting(node.GetKeys(), key, strategy);
}
@ -292,118 +247,6 @@ where TStrategy : IKeyStrategy<K>
return RefineRouting(index, node.Keys, node.Header.Count, key, strategy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SearchNumericKeysSIMD<T>(Span<T> keys, T key)
where T : struct, INumber<T> // Constraints ensure we deal with values
{
// 1. Vector Setup
int len = keys.Length;
int vectorSize = Vector<T>.Count;
int i = 0;
// Create a vector of [key, key, key...]
Vector<T> vKey = new Vector<T>(key);
// 2. Main SIMD Loop
ref T start = ref MemoryMarshal.GetReference(keys);
while (i <= len - vectorSize)
{
// Load data
Vector<T> vData = Unsafe.ReadUnaligned<Vector<T>>(
ref Unsafe.As<T, byte>(ref Unsafe.Add(ref start, i))
);
// Compare: GreaterThanOrEqual is not directly supported by Vector<T> 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<T> vLessThan = Vector.LessThan(vData, vKey);
// If NOT all are less than key (i.e., some are >= key), we found the block.
if (vLessThan != Vector<T>.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<T>.Default.Compare(Unsafe.Add(ref start, i + j), key) >= 0)
{
return i + j;
}
}
}
i += vectorSize;
}
// 3. Scalar Cleanup (Tail)
while (i < len)
{
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i), key) >= 0)
{
return i;
}
i++;
}
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SearchNumericRoutingSIMD<T>(Span<T> keys, T key)
where T : struct, INumber<T>
{
if (!Vector<T>.IsSupported) return LinearSearchRouting(keys, key);
int len = keys.Length;
int vectorSize = Vector<T>.Count;
int i = 0;
Vector<T> vKey = new Vector<T>(key);
ref T start = ref MemoryMarshal.GetReference(keys);
while (i <= len - vectorSize)
{
Vector<T> vData = Unsafe.ReadUnaligned<Vector<T>>(
ref Unsafe.As<T, byte>(ref Unsafe.Add(ref start, i))
);
// ROUTING LOGIC: We want STRICTLY GREATER (>).
// Vector.GreaterThan returns -1 (All 1s) for True, 0 for False.
Vector<T> vGreater = Vector.GreaterThan(vData, vKey);
if (vGreater != Vector<T>.Zero)
{
// Found a block with values > key. Find the exact one.
for (int j = 0; j < vectorSize; j++)
{
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i + j), key) > 0)
{
return i + j;
}
}
}
i += vectorSize;
}
// Tail cleanup
while (i < len)
{
if (Comparer<T>.Default.Compare(Unsafe.Add(ref start, i), key) > 0)
{
return i;
}
i++;
}
return i;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LinearSearchRouting<K, TStrategy>(Span<K> keys, K key, TStrategy strategy)
where TStrategy : IKeyStrategy<K>
@ -520,9 +363,12 @@ where TStrategy : IKeyStrategy<K>
// This fails if leaf.Values is a Span<V> 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<K>
// This fails if leaf.Values is a Span<V> 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<K>
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<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint+i];
for(int i=0; i<moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint+i];
}
// Update Counts
@ -591,7 +437,10 @@ where TStrategy : IKeyStrategy<K>
// 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<K>
// 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<K>
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<moveCount; i++) right._prefixes[i] = left._prefixes[splitPoint + 1 + i];
for(int i=0; i<moveCount; i++) right.AllPrefixes[i] = left.AllPrefixes[splitPoint + 1 + i];
// Move Children to Right
// Left has children 0..splitPoint. Right has children splitPoint+1..End
@ -685,7 +534,7 @@ where TStrategy : IKeyStrategy<K>
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<K>
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<K>
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<K>
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<K>
// 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<K>
// 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<K>
// 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<rCount-1; i++) rp[i] = rp[i+1];
rightInternal.SetCount(rCount - 1);
@ -873,7 +728,7 @@ where TStrategy : IKeyStrategy<K>
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<K>
// 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);

View file

@ -15,6 +15,9 @@ public interface IKeyStrategy<K>
long GetPrefix(K key);
bool UsesPrefixes => true;
//
bool IsLossless => false;
}
@ -58,17 +61,20 @@ public struct IntStrategy : IKeyStrategy<int>
[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<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);

View file

@ -49,9 +49,6 @@ internal struct InternalPrefixBuffer
public abstract class Node<K>
{
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<K>
}
public abstract Span<K> GetKeys();
// Abstract access to prefixes regardless of storage backing
public abstract Span<long> AllPrefixes { get; }
public Span<long> Prefixes => AllPrefixes.Slice(0, Header.Count);
// Keep this for Search (Read-Only): it limits the view to valid items only.
public Span<long> Prefixes => _prefixes.AsSpan(0, Header.Count);
public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0;
@ -88,49 +88,32 @@ public sealed class LeafNode<K, V> : Node<K>
{
public const int Capacity = 64;
public const int MergeThreshold = 8;
// Leaf stores Keys and Values
public K[] Keys;
public LeafNode<K, V>? Next; // For range scans
public K[]? Keys;
public V[] Values;
public LeafNode<K, V>? Next;
internal long[]? _prefixes;
public override Span<long> AllPrefixes => _prefixes != null ? _prefixes : Span<long>.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<K, V> 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<K> : Node<K>
{
public const int Capacity = 32;
// Inline buffer for children (no array object overhead)
// InlineArray storage
internal InternalPrefixBuffer _prefixBuffer;
public NodeBuffer<K> Children;
// Internal stores Keys (separators) and Children
public K[] Keys;
public K[]? Keys;
public override Span<long> 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<K> : Node<K>
: 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];
}

View file

@ -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<string, string> _immutableDict;