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); } } }