namespace UnmanagedMMU { using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using UnmanagedMMU.Allocators; /// /// Implementation of segmented Bump-Allocator. /// /// This implementation manages fixed-sized unmanaged memory segments. /// This ensures allocations are fast and contiguous within a segment. /// Once a segment is full, a new one is automatically allocated /// public unsafe sealed class SegmentedPool : IDisposable { /// /// The default size for a /// private const nuint _defaultSegmentSize = 4194304; // 4 MiB /// /// The size that a new should be /// private nuint _currentSegmentSize; /// /// Queue of free segments /// private readonly Stack _freeSegments = new(); /// /// List of active memory segments /// private readonly List _activeSegments = []; /// /// Tracks the total amount of allocated bytes /// private nuint _totalAllocated = 0; /// /// Tracks the total amount of allocated bytes current in use /// private nuint _totalUsed = 0; /// /// Internal lock, ensures thread safety while maintaining a simple interface /// private readonly Lock _lock = new(); /// /// Indicates whether the has been disposed. /// private volatile bool _disposed; /// /// Pointer to the currently in use /// private Segment* _current; private readonly IUnmanagedAllocator _allocator; /// /// Represents a memory segment in the . /// /// /// /// Pointer to the start of the unmanaged memory block /// indicates the current allocation position within the segment. Each new allocation advances this offset by the number of bytes allocated. /// is the total size, in bytes, of the segment. /// Segments are managed internally by the and should not be modified directly outside of the pool. /// A is a contiguous block of unmanaged memory from which allocations are served sequentially via bump allocation. /// /// private struct Segment { /// /// Pointer to the start of the unmanaged memory block. /// public byte* Ptr; /// /// The current offset into the where the next allocation will occur. /// public nuint Offset; /// /// Total size of the in bytes. /// public nuint Size; } /// /// Initializes a new with the specified default and count /// /// Size of each segment in bytes (default 4 MiB) /// Number of segments to pre-allocate to the pool /// /// Thrown if is zero, or if is less than 1. /// public SegmentedPool(nuint segmentSize = _defaultSegmentSize, int initialSegments = 4) : this(segmentSize, initialSegments, new DefaultUnmanagedAllocator()) { } /// /// Initializes a new with the specified default and count /// /// Size of each segment in bytes (default 4 MiB) /// Number of segments to pre-allocate to the pool /// IUnmanagedAllocator instance that implements the allocator /// /// Thrown if is zero, or if is less than 1. /// internal SegmentedPool(nuint segmentSize, int initialSegments, IUnmanagedAllocator allocator) { if (segmentSize == 0) { throw new ArgumentException("Segment size must be greater than zero.", nameof(segmentSize)); } if (initialSegments < 1) { throw new ArgumentException("Initial segments count must be at least 1.", nameof(initialSegments)); } _allocator = allocator; _currentSegmentSize = segmentSize; // Pre-allocate segments for (int i = 0; i < initialSegments; i++) { _freeSegments.Push((IntPtr)AllocateNewSegment(_currentSegmentSize)); } _current = (Segment*)_freeSegments.Pop(); _activeSegments.Add((IntPtr)_current); } /// /// Gets the number of free segments available for use in the pool. /// /// The number of free segments. public int FreeSegmentCount { get { return _freeSegments.Count; } } /// /// Gets the number of segments currently in use in the pool. /// /// The number of currently active Segments. public int ActiveSegmentCount { get { return _activeSegments.Count; } } /// /// Returns the total number of bytes that have currently been allocated. /// /// The total number of bytes that have been allocated. public nuint TotalAllocatedBytes { get { return _totalAllocated; } } /// /// Gets the total number of bytes currently in use across all active segments. /// /// The total number of bytes that are in use. public nuint TotalUsedBytes { get { return _totalUsed; } } /// /// Gets the size, in bytes, used when allocating new instances. /// /// /// This reflects the most recently configured size and affects only future allocations. /// public nuint CurrentSegmentSize { get { return _currentSegmentSize; } } /// /// Gets a value indicating whether this instance has been disposed. /// /// True, if the current instance has been disposed of, false otherwise. /// /// Once disposed, further calls to allocation or reset methods will throw . /// public bool IsDisposed { get { return _disposed; } } /// /// Sets the current size used for subsequent allocations. /// /// /// The new segment size, in bytes, that will be used when allocating future instances. /// Must be greater than zero. /// /// /// This does not affect any that have already been allocated or are currently active. /// Only new created after calling this method will use the updated size. /// /// /// Thrown if is zero. /// public void SetSegmentSize(nuint newSize) { ThrowIfDisposed(); if (newSize == 0) { throw new ArgumentOutOfRangeException(nameof(newSize), "Segment size must be greater than zero."); } _currentSegmentSize = newSize; } /// /// Resets the current size back to the default 4 MiB /// /// /// Future allocations will revert to using this default size /// Active and free are not modified. /// public void ResetSegmentSize() { ThrowIfDisposed(); _currentSegmentSize = _defaultSegmentSize; } /// /// Allocates a span of unmanaged memory of size for elements of type . /// /// The unmanaged value type to store in the allocated memory. Must be a struct or primitive type. /// The number of elements of type to allocate. /// A representing the allocated memory. The span is valid until the is reset or disposed. /// /// /// This allocation is performed in unmanaged memory and bypasses the .NET garbage collector. /// Accessing the memory after or has been called is undefined behavior and may lead to crashes. /// /// /// /// Thrown if is less than or equal to zero. /// /// /// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size. /// public Span Allocate(int count) where T : unmanaged { ThrowIfDisposed(); if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count), "Allocation count must be greater than zero."); } 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 bytes = (nuint)(count * sizeof(T)); lock (_lock) { // Enough space in current segment? if (_current->Offset + bytes > _current->Size) SwitchSegment(bytes); T* ptr = (T*)(_current->Ptr + _current->Offset); _current->Offset += bytes; _totalUsed += bytes; return new Span(ptr, count); } } /// /// Switches to a new when the current is full. /// /// The number of bytes required for the upcoming allocation. If the current does not have enough free space, a new will be used. private void SwitchSegment(nuint requiredBytes) { Segment* segment; // Allocate fresh Segment if needed if (_freeSegments.Count == 0 || requiredBytes > _currentSegmentSize) { segment = AllocateNewSegment(requiredBytes > _currentSegmentSize ? requiredBytes : _currentSegmentSize); } else { segment = (Segment*)_freeSegments.Pop(); segment->Offset = 0; } _activeSegments.Add((IntPtr)segment); _current = segment; } /// /// Allocates a new /// /// /// Optional size, in bytes, for the new . /// If null, the default size () is used. /// /// A pointer to the newly allocated private Segment* AllocateNewSegment(nuint size) { byte* ptr = (byte*)_allocator.Alloc(size); Segment* segment = (Segment*)_allocator.Alloc((nuint)sizeof(Segment)); segment->Ptr = ptr; segment->Offset = 0; segment->Size = size; _totalAllocated += size; return segment; } /// /// Frees unused in the free pool, reducing unmanaged memory usage. /// /// Minimum number of free segments to retain for future allocations. Defaults to 16. /// /// Segments beyond the retained count will have their unmanaged memory released back to the system. /// /// /// Thrown if is negative. /// public void Trim(int minFreeSegments = 16) { ThrowIfDisposed(); ArgumentOutOfRangeException.ThrowIfNegative(minFreeSegments, nameof(minFreeSegments)); lock (_lock) { while (_freeSegments.Count > minFreeSegments) { var ip = _freeSegments.Pop(); Segment* segment = (Segment*)ip; // Free the unmanaged memory _allocator.Free(segment->Ptr); _totalAllocated -= segment->Size; _allocator.Free(segment); } } } /// /// Resets the allocator, returning all active segments to the free pool. /// /// If true, trims the in the free pool. public void Reset(bool trim = false) { ThrowIfDisposed(); lock (_lock) { foreach (var ip in _activeSegments) { Segment* segment = (Segment*)ip; segment->Offset = 0; _freeSegments.Push(ip); } _activeSegments.Clear(); _totalUsed = 0; if (_freeSegments.Count > 0) { _current = (Segment*)_freeSegments.Pop(); _activeSegments.Add((IntPtr)_current); } else { // This should not be hit in normal circumstances as we always have _current _current = AllocateNewSegment(_currentSegmentSize); _activeSegments.Add((IntPtr)_current); } // Optionally trim excess free segments after reset if (trim) { Trim(); } } } /// /// Releases all unmanaged memory allocated by the and clears internal state. /// After calling this method, the pool can no longer be used for allocations. /// /// /// After disposal, all allocations become invalid and any further operations will throw . /// This method is thread-safe and may be called multiple times safely. /// public void Dispose() { lock (_lock) { if (_disposed) { return; } // Free active pages foreach (var ip in _activeSegments) { Segment* segment = (Segment*)ip; _allocator.Free(segment->Ptr); _allocator.Free(segment); } // Free free pages foreach (var ip in _freeSegments) { Segment* segment = (Segment*)ip; _allocator.Free(segment->Ptr); _allocator.Free(segment); } _activeSegments.Clear(); _freeSegments.Clear(); _current = null; _totalAllocated = 0; _totalUsed = 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); } } }