333 lines
9.6 KiB
C#
333 lines
9.6 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace PersistentOrderedMap;
|
|
|
|
[Flags]
|
|
public enum NodeFlags : byte
|
|
{
|
|
None = 0,
|
|
IsLeaf = 1 << 0,
|
|
IsRoot = 1 << 1,
|
|
HasPrefixes = 1 << 2
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
|
public struct NodeHeader
|
|
{
|
|
// 6 Bytes: OwnerId for Copy-on-Write (CoW)
|
|
public OwnerId Owner;
|
|
|
|
// 1 Byte: Number of items currently used
|
|
public byte Count;
|
|
|
|
// 1 Byte: Type flags (Leaf, Root, etc.)
|
|
public NodeFlags Flags;
|
|
|
|
public NodeHeader(OwnerId owner, byte count, NodeFlags flags)
|
|
{
|
|
Owner = owner;
|
|
Count = count;
|
|
Flags = flags;
|
|
}
|
|
}
|
|
|
|
[InlineArray(32)]
|
|
public struct KeyBuffer<K>
|
|
{
|
|
private K _element0;
|
|
}
|
|
|
|
// Constraint: Internal Nodes fixed at 32 children.
|
|
// This removes the need for a separate array allocation for children references.
|
|
[InlineArray(32)]
|
|
public struct NodeBuffer<V>
|
|
{
|
|
private Node<V>? _element0;
|
|
}
|
|
|
|
[InlineArray(32)]
|
|
internal struct InternalPrefixBuffer
|
|
{
|
|
private long _element0;
|
|
}
|
|
|
|
public abstract class Node<K>
|
|
{
|
|
public NodeHeader Header;
|
|
|
|
protected Node(OwnerId owner, NodeFlags flags)
|
|
{
|
|
Header = new NodeHeader(owner, 0, flags);
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
public bool IsLeaf => (Header.Flags & NodeFlags.IsLeaf) != 0;
|
|
|
|
public abstract Node<K> EnsureEditable(OwnerId transactionId);
|
|
|
|
public void SetCount(int newCount) => Header.Count = (byte)newCount;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public LeafNode<K, V> AsLeaf<V>()
|
|
{
|
|
// Zero-overhead cast. Assumes you checked IsLeaf or know logic flow.
|
|
return Unsafe.As<LeafNode<K, V>>(this);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public InternalNode<K> AsInternal()
|
|
{
|
|
// Zero-overhead cast. Assumes you checked !IsLeaf or know logic flow.
|
|
return Unsafe.As<InternalNode<K>>(this);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public PrefixInternalNode<K> AsPrefixInternal()
|
|
{
|
|
return Unsafe.As<PrefixInternalNode<K>>(this);
|
|
}
|
|
}
|
|
|
|
public sealed class LeafNode<K, V> : Node<K>
|
|
{
|
|
public const int Capacity = 64;
|
|
public const int MergeThreshold = 8;
|
|
|
|
public K[]? Keys;
|
|
public V[] Values;
|
|
|
|
internal long[]? _prefixes;
|
|
|
|
public override Span<long> AllPrefixes => _prefixes != null ? _prefixes : Span<long>.Empty;
|
|
|
|
public LeafNode(OwnerId owner, bool usePrefixes) : base(owner, NodeFlags.IsLeaf | (usePrefixes ? NodeFlags.HasPrefixes : NodeFlags.None))
|
|
{
|
|
Keys = new K[Capacity];
|
|
Values = new V[Capacity];
|
|
if (usePrefixes)
|
|
{
|
|
_prefixes = new long[Capacity];
|
|
}
|
|
}
|
|
|
|
// Copy Constructor for CoW
|
|
private LeafNode(LeafNode<K, V> original, OwnerId newOwner)
|
|
: base(newOwner, original.Header.Flags)
|
|
{
|
|
Keys = new K[Capacity];
|
|
Values = new V[Capacity];
|
|
Header.Count = original.Header.Count; _prefixes = new long[Capacity];
|
|
|
|
// Copy data
|
|
Array.Copy(original.Keys, Keys, original.Header.Count);
|
|
Array.Copy(original.Values, Values, original.Header.Count);
|
|
if (original._prefixes != null)
|
|
Array.Copy(original._prefixes, _prefixes, original.Header.Count);
|
|
}
|
|
|
|
public override Node<K> EnsureEditable(OwnerId transactionId)
|
|
{
|
|
// CASE 1: Persistent Mode (transactionId is None).
|
|
// We MUST create a copy, because we cannot distinguish "Shared Immutable Node (0)"
|
|
// from "New Mutable Node (0)" based on ID alone.
|
|
// However, since BTreeFunctions only calls this once before descending,
|
|
// we won't copy the same fresh node twice.
|
|
if (transactionId == OwnerId.None)
|
|
{
|
|
return new LeafNode<K, V>(this, OwnerId.None);
|
|
}
|
|
|
|
// CASE 2: Transient Mode.
|
|
// If we own the node, return it.
|
|
if (Header.Owner == transactionId)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
// CASE 3: CoW needed (Ownership mismatch).
|
|
return new LeafNode<K, V>(this, transactionId);
|
|
}
|
|
|
|
public override Span<K> GetKeys()
|
|
{
|
|
return Keys.AsSpan(0, Header.Count);
|
|
}
|
|
|
|
public Span<V> GetValues()
|
|
{
|
|
return Values.AsSpan(0, Header.Count);
|
|
}
|
|
}
|
|
|
|
public class InternalNode<K> : Node<K>
|
|
{
|
|
public const int Capacity = 32;
|
|
|
|
public KeyBuffer<K> Keys;
|
|
public NodeBuffer<K> Children;
|
|
|
|
public override Span<long> AllPrefixes => Span<long>.Empty;
|
|
|
|
public InternalNode(OwnerId owner, NodeFlags flags = NodeFlags.None)
|
|
: base(owner, flags)
|
|
{
|
|
}
|
|
|
|
// Fixed CoW Constructor
|
|
protected InternalNode(InternalNode<K> original, OwnerId newOwner, NodeFlags flags)
|
|
: base(newOwner, flags)
|
|
{
|
|
Header.Count = original.Header.Count;
|
|
|
|
// Fast struct blit for both Keys and Children.
|
|
// No loop required for InlineArrays!
|
|
this.Keys = original.Keys;
|
|
this.Children = original.Children;
|
|
}
|
|
|
|
// The missing method needed by BTreeFunctions for routing
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Span<Node<K>> GetChildren()
|
|
{
|
|
// An internal node always has (Count + 1) children
|
|
return MemoryMarshal.CreateSpan(ref Children[0], Header.Count + 1);
|
|
}
|
|
|
|
public override Span<K> GetKeys() => MemoryMarshal.CreateSpan(ref Keys[0], Header.Count);
|
|
|
|
public override Node<K> EnsureEditable(OwnerId transactionId)
|
|
{
|
|
if (transactionId == OwnerId.None) return new InternalNode<K>(this, OwnerId.None, Header.Flags);
|
|
if (Header.Owner == transactionId) return this;
|
|
return new InternalNode<K>(this, transactionId, Header.Flags);
|
|
}
|
|
}
|
|
|
|
|
|
public sealed class PrefixInternalNode<K> : InternalNode<K>
|
|
{
|
|
internal InternalPrefixBuffer _prefixBuffer;
|
|
|
|
public override Span<long> AllPrefixes => MemoryMarshal.CreateSpan(ref _prefixBuffer[0], Capacity);
|
|
|
|
public PrefixInternalNode(OwnerId owner)
|
|
: base(owner, NodeFlags.HasPrefixes)
|
|
{
|
|
}
|
|
|
|
// CoW Constructor
|
|
private PrefixInternalNode(PrefixInternalNode<K> original, OwnerId newOwner)
|
|
: base(original, newOwner, original.Header.Flags)
|
|
{
|
|
// Copy the base Keys and Children, then blit the prefix buffer
|
|
this._prefixBuffer = original._prefixBuffer;
|
|
}
|
|
|
|
public override Node<K> EnsureEditable(OwnerId transactionId)
|
|
{
|
|
if (transactionId == OwnerId.None) return new PrefixInternalNode<K>(this, OwnerId.None);
|
|
if (Header.Owner == transactionId) return this;
|
|
return new PrefixInternalNode<K>(this, transactionId);
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Auto, Pack = 1)]
|
|
public readonly struct OwnerId(uint id, ushort gen) : IEquatable<OwnerId>
|
|
{
|
|
private const int BatchSize = 100;
|
|
|
|
// The max of allocated IDs globally.
|
|
// Starts at 0, so the first batch reserves IDs 1 to 100.
|
|
private static long _globalHighWaterMark;
|
|
|
|
|
|
// These fields are unique to each thread. They initialize to 0/default.
|
|
// The current ID value this thread is handing out.
|
|
[ThreadStatic] private static long _localCurrentId;
|
|
|
|
// How many IDs are left in this thread's current batch.
|
|
[ThreadStatic] private static int _localRemaining;
|
|
|
|
// ---------------------------------------------------------
|
|
// Instance Data (6 Bytes)
|
|
// ---------------------------------------------------------
|
|
private readonly uint Id = id; // 4 bytes
|
|
private readonly ushort Gen = gen; // 2 bytes
|
|
|
|
/// <summary>
|
|
/// Generates the next unique OwnerId.
|
|
/// mostly non-blocking (thread-local), hits Interlocked only once per 100 IDs.
|
|
/// </summary>
|
|
public static OwnerId Next()
|
|
{
|
|
// We have IDs remaining in our local batch.
|
|
// This executes with zero locking overhead.
|
|
if (_localRemaining > 0)
|
|
{
|
|
_localRemaining--;
|
|
var val = ++_localCurrentId;
|
|
return new OwnerId((uint)val, (ushort)(val >> 32));
|
|
}
|
|
|
|
// SLOW PATH: We ran out (or this is the thread's first call).
|
|
return NextBatch();
|
|
}
|
|
|
|
private static OwnerId NextBatch()
|
|
{
|
|
// Atomically reserve a new block of IDs from the global counter.
|
|
// Only one thread contends for this cache line at a time.
|
|
var reservedEnd = Interlocked.Add(ref _globalHighWaterMark, BatchSize);
|
|
|
|
// Calculate the start of our new range.
|
|
var reservedStart = reservedEnd - BatchSize + 1;
|
|
|
|
// Reset the local cache.
|
|
// We set _localCurrentId to (start - 1) so that the first increment
|
|
// inside the logic below lands exactly on 'reservedStart'.
|
|
_localCurrentId = reservedStart - 1;
|
|
_localRemaining = BatchSize;
|
|
|
|
// Perform the generation logic (same as Fast Path)
|
|
_localRemaining--;
|
|
var val = ++_localCurrentId;
|
|
return new OwnerId((uint)val, (ushort)(val >> 32));
|
|
}
|
|
|
|
public static readonly OwnerId None = new(0, 0);
|
|
|
|
public bool IsNone => Id == 0 && Gen == 0;
|
|
|
|
public bool Equals(OwnerId other)
|
|
{
|
|
return Id == other.Id && Gen == other.Gen;
|
|
}
|
|
|
|
public override bool Equals(object? obj)
|
|
{
|
|
return obj is OwnerId other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(Id, Gen);
|
|
}
|
|
|
|
public static bool operator ==(OwnerId left, OwnerId right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
public static bool operator !=(OwnerId left, OwnerId right)
|
|
{
|
|
return !left.Equals(right);
|
|
}
|
|
}
|