Using new file structure. Use new regexir display method. Fix line dependency error in prefix analysing.

Signed-off-by: Nitload <YoumuLiu_NSI@whv0t.onmicrosoft.com>
This commit is contained in:
Nitload
2026-07-12 02:04:57 +08:00
parent ae8a09efc2
commit b6cd2fc256
476 changed files with 16440 additions and 502 deletions
@@ -0,0 +1,149 @@
// -----------------------------------------------------------------------------
// file="BufferStack"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace StgSharp.HighPerformance.Memory
{
[CollectionBuilder(typeof(BufferStackBuilder), nameof(BufferStackBuilder.Create))]
public unsafe class BufferStack<T> : IDisposable where T : unmanaged
{
private byte* _buffer;
private ulong _count;
public BufferStack(
int capacity
)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
AllocateBuffer(capacity);
}
public int Count => (int)_count;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
((Header*)_buffer)->Top = 0;
_count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
Marshal.FreeHGlobal((nint)_buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Pop()
{
Header* header = (Header*)_buffer;
if (header->Top == 0) {
throw new InvalidOperationException("Stack underflow");
}
header->Top--;
_count--;
T* dataStart = (T*)(_buffer + sizeof(Header));
return dataStart[header->Top];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Push(
T item
)
{
Header* header = (Header*)_buffer;
if (header->Top >= header->Capacity)
{
Expand();
header = (Header*)_buffer;
}
T* dataStart = (T*)(_buffer + sizeof(Header));
dataStart[header->Top] = item;
header->Top++;
_count++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPop(
out T value
)
{
Header* header = (Header*)_buffer;
if (header->Top == 0)
{
value = default;
return false;
}
header->Top--;
_count--;
T* dataStart = (T*)(_buffer + sizeof(Header));
value = dataStart[header->Top];
return true;
}
// Internal method for efficient bulk initialization
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe void InitializeFromSpan(
ReadOnlySpan<T> span
)
{
if (span.IsEmpty) {
return;
}
Header* header = (Header*)_buffer;
T* dataStart = (T*)(_buffer + sizeof(Header));
fixed (T* sourcePtr = span) {
Buffer.MemoryCopy(sourcePtr, dataStart, header->Capacity * (nuint)sizeof(T),
(nuint)span.Length * (nuint)sizeof(T));
}
header->Top = (nuint)span.Length;
_count = (ulong)span.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AllocateBuffer(
int capacity
)
{
nuint size = (nuint)sizeof(Header) + ((nuint)capacity * (nuint)sizeof(T));
_buffer = (byte*)NativeMemory.Alloc(size);
Header* header = (Header*)_buffer;
header->Top = 0;
header->Capacity = (nuint)capacity;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Expand()
{
Header* header = (Header*)_buffer;
nuint newCap = header->Capacity * 2;
nuint newSize = (nuint)sizeof(Header) + (newCap * (nuint)sizeof(T));
_buffer = (byte*)NativeMemory.Realloc(_buffer, newSize);
header = (Header*)_buffer;
header->Capacity = newCap;
}
private struct Header
{
public nuint Capacity;
public nuint Top;
}
}
}
@@ -0,0 +1,299 @@
// -----------------------------------------------------------------------------
// file="ConcurrentBufferStack"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Threading;
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace StgSharp.HighPerformance.Memory
{
public static class BufferStackBuilder
{
// For BufferStack collection builder
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BufferStack<T> Create<T>(
ReadOnlySpan<T> span
) where T : unmanaged
{
if (span.IsEmpty)
{
return new BufferStack<T>(4); // Default small capacity
}
BufferStack<T> stack = new(span.Length);
// Use internal method for efficient bulk initialization
stack.InitializeFromSpan(span);
return stack;
}
// For ConcurrentBufferStack collection builder
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ConcurrentBufferStack<T> CreateConcurrent<T>(
ReadOnlySpan<T> span
) where T : unmanaged
{
if (span.IsEmpty)
{
return new ConcurrentBufferStack<T>(4); // Default small capacity
}
ConcurrentBufferStack<T> stack = new(span.Length);
// Use internal method for efficient bulk initialization
stack.InitializeFromSpan(span);
return stack;
}
}
[CollectionBuilder(
builderType: typeof(BufferStackBuilder),
methodName: nameof(BufferStackBuilder.CreateConcurrent))]
public unsafe class ConcurrentBufferStack<T> : IDisposable where T : unmanaged
{
private byte* _buffer;
private readonly BufferExpansionLock _lock = new();
private long _count;
public ConcurrentBufferStack()
{
AllocateBuffer(4);
}
public ConcurrentBufferStack(
int capacity
)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
AllocateBuffer(capacity);
}
public ulong Count => (ulong)Volatile.Read(ref _count);
public void Clear()
{
_lock.EnterExpansionProcess();
try
{
_lock.EnterBufferCopy(); // exclusive while mutating Top
try
{
Header* header = (Header*)_buffer;
header->Top = 0;
Volatile.Write(ref _count, 0);
}
finally
{
_lock.ExitBufferCopy();
}
}
finally
{
_lock.ExitExpansionProcess();
}
}
public void Dispose()
{
_lock.Dispose();
NativeMemory.Free(_buffer);
}
public T Pop()
{
while (true)
{
_lock.EnterBufferRead(); // prevent buffer being reallocated while reading/modifying Top
try
{
Header* header = (Header*)_buffer;
int oldTop = Volatile.Read(ref header->Top);
if (oldTop <= 0) {
throw new InvalidOperationException("Stack underflow");
}
if (Interlocked.CompareExchange(ref header->Top, oldTop - 1, oldTop) == oldTop)
{
T* dataStart = (T*)(_buffer + sizeof(Header));
_ = Interlocked.Decrement(ref _count);
return dataStart[oldTop - 1];
}
}
finally
{
_lock.ExitBufferRead();
}
Thread.Sleep(0);
}
}
public void Push(
T item
)
{
while (true)
{
// Ensure capacity, expanding if needed
_lock.EnterMetaDataRead();
try
{
Header* header = (Header*)_buffer;
int topSnapshot = Volatile.Read(ref header->Top);
int cap = Volatile.Read(ref header->Capacity);
if (topSnapshot >= cap)
{
// Need expansion
_lock.ExitMetaDataRead();
_lock.EnterExpansionProcess();
try
{
header = (Header*)_buffer;
if (header->Top >= header->Capacity)
{
int newCap = header->Capacity * 2;
nuint newSize = ((nuint)sizeof(Header)) + (((nuint)newCap) * ((nuint)sizeof(T)));
_lock.EnterBufferCopy();
try
{
_buffer = (byte*)NativeMemory.Realloc(_buffer, newSize);
((Header*)_buffer)->Capacity = newCap;
}
finally
{
_lock.ExitBufferCopy();
}
}
}
finally
{
_lock.ExitExpansionProcess();
}
continue;
}
}
finally
{
if (_lock.IsThreadReadingMetaData) {
_lock.ExitMetaDataRead();
}
}
// Reserve and write under exclusive buffer lock to avoid consumers reading unwritten data
_lock.EnterBufferCopy();
try
{
Header* header = (Header*)_buffer;
int idx = header->Top; // reserve next slot
if (idx >= header->Capacity)
{
// lost race against expander or other producer; retry
continue;
}
T* dataStart = (T*)(_buffer + sizeof(Header));
dataStart[idx] = item; // write first
header->Top = idx + 1; // then publish by increasing top
_ = Interlocked.Increment(ref _count);
return;
}
finally
{
_lock.ExitBufferCopy();
}
}
}
public bool TryPop(
out T value
)
{
while (true)
{
_lock.EnterBufferRead();
try
{
Header* header = (Header*)_buffer;
int oldTop = Volatile.Read(ref header->Top);
if (oldTop <= 0)
{
value = default;
return false;
}
if (Interlocked.CompareExchange(ref header->Top, oldTop - 1, oldTop) == oldTop)
{
T* dataStart = (T*)(_buffer + sizeof(Header));
value = dataStart[oldTop - 1];
_ = Interlocked.Decrement(ref _count);
return true;
}
}
finally
{
_lock.ExitBufferRead();
}
Thread.Sleep(0);
}
}
// Internal method for efficient bulk initialization
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe void InitializeFromSpan(
ReadOnlySpan<T> span
)
{
if (span.IsEmpty) {
return;
}
Header* header = (Header*)_buffer;
T* dataStart = (T*)(_buffer + sizeof(Header));
fixed (T* sourcePtr = span) {
Buffer.MemoryCopy(sourcePtr, dataStart,
((nuint)header->Capacity) * ((nuint)sizeof(T)),
((nuint)span.Length) * ((nuint)sizeof(T)));
}
header->Top = span.Length;
Volatile.Write(ref _count, span.Length);
}
private void AllocateBuffer(
int capacity
)
{
nuint size = ((nuint)sizeof(Header)) + (((nuint)capacity) * ((nuint)sizeof(T)));
_buffer = (byte*)NativeMemory.Alloc(size);
Header* header = (Header*)_buffer;
header->Top = 0;
header->Capacity = capacity;
}
private struct Header
{
public int Capacity; // max capacity of the stack (number of elements)
public int Top; // next available index in the stack
}
}
}
@@ -0,0 +1,159 @@
# TLSF (Two-Layer Segregated Fit) Allocator
This document describes the current StgSharp native arena allocator, `StgSharp.HighPerformance.Memory.TwoLayerSegregatedFitAllocator`. The file name is historical; the implementation described below is TLSF rather than the older HLSF allocator.
## Overview
- Single contiguous native arena allocated up front
- Headerless returned blocks; allocator metadata is stored in 32-byte nodes carved from the tail of the same arena
- Requests rounded up to 64-byte granularity, with 64 bytes as the minimum block size
- Free blocks grouped by coarse size level derived from the highest set bit of the block size
- Exact-level allocation uses first-fit traversal inside the selected bucket; higher levels fall back to the head of the first non-empty bucket
- `Free` performs bounded local coalescing; `Collect` performs full free-run coalescing across the arena
- Single-threaded implementation; concurrent access requires external synchronization
- No arena growth and no implicit fallback to `NativeMemory.AlignedAlloc` for oversized requests
## Quick Use
```csharp
using StgSharp.HighPerformance.Memory;
using var tlsf = new TwoLayerSegregatedFitAllocator(64 * 1024 * 1024);
var handle = tlsf.Alloc(1024);
Span<byte> buffer = handle.AsSpan();
buffer[0] = 42;
tlsf.Free(handle);
```
## Constructors
- `TwoLayerSegregatedFitAllocator(nuint byteSize)`
- `TwoLayerSegregatedFitAllocator(nuint byteSize, uint maxBlockSize)`
- `TwoLayerSegregatedFitAllocator(nuint byteSize, int align)`
- `TwoLayerSegregatedFitAllocator(nuint byteSize, int align, nuint maxBlockSize)`
Behavior summary:
- `byteSize` defines the total arena size
- `maxBlockSize` defaults to 32 MB and defines the largest single request accepted by `Alloc` and `TryAlloc`
- `align` is passed to the backing `NativeMemory.AlignedAlloc` call for arena allocation; request sizes inside the arena are still quantized to 64-byte blocks
## Public API
### `Alloc(nuint size)`
- Rounds `size` up to the next 64-byte block, with a minimum of 64 bytes
- Throws `OverflowException` if the normalized size exceeds `maxBlockSize` or if the arena cannot provide a block
- Returns a `Handle` containing block address and block size
- Splits a sufficiently large tail remainder into a new free node and re-buckets that node
### `TryAlloc(nuint size, out Handle handle)`
- Uses the same size normalization and maximum block limit as `Alloc`
- Returns `false` when the request exceeds the configured limit or when the arena is exhausted
- Does not fall back to the system allocator
### `Free(Handle handle, int collectScanWindow = 4)`
- Marks the block as free
- Scans up to `collectScanWindow` neighbors toward lower addresses and then up to `collectScanWindow` neighbors toward higher addresses
- Merges any contiguous free neighbors found during those scans
- Reclassifies the merged block back into the coarse bucket set
### `Collect()`
- Walks the full position chain from low address to high address
- Merges consecutive free runs without the local scan-window limit used by `Free`
- Restores larger reusable spans and improves bucket coverage after fragmentation-heavy phases
### `Dispose()`
- Releases the backing arena through the stored free callback
### `Handle`
- Readonly value type carrying the allocator metadata reference, block address, and block size
- `Address` exposes the native pointer
- `Size` exposes the normalized block size
- `AsSpan(int range = -1)` exposes a span view over the block
- The indexer provides byte-level access by offset
## Allocation Model
### Level Classification
- `GetLevel(size)` maps the normalized size to `57 - BitOperations.LeadingZeroCount(size)` and clamps the result to `_maxLevel`
- Each bucket corresponds to one coarse size class rather than an exact size
- The target bucket can therefore contain blocks of different sizes within the same coarse level
### Allocation Path
1. Normalize the requested size to a 64-byte multiple.
2. Traverse the target bucket and select the first free block whose size is at least the requested size.
3. If the target bucket has no suitable block, pop the head of the first non-empty higher-level bucket.
4. If all buckets are empty, carve a fresh block from the spare region tracked by the arena sentinel.
5. Split the tail remainder when both remainder size and metadata availability allow it.
### Metadata Layout
- `Metadata` is a 32-byte node
- `NextNear` and `PreviousNear` form the address-ordered position chain
- `NextLevel` and `PreviousLevel` form either the bucket chain or the recycled-metadata chain, depending on node state
- The spare sentinel at the end of the arena tracks remaining virgin space, recycled metadata nodes, and position-chain anchors
- Returned blocks remain headerless from the caller's point of view
### Coalescing Model
- `Free` performs bounded local coalescing to keep the hot path short
- `Collect` performs full coalescing of consecutive free runs across the entire position chain
- Recycled metadata nodes are returned to the spare metadata list and reused by later splits or fresh arena chunks
## Complexity and Runtime Characteristics
- `Alloc`: O(1) bucket index calculation plus target-level first-fit traversal; higher-level fallback is O(1)
- `Free`: O(`collectScanWindow`) local merge cost
- `Collect`: O(number of metadata nodes in the position chain)
- Best steady-state behavior appears on repeated small and medium native allocations inside a pre-allocated arena
## BenchmarkDotNet (TLSF vs Libc)
Environment: .NET 8.0.26 (PerformanceDebug), x64 RyuJIT AVX2, Windows 11, AMD Ryzen 7 6800H, BenchmarkDotNet v0.15.8, InProcessEmitToolchain, LaunchCount=1, WarmupCount=6, IterationCount=15, InvocationCount=3.
Workload summary:
- Arena size: 512 MB
- Arena alignment: 64 bytes
- Allocation count per batch: 2048
- Batch count: 256
- Request set before normalization: 64, 100, 200, 400, 1024, 4096 bytes
- Free order: shuffled per batch
- `Free` local merge window: 4
Best observed run under the current harness:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|------- |-----:|------:|-------:|------:|--------:|----------:|------------:|
| Tlsf | 79.11 ns | 5.944 ns | 5.560 ns | 0.65 | 0.09 | - | NA |
| Libc | 123.64 ns | 15.955 ns | 14.144 ns | 1.01 | 0.16 | - | NA |
Worst observed run under the same harness:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|------- |-----:|------:|-------:|------:|--------:|----------:|------------:|
| Tlsf | 82.95 ns | 6.337 ns | 5.617 ns | 0.81 | 0.07 | - | NA |
| Libc | 102.63 ns | 8.590 ns | 7.173 ns | 1.00 | 0.09 | - | NA |
Interpretation:
- The current single-pass bucket selection fast path materially reduces allocator-side latency
- The reported ratio still drifts across repeated compile-and-run cycles because the libc baseline also moves under the current InProcess harness
- Recent repeated runs place the transient best case near 0.65 and the longer-running steady band near 0.75 to 0.80
## Operational Notes
- Maximum single-allocation size is fixed at construction time
- Arena growth is not implemented
- `Collect` is the global defragmentation pass; `Free` only performs bounded local coalescing
- The allocator is intended for explicit native-memory workflows rather than as a drop-in general-purpose heap replacement
@@ -0,0 +1,97 @@
// -----------------------------------------------------------------------------
// file="L4.CacheLine"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.HighPerformance.ProcessorAbstraction;
using System.Runtime.InteropServices;
namespace StgSharp.HighPerformance.Memory
{
public partial class L4
{
internal enum AllocationState : byte
{
SpareOrCollect = 0,
Free = 1,
Alloc = 2,
}
[StructLayout(LayoutKind.Explicit, Size = 32, Pack = 32)]
private unsafe struct CacheLineMetadata
{
internal const uint MaxBufferSize = 0xFFFFFFFF - 64U;
[FieldOffset(0)] private readonly M256 FillingMask;
[FieldOffset(24)] internal nuint Origin;
public static uint SizeOf { get; } = (uint)Unsafe.SizeOf<CacheLineMetadata>();
#region tlsf linked list
[FieldOffset(0)] public short NextLevel;
[FieldOffset(2)] public short NextNear;
[FieldOffset(4)] public short PreviousLevel;
[FieldOffset(6)] public short PreviousNear;
#endregion
#region buffer
[FieldOffset(8)] public uint PositionMask;
[FieldOffset(12)] public uint SizeMask;
#endregion
#region mask
[FieldOffset(16)] internal AllocationState State;
[FieldOffset(17)] internal byte ReversedProfile;
[FieldOffset(18)] internal ushort CustomizeProfile;
[FieldOffset(20)] internal int RefCount;
#endregion
}
#pragma warning disable IDE0032
[StructLayout(LayoutKind.Explicit)]
public unsafe struct Handle : IDisposable
{
[FieldOffset(8)] private readonly CacheLineMetadata* _head;
[FieldOffset(0)] private nuint _handle;
private Handle(
nuint handle,
CacheLineMetadata* cacheRefCount
)
{
_handle = handle;
_head = cacheRefCount;
}
[Obsolete("You should not create a CacheLineHandle instance outside a L4", true)]
public Handle() { }
public readonly nuint Address => _handle;
public void Dispose()
{
if (_handle == 0) {
return;
}
_handle = 0;
_ = Interlocked.Decrement(ref _head->RefCount);
}
}
#pragma warning restore IDE0032
}
}
@@ -0,0 +1,92 @@
// -----------------------------------------------------------------------------
// file="L4.Debug"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
/**/
public partial class L4
{
public void DebugAgeAll()
{
// AgeAll();
}
public bool DebugContains(
nuint origin
)
{
return _map.TryGet(origin, out _);
}
public int DebugEvict(
int count = 1
)
{
return Evict(count);
}
public unsafe DebugSnapshot GetDebugSnapshot()
{
throw new NotImplementedException();
/*
int maxId = _maxId;
byte[] rrpv = new byte[maxId];
byte* p = (byte*)_eviction;
for (int i = 0; i < maxId; i++) {
rrpv[i] = p[i];
}
return new DebugSnapshot(_activeCount, _aheadCount, maxId, rrpv);
/**/
}
public readonly struct DebugSnapshot
{
/// <summary>
/// RRPV value for each line(0~3), length == MaxId
/// </summary>
public readonly byte[] RrpvSnapshot;
/// <summary>
/// Count of activated cache line handle
/// </summary>
public readonly int ActiveCount;
/// <summary>
/// Count of predictor ahead of current map position(aheadCount)
/// </summary>
public readonly int AheadCount;
/// <summary>
/// Max ID of the line being using or once used(maxId)
/// </summary>
public readonly int MaxId;
internal DebugSnapshot(
int activeCount,
int aheadCount,
int maxId,
byte[] rrpv
)
{
ActiveCount = activeCount;
AheadCount = aheadCount;
MaxId = maxId;
RrpvSnapshot = rrpv;
}
}
}
/**/
}
@@ -0,0 +1,64 @@
// -----------------------------------------------------------------------------
// file="L4.Evict"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
namespace StgSharp.HighPerformance.Memory
{
public partial class L4
{
/// <summary>
///
/// </summary>
/// <param name="count">
///
/// </param>
/// <returns>
/// Count of lines not evicted
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe int Evict(
int count = 1
)
{
count = (count < 0) ? 0 : count;
int* pPtr = _predictCount, mPtr = _mapCount;
int remain = count;
for (int i = 0; i < CacheLineCount; i++)
{
if (pPtr[i] > mPtr[i])
{
continue;
}
remain -= TryEvictAt(i) ? 1 : 0;
}
return remain;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe bool TryEvictAt(
int index
)
{
CacheLineMetadata* head = &_head[index];
if (Interlocked.CompareExchange(ref head->RefCount, int.MinValue, 0) == 0)
{
// EvictLine(i + index);
_ = _map.TryRemove(head->Origin, out _);
IL4Predict predict = CurrentPredict;
predict.WriteBack(head->Origin, head->CustomizeProfile, new ReadOnlySpan<byte>((byte*)(ulong)(head->PositionMask + _baseAddress), (int)(ulong)head->SizeMask));
_bufferAllocator.Free(index);
_mapCount[index] = 0;
_predictCount[index] = int.MaxValue;
return true;
}
return false;
}
}
}
@@ -0,0 +1,34 @@
// -----------------------------------------------------------------------------
// file="L4.Exceptions"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
namespace StgSharp.HighPerformance.Memory
{
#pragma warning disable CA1032 // Implement standard exception constructors
public sealed class L4PredictUnregisteredException : Exception
{
public L4PredictUnregisteredException(
int id
)
: base($"L4 predictor with id {id} is null. Maybe it has been unregistered.") { }
}
public sealed class L4PredictConflictException : Exception
{
public L4PredictConflictException(
nuint basePtr,
ushort policy,
int current
)
: base($"L4 predictor with id {current} has already registered a cache line with base pointer {basePtr} and policy {policy}.") { }
}
#pragma warning restore CA1032 // Implement standard exception constructors
}
@@ -0,0 +1,38 @@
// -----------------------------------------------------------------------------
// file="L4.Predict"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Collections;
using StgSharp.HighPerformance.Memory;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
public partial class L4
{
private IL4Predict CurrentPredict { get; set; }
public void ResetPredict(
IL4Predict predict
)
{
if (CurrentPredict is null)
{
CurrentPredict = predict;
return;
}
// TODO: write back all and register new predictor
}
}
}
@@ -0,0 +1,146 @@
// -----------------------------------------------------------------------------
// file="L4.Predictor"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
[Flags]
public enum PredictResult
{
Success = 1,
Exhausted = 2,
}
public interface IL4Predict
{
PredictResult Predict(
Span<CacheLineDescription> node,
out int count
);
/// <summary>
/// Materializes the cache line identified by <paramref name="origin" /> into <paramref ///
/// name="cache" />.
/// </summary>
/// <param name="origin">
/// Source address associated with the cache line.
/// </param>
/// <param name="policy">
/// Mapping policy associated with the cache line.
/// </param>
/// <param name="cache">
/// Destination cache line buffer.
/// </param>
void Prefetch(
nuint origin,
uint policy,
Span<byte> cache
);
/// <summary>
/// Writes the cache line in <paramref name="cache" /> back to <paramref name="origin" />.
/// </summary>
/// <param name="origin">
/// Source address associated with the cache line.
/// </param>
/// <param name="policy">
/// Mapping policy associated with the cache line.
/// </param>
/// <param name="cache">
/// Source cache line buffer.
/// </param>
void WriteBack(
nuint origin,
uint policy,
ReadOnlySpan<byte> cache
);
}
[StructLayout(LayoutKind.Explicit)]
public struct CacheLineDescription : IEquatable<CacheLineDescription>
{
public override readonly bool Equals(
object? obj
)
{
return (obj is CacheLineDescription prediction) &&
Address.Equals(prediction.Address) &&
(MapPolicy == prediction.MapPolicy);
}
public readonly bool Equals(
CacheLineDescription other
)
{
return
(Address == other.Address) && (MapPolicy == other.MapPolicy);
}
public override readonly int GetHashCode()
{
return (int)ComposePredictionKey64(Address, MapPolicy);
}
public readonly nuint GetHashCodeLong()
{
return ComposePredictionKey64(Address, MapPolicy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint ComposePredictionKey64(
nuint address,
ushort mapPolicy
)
{
nuint x = address;
nuint y = mapPolicy;
unchecked
{
nuint mixed = x ^ (y * ((nuint)0x9e3779b97f4a7c15UL));
mixed ^= mixed >> 30;
mixed *= (nuint)0xbf58476d1ce4e5b9UL;
mixed ^= mixed >> 27;
mixed *= (nuint)0x94d049bb133111ebUL;
mixed ^= mixed >> 31;
return mixed;
}
}
public static bool operator !=(
CacheLineDescription left,
CacheLineDescription right
)
{
return !(left == right);
}
public static bool operator ==(
CacheLineDescription left,
CacheLineDescription right
)
{
return left.Equals(right);
}
#pragma warning disable CA1051
[FieldOffset(0)]public nuint Address;
[FieldOffset(8)]public uint Size;
[FieldOffset(12)]public ushort MapPolicy;
[FieldOffset(14)]internal short Predictor;
#pragma warning restore CA1051
}
}
@@ -0,0 +1,102 @@
// -----------------------------------------------------------------------------
// file="L4.Prefetch"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System.ComponentModel;
using System.Numerics;
namespace StgSharp.HighPerformance.Memory
{
public unsafe partial class L4
{
private partial void Prefetch()
{
if (CurrentPredict is null) {
return;
}
IL4Predict predict = CurrentPredict;
Span<CacheLineDescription> span = new(_prediction, PredictionCount);
PredictResult result = predict.Predict(span, out int actualCount);
_exhausted = (result & PredictResult.Exhausted) != 0;
if (actualCount <= 0)
{
_exhausted = true;
return;
}
int missIndex = 0;
for (int i = 0; i < actualCount; i++)
{
ref CacheLineDescription handle = ref span[i];
nuint address = handle.Address;
if (!_map.TryGet(address, out _))
{
// miss and record
span[missIndex] = handle;
missIndex++;
}
}
// now prefetch the misses
for (int i = 0; i < missIndex; i++)
{
CacheLineMetadata* head;
ref CacheLineDescription pHandle = ref span[i];
nuint address = pHandle.Address;
int index;
if (_map.TryGet(address, out nuint handle))
{
head = (CacheLineMetadata*)handle;
index = (int)(head - _head);
_predictCount[index] = 0; // reset eviction counter
#region update predictor belonging
if (_predictCount[index] > short.MaxValue / 2)
{
int predictCount = Volatile.Read(ref _predictCount[index]);
int mapCount = Volatile.Read(ref _mapCount[index]);
int decrement = (int.Min(predictCount, mapCount) + 1) / 2;
_ = Interlocked.Add(ref _predictCount[index], -decrement);
_ = Interlocked.Add(ref _mapCount[index], -decrement);
}
_predictCount[index]++;
#endregion
continue;
}
int retryCount = 0, remain = missIndex - i;
const int maxRetry = 1024;
while (!_bufferAllocator.TryAlloc(pHandle.Size, out index))
{
remain = Evict(remain);
retryCount++;
if (retryCount > maxRetry) {
throw new OverflowException($"Failed to evict cache line after {maxRetry} retries.");
}
if (retryCount > maxRetry / 2) {
Thread.Sleep(1);
}
}
head = _head + index;
head->Origin = address;
head->CustomizeProfile = pHandle.MapPolicy;
head->RefCount = 0;
_predictCount[index] = 1;
_mapCount[index] = 1;
// pre fetch them here
predict.Prefetch(head->Origin, head->CustomizeProfile, new Span<byte>((byte*)(head->PositionMask + _baseAddress), (int)head->SizeMask));
_ = _map.TryAddOrSet(address, (nuint)head, out _);
}
_ = Interlocked.Add(ref _aheadCount, actualCount);
}
}
}
@@ -0,0 +1,519 @@
// -----------------------------------------------------------------------------
// file="L4.Tlsf"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.HighPerformance.ProcessorAbstraction;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
public partial class L4
{
private unsafe class TLSF
{
public const int MaxCacheLineCount = 8192;
public static readonly nuint RequestedAllocation = (nuint)MaxCacheLineCount * CacheLineMetadata.SizeOf;
private readonly short[] _bucket;
private readonly CacheLineMetadata* _spareMemory;
private readonly int _align;
private readonly int _maxLevel;
private int _metadataEnd;
private readonly Ptr64 _basePtr;
private readonly uint _maxAllocSize;
private readonly uint _totalSize;
internal TLSF(
uint byteSize,
uint maxBlockSize,
void* externalPointer
)
{
_totalSize = byteSize;
_basePtr = (Ptr64)(ulong)externalPointer;
_align = 4096; // align of a memory page
_maxAllocSize = uint.Max(64, (~64U) & (uint.Min(maxBlockSize, CacheLineMetadata.MaxBufferSize) + 63U));
_maxLevel = int.MaxValue;
_maxLevel = GetLevel(_maxAllocSize);
_bucket = new short[GetLevel(_maxAllocSize) + 2];
_spareMemory = (CacheLineMetadata*)(ulong)(_basePtr + _totalSize);
_metadataEnd = 0;
// init spare memory node
_spareMemory->PositionMask = 0;
_spareMemory->SizeMask = _totalSize;
_spareMemory->NextLevel = 0;
_spareMemory->NextNear = 0;
_spareMemory->PreviousLevel = 0;
_spareMemory->PreviousNear = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetLevel(
uint size
)
{
int origin = 25 - BitOperations.LeadingZeroCount(size);
return int.Min(origin, _maxLevel);
}
/// <summary>
/// Put a metadata node back to free list and waiting for next allocation.
/// </summary>
/// <param name="node">
///
/// </param>
private void RecycleNode(
CacheLineMetadata* node
)
{
short nodeIdx = (short)(_spareMemory - node);
int level = GetLevel(node->SizeMask);
short nextLevelIdx = node->NextLevel;
short prevLevelIdx = node->PreviousLevel;
RemoveFromBucket(level, node, nodeIdx);
// remove from position chain
short prevNearIdx = node->PreviousNear;
short nextNearIdx = node->NextNear;
CacheLineMetadata* prevNear = &_spareMemory[prevNearIdx];
prevNear->NextNear = nextNearIdx;
if (nextNearIdx != 0)
{
CacheLineMetadata* nextNear = &_spareMemory[nextNearIdx];
nextNear->PreviousNear = prevNearIdx;
} else
{
_spareMemory->PreviousNear = prevNearIdx;
}
node->PreviousNear = 0;
node->NextNear = 0;
node->PositionMask = 0;
node->SizeMask = 0;
node->State = AllocationState.SpareOrCollect;
node->PreviousLevel = 0;
short nextIdx = _spareMemory->NextLevel;
if (nextIdx != 0)
{
CacheLineMetadata* next = &_spareMemory[nextIdx];
next->PreviousLevel = nodeIdx;
}
node->NextLevel = nextIdx;
_spareMemory->NextLevel = nodeIdx;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveFromBucket(
CacheLineMetadata* node,
int nodeIdx
)
{
RemoveFromBucket(GetLevel(node->SizeMask), node, nodeIdx);
}
private void RemoveFromBucket(
int level,
CacheLineMetadata* node,
int nodeIdx
)
{
int currentLevel = level;
short currentNextLevelIdx = node->NextLevel;
short currentPrevLevelIdx = node->PreviousLevel;
if (_bucket[currentLevel] == nodeIdx)
{
_bucket[currentLevel] = currentNextLevelIdx;
} else if (currentPrevLevelIdx != 0)
{
CacheLineMetadata* prevLevel = &_spareMemory[currentPrevLevelIdx];
prevLevel->NextLevel = currentNextLevelIdx;
}
if (currentNextLevelIdx != 0)
{
CacheLineMetadata* nextLevel = &_spareMemory[currentNextLevelIdx];
nextLevel->PreviousLevel = currentPrevLevelIdx;
}
node->PreviousLevel = 0;
node->NextLevel = 0;
}
// Assuming node to remove is head f bucket
private void RemoveFromBucketHead(
int level,
CacheLineMetadata* node,
int nodeIdx
)
{
int currentLevel = level;
short currentNextLevelIdx = node->NextLevel;
// int currentPrevLevelIdx = 0;
if (_bucket[currentLevel] == nodeIdx)
{
_bucket[currentLevel] = currentNextLevelIdx;
}
/*
else if (currentPrevLevelIdx != 0)
{
Metadata* prevLevel = &_spareMemory[currentPrevLevelIdx];
prevLevel->NextLevel = currentNextLevelIdx;
}
/**/
if (currentNextLevelIdx != 0)
{
CacheLineMetadata* nextLevel = &_spareMemory[currentNextLevelIdx];
nextLevel->PreviousLevel = 0;
}
node->PreviousLevel = 0;
node->NextLevel = 0;
}
private bool TryAllocNewChunk(
uint size,
out CacheLineMetadata* meta
)
{
short idx = _spareMemory->NextLevel;
uint spareSize = _spareMemory->SizeMask;
if (idx != 0)
{
if (spareSize < size)
{
meta = null;
return false;
}
meta = &_spareMemory[idx];
short nextSpareIdx = meta->NextLevel;
if (nextSpareIdx != 0)
{
CacheLineMetadata* nextSpare = &_spareMemory[nextSpareIdx];
nextSpare->PreviousLevel = 0;
}
_spareMemory->NextLevel = nextSpareIdx;
} else
if (!TryGetNewMetadata(out meta)) {
return false;
}
spareSize -= size;
meta->SizeMask = size;
meta->PositionMask = _spareMemory->PositionMask;
meta->State = AllocationState.Free;
meta->PreviousLevel = 0;
meta->NextLevel = 0;
_spareMemory->SizeMask = spareSize;
_spareMemory->PositionMask += size;
// insert the new chunk to position chain
short nearIdx = _spareMemory->PreviousNear;
CacheLineMetadata* near = &_spareMemory[nearIdx];
near->NextNear = idx;
meta->PreviousNear = nearIdx;
_spareMemory->PreviousNear = idx;
meta->NextNear = 0;
return true;
}
private bool TryGetNewMetadata(
out CacheLineMetadata* meta
)
{
ulong spareSize = _spareMemory->SizeMask;
if (_metadataEnd >= MaxCacheLineCount)
{
meta = null;
return false;
}
_metadataEnd++;
meta = &_spareMemory[_metadataEnd];
return true;
}
private bool TryTakeBucketEntry(
uint minimumSize,
out CacheLineMetadata* entry
)
{
int level = GetLevel(minimumSize);
// first try to find in the target level
{
int idx = _bucket[level];
while (idx != 0)
{
CacheLineMetadata* candidate = &_spareMemory[idx];
if (candidate->SizeMask >= minimumSize)
{
RemoveFromBucket(level, candidate, idx);
entry = candidate;
return true;
}
idx = candidate->NextLevel;
}
}
for (int i = level + 1; i < _bucket.Length; i++)
{
int idx = _bucket[i];
if (idx != 0)
{
CacheLineMetadata* candidate = &_spareMemory[idx];
RemoveFromBucketHead(i, candidate, idx);
entry = candidate;
return true;
}
}
entry = null;
return false;
}
#region alloc and free
public int Alloc(
uint size
)
{
uint allocSize = uint.Max(64, (~63U) & (size + 63));
if (allocSize > _maxAllocSize) {
throw new OverflowException("Requested allocation size exceeds the maximum block size.");
}
if (!TryTakeBucketEntry(allocSize, out CacheLineMetadata* entry) &&
!TryAllocNewChunk(allocSize, out entry)) {
throw new OverflowException("No available ");
}
short entryIdx = (short)(_spareMemory - entry);
uint entrySize = entry->SizeMask;
if (entrySize < allocSize) {
throw new OverflowException("Bucket returned an undersized block.");
}
uint remain = entrySize - allocSize;
// slice tail
if (remain > ulong.Min(4096UL, entrySize >> 7) &&
TryGetNewMetadata(out CacheLineMetadata* newEntry))
{
short newEntryIdx = (short)(_spareMemory - newEntry);
newEntry->PositionMask = entry->PositionMask + allocSize;
newEntry->SizeMask = remain;
newEntry->State = AllocationState.Free;
int newLevel = GetLevel(remain);
short bucketHeadIdx = _bucket[newLevel];
if (bucketHeadIdx != 0)
{
// if there is already an entry in the bucket, link it to the new entry
CacheLineMetadata* bucketHead = &_spareMemory[bucketHeadIdx];
bucketHead->PreviousLevel = newEntryIdx;
}
newEntry->NextLevel = bucketHeadIdx;
newEntry->PreviousLevel = 0;
_bucket[newLevel] = newEntryIdx;
// push to position chain
short afterIdx = entry->NextNear;
CacheLineMetadata* after = &_spareMemory[afterIdx];
newEntry->NextNear = afterIdx;
after->PreviousNear = newEntryIdx;
entry->NextNear = newEntryIdx;
newEntry->PreviousNear = entryIdx;
}
entry->State = AllocationState.Alloc;
if (entry->PositionMask - (ulong)_basePtr > _totalSize) {
throw new OverflowException("Allocated memory address exceeds the maximum block size.");
}
return entryIdx;
}
public void Collect()
{
short currentIdx = _spareMemory->NextNear;
do
{
if (currentIdx == 0)
{
break;
}
CacheLineMetadata* current = &_spareMemory[currentIdx];
if (current->State == AllocationState.Free)
{
// scan next
uint size = 0;
int nextIdx = current->NextNear;
while (nextIdx != 0)
{
CacheLineMetadata* next = &_spareMemory[nextIdx];
if (next->State != AllocationState.Free)
{
break;
}
size += next->SizeMask;
RecycleNode(next);
nextIdx = current->NextNear;
}
if (size > 0)
{
current->SizeMask += size;
RemoveFromBucket(current, currentIdx);
int newLevel = GetLevel(current->SizeMask);
short bucketHeadIdx = _bucket[newLevel];
if (bucketHeadIdx != 0)
{
CacheLineMetadata* bucketHead = &_spareMemory[bucketHeadIdx];
bucketHead->PreviousLevel = currentIdx;
}
current->NextLevel = bucketHeadIdx;
current->PreviousLevel = 0;
_bucket[newLevel] = currentIdx;
}
}
currentIdx = current->NextNear;
} while (true);
}
public void Free(
int handle,
int collectScanWindow = 4
)
{
CacheLineMetadata* head = &_spareMemory[handle];
short headIdx = (short)(_spareMemory - head);
uint size = head->SizeMask;
for (int i = 0; i < collectScanWindow; i++)
{
// forward scan, previous direction
int forwardIdx = head->PreviousNear;
if (forwardIdx == 0)
{
break;
}
CacheLineMetadata* prev = &_spareMemory[forwardIdx];
if (prev->State != AllocationState.Free)
{
break;
}
size += prev->SizeMask;
head->PositionMask = prev->PositionMask;
RecycleNode(prev);
}
for (int i = 0; i < collectScanWindow; i++)
{
// backward scan, next direction
int nextIdx = head->NextNear;
if (nextIdx == 0)
{
break;
}
CacheLineMetadata* prev = &_spareMemory[nextIdx];
if (prev->State != AllocationState.Free)
{
break;
}
size += prev->SizeMask;
RecycleNode(prev);
}
// recycle to level;
head->SizeMask = size;
head->State = AllocationState.Free;
head->PreviousLevel = 0;
int level = GetLevel(head->SizeMask);
short bucketHeadIdx = _bucket[level];
head->NextLevel = 0;
if (bucketHeadIdx != 0)
{
CacheLineMetadata* bucketHead = &_spareMemory[bucketHeadIdx];
bucketHead->PreviousLevel = headIdx;
head->NextLevel = bucketHeadIdx;
}
_bucket[level] = headIdx;
return;
}
public bool TryAlloc(
uint size,
out int h
)
{
uint allocSize = uint.Max(64, (~63U) & (size + 63));
if (allocSize > _maxAllocSize)
{
h = -1;
return false;
}
if (!TryTakeBucketEntry(allocSize, out CacheLineMetadata* entry) &&
!TryAllocNewChunk(allocSize, out entry))
{
h = -1;
return false;
}
short entryIdx = (short)(_spareMemory - entry);
uint entrySize = entry->SizeMask;
if (entrySize < allocSize)
{
h = -1;
return false;
}
uint remain = entrySize - allocSize;
// slice tail
if (remain > (entrySize >> 7) && TryGetNewMetadata(out CacheLineMetadata* newEntry))
{
short newEntryIdx = (short)(_spareMemory - newEntry);
newEntry->PositionMask = entry->PositionMask + allocSize;
newEntry->SizeMask = remain;
newEntry->State = AllocationState.Free;
int newLevel = GetLevel(remain);
short bucketHeadIdx = _bucket[newLevel];
if (bucketHeadIdx != 0)
{
// if there is already an entry in the bucket, link it to the new entry
CacheLineMetadata* bucketHead = &_spareMemory[bucketHeadIdx];
newEntry->NextLevel = bucketHeadIdx;
bucketHead->PreviousLevel = newEntryIdx;
} else
{
newEntry->NextLevel = 0;
}
newEntry->PreviousLevel = 0;
_bucket[newLevel] = newEntryIdx;
// push to position chain
short afterIdx = entry->NextNear;
CacheLineMetadata* after = &_spareMemory[afterIdx];
newEntry->NextNear = afterIdx;
after->PreviousNear = newEntryIdx;
entry->NextNear = newEntryIdx;
newEntry->PreviousNear = entryIdx;
}
entry->State = AllocationState.Alloc;
if (entry->PositionMask - (ulong)_basePtr > _totalSize)
{
h = default;
throw new OverflowException("Allocated memory address exceeds the maximum block size.");
}
h = entryIdx;
return true;
}
#endregion
}
}
}
@@ -0,0 +1,188 @@
// -----------------------------------------------------------------------------
// file="L4"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Collections;
using StgSharp.HighPerformance.Memory;
using StgSharp.HighPerformance.ProcessorAbstraction;
using System.Data;
using System.Runtime.InteropServices;
namespace StgSharp.HighPerformance.Memory
{
public sealed unsafe partial class L4 : IDisposable
{
private const int CacheLineCount = 8192;
private const nuint PageSize = 4096;
private const int PredictionCount = 2048;
private const int PredictionLeastAhead = 1024;
/// <summary>
/// Array of metadata for each cache line, also used as metadata for tlsf allocator.
/// </summary>
private readonly CacheLineMetadata* _head;
/// <summary>
/// Count of cache lines mapped for each prediction, used for eviction policy.
/// </summary>
private readonly int* _mapCount;
/// <summary>
/// Count of times each cache line is predicted, used for eviction policy.
/// </summary>
private readonly int* _predictCount;
/// <summary>
/// Original memory view for prediction span.
/// </summary>
private readonly CacheLineDescription* _prediction;
private bool _disposed;
private bool _exhausted;
private int _aheadCount;
private readonly int _bindedNuma;
private volatile int _prefetching;
private readonly nuint _baseAddress;
private readonly SwissTable _map;
private readonly TLSF _bufferAllocator;
public L4(
int bindedNuma,
uint bufferSize = 8192 * 8192,
int align = 16,
uint maxCacheLineSize = 8192
)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(bufferSize, CacheLineMetadata.MaxBufferSize);
ArgumentOutOfRangeException.ThrowIfGreaterThan(maxCacheLineSize, CacheLineMetadata.MaxBufferSize);
_bindedNuma = bindedNuma;
// init order is fallow by align requirement of each component.
nuint headSize = ((nuint)Unsafe.SizeOf<CacheLineMetadata>()) * CacheLineCount; // 16 byte align
nuint predictionSize = ((nuint)Unsafe.SizeOf<CacheLineDescription>()) * PredictionCount; //16 byte align
nuint mapCountSize = SwissTable.RequestedNativeMemorySize; // 16 byte align
nuint predictionCountSize = sizeof(int) * CacheLineCount; // 16 byte align
nuint normalizedBufferSize = (bufferSize + (PageSize - 1)) & (~(PageSize - 1)); // align to 4096 byte, page size
ByteCapacity = normalizedBufferSize;
AllocSize = headSize + mapCountSize + predictionSize + (predictionCountSize * 2) + normalizedBufferSize;
// malloc all memory at once, and free at once in Dispose.
// This is to avoid fragmentation and improve performance.
_baseAddress = (nuint)Numa.Alloc(bindedNuma, AllocSize);
nuint ptr = _baseAddress;
// init all components with the buffer, and move the buffer pointer forward.
nuint _buffer = _baseAddress;
_bufferAllocator = new TLSF(bufferSize, maxCacheLineSize, (void*)_baseAddress);
_buffer += normalizedBufferSize;
_head = (CacheLineMetadata*)ptr;
ptr += headSize;
_prediction = (CacheLineDescription*)ptr;
ptr += predictionSize;
_map = SwissTable.Create(ptr, _ => { });
ptr += mapCountSize;
_predictCount = (int*)ptr;
ptr += predictionCountSize;
_mapCount = (int*)ptr;
ptr += predictionCountSize;
M512* mPtr = (M512*)_head;
for (uint i = 1; i < CacheLineCount; i++) {
mPtr->BroadCastFrom<ulong>(0);
}
}
public nuint ByteCapacity { get; init; }
public nuint AllocSize { get; init; }
// public IL4Predict PrefetchPredict { get; set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ContinueOnce(
Handle line
)
{
if (line.Address > 0) {
_ = Interlocked.Decrement(ref _aheadCount);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1063:Implement IDisposable correctly",
Justification = """
Only owns native memory via Numa.Free();
no managed IDisposable fields.
Dispose(bool) pattern is unnecessary.
""")]
public void Dispose()
{
if (_disposed) {
return;
}
_disposed = true;
_map.Dispose();
#pragma warning disable CA1816
GC.SuppressFinalize(_bufferAllocator);
#pragma warning restore CA1816
// init order is fallow by align requirement of each component.
Numa.Free((void*)_baseAddress, _bindedNuma, AllocSize);
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Handle Map(
ref CacheLineDescription prediction
)
{
if ((_aheadCount < PredictionLeastAhead) && !_exhausted)
{
if (Interlocked.CompareExchange(ref _prefetching, 1, 0) == 0)
{
try
{
if (_aheadCount < PredictionLeastAhead)
{
// do prefetch in work load now, sync
Prefetch();
}
}
finally
{
_prefetching = 0;
}
}
}
while (true)
{
if (_map.TryGet(prediction.GetHashCodeLong(), out nuint entryHandle))
{
// cache hit
CacheLineMetadata* entry = (CacheLineMetadata*)entryHandle;
if (Interlocked.Increment(ref entry->RefCount) > 0)
{
_predictCount[entry - _head] = 0;
_ = Interlocked.Decrement(ref _aheadCount);
_ = Interlocked.Decrement(ref _mapCount[entry - _head]);
nuint* hPtr = stackalloc nuint[2] { entry->PositionMask + _baseAddress, (nuint)entry };
return *(Handle*)hPtr;
} else
{
_ = Interlocked.Decrement(ref entry->RefCount);
}
}
Thread.Sleep(0);
}
}
private partial void Prefetch();
}
}
@@ -0,0 +1,762 @@
# L4 Software Cache Design Specification
> Project: StgSharp
> Author: Nitload
> Version: Draft v0.3
> Date: 2026-03-10
---
## 1. Purpose
### 1.1 Problem Background
StgSharp's matrix computation system is migrating from a "single global context table" to a
"per-thread context" architecture to adapt to the scheduling characteristics of hybrid-architecture
CPUs (e.g., Intel P-Core / E-Core). After the migration, each thread independently holds its own
computation context, but matrix data may still be allocated by the HLSF allocator or global
`malloc` on a remote NUMA node, resulting in significantly increased cross-node access latency.
At the same time, in matrix multiplication (FMA), the core row data of matrix A in the
checkerboard-shaped kernel grid layout must be repacked (Packed) into a micro-kernel-friendly
contiguous arrangement. In libraries such as OpenBLAS, this Pack operation is re-executed on
every tile switch, whereas L4 can cache the Pack results to avoid repeated reordering.
Additionally, the segmented accumulation (segment) of matrix multiplication requires storing
intermediate partial sums locally to avoid cross-NUMA writes to the target matrix on every
segment. L4 can serve as a local accumulation buffer for partial sums, writing them back in a
single operation after all segments complete locally.
### 1.2 Solution
The L4 cache is a purely software-implemented **data caching layer** logically positioned
between the CPU hardware caches (L1/L2/L3) and main memory. Its core responsibilities:
1. **Pack result caching**: Cache the repack results of FMA core rows in matrix A to avoid repeated Pack operations
2. **NUMA migration cache**: Migrate data from remote NUMA nodes to local memory and cache it, eliminating cross-node latency
3. **Segment accumulation buffer**: Temporarily store partial sums from FMA segmented computation to reduce remote write operations
4. **PLU intermediate data buffer**: Provide a local contiguous workspace for PLU decomposition
### 1.3 Design Goals
- Provide NUMA-friendly local data access for per-thread matrix computation contexts
- Fixed capacity (64 MB), no runtime expansion, deterministic latency
- Transparent pointer redirection: upper-layer code queries via original pointers; L4 returns local cache pointers
- Cache eviction policy based on **RRPV mask + windowed OPT prediction**
- Support both **StreamTransparent** and **CacheBuffer** working modes
- Support **writable cache lines** that invoke registered callback functions for pre-processing and write-back upon eviction or explicit flush
---
## 2. Overall Architecture
### 2.1 Three-Component Model
```
┌���─────────────────────────────────────────────────────────────────┐
│ L4 │
│ │
│ ┌──────────────┐ ┌────────────────────┐ ┌──────────────────┐ │
│ │ CacheLine │ │ L4 Core │ │ Predictor │ │
│ │ Handle │ │ Cache │ │ Prefetch │ │
│ │ │ │ │ │ Predictor │ │
│ │ • local addr │ │ • Swiss Table │ │ • 2048-entry buf │ │
│ │ • ref count │ │ • Slab Allocator │ │ • threshold 1024 │ │
│ │ • Dispose │ │ • RRPV mask │ │ • windowed OPT │ │
│ │ drops ref │ │ • SIMD eviction │ │ • active key set │ │
│ │ │ │ • writeback mgmt │ │ │ │
│ └──────────────┘ └────────────────────┘ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 2.2 Data Access Path
```
Compute thread accessing data:
┌─────────────────────────────────────────────────────────┐
│ Read-only path: L4.Map(originKey) │
│ ├─ StreamTransparent + no repack needed? │
│ │ └─ Yes → return original pointer directly │
│ │ (zero overhead) │
│ │ │
│ └─ CacheBuffer mode │
│ ├─ Swiss Table lookup (2.6ns, lock-free) │
│ ├─ Hit → update RRPV=0, return CacheLine handle │
│ └─ Miss → SpinWait for Predictor prefetch │
├─────────────────────────────────────────────────────────┤
│ Writable path: L4.MapWritable(originKey, preprocess, writeback) │
│ ├─ Hit → return existing partial sum cache line │
│ └─ Miss → allocate zero-initialized cache line, │
│ register writeback callbacks │
│ cache line marked dirty (dirty=1) │
│ eviction invokes preprocess + writeback │
├─────────────────────────────────────────────────────────┤
│ After computation: │
│ 3. CacheLine.Dispose() (refCount--) │
│ 4. L4.StepComplete() │
│ ├─ Predictor.AdvanceExecutionPoint() │
│ └─ Predictor.NeedsPrefetch? │
│ └─ Yes → CAS-acquire lock, execute prefetch │
└─────────────────────────────────────────────────────────┘
```
### 2.3 Key Parameters
| Parameter | Value | Description |
|------------------------|--------------------------|------------------------------------------------------|
| Total cache capacity | 64 MB | Per-NUMA-node contiguous memory |
| Cache line size | 8192 Bytes | = 64 × `double MAT_KERNEL` |
| Cache line count | 8192 | 64MB / 8192B |
| Mapping table type | Swiss Table | 1024 buckets × 16 slots, SIMD-accelerated |
| Cache line pool | Slab Allocator | Non-concurrent, fixed capacity, no auto-expansion |
| Eviction policy | RRPV mask + windowed OPT | SoA layout + SSE2 batch scan |
| Prefetch policy | Predictor windowed OPT | 2048-entry buffer, 1024-step lookahead |
| Writeback policy | Callback-driven | Invokes registered callbacks on dirty eviction/flush |
---
## 3. Key Components
### 3.1 Swiss Table (Pointer Mapping Table)
**Responsibility**: Maintains the `original pointer → cache pointer` mapping.
- Key: original pointer `nint`, pointing to data allocated by HLSF/malloc on a remote node
- Value: cache pointer `nint`, pointing to locally Slab-allocated cache line
- Hash: hardware `CRC32C` instruction on the 64-bit pointer
- Low 10 bits → bucket index (1024 buckets)
- Bits 10-16 → H2 control byte (used for SIMD pre-filtering)
- Scan: SSE2 `pmovmskb` instruction for parallel comparison of 16 control bytes
- Tombstone mechanism: deleted entries set ctrl to `0xFE`, preserving probe chain integrity
- Capacity: 1024 × 16 = 16,384 mappings, significantly larger than total cache line count (8192 lines),
ensuring extremely low hash collision rate
**Measured Performance** (Ryzen 7 6800H, .NET 8, BenchmarkDotNet):
| Operation | Swiss Table | Dictionary | Speedup |
|--------------------------|-------------|-------------|---------|
| Zipfian Lookup (8192×) | 20.0 μs | 76.1 μs | 3.8× |
| Uniform Lookup (8192×) | 18.6 μs | 50.6 μs | 2.7× |
| Insert (4096 entries) | 18.5 μs | 46.5 μs | 2.5× |
| SteadyState (4096 ops) | 25.3 μs | 68.3 μs | 2.7× |
Single lookup latency ≈ 2.6 ns, GC allocation = 0.
### 3.2 Slab Allocator (Cache Line Pool)
**Responsibility**: Manages allocation and reclamation of fixed-size (8192B) cache lines.
- Mode: **Non-concurrent, fixed capacity** (`FixedSlabAllocator<T>` mode),
reusing the existing `SlabAllocator.FromBuffer<T>()` interface
- No auto-expansion: capacity is fixed at initialization (64MB / 8192B = 8192 lines);
allocation failure triggers eviction rather than expansion
- Allocation complexity: O(1), pops from free stack
- Reclamation complexity: O(1), pushes back to free stack
- Alignment: all cache lines are 64B aligned, ensuring no cross-hardware-cache-line boundaries
### 3.3 Eviction Metadata (SoA Layout + RRPV Mask + Writeback Callbacks)
**Responsibility**: Tracks per-cache-line reference state, heat, and writeback information to determine eviction priority.
Replaces the four-level circular linked list from v0.1 with parallel arrays in Structure-of-Arrays layout:
```
SoA Layout — Parallel Arrays
_refFlags[i] : byte Reference count snapshot (0=free, non-zero=held)
_evictionFlags[i] : byte dirty(1bit) + RRPV(2bit) + costClass(1bit)
_originKeys[i] : nuint Original pointer (reverse-deregisters Swiss Table entry on eviction)
_preprocess[i] : nint Writeback pre-processing function pointer (called before dirty eviction)
_writeback[i] : nint Writeback function pointer (called during dirty eviction)
_callbackCtx[i] : nuint Callback context (passed to preprocess/writeback)
```
**Eviction Flag Bit Layout** (`_evictionFlags[i]`):
The RRPV array uses byte storage for each cache line's usage state; the low 2 bits encode the RRPV value.
**RRPV Semantics**:
| RRPV | Meaning | Eviction Priority |
|------|---------------------------------|---------------------------|
| 0 | Will be re-accessed very soon | Lowest (do not evict) |
| 1 | Will be accessed shortly | Low |
| 2 | Will be accessed in a while | Medium |
| 3 | May never be accessed again | Highest (evict first) |
**RRPV Update Events**:
| Event | Operation | Description |
|--------------------|---------------------------------|------------------------------------------------------|
| Load (insert) | RRPV = 0 | Freshly loaded data will be used soon |
| Hit | `_evictionFlags[i] = 0` | Bits 1-2 cleared, RRPV reset to 0 |
| Eviction scan | Find lines with RRPV=3 to evict | If none found, increment all RRPV +1 (aging) |
**Advantages over the v0.1 four-level circular linked list**:
| Comparison | v0.1 Circular List | v0.3 RRPV Mask + Callbacks |
|----------------------|-------------------------------------|--------------------------------------|
| Data structure | 4 doubly-linked lists + next ptrs | Byte array + function pointer arrays |
| Warm-up operation | Unlink + relink (4 pointer writes) | `+= 1` (1-byte write) |
| Aging operation | Per-node list migration | SIMD batch +1 (16 entries/cycle) |
| Eviction scan | Iterate list head (pointer chasing) | SIMD pattern matching (16/cycle) |
| Cache friendliness | Poor (pointer chasing) | Good (contiguous memory scan) |
| Reference protection | None | Lines with refCount > 0 not evicted |
| Writeback support | None | Callback-function driven |
### 3.4 Writeback Callback Mechanism
**Responsibility**: Provides flexible writeback strategies for writable cache lines, using native function pointers to avoid managed overhead.
**Design Principle**: All writable cache lines are treated as dirty by default. On eviction or explicit flush,
L4 itself does not manage the writeback strategy; instead, the bound predictor is responsible for implementing it.
The predictor is defined via the following interface:
```
public interface IPrefetchPredict
{
int Predict(
Span<CacheLinePrediction> node
);
bool Prefetch(
nuint origin,
ulong policy,
nuint cache
);
void WriteBack(
nuint origin,
ulong policy,
nuint cache
);
}
```
**Typical Callback Implementations**:
```
Scenario 1: Segment accumulation (FMA partial sum)
preprocess = null (no pre-processing needed; data is already a double array)
writeback = AccumulateAdd (element-wise dst[i] += src[i], AVX2-vectorized)
Scenario 2: Writeback of packed intermediate results
preprocess = UnpackKernelRow (reverse-repack from Pack format to row-major order)
writeback = AccumulateAdd
Scenario 3: PLU decomposition intermediate data
preprocess = null
writeback = DirectCopy (direct overwrite memcpy)
```
### 3.5 CacheLine (Cache Line Handle)
**Responsibility**: A cache line access handle held by the compute thread; prevents in-use lines from being evicted via reference counting.
```csharp
public ref struct CacheLine : IDisposable
{
private ref int _activeCount; // L4 global active count
private ref int _refCount; // Per-line reference count
private nuint _handle; // Local cache data address
// Construction: Interlocked.Increment(ref _activeCount)
// Interlocked.Increment(ref _refCount)
// Dispose: Interlocked.Decrement on both counters
}
```
- `ref struct` ensures it cannot escape to the heap
- Reference counts use `Interlocked` atomic operations for thread safety
- Lines with `_refCount > 0` are skipped during eviction scans
- StreamTransparent mode returns a CacheLine that does not occupy a cache slot
- Both read-only and writable paths return the same CacheLine type,
callers decide read/write behavior via `.Address`
### 3.6 Predictor (Prefetch Predictor)
**Responsibility**: Provides deterministic prefetch predictions for L4 based on the windowed OPT strategy.
**Core Principle**: Matrix computation task sequences are deterministic — the scheduler pre-allocates
task blocks `(kr, kc, zz)` for each thread, allowing precise computation of the cache line keys
required at each future step. This gives L4 a predictive capability that hardware caches cannot
possess: **direct visibility into the future**, rather than inferring the future from history as
strategies like Hawkeye do.
```
Predictor Sliding Window:
Task sequence: ... [T_now] [T+1] [T+2] ... [T+1023] ... [T+2047]
↑ execution point ↑ buffer tail
Window capacity = 2048 prediction entries
Prefetch trigger threshold = trigger refill when prefetch frontier
leads execution point by fewer than 1024 steps
Refill batch size = 64 entries (avoids excessive single-refill latency)
```
**数据结构**:
```csharp
internal class Predictor
{
private const int WindowCapacity = 2048;
private const int RefillThreshold = 1024;
private PrefetchEntry[] _buffer; // Circular prediction buffer
private int _head; // Prefetch frontier (prefetched up to here)
private int _tail; // Production position (scheduler fills up to here)
private int _executed; // Execution point (compute thread has completed up to here)
private HashSet<nuint> _activeKeys; // All active keys within the window (deduplicated)
}
struct PrefetchEntry
{
public nuint Key; // Cache line identifier
public nuint SourceAddress; // Source data address
public bool NeedsRepack; // Whether non-contiguous repacking is needed
}
```
**Cooperation with Eviction Policy**:
```
Eviction priority (from highest to lowest priority):
1. refCount==0, not in window, RRPV==3, dirty==0 ← First (zero-cost eviction)
2. refCount==0, not in window, RRPV==3, dirty==1 ← Second (needs writeback but cold)
3. refCount==0, not in window, RRPV<3, dirty==0 ← Ordered by RRPV
4. refCount==0, not in window, RRPV<3, dirty==1 ← Ordered by RRPV
5. refCount==0, in window ← Avoid evicting if possible
6. refCount>0 ← Never evict
```
Windowed OPT means most eviction decisions do not depend on RRPV — "not in window" alone is
sufficient to determine evictability. RRPV provides fine-grained differentiation among
out-of-window lines; the dirty bit preferentially evicts clean lines at equal RRPV to avoid writeback overhead.
### 3.7 Concurrency Model
L4 uses a **no-extra-threads** design; all operations are performed by compute threads themselves:
| Operation | Executor | Concurrency |
|------------------------------------|---------------------------------|--------------------------------------|
| Lookup (Swiss Table read) | Any compute thread | ✅ Fully parallel, lock-free |
| HIT RRPV update | Any compute thread | ✅ Single-byte write, no sync needed |
| refCount increment/decrement | Any compute thread | ✅ Interlocked atomic operations |
| Prefetch refresh (eviction + load) | CAS-winner thread | Serial (only one thread enters) |
| Dirty writeback | CAS-winner thread | Serial (within eviction flow) |
| FlushAllDirty | Single thread after computation | Serial (no concurrency) |
**Prefetch Refresh CAS Lock**:
```csharp
private int _refreshLock; // 0=free, 1=locked
private void TryRefreshPrefetch()
{
if (Interlocked.CompareExchange(ref _refreshLock, 1, 0) != 0)
return; // Another thread is doing it; current thread continues computing
try { /* eviction + prefetch */ }
finally { Volatile.Write(ref _refreshLock, 0); }
}
```
**Swiss Table Concurrent Read Safety Guarantee**: The prefetch refresh thread follows strict write ordering
on insertion: write Slab data first, then write eviction metadata and callback pointers, `sfence`, then
write the Swiss Table control byte and key/value, ensuring that when a compute thread reads the control
byte, the data is already fully visible.
---
## 4. Operation Flows
### 4.1 Cache Lookup (Read-Only Hit)
1. Compute CRC32C hash of the original pointer
2. Swiss Table lookup: SIMD scan ctrl → compare key → return cache pointer
3. Update RRPV: `_evictionFlags[slot] &= 0xF9` (RRPV zeroed, dirty bit unchanged)
4. Construct CacheLine handle (refCount++)
5. Return CacheLine; upper-layer code accesses local data via `.Address`
### 4.2 Cache Lookup (Writable Hit)
1. Swiss Table lookup hits
2. Update RRPV: `_evictionFlags[slot] &= 0xF9`
3. Construct CacheLine handle (refCount++)
4. Return CacheLine; upper-layer code reads/writes the cache line via `.Address` (e.g., partial sum +=)
### 4.3 Cache Miss (Read-Only)
1. Swiss Table lookup misses
2. Compute thread enters `Thread.Sleep(0)` spin loop
3. Wait for Predictor-driven prefetch refresh to load the line into L4
4. Exit wait loop when Swiss Table shows a hit
5. Construct CacheLine handle and return
Under normal operation, the Predictor always leads the execution point by 1024+ steps,
misses occur only during cold start or extreme bursts.
### 4.4 Cache Miss (Writable)
1. Swiss Table lookup misses
2. Allocate a cache line from Slab
3. If Slab has no free slots → trigger eviction (see 4.6)
4. Zero-initialize the cache line (`Unsafe.InitBlock(dest, 0, 8192)`)
5. Register writeback callbacks: `_preprocess[slot]`, `_writeback[slot]`, `_callbackCtx[slot]`
6. Set eviction flags: `dirty=1, RRPV=0`
7. Write `_originKeys[slot]`
8. `sfence`
9. Write Swiss Table mapping
10. Construct CacheLine handle and return
### 4.5 Prefetch Refresh
Triggered by compute threads in `StepComplete()`; CAS lock ensures single-thread execution:
1. Synchronize refFlags snapshot (`_refCounts[i] > 0 → _refFlags[i] = 1`)
2. Retrieve pending prefetch entries from Predictor (up to 64 at a time)
3. For each entry:
- Swiss Table hit → mark RRPV=0 (hot-line protection)
- Swiss Table miss → execute load:
a. Allocate cache line from Slab
b. If Slab has no free slots → trigger eviction (see 4.6)
c. Execute Pack reorder or memcpy based on `NeedsRepack` flag
d. Write eviction metadata (originKey, RRPV=0, dirty=0, callbacks=null)
e. `sfence`
f. Write Swiss Table mapping
### 4.6 Cache Eviction (SIMD Scan + Writeback)
```
Eviction scan pseudocode:
Pass 1: Find refCount==0, RRPV==3, dirty==0 (zero-cost eviction)
for i in range(scanCursor, CacheLineCount), step=16:
refChunk = SSE2.Load(refFlags + i)
refFreeMask = SSE2.MoveMask(SSE2.CompareEqual(refChunk, zero))
evChunk = SSE2.Load(evictionFlags + i)
// RRPV==3 and dirty==0 → byte & 0x07 == 0x06
masked = SSE2.And(evChunk, 0x07)
coldCleanMask = SSE2.MoveMask(SSE2.CompareEqual(masked, 0x06))
candidates = refFreeMask & coldCleanMask
while candidates != 0:
bit = TrailingZeroCount(candidates)
idx = i + bit
if originKeys[idx] not in Predictor.ActiveKeys:
evict(idx) // No writeback, discard directly
return
candidates &= candidates - 1
Pass 2: Find refCount==0, RRPV==3, dirty==1 (needs writeback)
Same as above, but match byte & 0x07 == 0x07
evict triggers writeback callbacks
Pass 3: Increment all RRPV+1 (aging), retry
Up to 3 rounds
```
**逐出执行**:
```csharp
private void Evict(int slot)
{
byte flags = _evictionFlags[slot];
// Dirty writeback
if ((flags & 0x01) != 0)
{
nuint origin = _originKeys[slot];
nuint cached = SlotAddressFromIndex(slot);
nuint ctx = _callbackCtx[slot];
// 1. Pre-processing (optional)
var preprocess = (delegate* unmanaged<nuint, nuint, void>)_preprocess[slot];
if (preprocess != null)
preprocess(cached, ctx);
// 2. Writeback
var writeback = (delegate* unmanaged<nuint, nuint, nuint, void>)_writeback[slot];
if (writeback != null)
writeback(origin, cached, ctx);
}
// Standard eviction
_map.TryRemove(_originKeys[slot], out _);
_slab.Free(SlotAddressFromIndex(slot));
_evictionFlags[slot] = 0;
_originKeys[slot] = 0;
_preprocess[slot] = 0;
_writeback[slot] = 0;
_callbackCtx[slot] = 0;
}
```
### 4.7 显式刷新
```csharp
/// <summary>
/// Explicitly writes back the specified dirty cache line. Called after all segments complete.
/// </summary>
public void FlushDirty(nuint originKey)
{
if (_map.TryGet(originKey, out nuint addr))
{
int slot = SlotIndex(addr);
if ((_evictionFlags[slot] & 0x01) != 0)
{
// Execute pre-processing + writeback
InvokeWriteBack(slot);
_evictionFlags[slot] &= 0xFE; // Clear dirty bit
}
}
}
/// <summary>
/// Writes back all dirty cache lines in bulk. Called after entire matrix computation completes.
/// </summary>
public void FlushAllDirty()
{
for (int i = 0; i < CacheLineCount; i++)
{
if ((_evictionFlags[i] & 0x01) != 0 && _originKeys[i] != 0)
{
InvokeWriteBack(i);
_evictionFlags[i] &= 0xFE;
}
}
}
```
### 4.8 Working Mode Switch
```csharp
public enum L4Mode
{
/// <summary>
/// Pass-through mode: Map() returns the original pointer directly; L4 does not intervene.
/// Suitable for single-NUMA + no-repack scenarios (e.g., vector addition, scalar multiplication).
/// </summary>
StreamTransparent,
/// <summary>
/// Buffer mode: full lookup/Pack/cache/eviction pipeline.
/// Suitable for FMA scenarios requiring repacking or multi-NUMA scenarios.
/// </summary>
CacheBuffer
}
```
Mode is determined at construction time and does not change at runtime. Decision logic:
- Requires Pack reordering (FMA core rows of matrix A) → CacheBuffer
- Multiple NUMA nodes → CacheBuffer
- Requires segment accumulation → CacheBuffer
- Single NUMA + no reordering + no segment → StreamTransparent
---
## 5. Segment Accumulation Scenario
### 5.1 Problem
For matrix multiplication `C = A × B` when the K dimension is large, segmented accumulation is required:
```
segment 0: C_partial = A[kr, 0..KC] × B[0..KC, kc]
segment 1: C_partial += A[kr, KC..2KC] × B[KC..2KC, kc]
...
segment N: C_partial += A[kr, N*KC..K] × B[N*KC..K, kc]
Final: C[kr, kc] = Σ all segment partial sums
```
If each segment's result is `+=` directly to the C matrix,
and C resides on a remote NUMA node, every segment incurs cross-node write latency.
### 5.2 L4 Solution
Temporarily store C's partial sums in L4's writable cache lines:
```
segment 0: c = l4.MapWritable(C_key, null, &AccumulateAdd)
// First miss → allocate zero-initialized cache line
FMA(c.Address, a.Address, b.Address) // c += a*b
c.Dispose()
segment 1: c = l4.MapWritable(C_key, null, &AccumulateAdd)
// Hit! Returns existing partial sum cache line
FMA(c.Address, a.Address, b.Address) // c += a*b
c.Dispose()
... after all segments complete ...
l4.FlushDirty(C_key)
// Calls AccumulateAdd(C_origin, cacheline, ctx)
// → Adds local partial sum back to remote C matrix in one shot
```
### 5.3 Benefits
```
Without segment buffering (OpenBLAS approach):
Each segment writes C matrix across NUMA
Remote writes per output tile = Zk times
Per-write latency = ~200-300ns (cross-node)
With L4 segment buffering:
All segment += operations complete locally in L4 cache lines
Partial sums likely remain in L1/L2 (8KB << L1 capacity)
Remote writes per output tile = 1 (final FlushDirty)
Writeback uses AVX2-vectorized stores: 8192B = 32 × 256-bit stores
Reduction ratio = Zk : 1
```
---
## 6. Uniqueness and Advantages
### 6.1 Deterministic Prefetch (Windowed OPT)
Essential difference from hardware cache replacement policies (LRU, RRIP, Hawkeye):
| Policy | Information Source | Prediction Method | Accuracy |
|---------------------|-------------------------|-------------------------------|---------------------------|
| LRU | Past access order | Least recently used | Moderate |
| RRIP | Insert/hit markers | Predict reuse distance | Moderate-High |
| Hawkeye | OPTgen retrospection | Learn past to infer future | High |
| **L4 Predictor** | **Future task queue** | **Direct future visibility** | **Perfect** (within window) |
L4's Predictor is a **deterministic Oracle** — it does not predict; it knows.
Because matrix computation task sequences are fully determined at scheduler allocation time.
### 6.2 NUMA Adaptability
The same code automatically gains NUMA benefits on different hardware configurations:
| Hardware Configuration | L4 Behavior |
|-------------------------|-------------------------------------------------------------|
| Single-socket CPU | Pack cache + segment accumulation buffer |
| Dual-socket CPU | Pack cache + NUMA migration cache + segment accum. |
| Hybrid-architecture CPU | Pack cache + cross-domain migration cache + segment accum. |
### 6.3 Callback-Driven Writeback Strategy
Using native function pointers for writeback avoids:
- Virtual method call overhead
- Interface dispatch overhead
- Delegate GC pressure
- Hard-coding specific writeback logic
Callers register `preprocess` and `writeback` callbacks at `MapWritable` time,
L4 invokes them directly through function pointers on eviction or Flush,
**zero managed overhead, zero virtual dispatch, performance equivalent to C function calls**.
### 6.4 Reference Count Protection
Through `CacheLine` handle reference counting, cache lines actively in use by compute threads are
absolutely never evicted (including writeback not being triggered). The SIMD eviction scan skips
lines with `refCount > 0` immediately via the `_refFlags` array, without checking linked lists or acquiring locks.
### 6.5 Cache Line Size Aligned to Matrix Kernel
The 8192B cache line size is not arbitrary:
- 1 `double MAT_KERNEL` (4×4 double matrix) = 128 Bytes
- 1 cache line = 64 `MAT_KERNEL` units
- Matrix multiplication micro-kernels operate on data sizes perfectly aligned to cache lines
- Compute kernel functions operate on cache lines as units; no cross-cache-line accesses occur
### 6.6 Fixed-Capacity Determinism
- Slab allocator never expands → memory footprint upper bound is known (64 MB)
- SIMD eviction scan → latency upper bound is known (at most 3 aging rounds × 512 SIMD comparisons)
- Swiss Table fixed 16,384 slots → lookup latency upper bound is known
- Writeback callback latency is controlled by the caller → predictable
- Worst-case L4 latency = lookup + eviction (incl. writeback) + allocation + memcpy, all statically computable
### 6.7 Reuse of Existing StgSharp Infrastructure
| L4 Component | Reused StgSharp Infrastructure |
|-----------------------|-------------------------------------------------------|
| Mapping table | `SwissTable` (implemented and benchmark-validated) |
| Mapping table SIMD | `IBucketScanner` + `SIMDID` architecture detection |
| Cache line alloc | `SlabAllocator.FromBuffer<T>()` |
| Hash computation | Hardware `CRC32C` instruction |
| Memory allocation | `NativeMemory.AlignedAlloc` |
| Future multi-platform | `IBucketScanner` + NRGA compilation pipeline |
### 6.8 Originality Analysis
After surveying existing mainstream numerical computation libraries, L4's design has
**no direct precedent or equivalent in known public implementations**.
#### NUMA Strategy Comparison with Existing Libraries
| Library / Framework | NUMA Strategy | Software Cache Layer | Mapping Table | Eviction Policy | Dirty Writeback |
|---------------------|-------------------------------------|---------------------|-----------------|-------------------------|--------------------|
| Intel MKL | Thread pinning + `numactl` | None | None | None | None |
| OpenBLAS | Thread affinity + block alignment | None | None | None | None |
| BLIS | Multi-level block alignment L1/L2/L3| None | None | None | None |
| Eigen | No explicit NUMA handling | None | None | None | None |
| cuBLAS / rocBLAS | GPU memory (hardware-solved) | None | None | None | None |
| **StgSharp L4** | **Software-managed thread-local cache** | **Yes** | **Swiss Table** | **RRPV + windowed OPT** | **Callback-driven** |
---
## 7. 从 v0.1 到 v0.3 的变更摘要
| 项目 | v0.1 | v0.3 |
|-----------------|-------------------------------|---------------------------------------|
| 逐出数据结构 | 4 级环形双向链表 | RRPV 2-bit 掩码 + 8KB SoA 数组 |
| 逐出扫描方式 | 链表头指针遍历 | SSE2 批量模式匹配(16 行/周期) |
| 热���更新 | 链表节点摘插(4 指针写) | 单字节位运算 `&= 0x03` |
| 老化操作 | 逐节点移链表 | SIMD 批量 +1 |
| 引用保护 | 无 | refCount > 0 时 SIMD 扫描跳过 |
| 预取策略 | 无 | Predictor 窗口 OPT(2048 条缓冲) |
| 线程模型 | L4 服务线程 + Command Queue | 无额外线程,CAS 抢锁 |
| 命令队列 | 链式 CommandCache 分段 | 不需要(预取刷新替代命令驱动) |
| 缓存行元数据 | Slab 分配的 CacheLineHead 结构 | SoA 平行数组 + 函数指针数组 |
| 工作模式 | 单一模式 | StreamTransparent / CacheBuffer 切换 |
| 写回策略 | 无(只读缓存) | dirty 位 + preprocess/writeback 回调 |
| 写回机制 | 无 | 本机函数指针,零托管开销 |
---
## 8. 未来工作
- [ ] L4 根类型重构(组合 Swiss Table、Slab、SoA 元数据、Predictor)
- [ ] Predictor 与矩阵乘法调度器的集成
- [ ] Segment 累加写回的 AVX2 向量化实现
- [ ] SIMD 逐出扫描的 AVX2 升级(32 行/周期)
- [ ] Swiss Table ARM NEON 实现
- [ ] Swiss Table AVX-512 升级(64 槽扫描从 16 次降至 4 次)
- [ ] 墓碑累积监控与自动 rehash 策略
- [ ] 性能基准测试(NUMA 本地 vs 远端访问延迟对比)
- [ ] Segment 累加 vs 直接写回的 benchmark 对比
- [ ] 与逐线程矩阵上下文的集成验证
- [ ] 双路 6242R 环境下的 NUMA 搬运性能验证
- [ ] 远期:PPC64LE 平台的 VSX 原生 Swiss Table 扫描支持
---
## 9. 参考
- Google Abseil Swiss Tables Design Notes
- HP Labs RRIP (Re-Reference Interval Prediction) 论文
- Hawkeye: Efficient Cache Replacement via OPTgen (ISCA 2017)
- StgSharp HLSF Allocator 设计文档
- StgSharp SIMD Architecture (`SIMDID` 体系)
- StgSharp NRGA Roadmap
- StgSharp `SlabAllocator<T>` 实现
- StgSharp `SwissTable` 实现及 Benchmark
@@ -0,0 +1,364 @@
// -----------------------------------------------------------------------------
// file="Ptr64"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
#pragma warning disable CA2225
public readonly struct Ptr64 : IEquatable<Ptr64>, IComparable<Ptr64>, IFormattable
{
public Ptr64(
ulong value
)
{
Value = value;
}
public ulong Value { get; }
public int CompareTo(
Ptr64 other
)
{
return Value.CompareTo(other.Value);
}
public bool Equals(
Ptr64 other
)
{
return Value == other.Value;
}
public override bool Equals(
object? obj
)
{
return obj is Ptr64 other && Equals(other);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return Value.ToString();
}
public string ToString(
string? format,
IFormatProvider? formatProvider
)
{
return Value.ToString(format, formatProvider);
}
public static Ptr64 operator -(
Ptr64 left,
ulong right
)
{
return new(left.Value - right);
}
public static ulong operator -(
Ptr64 left,
Ptr64 right
)
{
return left.Value - right.Value;
}
public static bool operator !=(
Ptr64 left,
Ptr64 right
)
{
return left.Value != right.Value;
}
public static Ptr64 operator &(
Ptr64 left,
ulong mask
)
{
return new(left.Value & mask);
}
public static Ptr64 operator |(
Ptr64 left,
ulong mask
)
{
return new(left.Value | mask);
}
public static Ptr64 operator +(
Ptr64 left,
ulong right
)
{
return new(left.Value + right);
}
public static bool operator <(
Ptr64 left,
Ptr64 right
)
{
return left.Value < right.Value;
}
public static Ptr64 operator <<(
Ptr64 left,
int shift
)
{
return new Ptr64(left.Value << shift);
}
public static bool operator <=(
Ptr64 left,
Ptr64 right
)
{
return left.Value <= right.Value;
}
public static bool operator ==(
Ptr64 left,
Ptr64 right
)
{
return left.Value == right.Value;
}
public static bool operator >(
Ptr64 left,
Ptr64 right
)
{
return left.Value > right.Value;
}
public static bool operator >=(
Ptr64 left,
Ptr64 right
)
{
return left.Value >= right.Value;
}
public static Ptr64 operator >>(
Ptr64 left,
int shift
)
{
return new Ptr64(left.Value >> shift);
}
public static implicit operator Ptr64(
ulong value
)
{
return new(value);
}
public static implicit operator ulong(
Ptr64 value
)
{
return value.Value;
}
}
public readonly struct SPtr64 : IEquatable<SPtr64>, IComparable<SPtr64>, IFormattable
{
public SPtr64(
long value
)
{
Value = value;
}
public long Value { get; }
public int CompareTo(
SPtr64 other
)
{
return Value.CompareTo(other.Value);
}
public bool Equals(
SPtr64 other
)
{
return Value == other.Value;
}
public override bool Equals(
object? obj
)
{
return obj is SPtr64 other && Equals(other);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return Value.ToString();
}
public string ToString(
string? format,
IFormatProvider? formatProvider
)
{
return Value.ToString(format, formatProvider);
}
public static SPtr64 operator -(
SPtr64 left,
long right
)
{
return new(left.Value - right);
}
public static long operator -(
SPtr64 left,
SPtr64 right
)
{
return left.Value - right.Value;
}
public static bool operator !=(
SPtr64 left,
SPtr64 right
)
{
return left.Value != right.Value;
}
public static SPtr64 operator &(
SPtr64 left,
long mask
)
{
return new(left.Value & mask);
}
public static SPtr64 operator |(
SPtr64 left,
long mask
)
{
return new(left.Value | mask);
}
public static SPtr64 operator +(
SPtr64 left,
long right
)
{
return new(left.Value + right);
}
public static bool operator <(
SPtr64 left,
SPtr64 right
)
{
return left.Value < right.Value;
}
public static SPtr64 operator <<(
SPtr64 left,
int shift
)
{
return new SPtr64(left.Value << shift);
}
public static bool operator <=(
SPtr64 left,
SPtr64 right
)
{
return left.Value <= right.Value;
}
public static bool operator ==(
SPtr64 left,
SPtr64 right
)
{
return left.Value == right.Value;
}
public static bool operator >(
SPtr64 left,
SPtr64 right
)
{
return left.Value > right.Value;
}
public static bool operator >=(
SPtr64 left,
SPtr64 right
)
{
return left.Value >= right.Value;
}
public static SPtr64 operator >>(
SPtr64 left,
int shift
)
{
return new SPtr64(left.Value >> shift);
}
public static implicit operator long(
SPtr64 value
)
{
return value.Value;
}
public static implicit operator SPtr64(
long value
)
{
return new(value);
}
}
#pragma warning disable CA2225
}
@@ -0,0 +1,39 @@
// -----------------------------------------------------------------------------
// file="SlabAllocationHandle"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using System;
using System.Runtime.CompilerServices;
namespace StgSharp.HighPerformance.Memory
{
public unsafe struct SlabAllocationHandle<T> : IDisposable where T : unmanaged
{
private readonly SlabAllocator<T> _allocator;
internal SlabAllocationHandle(
SlabAllocator<T> allocator,
bool locked
)
{
_allocator = allocator;
Locked = locked;
}
public bool Locked { get; set; }
public void Dispose()
{
if (Locked)
{
_allocator.ExitBufferReading();
Locked = false;
}
}
}
}
@@ -0,0 +1,118 @@
// -----------------------------------------------------------------------------
// file="SlabAllocator.Chunked"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Threading;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace StgSharp.HighPerformance.Memory
{
internal sealed unsafe class ChunkedSlabAllocator<T> : SlabAllocator<T> where T : unmanaged
{
private bool _disposed;
private BufferStack<nuint> _buffers;
private BufferStack<nuint> _recycle;
private readonly int _elementSize = Unsafe.SizeOf<T>();
private nuint _currentBuffer;
private nuint _currentCapacity, _currentIndex;
public ChunkedSlabAllocator(
nuint count
)
{
_currentBuffer = (nuint)NativeMemory.Alloc(count * ((nuint)_elementSize));
_currentCapacity = count;
int initCount = count switch
{
<= 4 => 4,
> 64 => 64,
_ => (int)count
};
Span<nuint> span = stackalloc nuint[initCount];
for (int i = 0; i < initCount; i++) {
span[i] = _currentBuffer + ((nuint)(i * _elementSize));
}
_buffers = new(4);
_buffers.Push(_currentBuffer);
_recycle = BufferStackBuilder.Create(span);
_currentCapacity = count;
_currentIndex = (nuint)initCount;
}
public override nuint Allocate()
{
if (_recycle.TryPop(out nuint handle)) {
return handle;
}
// nuint bufferHandle = _currentBuffer;
nuint curIndex = _currentIndex;
if (curIndex >= _currentCapacity)
{
nuint cap = _currentCapacity;
nuint newBuffer = (nuint)NativeMemory.Alloc(cap * ((nuint)_elementSize));
_buffers.Push(newBuffer);
_currentBuffer = newBuffer;
_currentIndex = 0;
}
curIndex = _currentIndex;
_currentIndex += 1;
return _currentBuffer + (((nuint)_elementSize) * curIndex);
}
public override void Dispose()
{
if (_disposed) {
return;
}
_disposed = true;
// NativeMemory.Free((void*)_currentBuffer);
while (_buffers.TryPop(out nuint buffer))
{
NativeMemory.Free((void*)buffer);
}
_buffers.Dispose();
_recycle.Dispose();
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void EnterBufferReading() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void ExitBufferReading() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Free(
nuint index
)
{
_recycle.Push(index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override SlabAllocationHandle<T> ReadAllocation()
{
return new SlabAllocationHandle<T>(this, false);
}
~ChunkedSlabAllocator()
{
Dispose();
}
}
}
@@ -0,0 +1,142 @@
// -----------------------------------------------------------------------------
// file="SlabAllocator.ChunkedConcurrent"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
internal sealed unsafe class ConcurrentChunkedSlabAllocator<T> : SlabAllocator<T>
where T : unmanaged
{
private bool _disposed;
private readonly BufferExpansionLock _lock = new();
private ConcurrentBufferStack<nuint> _buffers;
private ConcurrentBufferStack<nuint> _recycle;
private readonly int _elementSize = Unsafe.SizeOf<T>();
private nuint _currentBuffer;
private nuint _currentCapacity, _currentIndex;
public ConcurrentChunkedSlabAllocator(
nuint count
)
{
_currentBuffer = (nuint)NativeMemory.Alloc(count * (nuint)_elementSize);
_currentCapacity = count;
int initCount = count switch
{
<= 4 => 4,
> 64 => 64,
_ => (int)count
};
Span<nuint> span = stackalloc nuint[initCount];
for (int i = 0; i < initCount; i++) {
span[i] = _currentBuffer + (nuint)(i * _elementSize);
}
_buffers = new(4);
_recycle = BufferStackBuilder.CreateConcurrent(span);
_currentCapacity = count;
_currentIndex = (nuint)initCount;
}
public override nuint Allocate()
{
if (_recycle.TryPop(out nuint handle)) {
return handle;
}
while (true)
{
nuint bufferHandle = Volatile.Read(ref _currentBuffer);
nuint curIndex = Volatile.Read(ref _currentIndex);
_lock.EnterMetaDataRead();
try
{
if (curIndex >= _currentCapacity)
{
_lock.ExitMetaDataRead();
_lock.EnterExpansionProcess();
try
{
nuint cap = Volatile.Read(ref _currentCapacity);
nuint newBuffer = (nuint)NativeMemory.Alloc(cap * (nuint)_elementSize);
_buffers.Push(bufferHandle);
_ = Interlocked.Exchange(ref _currentBuffer, newBuffer);
_ = Interlocked.Exchange(ref _currentIndex, 0);
}
finally
{
_lock.ExitExpansionProcess();
}
}
curIndex = Volatile.Read(ref _currentIndex);
_ = Interlocked.Add(
ref Unsafe.As<nuint, ulong>(ref _currentIndex), (ulong)_elementSize);
return _currentBuffer + ((nuint)_elementSize * curIndex);
}
finally
{
if (_lock.IsThreadReadingMetaData) {
_lock.ExitMetaDataRead();
}
}
}
}
public override void Dispose()
{
if (_disposed) {
return;
}
_disposed = true;
NativeMemory.Free((void*)_currentBuffer);
while (_buffers.TryPop(out nuint buffer)) {
NativeMemory.Free((void*)buffer);
}
_lock.Dispose();
_buffers.Dispose();
_recycle.Dispose();
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void EnterBufferReading() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void ExitBufferReading() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Free(
nuint index
)
{
_recycle.Push(index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override SlabAllocationHandle<T> ReadAllocation()
{
return new SlabAllocationHandle<T>(this, false);
}
~ConcurrentChunkedSlabAllocator()
{
Dispose();
}
}
}
@@ -0,0 +1,111 @@
// -----------------------------------------------------------------------------
// file="SlabAllocator.Fixed"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Collections;
using StgSharp.HighPerformance.Memory;
using StgSharp.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.HighPerformance.Memory
{
internal unsafe class FixedSlabAllocator<T> : SlabAllocator<T> where T : unmanaged
{
private Action<nuint> _freeHandle;
private bool _disposed;
private CapacityFixedStack<nuint> _recycle;
private readonly int _elementSize = Unsafe.SizeOf<T>();
private nuint _buffer;
private nuint _Capacity, _currentIndex;
internal FixedSlabAllocator(
nuint buffer,
nuint count,
Action<nuint> freeHandle
)
{
_freeHandle = freeHandle;
_buffer = buffer;
_Capacity = count;
int initCount = count switch
{
<= 4 => 4,
> 64 => 64,
_ => (int)count
};
Span<nuint> span = stackalloc nuint[initCount];
for (int i = 0; i < initCount; i++) {
span[i] = _buffer + ((nuint)(i * _elementSize));
}
_recycle = CapacityFixedStackBuilder.Create(span);
_Capacity = count;
_currentIndex = (nuint)initCount;
}
public override nuint Allocate()
{
if (_recycle.TryPop(out nuint handle)) {
return handle;
}
// nuint bufferHandle = _currentBuffer;
nuint curIndex = _currentIndex;
if (curIndex >= _Capacity) {
return 0;
}
curIndex = _currentIndex;
_currentIndex += 1;
return _buffer + (((nuint)_elementSize) * curIndex);
}
public override void Dispose()
{
if (_disposed) {
return;
}
_disposed = true;
_freeHandle(_buffer);
_recycle = null!;
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void EnterBufferReading() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void ExitBufferReading() { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Free(
nuint index
)
{
_recycle.Push(index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override SlabAllocationHandle<T> ReadAllocation()
{
return new SlabAllocationHandle<T>(this, false);
}
~FixedSlabAllocator()
{
Dispose();
}
}
}
@@ -0,0 +1,146 @@
// -----------------------------------------------------------------------------
// file="SlabAllocator.SequencialConcurrent"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.Threading;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace StgSharp.HighPerformance.Memory
{
public sealed unsafe class ConcurrentSequentialSlabAllocator<T> : SlabAllocator<T>
where T : unmanaged
{
private T* _buffer;
private bool _disposed;
private readonly BufferExpansionLock _lock = new();
private readonly ConcurrentBufferStack<nuint> _stack;
private readonly int _elementSize;
private nuint _highestAllocated;
private ulong _capacity;
public ConcurrentSequentialSlabAllocator(
nuint initialCapacity
)
{
_elementSize = Unsafe.SizeOf<T>();
_capacity = initialCapacity;
_highestAllocated = 0;
_buffer = (T*)NativeMemory.Alloc(((nuint)_capacity) * ((nuint)_elementSize));
int initCount = _capacity switch
{
<= 4 => 4,
> 64 => 64,
_ => ((int)_capacity) / 4
};
Span<nuint> span = stackalloc nuint[initCount];
for (int i = 0; i < initCount; i++) {
span[i] = (nuint)i;
}
_stack = BufferStackBuilder.CreateConcurrent(span);
_highestAllocated += (nuint)initCount;
}
internal T* BasePointer => _buffer;
public override nuint Allocate()
{
if (_stack.TryPop(out nuint index)) {
return index;
}
while (true)
{
index = Volatile.Read(ref _highestAllocated);
_lock.EnterMetaDataRead();
try
{
if (index >= _capacity)
{
_lock.ExitMetaDataRead();
_lock.EnterExpansionProcess();
try
{
_capacity = Volatile.Read(ref _capacity) * 2;
nuint newSize = ((nuint)_capacity) * ((nuint)_elementSize);
_lock.EnterBufferCopy();
try
{
_buffer = (T*)NativeMemory.Realloc(_buffer, newSize);
}
finally
{
_lock.ExitBufferCopy();
}
}
finally
{
_lock.ExitExpansionProcess();
}
}
_ = Interlocked.Increment(ref Unsafe.As<nuint, ulong>(ref _highestAllocated));
return index;
}
finally
{
if (_lock.IsThreadReadingMetaData) {
_lock.ExitMetaDataRead();
}
}
}
}
public override void Dispose()
{
if (_disposed) {
return;
}
_disposed = true;
_stack.Dispose();
NativeMemory.Free(_buffer);
_lock.Dispose();
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void EnterBufferReading()
{
_lock.EnterBufferRead();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void ExitBufferReading()
{
_lock.ExitBufferRead();
}
public override void Free(
nuint index
)
{
_stack.Push(index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override SlabAllocationHandle<T> ReadAllocation()
{
EnterBufferReading();
return new SlabAllocationHandle<T>(this, true);
}
~ConcurrentSequentialSlabAllocator()
{
Dispose();
}
}
}
@@ -0,0 +1,115 @@
// -----------------------------------------------------------------------------
// file="SlabAllocator"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.HighPerformance.Memory;
using System;
namespace StgSharp.HighPerformance.Memory
{
public static class SlabAllocator
{
/// <summary>
/// Creates a new SLAB allocator instance with the specified configuration.
/// </summary>
/// <param name="count">
/// Initial number of objects that can be allocated.
/// </param>
/// <param name="layout">
/// Memory layout strategy for the SLAB allocator.
/// </param>
/// <param name="concurrentSupport">
/// Whether to enable concurrent access support.
/// </param>
/// <returns>
/// A new SLAB allocator instance.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when an invalid layout is specified.
/// </exception>
/// <exception cref="NotSupportedException">
/// Thrown when requesting unsupported configurations.
/// </exception>
/// <remarks>
/// <para><strong> Important Threading Considerations: </strong></para> <para>While SLAB
/// allocators themselves may be designed for single-threaded use, allocated objects are
/// frequently shared across multiple threads in real-world applications. Due to the
/// special implementation of expandable concurrent sequential SLAB allocators, they rely
/// on internal expansion locks to ensure that memory buffer expansions (which involve
/// copying the entire buffer to a new location) do not occur while other threads are
/// reading from allocated regions. </para> <para><strong> Sequential Layout Limitations:
/// </strong><br /> Sequential layout SLAB allocators cannot provide a single- threaded
/// version because:<list type="bullet"><item><description> They use contiguous memory
/// layout that requires buffer expansion when capacity is exceeded
/// </description></item><item><description> Buffer expansion involves copying all
/// existing data to a new memory location </description></item><item><description>Without
/// proper synchronization, concurrent access to allocated objects during synchronization,
/// concurrent access to allocated objects during expansion would cause memory
/// corruption</description></item><item><description>The expansion lock mechanism is
/// essential even if allocation itself is single- threaded
/// </description></item></list></para> <para><strong> Chunked Layout: </strong><br />
/// Chunked layout allocators can optionally provide single-threaded versions since they
/// don't require buffer expansion and copying. Each chunk is allocated independently,
/// avoiding the memory relocation issues present in sequential layouts. </para>
/// </remarks>
public static SlabAllocator<T> Create<T>(
nuint count,
SlabBufferLayout layout,
bool concurrentSupport = true
) where T : unmanaged
{
return layout switch
{
SlabBufferLayout.Sequential => concurrentSupport ?
new ConcurrentSequentialSlabAllocator<T>(count) :
throw (new NotSupportedException("Non-concurrent version of sequential layout SLAB is not supported due to expansion lock requirements for memory safety during buffer expansion.")),
SlabBufferLayout.Chunked => concurrentSupport ?
new ConcurrentChunkedSlabAllocator<T>(count) :
new ChunkedSlabAllocator<T>(count),
_ => throw (new ArgumentOutOfRangeException(nameof(layout), layout, null))
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SlabAllocator<T> FromBuffer<T>(
nuint buffer,
nuint count,
Action<nuint> freeHandle
) where T : unmanaged
{
return new FixedSlabAllocator<T>(buffer, count, freeHandle);
}
}
public abstract class SlabAllocator<T> : IDisposable where T : unmanaged
{
public abstract nuint Allocate();
public abstract void Dispose();
public abstract void EnterBufferReading();
public abstract void ExitBufferReading();
public abstract void Free(
nuint index
);
public abstract SlabAllocationHandle<T> ReadAllocation();
}
public enum SlabBufferLayout
{
Sequential,
Chunked
}
}
@@ -0,0 +1,669 @@
// -----------------------------------------------------------------------------
// file="TwoLayerSegregatedFitAllocator"
// Project: StgSharp
// Copyright (c) Nitload.
// SPDX-License-Identifier: MIT
// -----------------------------------------------------------------------------
using StgSharp.HighPerformance.Memory;
using StgSharp.HighPerformance.ProcessorAbstraction;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using static StgSharp.HighPerformance.Memory.TwoLayerSegregatedFitAllocator;
namespace StgSharp.HighPerformance.Memory
{
public unsafe class TwoLayerSegregatedFitAllocator : IDisposable
{
private const ulong Mask48Bits = 0x0000FFFFFFFFFFFF;
private readonly int[] _bucket;
private readonly Metadata* _spareMemory;
private readonly Action<nuint> _freeHandle;
private bool disposedValue;
private readonly int _align;
private readonly int _maxLevel;
private int _metadataEnd;
private readonly Ptr64 _basePtr;
private readonly ulong _maxAllocSize;
private readonly ulong _totalSize;
internal TwoLayerSegregatedFitAllocator(
nuint byteSize,
int align,
ulong maxBlockSize,
Func<nuint, int, nuint> allocator,
Action<nuint> freeHandle
)
: this(byteSize, align, maxBlockSize, (void*)allocator(byteSize, align), freeHandle) { }
internal TwoLayerSegregatedFitAllocator(
nuint byteSize,
int align,
ulong maxBlockSize,
void* externalPointer,
Action<nuint> freeHandle
)
{
_totalSize = byteSize;
_basePtr = (Ptr64)(ulong)externalPointer;
_align = align;
_maxAllocSize = ulong.Max(64, (~64UL) & (ulong.Min(maxBlockSize + 63, Mask48Bits)));
_maxLevel = int.MaxValue;
_maxLevel = GetLevel(_maxAllocSize);
_bucket = new int[GetLevel(_maxAllocSize) + 2];
_spareMemory = &((Metadata*)(ulong)(_basePtr + _totalSize))[-1];
_freeHandle = freeHandle ?? (static_ => { });
_metadataEnd = 0;
// init spare memory node
_spareMemory->Position = _basePtr;
_spareMemory->SizeForSpare = _totalSize - Metadata.SizeOf;
_spareMemory->NextLevel = 0;
_spareMemory->NextNear = 0;
_spareMemory->PreviousLevel = 0;
_spareMemory->PreviousNear = 0;
}
public TwoLayerSegregatedFitAllocator(
nuint byteSize
)
: this(byteSize, 16, 32U * 1024U * 1024U, NativeMemory.AlignedAlloc(byteSize, 16),
ptr => NativeMemory.AlignedFree((void*)ptr)) { }
public TwoLayerSegregatedFitAllocator(
nuint byteSize,
uint maxBlockSize
)
: this(byteSize, 16, maxBlockSize, NativeMemory.AlignedAlloc(byteSize, 16),
ptr => NativeMemory.AlignedFree((void*)ptr)) { }
public TwoLayerSegregatedFitAllocator(
nuint byteSize,
int align
)
: this(byteSize, align, 32U * 1024U * 1024U, NativeMemory.AlignedAlloc(byteSize, 16),
ptr => NativeMemory.AlignedFree((void*)ptr)) { }
public TwoLayerSegregatedFitAllocator(
nuint byteSize,
int align,
nuint maxBlockSize
)
: this(byteSize, align, maxBlockSize, NativeMemory.AlignedAlloc(byteSize, 16),
ptr => NativeMemory.AlignedFree((void*)ptr)) { }
public void Dispose()
{
Dispose(disposing:true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(
bool disposing
)
{
if (!disposedValue)
{
if (disposing) {
_freeHandle((nuint)_basePtr);
}
disposedValue = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetLevel(
ulong size
)
{
int origin = 57 - BitOperations.LeadingZeroCount(size);
return int.Min(origin, _maxLevel);
}
/// <summary>
/// Put a metadata node back to free list and waiting for next allocation.
/// </summary>
/// <param name="node">
///
/// </param>
private void RecycleNode(
Metadata* node
)
{
int nodeIdx = (int)(_spareMemory - node);
int level = GetLevel(node->Size);
int nextLevelIdx = node->NextLevel;
int prevLevelIdx = node->PreviousLevel;
RemoveFromBucket(level, node, nodeIdx);
// remove from position chain
int prevNearIdx = node->PreviousNear;
int nextNearIdx = node->NextNear;
Metadata* prevNear = &_spareMemory[-prevNearIdx];
prevNear->NextNear = nextNearIdx;
if (nextNearIdx != 0)
{
Metadata* nextNear = &_spareMemory[-nextNearIdx];
nextNear->PreviousNear = prevNearIdx;
} else
{
_spareMemory->PreviousNear = prevNearIdx;
}
node->PreviousNear = 0;
node->NextNear = 0;
node->Position = 0;
node->SizeForSpare = 0;
node->State = AllocationState.SpareOrCollect;
node->PreviousLevel = 0;
int nextIdx = _spareMemory->NextLevel;
if (nextIdx != 0)
{
Metadata* next = &_spareMemory[-nextIdx];
next->PreviousLevel = nodeIdx;
}
node->NextLevel = nextIdx;
_spareMemory->NextLevel = nodeIdx;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RemoveFromBucket(
Metadata* node,
int nodeIdx
)
{
RemoveFromBucket(GetLevel(node->Size), node, nodeIdx);
}
private void RemoveFromBucket(
int level,
Metadata* node,
int nodeIdx
)
{
int currentLevel = level;
int currentNextLevelIdx = node->NextLevel;
int currentPrevLevelIdx = node->PreviousLevel;
if (_bucket[currentLevel] == nodeIdx)
{
_bucket[currentLevel] = currentNextLevelIdx;
} else if (currentPrevLevelIdx != 0)
{
Metadata* prevLevel = &_spareMemory[-currentPrevLevelIdx];
prevLevel->NextLevel = currentNextLevelIdx;
}
if (currentNextLevelIdx != 0)
{
Metadata* nextLevel = &_spareMemory[-currentNextLevelIdx];
nextLevel->PreviousLevel = currentPrevLevelIdx;
}
node->PreviousLevel = 0;
node->NextLevel = 0;
}
// Assuming node to remove is head f bucket
private void RemoveFromBucketHead(
int level,
Metadata* node,
int nodeIdx
)
{
int currentLevel = level;
int currentNextLevelIdx = node->NextLevel;
// int currentPrevLevelIdx = 0;
if (_bucket[currentLevel] == nodeIdx)
{
_bucket[currentLevel] = currentNextLevelIdx;
}
/*
else if (currentPrevLevelIdx != 0)
{
Metadata* prevLevel = &_spareMemory[-currentPrevLevelIdx];
prevLevel->NextLevel = currentNextLevelIdx;
}
/**/
if (currentNextLevelIdx != 0)
{
Metadata* nextLevel = &_spareMemory[-currentNextLevelIdx];
nextLevel->PreviousLevel = 0;
}
node->PreviousLevel = 0;
node->NextLevel = 0;
}
private bool TryAllocNewChunk(
ulong size,
out Metadata* meta
)
{
int idx = _spareMemory->NextLevel;
ulong spareSize = _spareMemory->Size;
if (idx != 0)
{
if (spareSize < size)
{
meta = null;
return false;
}
meta = &_spareMemory[-idx];
int nextSpareIdx = meta->NextLevel;
if (nextSpareIdx != 0)
{
Metadata* nextSpare = &_spareMemory[-nextSpareIdx];
nextSpare->PreviousLevel = 0;
}
_spareMemory->NextLevel = nextSpareIdx;
} else
{
if (spareSize < Metadata.SizeOf + size)
{
meta = null;
return false;
}
_metadataEnd++;
idx = _metadataEnd;
meta = &_spareMemory[-idx];
spareSize -= Metadata.SizeOf;
}
spareSize -= size;
meta->Size = size;
meta->Position = _spareMemory->Position;
meta->State = AllocationState.Free;
meta->PreviousLevel = 0;
meta->NextLevel = 0;
_spareMemory->Size = spareSize;
_spareMemory->Position += size;
// insert the new chunk to position chain
int nearIdx = _spareMemory->PreviousNear;
Metadata* near = &_spareMemory[-nearIdx];
near->NextNear = idx;
meta->PreviousNear = nearIdx;
_spareMemory->PreviousNear = idx;
meta->NextNear = 0;
return true;
}
private bool TryGetNewMetadata(
out Metadata* meta
)
{
ulong spareSize = _spareMemory->Size;
if (spareSize < Metadata.SizeOf)
{
meta = null;
return false;
}
_metadataEnd++;
meta = &_spareMemory[-_metadataEnd];
spareSize -= Metadata.SizeOf;
_spareMemory->Size = spareSize;
return true;
}
private bool TryTakeBucketEntry(
ulong minimumSize,
out Metadata* entry
)
{
int level = GetLevel(minimumSize);
// first try to find in the target level
{
int idx = _bucket[level];
while (idx != 0)
{
Metadata* candidate = &_spareMemory[-idx];
if (candidate->Size >= minimumSize)
{
RemoveFromBucket(level, candidate, idx);
entry = candidate;
return true;
}
idx = candidate->NextLevel;
}
}
for (int i = level + 1; i < _bucket.Length; i++)
{
int idx = _bucket[i];
if (idx != 0)
{
Metadata* candidate = &_spareMemory[-idx];
RemoveFromBucketHead(i, candidate, idx);
entry = candidate;
return true;
}
}
entry = null;
return false;
}
[StructLayout(LayoutKind.Explicit)]
public readonly struct Handle
{
[FieldOffset(0)] internal readonly Metadata* _meta;
[FieldOffset(8)] public readonly Ptr64 Address;
[FieldOffset(16)] public readonly Ptr64 Size;
internal Handle(
Metadata* meta,
Ptr64 baseAddress
)
{
_meta = meta;
Address = (ulong)meta->Position;
Size = meta->Size;
}
[Obsolete("You should not create instance from default. Alloc from allocator please")]
public Handle() { }
public ref byte this[
int offset
] => ref ((byte*)(ulong)Address)[offset];
public Span<byte> AsSpan(
int range = -1
)
{
return new Span<byte>((byte*)(ulong)Address, range < 0 ? (int)(ulong)Size : range);
}
}
internal enum AllocationState : byte
{
SpareOrCollect = 0,
Free = 1,
Alloc = 2,
}
[StructLayout(LayoutKind.Explicit, Size = 32)]
internal struct Metadata
{
[FieldOffset(16)] private ulong PositionMask;
[FieldOffset(24)] private ulong SizeMask;
[FieldOffset(30)] internal AllocationState State;
[FieldOffset(31)] internal sbyte Level;
[FieldOffset(0)] public int NextLevel;
[FieldOffset(4)] public int NextNear;
[FieldOffset(8)] public int PreviousLevel;
[FieldOffset(12)] public int PreviousNear;
public Ptr64 Position
{
readonly get => PositionMask;
set => PositionMask = value;
}
public Ptr64 Size
{
readonly get => (Ptr64)(SizeMask & Mask48Bits);
set => SizeMask = (SizeMask & ~Mask48Bits) | ((ulong)value & Mask48Bits);
}
public Ptr64 SizeForSpare
{
readonly get => SizeMask;
set => SizeMask = value;
}
public static uint SizeOf { get; } = (uint)Unsafe.SizeOf<Metadata>();
}
#region alloc and free
public Handle Alloc(
nuint size
)
{
ulong allocSize = ulong.Max(64, (~63UL) & ((ulong)size + 63));
if (allocSize > _maxAllocSize) {
throw new OverflowException("Requested allocation size exceeds the maximum block size.");
}
if (!TryTakeBucketEntry(allocSize, out Metadata* entry) &&
!TryAllocNewChunk(allocSize, out entry)) {
throw new OverflowException("No available ");
}
int entryIdx = (int)(_spareMemory - entry);
ulong entrySize = entry->Size;
if (entrySize < allocSize) {
throw new OverflowException("Bucket returned an undersized block.");
}
ulong remain = entrySize - allocSize;
// slice tail
if (remain > (entrySize >> 7) && TryGetNewMetadata(out Metadata* newEntry))
{
int newEntryIdx = (int)(_spareMemory - newEntry);
newEntry->Position = entry->Position + allocSize;
newEntry->Size = remain;
newEntry->State = AllocationState.Free;
int newLevel = GetLevel(remain);
int bucketHeadIdx = _bucket[newLevel];
if (bucketHeadIdx != 0)
{
// if there is already an entry in the bucket, link it to the new entry
Metadata* bucketHead = &_spareMemory[-bucketHeadIdx];
bucketHead->PreviousLevel = newEntryIdx;
}
newEntry->NextLevel = bucketHeadIdx;
newEntry->PreviousLevel = 0;
_bucket[newLevel] = newEntryIdx;
// push to position chain
int afterIdx = entry->NextNear;
Metadata* after = &_spareMemory[-afterIdx];
newEntry->NextNear = afterIdx;
after->PreviousNear = newEntryIdx;
entry->NextNear = newEntryIdx;
newEntry->PreviousNear = entryIdx;
}
entry->State = AllocationState.Alloc;
if (entry->Position - (ulong)_basePtr > _totalSize) {
throw new OverflowException("Allocated memory address exceeds the maximum block size.");
}
return new Handle(entry, _basePtr);
}
public void Collect()
{
int currentIdx = _spareMemory->NextNear;
do
{
if (currentIdx == 0)
{
break;
}
Metadata* current = &_spareMemory[-currentIdx];
if (current->State == AllocationState.Free)
{
// scan next
Ptr64 size = 0;
int nextIdx = current->NextNear;
while (nextIdx != 0)
{
Metadata* next = &_spareMemory[-nextIdx];
if (next->State != AllocationState.Free)
{
break;
}
size += next->Size;
RecycleNode(next);
nextIdx = current->NextNear;
}
if (size > 0)
{
current->Size += size;
RemoveFromBucket(current, currentIdx);
int newLevel = GetLevel(current->Size);
int bucketHeadIdx = _bucket[newLevel];
if (bucketHeadIdx != 0)
{
Metadata* bucketHead = &_spareMemory[-bucketHeadIdx];
bucketHead->PreviousLevel = currentIdx;
}
current->NextLevel = bucketHeadIdx;
current->PreviousLevel = 0;
_bucket[newLevel] = currentIdx;
}
}
currentIdx = current->NextNear;
} while (true);
}
public void Free(
Handle handle,
int collectScanWindow = 4
)
{
Metadata* head = handle._meta;
int headIdx = (int)(_spareMemory - head);
Ptr64 size = handle.Size;
for (int i = 0; i < collectScanWindow; i++)
{
// forward scan, previous direction
int forwardIdx = head->PreviousNear;
if (forwardIdx == 0)
{
break;
}
Metadata* prev = &_spareMemory[-forwardIdx];
if (prev->State != AllocationState.Free)
{
break;
}
size += prev->Size;
head->Position = prev->Position;
RecycleNode(prev);
}
for (int i = 0; i < collectScanWindow; i++)
{
// backward scan, next direction
int nextIdx = head->NextNear;
if (nextIdx == 0)
{
break;
}
Metadata* prev = &_spareMemory[-nextIdx];
if (prev->State != AllocationState.Free)
{
break;
}
size += prev->Size;
RecycleNode(prev);
}
// recycle to level;
head->Size = size;
head->State = AllocationState.Free;
head->PreviousLevel = 0;
int level = GetLevel(head->Size);
int bucketHeadIdx = _bucket[level];
head->NextLevel = 0;
if (bucketHeadIdx != 0)
{
Metadata* bucketHead = &_spareMemory[-bucketHeadIdx];
bucketHead->PreviousLevel = headIdx;
head->NextLevel = bucketHeadIdx;
}
_bucket[level] = headIdx;
return;
}
public bool TryAlloc(
nuint size,
out Handle h
)
{
ulong allocSize = ulong.Max(64, (~63UL) & ((ulong)size + 63));
if (allocSize > _maxAllocSize)
{
h = default;
return false;
}
if (!TryTakeBucketEntry(allocSize, out Metadata* entry) &&
!TryAllocNewChunk(allocSize, out entry))
{
h = default;
return false;
}
int entryIdx = (int)(_spareMemory - entry);
ulong entrySize = entry->Size;
if (entrySize < allocSize)
{
h = default;
return false;
}
ulong remain = entrySize - allocSize;
// slice tail
if (remain > (entrySize >> 7) && TryGetNewMetadata(out Metadata* newEntry))
{
int newEntryIdx = (int)(_spareMemory - newEntry);
newEntry->Position = entry->Position + allocSize;
newEntry->Size = remain;
newEntry->State = AllocationState.Free;
int newLevel = GetLevel(remain);
int bucketHeadIdx = _bucket[newLevel];
if (bucketHeadIdx != 0)
{
// if there is already an entry in the bucket, link it to the new entry
Metadata* bucketHead = &_spareMemory[-bucketHeadIdx];
newEntry->NextLevel = bucketHeadIdx;
bucketHead->PreviousLevel = newEntryIdx;
} else
{
newEntry->NextLevel = 0;
}
newEntry->PreviousLevel = 0;
_bucket[newLevel] = newEntryIdx;
// push to position chain
int afterIdx = entry->NextNear;
Metadata* after = &_spareMemory[-afterIdx];
newEntry->NextNear = afterIdx;
after->PreviousNear = newEntryIdx;
entry->NextNear = newEntryIdx;
newEntry->PreviousNear = entryIdx;
}
entry->State = AllocationState.Alloc;
if (entry->Position - (ulong)_basePtr > _totalSize)
{
h = default;
throw new OverflowException("Allocated memory address exceeds the maximum block size.");
}
h = new Handle(entry, _basePtr);
return true;
}
#endregion
}
}