namespace UnmanagedMMU { using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; 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 { /// /// 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 = new(); /// /// Free lists for medium allocations keyed by the exactly allocated size, in bytes, and sorted for best-fit. /// private readonly SortedDictionary> _mediumFree = new(); /// /// Free lists for large allocations keyed by the exactly allocated size, in bytes, sorted for tolerance-based reuse. /// private readonly SortedDictionary> _largeFree = new(); /// /// 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; /// /// Internal header prepended to each allocation to track its size. /// [StructLayout(LayoutKind.Sequential)] private struct BlockHeader { /// /// Size of the allocation in bytes. /// public nuint Size; /// /// Padding to ensure is 32-byte aligned /// private readonly nuint _pad1; /// /// Padding to ensure is 32-byte aligned /// private readonly nuint _pad2; /// /// Padding to ensure is 32-byte aligned /// private readonly nuint _pad3; } /// /// 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 (var size in _sizeClasses) _smallFree[size] = new Stack(); } /// /// 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. /// /// Pointer to the allocated block (header included). /// private IntPtr AllocateNew(nuint payloadSize) { nuint total = payloadSize + (nuint)sizeof(BlockHeader); void* raw = _allocator.Alloc(total); _totalReserved += total; _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. public IMemoryHandle Allocate(int count, 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); lock (_lock) { ThrowIfDisposed(); void* ptr = null; if (size <= _smallThreshold) { ptr = AllocateSmall(size, zero); } else if (size <= _mediumThreshold) { ptr = AllocateMedium(size, zero); } else { ptr = AllocateLarge(size, zero); } return new PersistentMemoryHandle((T*)ptr, size, this); } } /// Allocates a small-size block using bucketed free lists. private void* AllocateSmall(nuint size, bool zero) { nuint bucket = GetSizeClass(size); Stack stack = _smallFree[bucket]; IntPtr block = stack.Count > 0 ? stack.Pop() : AllocateNew(bucket); BlockHeader* header = (BlockHeader*)block; header->Size = bucket; _totalInUse += bucket; void* user = header + 1; if (zero) Unsafe.InitBlockUnaligned(user, 0, (uint)bucket); return user; } /// Allocates a medium-size block using best-fit reuse. private void* AllocateMedium(nuint size, bool zero) { foreach (var kv in _mediumFree) { if (kv.Key >= size && kv.Value.Count > 0) { var block = kv.Value.Pop(); var header = (BlockHeader*)block; _totalInUse += header->Size; void* user = header + 1; if (zero) Unsafe.InitBlockUnaligned(user, 0, (uint)header->Size); return user; } } var newBlock = AllocateNew(size); var newHeader = (BlockHeader*)newBlock; newHeader->Size = size; _totalInUse += size; void* newUser = newHeader + 1; if (zero) { Unsafe.InitBlockUnaligned(newUser, 0, (uint)size); } return newUser; } /// Allocates a large block using tolerance-based reuse (smallest ≥ requested within waste bounds). private void* AllocateLarge(nuint size, bool zero) { foreach (var kv in _largeFree) { nuint blockSize = kv.Key; if (blockSize < size || kv.Value.Count == 0) { continue; } nuint waste = blockSize - size; bool acceptable = waste <= _largeMaxWasteBytes || ((double)blockSize / size) <= _largeWasteRatioLimit; if (!acceptable) { continue; } var block = kv.Value.Pop(); var header = (BlockHeader*)block; _totalInUse += header->Size; void* user = header + 1; if (zero) { Unsafe.InitBlockUnaligned(user, 0, (uint)header->Size); } return user; } var newBlock = AllocateNew(size); var newHeader = (BlockHeader*)newBlock; newHeader->Size = size; _totalInUse += size; void* newUser = newHeader + 1; if (zero) { Unsafe.InitBlockUnaligned(newUser, 0, (uint)size); } return newUser; } /// /// 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) { var header = ((BlockHeader*)handle.Pointer) - 1; nuint size = header->Size; _totalInUse -= size; if (size <= _smallThreshold) _smallFree[size].Push((IntPtr)header); else if (size <= _mediumThreshold) { if (!_mediumFree.TryGetValue(size, out var stack)) _mediumFree[size] = stack = new Stack(); stack.Push((IntPtr)header); } else { if (!_largeFree.TryGetValue(size, out var stack)) _largeFree[size] = stack = new Stack(); stack.Push((IntPtr)header); } } } /// /// 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 (var kv in dict) { var stack = kv.Value; while (stack.Count > 0) { var block = stack.Pop(); var header = (BlockHeader*)block; _allocator.Free((void*)block); _totalReserved -= (header->Size + (nuint)sizeof(BlockHeader)); } } } /// /// 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); } } }