namespace UnmanagedMMU
{
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnmanagedMMU.Allocators;
using UnmanagedMMU.Handles;
using UnmanagedMMU.Handles.Internal;
///
/// Provides an unmanaged heap for long-lived allocations with reuse.
///
///
///
/// minimizes calls to the underlying allocator by retaining
/// freed blocks in size-segregated free lists and reusing them when possible.
///
///
/// Allocation strategy:
///
/// -
/// Small allocations (≤ 1 KB) use fixed size buckets
///
/// -
/// Medium allocations (≤ 256 KB) use best-fit reuse
///
/// -
/// Large allocations (> 256 KB) use tolerance-based reuse
///
///
///
///
public unsafe sealed class WorkspaceHeap : IDisposable, IUnmanagedMemoryOwner
{
///
/// Struct defining metadata to be stored with each allocation
///
private struct AllocationMetadata
{
public IntPtr Block;
public nuint Alignment;
}
///
/// The maximum size, in bytes, for "small" allocations. Uses fixed-size buckets
///
private const nuint _smallThreshold = 1024;
///
/// The maximum size, in bytes, for allocations considered "medium". Uses best-fit reuse
///
private const nuint _mediumThreshold = 256 * 1024; // 256 KB
///
/// The maximum absolute number of bytes that may be wasted when reusing a large block for a "large" allocation.
///
private const nuint _largeMaxWasteBytes = 256 * 1024; // 256 KB
///
/// The maximum allowed size ratio when reusing a large allocation block.
///
///
/// For example, a value of 1.25 allows a block up to 25% larger than
/// the requested size to be reused.
///
private const double _largeWasteRatioLimit = 1.25;
///
/// Predefined bucket sizes used for small allocation reuse.
///
private static readonly nuint[] _sizeClasses =
[
32, 64, 96, 128, 160, 192, 224, 256,
288, 320, 352, 384, 416, 448, 480, 512,
544, 576, 608, 640, 672, 704, 736, 768,
800, 832, 864, 896, 928, 960, 992, 1024
];
///
/// Allocator interface used for all underlying unmanaged memory operations.
///
private readonly IUnmanagedAllocator _allocator;
///
/// Internal lock, ensures thread safety while maintaining a simple interface
///
private readonly Lock _lock = new();
///
/// Free lists for small allocations, keyed by the bucket size, in bytes.
///
///
/// For a given bucket, the corresponding stack contains previously allocated blocks that are available to be used
///
private readonly Dictionary> _smallFree = [];
///
/// Free lists for medium allocations keyed by the exactly allocated size, in bytes, and sorted for best-fit.
///
private readonly SortedDictionary> _mediumFree = [];
///
/// Free lists for large allocations keyed by the exactly allocated size, in bytes, sorted for tolerance-based reuse.
///
private readonly SortedDictionary> _largeFree = [];
///
/// Tracks the total bytes of memory reserved from the provided .
///
private nuint _totalReserved;
///
/// Total memory currently in use by active allocations, in bytes.
///
private nuint _totalInUse;
///
/// Counts actual underlying OS allocations.
///
private nuint _totalAllocations;
///
/// Indicates whether this has been disposed.
///
private volatile bool _disposed;
///
/// Creates a new .
///
public WorkspaceHeap()
: this(new DefaultUnmanagedAllocator())
{
}
///
/// Creates a new using the specified .
///
/// Allocator implementing .
internal WorkspaceHeap(IUnmanagedAllocator allocator)
{
_allocator = allocator;
// Initialize small-size buckets
foreach (nuint size in _sizeClasses)
{
_smallFree[size] = [];
}
}
///
/// Gets the total number of bytes currently allocated from the underlying allocator.
///
///
/// This includes both active allocations and freed blocks retained for reuse.
///
public nuint TotalReservedBytes
{
get
{
lock (_lock)
return _totalReserved;
}
}
///
/// Gets the total number of bytes currently in use by active allocations.
///
///
/// This value decreases when memory is freed and increases when new allocations occur.
///
public nuint TotalUsedBytes
{
get
{
lock (_lock)
{
return _totalInUse;
}
}
}
///
/// Gets the total number of allocation operations performed by this heap.
///
///
/// This counts new underlying OS allocations, not reuse from free lists.
/// Useful for performance diagnostics and testing reuse behavior.
///
public nuint TotalAllocationCount
{
get
{
lock (_lock)
{
return _totalAllocations;
}
}
}
///
/// Indicates whether the has been disposed.
///
public bool IsDisposed
{
get { return _disposed; }
}
///
/// Determines the small size bucket for a requested allocation.
///
/// The allocation size to get the bucket size for
/// The small size bucket for the requested allocation
private static nuint GetSizeClass(nuint size)
{
foreach (nuint s in _sizeClasses)
{
if (size <= s)
{
return s;
}
}
return size;
}
///
/// Allocates a new block from the underlying allocator including a header.
///
/// Requested payload size in bytes.
/// FOO
///
/// Pointer to the allocated block (header included).
///
private IntPtr AllocateNewAligned(nuint payloadSize, nuint alignment)
{
void* raw = _allocator.AllocAligned(payloadSize, alignment);
_totalReserved += payloadSize;
_totalAllocations++;
return (IntPtr)raw;
}
///
/// Allocates unmanaged memory from the workspace heap.
///
/// Number of elements to allocate.
/// If true, memory is zero-initialized.
/// An to the allocated memory.
/// Thrown if heap is disposed.
/// Thrown if size is zero.
/// The memory returned by this method is always 16-byte aligned, for other alignemnts use
public IMemoryHandle Allocate(int count, bool zero = false) where T : unmanaged
{
return AllocateAligned(count, SegmentAlignment.Aligned16, zero);
}
///
///
///
///
///
///
///
///
///
public IMemoryHandle AllocateAligned(int count, SegmentAlignment alignment, bool zero = false) where T: unmanaged
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
ArgumentOutOfRangeException.ThrowIfZero(count);
if ((nuint)count > nuint.MaxValue / (nuint)(sizeof(T)))
{
throw new OverflowException($"Requested allocation of {count} elements of type {typeof(T)} exceeds allowable maximum memory size.");
}
nuint size = (nuint)count * (nuint)sizeof(T);
nuint requestedAlignment = (nuint)alignment;
nuint effectiveAlignment = requestedAlignment < (nuint)sizeof(T) ? (nuint)sizeof(T) : requestedAlignment;
lock (_lock)
{
ThrowIfDisposed();
void* ptr;
if (size <= _smallThreshold)
ptr = AllocateSmallAligned(size, effectiveAlignment, zero);
else if (size <= _mediumThreshold)
ptr = AllocateMediumAligned(size, effectiveAlignment, zero);
else
ptr = AllocateLargeAligned(size, effectiveAlignment, zero);
return new PersistentMemoryHandle((T*)ptr, size, effectiveAlignment, this);
}
}
///
/// TODO: Fill in
///
///
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int FindIndexAlignmentMatch(nuint alignment, List list)
{
// Attempt to find an index match the requested size
int mIdx = -1;
for (int i = 0; i < list.Count; i++)
{
if (list[i].Alignment >= alignment)
{
mIdx = i;
break;
}
}
return mIdx;
}
///
/// Allocates a small-size block using bucketed free lists.
///
private void* AllocateSmallAligned(nuint size, nuint alignment, bool zero)
{
nuint bucket = GetSizeClass(size);
List free = _smallFree[bucket];
// Attempt to find an index match the requested size
int mIdx = FindIndexAlignmentMatch(alignment, free);
if (mIdx >= 0)
{
AllocationMetadata meta = free[mIdx];
free.RemoveAt(mIdx);
_totalInUse += bucket;
void* user = (void*)meta.Block;
if (zero)
{
Unsafe.InitBlockUnaligned(user, 0, (uint)bucket);
}
return user;
}
// allocate a new small block
IntPtr block = AllocateNewAligned(bucket, alignment);
_totalInUse += bucket;
void* nUser = (void*)block;
if (zero)
{
Unsafe.InitBlockUnaligned(nUser, 0, (uint)bucket);
}
return nUser;
}
/// Allocates a medium-size block using best-fit reuse.
private void* AllocateMediumAligned(nuint size, nuint alignment, bool zero)
{
foreach (KeyValuePair> kv in _mediumFree)
{
if (kv.Key >= size && kv.Value.Count > 0)
{
List free = kv.Value;
// Attempt to find an index match the requested size
int mIdx = FindIndexAlignmentMatch(alignment, free);
if (mIdx >= 0)
{
AllocationMetadata meta = free[mIdx];
free.RemoveAt(mIdx);
_totalInUse += kv.Key;
void* user = (void*)meta.Block;
if (zero)
{
Unsafe.InitBlockUnaligned(user, 0, (uint)kv.Key);
}
return user;
}
}
}
// no match
IntPtr newBlock = AllocateNewAligned(size, alignment);
_totalInUse += size;
void* nUser = (void*)newBlock;
if (zero)
{
Unsafe.InitBlockUnaligned(nUser, 0, (uint)size);
}
return nUser;
}
///
/// Allocates a large block using tolerance-based reuse (smallest ≥ requested within waste bounds).
///
private void* AllocateLargeAligned(nuint size, nuint alignment, bool zero)
{
foreach (KeyValuePair> kv in _largeFree)
{
nuint blockSize = kv.Key;
if (blockSize < size || kv.Value.Count == 0)
{
continue;
}
// check waste tolerance before attempting to find an alignment match
nuint waste = blockSize - size;
bool acceptable = waste <= _largeMaxWasteBytes || ((double)blockSize / size) <= _largeWasteRatioLimit;
if (!acceptable)
{
continue;
}
List free = kv.Value;
// Attempt to find an index match the requested size
int mIdx = FindIndexAlignmentMatch(alignment, free);
if (mIdx >= 0)
{
AllocationMetadata meta = free[mIdx];
free.RemoveAt(mIdx);
_totalInUse += blockSize;
void* user = (void*)meta.Block;
if (zero)
{
Unsafe.InitBlockUnaligned(user, 0, (uint)blockSize);
}
return user;
}
}
// no match
IntPtr block = AllocateNewAligned(size, alignment);
_totalInUse += size;
void* nUser = (void*)block;
if (zero)
{
Unsafe.InitBlockUnaligned(nUser, 0, (uint)size);
}
return nUser;
}
///
/// Frees a previously allocated block, returning it to the appropriate free list.
///
void IUnmanagedMemoryOwner.Free(IOwnedHandle handle)
{
ThrowIfDisposed();
if (handle.Pointer == null)
{
return;
}
if (handle.GetOwner() != this)
{
throw new InvalidOperationException(
"Attempted to free a handle from a different allocator pool.");
}
lock (_lock)
{
nuint size = handle.ByteCount;
nuint alignment = handle.Alignment;
_totalInUse -= size;
if (size <= _smallThreshold)
{
_smallFree[size].Add(new AllocationMetadata
{
Block = (IntPtr)handle.Pointer,
Alignment = alignment
});
}
else if (size <= _mediumThreshold)
{
if (!_mediumFree.TryGetValue(size, out List? free))
{
_mediumFree[size] = free = [];
}
free.Add(new AllocationMetadata
{
Block = (IntPtr)handle.Pointer,
Alignment = alignment
});
}
else
{
if (!_largeFree.TryGetValue(size, out List? free))
{
_largeFree[size] = free = [];
}
free.Add(new AllocationMetadata
{
Block = (IntPtr)handle.Pointer,
Alignment = alignment
});
}
}
}
///
/// Releases all unused blocks back to the underlying allocator.
///
public void Prune()
{
ThrowIfDisposed();
lock (_lock)
{
PruneDictionary(_smallFree);
PruneDictionary(_mediumFree);
PruneDictionary(_largeFree);
}
}
/// Helper to free all blocks in a dictionary of free stacks.
private void PruneDictionary(IDictionary> dict)
{
foreach (KeyValuePair> kv in dict)
{
List list = kv.Value;
while (list.Count > 0)
{
var item = list[^1];
list.RemoveAt(list.Count - 1);
_allocator.FreeAligned((void*)item.Block, item.Alignment);
_totalReserved -= kv.Key;
}
}
}
///
/// Releases all memory and marks the heap as disposed.
///
public void Dispose()
{
if (_disposed)
{
return;
}
lock (_lock)
{
if (_disposed)
{
return;
}
if (_totalInUse > 0)
{
throw new InvalidOperationException(
"Cannot dispose WorkspaceHeap while active allocations exist. " +
"Dispose all handles returned from this heap before disposing the heap.");
}
Prune();
// Reset stats
_totalInUse = 0;
_totalAllocations = 0;
_disposed = true;
}
}
///
/// Throws an if the has already been disposed.
///
///
/// Thrown when this instance is no longer valid for use.
///
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}
}