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