948 lines
39 KiB
C#
948 lines
39 KiB
C#
namespace UnmanagedMMU
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using UnmanagedMMU.Allocators;
|
|
using UnmanagedMMU.Diagnostics;
|
|
using UnmanagedMMU.Handles;
|
|
using UnmanagedMMU.Handles.Internal;
|
|
|
|
|
|
/// <summary>
|
|
/// Represents configurable alignment requirements for memory segments and allocations.
|
|
/// Values are powers of 2 and reflect common hardware requirements (SIMD, cache lines, native pointer size).
|
|
/// </summary>
|
|
public enum SegmentAlignment
|
|
{
|
|
/// <summary>
|
|
/// 8-byte alignment. Minimum for 64-bit pointers and primitives (long, double).
|
|
/// </summary>
|
|
Aligned8 = 8,
|
|
|
|
/// <summary>
|
|
/// 16-byte alignment. Required for Vector128 (SSE/NEON).
|
|
/// Common default for general-purpose SIMD workloads.
|
|
/// </summary>
|
|
Aligned16 = 16,
|
|
|
|
/// <summary>
|
|
/// 32-byte alignment. Required for Vector256 (AVX).
|
|
/// Recommended default for SIMD-heavy applications.
|
|
/// </summary>
|
|
Aligned32 = 32,
|
|
|
|
/// <summary>
|
|
/// 64-byte alignment. Matches standard CPU cache-line size.
|
|
/// Ensures segment bases align to cache line boundaries, minimizing cache-line splits.
|
|
/// </summary>
|
|
Aligned64 = 64,
|
|
|
|
/// <summary>
|
|
/// 128-byte alignment.
|
|
/// Advanced optimization for specific cache-aware algorithms or AVX-512 contexts.
|
|
/// </summary>
|
|
Aligned128 = 128
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public unsafe sealed class SegmentedPool : IDisposable, IUnmanagedMemoryOwner
|
|
{
|
|
/// <summary>
|
|
/// The default size for a <see cref="Segment"/>
|
|
/// </summary>
|
|
private const nuint _defaultSegmentSize = 4194304; // 4 MiB
|
|
|
|
/// <summary>
|
|
/// The size that a new <see cref="Segment"/> should be
|
|
/// </summary>
|
|
private nuint _currentSegmentSize;
|
|
|
|
/// <summary>
|
|
/// The alignment that each new <see cref="Segment"/> should be on
|
|
/// </summary>
|
|
private readonly nuint _segmentAlignment;
|
|
|
|
/// <summary>
|
|
/// Queue of free segments
|
|
/// </summary>
|
|
private readonly Stack<IntPtr> _freeSegments = new();
|
|
|
|
/// <summary>
|
|
/// List of active memory segments
|
|
/// </summary>
|
|
private readonly List<IntPtr> _activeSegments = [];
|
|
|
|
/// <summary>
|
|
/// Tracks the total bytes of memory reserved from the provided <see cref="IUnmanagedAllocator"/>
|
|
/// </summary>
|
|
private nuint _totalReserved = 0;
|
|
|
|
/// <summary>
|
|
/// Tracks the total amount of allocated bytes currently in use
|
|
/// </summary>
|
|
private nuint _totalUsed = 0;
|
|
|
|
/// <summary>
|
|
/// Internal lock, ensures thread safety while maintaining a simple interface
|
|
/// </summary>
|
|
private readonly Lock _lock = new();
|
|
|
|
/// <summary>
|
|
/// Indicates whether this <see cref="SegmentedPool"/> has been disposed.
|
|
/// </summary>
|
|
private volatile bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Pointer to the currently in use <see cref="Segment"/>
|
|
/// </summary>
|
|
private Segment* _current;
|
|
|
|
/// <summary>
|
|
/// Allocator interface used for all underlying unmanaged memory operations.
|
|
/// </summary>
|
|
private readonly IUnmanagedAllocator _allocator;
|
|
|
|
/// <summary>
|
|
/// Indicates if allocations from the pool should be zeroed.
|
|
/// </summary>
|
|
private readonly bool _zeroMemory;
|
|
|
|
/// <summary>
|
|
/// Represents a memory segment in the <see cref="SegmentedPool"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <list type="bullet">
|
|
/// <item><description><see cref="Ptr"/> Pointer to the start of the unmanaged memory block</description></item>
|
|
/// <item><description><see cref="Offset"/> indicates the current allocation position within the segment. Each new allocation advances this offset by the number of bytes allocated.</description></item>
|
|
/// <item><description><see cref="Size"/> is the total size, in bytes, of the segment.</description></item>
|
|
/// <item><description>Segments are managed internally by the <see cref="SegmentedPool"/> and should not be modified directly outside of the pool.</description></item>
|
|
/// <item><description>A <see cref="Segment"/> is a contiguous block of unmanaged memory from which allocations are served sequentially via bump allocation.</description></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
private struct Segment
|
|
{
|
|
/// <summary>
|
|
/// Pointer to the start of the unmanaged memory block.
|
|
/// </summary>
|
|
public byte* Ptr;
|
|
|
|
/// <summary>
|
|
/// The current offset into the <see cref="Segment"/> where the next allocation will occur.
|
|
/// </summary>
|
|
public nuint Offset;
|
|
|
|
/// <summary>
|
|
/// Total size of the <see cref="Segment"/> in bytes.
|
|
/// </summary>
|
|
public nuint Size;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Readonly snapshot of the current pool state for diagnostics.
|
|
/// </summary>
|
|
public readonly struct PoolState
|
|
{
|
|
/// <summary>
|
|
/// The configured segment alignment setting for this pool instance.
|
|
/// This is the MINIMUM alignment requirement for the segment base.
|
|
/// </summary>
|
|
public nuint SegmentAlignment { get; init; }
|
|
|
|
/// <summary>
|
|
/// The size, in bytes, of each segment allocated by the pool.
|
|
/// </summary>
|
|
public nuint SegmentSize { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total bytes currently reserved from the system.
|
|
/// </summary>
|
|
public nuint TotalReserved { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total bytes currently used by allocations.
|
|
/// </summary>
|
|
public nuint TotalUsed { get; init; }
|
|
|
|
/// <summary>
|
|
/// Current active segment base address.
|
|
/// </summary>
|
|
public nuint CurrentBase { get; init; }
|
|
|
|
/// <summary>
|
|
/// Current offset within the segment.
|
|
/// </summary>
|
|
public nuint CurrentOffset { get; init; }
|
|
|
|
/// <summary>
|
|
/// Checks if the current segment base is aligned to the configured <see cref="SegmentAlignment"/> setting.
|
|
/// </summary>
|
|
public bool BaseAligned { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of active segments currently being used.
|
|
/// </summary>
|
|
public int ActiveSegmentCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of recycled segments available for reuse.
|
|
/// </summary>
|
|
public int FreeSegmentCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total number of segments in the pool (Active + Free).
|
|
/// </summary>
|
|
public int TotalSegmentCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Bytes lost to alignment padding vs actual data in current segment.
|
|
/// Computed as: CurrentOffset - TotalUsedBytes.
|
|
/// </summary>
|
|
public nuint PaddingBytes { get; init; }
|
|
|
|
/// <summary>
|
|
/// Memory that could be freed if Trim() is called with default args (minFreeSegments: 16).
|
|
/// </summary>
|
|
public nuint PotentialSavings { get; init; }
|
|
|
|
public string Suggestion { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Readonly snapshot of a specific segment.
|
|
/// </summary>
|
|
public readonly struct SegmentInfo
|
|
{
|
|
/// <summary>
|
|
/// Segment index within the active list.
|
|
/// </summary>
|
|
public int Index { get; init; }
|
|
|
|
/// <summary>
|
|
/// Base address of the segment (actual unmanaged memory pointer).
|
|
/// </summary>
|
|
public nuint BaseAddress { get; init; }
|
|
|
|
/// <summary>
|
|
/// Current offset usage (bytes used since segment reset).
|
|
/// </summary>
|
|
public nuint UsedBytes { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total capacity of the segment.
|
|
/// </summary>
|
|
public nuint Size { get; init; }
|
|
|
|
/// <summary>
|
|
/// Indicates if this is the currently active segment.
|
|
/// </summary>
|
|
public bool IsActive { get; init; }
|
|
|
|
/// <summary>
|
|
/// The alignment requirement for the segment base itself.
|
|
/// </summary>
|
|
public nuint AlignmentRequirement { get; init; }
|
|
|
|
/// <summary>
|
|
/// True if <see cref="BaseAddress"/> is a multiple of <see cref="AlignmentRequirement"/>.
|
|
/// </summary>
|
|
public bool IsAligned { get; init; }
|
|
|
|
public override string ToString()
|
|
{
|
|
bool aligned = IsAligned;
|
|
string status = IsActive ? "CURRENT" : "INACTIVE";
|
|
double usagePercent = Size > 0 ? ((double)UsedBytes / Size) * 100 : 0;
|
|
string alignStatus = aligned ? "Aligned" : "Misaligned";
|
|
|
|
return $" Segment #{Index} [{status}]\n" +
|
|
$" Base Address: 0x{(nuint)BaseAddress:x}\n" +
|
|
$" Size: {FormatBytes(Size)}\n" +
|
|
$" Used: {FormatBytes(UsedBytes)} ({usagePercent:F2}%)\n" +
|
|
$" Base Alignment: {AlignmentRequirement} bytes - {alignStatus}";
|
|
}
|
|
|
|
private static string FormatBytes(nuint bytes)
|
|
{
|
|
if (bytes >= 1073741824) return $"{bytes / 1073741824} GiB";
|
|
if (bytes >= 1048576) return $"{bytes / 1048576} MiB";
|
|
if (bytes >= 1024) return $"{bytes / 1024} KiB";
|
|
return $"{bytes} B";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count
|
|
/// </summary>
|
|
/// <param name="segmentSize">Size of each segment in bytes (default 4 MiB)</param>
|
|
/// <param name="segmentAlignment">Alignment requirement for each allocated segment and . Must be a power of 2 (Default 32)</param>
|
|
/// <param name="initialSegments">Number of segments to pre-allocate to the pool</param>
|
|
/// <param name="zeroMemory">When true, memory returned from the pool is zero-initialized.
|
|
/// When false, memory may contain previously used data and it is the caller's responsibility to clear it if required.</param>
|
|
/// <exception cref="ArgumentException">
|
|
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
|
|
/// </exception>
|
|
public SegmentedPool(nuint segmentSize = _defaultSegmentSize, SegmentAlignment segmentAlignment = SegmentAlignment.Aligned32, int initialSegments = 4, bool zeroMemory = false)
|
|
: this(segmentSize, segmentAlignment, initialSegments, zeroMemory, new DefaultUnmanagedAllocator())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count
|
|
/// </summary>
|
|
/// <param name="segmentSize">Size of each segment in bytes (default 4 MiB)</param>
|
|
/// <param name="segmentAlignment">Alignment requirement for each allocated segment. Must be a power of 2 (Default 32)</param>
|
|
/// <param name="initialSegments">Number of segments to pre-allocate to the pool</param>
|
|
/// <param name="zeroMemory">When true, memory returned from the pool is zero-initialized.
|
|
/// When false, memory may contain previously used data and it is the caller's responsibility to clear it if required.</param>
|
|
/// <param name="allocator">IUnmanagedAllocator instance that implements the allocator</param>
|
|
/// <exception cref="ArgumentException">
|
|
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
|
|
/// </exception>
|
|
internal SegmentedPool(nuint segmentSize, SegmentAlignment segmentAlignment, int initialSegments, bool zeroMemory, IUnmanagedAllocator allocator)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(segmentSize);
|
|
ArgumentOutOfRangeException.ThrowIfLessThan(initialSegments, 1);
|
|
_allocator = allocator;
|
|
_currentSegmentSize = segmentSize;
|
|
_segmentAlignment = (nuint)segmentAlignment;
|
|
_zeroMemory = zeroMemory;
|
|
|
|
Segment* seg;
|
|
// Pre-allocate segments
|
|
for (int i = 0; i < initialSegments; i++)
|
|
{
|
|
seg = AllocateNewSegment(_currentSegmentSize);
|
|
ZeroSegment(seg);
|
|
_freeSegments.Push((IntPtr)seg);
|
|
}
|
|
|
|
|
|
_current = (Segment*)_freeSegments.Pop();
|
|
_activeSegments.Add((IntPtr)_current);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of free segments available for use in the pool.
|
|
/// </summary>
|
|
/// <returns>The number of free segments.</returns>
|
|
public int FreeSegmentCount
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _freeSegments.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of segments currently in use in the pool.
|
|
/// </summary>
|
|
/// <returns>The number of currently active Segments.</returns>
|
|
public int ActiveSegmentCount
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _activeSegments.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the total number of bytes that have currently been allocated.
|
|
/// </summary>
|
|
/// <returns>The total number of bytes that have been allocated.</returns>
|
|
public nuint TotalAllocatedBytes
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _totalReserved;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total number of bytes currently in use across all active segments.
|
|
/// </summary>
|
|
/// <returns>The total number of bytes that are in use.</returns>
|
|
public nuint TotalUsedBytes
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _totalUsed;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the size, in bytes, used when allocating new <see cref="Segment"/> instances.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This reflects the most recently configured size and affects only future <see cref="Segment"/> allocations.
|
|
/// </remarks>
|
|
public nuint CurrentSegmentSize
|
|
{
|
|
get { return _currentSegmentSize; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this <see cref="SegmentedPool"/> instance has been disposed.
|
|
/// </summary>
|
|
/// <returns>True, if the current <see cref="SegmentedPool"/> instance has been disposed of, false otherwise.</returns>
|
|
/// <remarks>
|
|
/// Once disposed, further calls to allocation or reset methods will throw <see cref="ObjectDisposedException"/>.
|
|
/// </remarks>
|
|
public bool IsDisposed
|
|
{
|
|
get { return _disposed; }
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static nuint AlignUp(nuint value, nuint alignment)
|
|
{
|
|
return (value + alignment - 1) & ~(alignment - 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Zeroes the memory <see cref="Segment"></see> if the <see cref="SegmentedPool"/> is configured to do so.
|
|
/// Called whenever a segment becomes active for use (new or reused).
|
|
/// </summary>
|
|
/// <param name="segment">Pointer to the Segment struct to initialize.</param>
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void ZeroSegment(Segment* segment)
|
|
{
|
|
if (_zeroMemory)
|
|
{
|
|
Unsafe.InitBlockUnaligned(segment->Ptr, 0, (uint)segment->Size);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allocates a block of unmanaged memory of size <paramref name="count"/> for elements of type <typeparamref name="T"/> and returns a pointer to the allocated memory.
|
|
/// </summary>
|
|
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam>
|
|
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param>
|
|
/// <returns>A <typeparamref name="T"/> pointer to the first element of the allocated unmanaged memory block. The memory is valid until the <see cref="SegmentedPool"/> is reset or disposed.</returns>
|
|
/// <remarks>
|
|
/// <list type="bullet">
|
|
/// <item><description>This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.</description></item>
|
|
/// <item><description>Accessing the memory after <see cref="Reset"/> or <see cref="Dispose"/> has been called is undefined behavior and may lead to crashes.</description></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentOutOfRangeException">
|
|
/// Thrown if <paramref name="count"/> is less than or equal to zero.
|
|
/// </exception>
|
|
/// <exception cref="OverflowException">
|
|
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
|
/// </exception>
|
|
private T* Alloc<T>(int count) where T : unmanaged
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(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 bytes = (nuint)(count * sizeof(T));
|
|
nuint alignment = _segmentAlignment > (nuint)sizeof(T) ? _segmentAlignment : (nuint)sizeof(T);
|
|
|
|
lock (_lock)
|
|
{
|
|
ThrowIfDisposed();
|
|
nuint currentPtr = (nuint)_current->Ptr + _current->Offset;
|
|
nuint alignedPtr = AlignUp(currentPtr, alignment);
|
|
nuint alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
|
// Check space INCLUDING padding
|
|
if (alignedOffset + bytes > _current->Size)
|
|
{
|
|
SwitchSegment(bytes);
|
|
|
|
// Recalcuate from new the base of the new segment
|
|
currentPtr = (nuint)_current->Ptr;
|
|
alignedPtr = AlignUp(currentPtr, alignment);
|
|
alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
|
}
|
|
|
|
T* ptr = (T*)(_current->Ptr + alignedOffset);
|
|
_current->Offset = alignedOffset + bytes;
|
|
_totalUsed += bytes;
|
|
return ptr;
|
|
}
|
|
}
|
|
|
|
private T* AllocateWithAlignment<T>(int count, nuint alignment) where T : unmanaged
|
|
{
|
|
|
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(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 bytes = (nuint)(count * sizeof(T));
|
|
|
|
lock (_lock)
|
|
{
|
|
ThrowIfDisposed();
|
|
nuint currentPtr = (nuint)_current->Ptr + _current->Offset;
|
|
nuint alignedPtr = AlignUp(currentPtr, alignment);
|
|
nuint alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
|
|
|
if (alignedOffset + bytes > _current->Size)
|
|
{
|
|
SwitchSegment(bytes);
|
|
currentPtr = (nuint)_current->Ptr;
|
|
alignedPtr = AlignUp(currentPtr, alignment);
|
|
alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
|
}
|
|
|
|
T* ptr = (T*)(_current->Ptr + alignedOffset);
|
|
_current->Offset = alignedOffset + bytes;
|
|
_totalUsed += bytes;
|
|
return ptr;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switches to a new <see cref="Segment"/> when the current <see cref="Segment"/> is full.
|
|
/// </summary>
|
|
/// <param name="requiredBytes">The number of bytes required for the upcoming allocation. If the current <see cref="Segment"/> does not have enough free space, a new <see cref="Segment"/> will be used.</param>
|
|
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;
|
|
}
|
|
ZeroSegment(segment);
|
|
_activeSegments.Add((IntPtr)segment);
|
|
_current = segment;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allocates a new <see cref="Segment"/>
|
|
/// </summary>
|
|
/// <param name="size"> Size, in bytes, for the new <see cref="Segment"/>. </param>
|
|
/// <returns>A pointer to the newly allocated <see cref="Segment"/></returns>
|
|
private Segment* AllocateNewSegment(nuint size)
|
|
{
|
|
byte* ptr = (byte*)_allocator.AllocAligned(size, _segmentAlignment);
|
|
// Allocate metadata struct with its natural alignment (8 bytes for 64-bit nuint)
|
|
Segment* segment = (Segment*)_allocator.AllocAligned((nuint)sizeof(Segment), 8);
|
|
segment->Ptr = ptr;
|
|
segment->Offset = 0;
|
|
segment->Size = size;
|
|
|
|
_totalReserved += size;
|
|
return segment;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the current <see cref="Segment"/> size used for subsequent allocations.
|
|
/// </summary>
|
|
/// <param name="newSize">
|
|
/// The new segment size, in bytes, that will be used when allocating future <see cref="Segment"/> instances.
|
|
/// Must be greater than zero.
|
|
/// </param>
|
|
/// <remarks>
|
|
/// This does not affect any <see cref="Segment"/> that have already been allocated or are currently active.
|
|
/// Only new <see cref="Segment"/> created after calling this method will use the updated size.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentOutOfRangeException">
|
|
/// Thrown if <paramref name="newSize"/> is zero.
|
|
/// </exception>
|
|
public void SetSegmentSize(nuint newSize)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (newSize == 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(newSize), "Segment size must be greater than zero.");
|
|
}
|
|
_currentSegmentSize = newSize;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the current <see cref="Segment"/> size back to the default 4 MiB
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Future <see cref="Segment"/> allocations will revert to using this default size
|
|
/// Active and free <see cref="Segment"/> are not modified.
|
|
/// </remarks>
|
|
public void ResetSegmentSize()
|
|
{
|
|
ThrowIfDisposed();
|
|
_currentSegmentSize = _defaultSegmentSize;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allocates a block of unmanaged memory of size <paramref name="count"/> for elements of type <typeparamref name="T"/> and returns a handle representing the allocation.
|
|
/// </summary>
|
|
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam>
|
|
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param>
|
|
/// <returns>A <see cref="IMemoryHandle{T}"/> representing the allocated memory. The handle is valid until either <see cref="Reset"/> or <see cref="Dispose"/> is called on this <see cref="SegmentedPool"/>.</returns>
|
|
/// <remarks>
|
|
/// <list type="bullet">
|
|
/// <item><description>This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.</description></item>
|
|
/// <item><description>Accessing the memory after <see cref="Reset"/> or <see cref="Dispose"/> has been called is undefined behavior.</description></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentOutOfRangeException">
|
|
/// Thrown if <paramref name="count"/> is less than or equal to zero.
|
|
/// </exception>
|
|
/// <exception cref="OverflowException">
|
|
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
|
/// </exception>
|
|
public IMemoryHandle<T> Allocate<T>(int count) where T : unmanaged
|
|
{
|
|
T* ptr = Alloc<T>(count);
|
|
nuint byteLength = (nuint)count * (nuint)sizeof(T);
|
|
return new SegmentedMemoryHandle<T>(ptr, byteLength, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allocates a block of unmanaged memory of size <paramref name="count"/> for elements of type <typeparamref name="T"/> with the specified <paramref name="alignment"/> and returns a handle representing the allocation.
|
|
/// </summary>
|
|
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam>
|
|
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param>
|
|
/// <param name="alignment">The alignment to aliign the allocation to inside of the currently active <see cref="Segment"/></param>
|
|
/// <returns>A <see cref="IMemoryHandle{T}"/> representing the allocated memory. The handle is valid until either <see cref="Reset"/> or <see cref="Dispose"/> is called on this <see cref="SegmentedPool"/>.</returns>
|
|
/// <remarks>
|
|
/// <list type="bullet">
|
|
/// <item><description>This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.</description></item>
|
|
/// <item><description>Accessing the memory after <see cref="Reset"/> or <see cref="Dispose"/> has been called is undefined behavior.</description></item>
|
|
/// </list>
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentOutOfRangeException">
|
|
/// Thrown if <paramref name="count"/> is less than or equal to zero.
|
|
/// </exception>
|
|
/// <exception cref="OverflowException">
|
|
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
|
/// </exception>
|
|
public IMemoryHandle<T> AllocateAligned<T>(int count, SegmentAlignment alignment) where T : unmanaged
|
|
{
|
|
nuint requestedAlignment = (nuint)alignment;
|
|
nuint typeSize = (nuint)sizeof(T);
|
|
// Ensure alignment is at least the size of T (never under-align for types)
|
|
nuint effectiveAlignment = requestedAlignment < typeSize ? typeSize : requestedAlignment;
|
|
|
|
T* ptr = AllocateWithAlignment<T>(count, effectiveAlignment);
|
|
nuint byteLength = (nuint)count * (nuint)sizeof(T);
|
|
return new SegmentedMemoryHandle<T>(ptr, byteLength, this);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees unused <see cref="Segment"/> in the free pool, reducing unmanaged memory usage.
|
|
/// </summary>
|
|
/// <param name="minFreeSegments"> Minimum number of free segments to retain for future allocations. Defaults to <c>16</c>. </param>
|
|
/// <remarks>
|
|
/// Segments beyond the retained count will have their unmanaged memory released back to the system.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentOutOfRangeException">
|
|
/// Thrown if <paramref name="minFreeSegments"/> is negative.
|
|
/// </exception>
|
|
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.FreeAligned(segment->Ptr, _segmentAlignment);
|
|
_totalReserved -= segment->Size;
|
|
_allocator.FreeAligned(segment, 8);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the allocator, returning all active segments to the free pool.
|
|
/// </summary>
|
|
/// <param name="trim">If true, trims the <see cref="Segment"/> in the free pool. <see cref="Trim"/></param>
|
|
public void Reset(bool trim = false)
|
|
{
|
|
ThrowIfDisposed();
|
|
lock (_lock)
|
|
{
|
|
foreach (var ip in _activeSegments)
|
|
{
|
|
Segment* segment = (Segment*)ip;
|
|
segment->Offset = 0;
|
|
_freeSegments.Push(ip);
|
|
|
|
// Zero memory if requested
|
|
ZeroSegment(segment);
|
|
}
|
|
|
|
_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);
|
|
// Zero newly allocated segment if requested
|
|
ZeroSegment(_current);
|
|
}
|
|
|
|
// Optionally trim excess free segments after reset
|
|
if (trim)
|
|
{
|
|
Trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees <paramref name="handle"/>
|
|
/// </summary>
|
|
/// <param name="handle"></param>
|
|
/// <exception cref="NotImplementedException"></exception>
|
|
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.");
|
|
}
|
|
|
|
//
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases all unmanaged memory allocated by the <see cref="SegmentedPool"/> and clears internal state.
|
|
/// After calling this method, the pool can no longer be used for allocations.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// After disposal, all allocations become invalid and any further operations will throw <see cref="ObjectDisposedException"/>.
|
|
/// This method is thread-safe and may be called multiple times safely.
|
|
/// </remarks>
|
|
public void Dispose()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Free active segments
|
|
foreach (var ip in _activeSegments)
|
|
{
|
|
Segment* segment = (Segment*)ip;
|
|
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
|
_allocator.FreeAligned(segment, 8);
|
|
}
|
|
|
|
// Free free segments
|
|
foreach (var ip in _freeSegments)
|
|
{
|
|
Segment* segment = (Segment*)ip;
|
|
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
|
_allocator.FreeAligned(segment, 8);
|
|
}
|
|
|
|
_activeSegments.Clear();
|
|
_freeSegments.Clear();
|
|
_current = null;
|
|
_totalReserved = 0;
|
|
_totalUsed = 0;
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="SegmentedPool"/> has already been disposed.
|
|
/// </summary>
|
|
/// <exception cref="ObjectDisposedException">
|
|
/// Thrown when this <see cref="SegmentedPool"/> instance is no longer valid for use.
|
|
/// </exception>
|
|
private void ThrowIfDisposed()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets a snapshot of the current pool state for diagnostics.
|
|
/// Thread-safe and produces no garbage.
|
|
/// </summary>
|
|
/// <returns>A <see cref="PoolState"/> containing current pool metrics.</returns>
|
|
public PoolState GetPoolState()
|
|
{
|
|
ThrowIfDisposed();
|
|
lock (_lock)
|
|
{
|
|
nuint alignment = _segmentAlignment;
|
|
nuint basePtr = (nuint)_current->Ptr;
|
|
int active = _activeSegments.Count;
|
|
int free = _freeSegments.Count;
|
|
int total = active + free;
|
|
|
|
// 1. Calculate Padding (Alignment Overhead) for Current Segment
|
|
// This is dynamic: Offset tracks total bytes written, TotalUsed tracks actual allocation bytes
|
|
nuint padding = _current->Offset - _totalUsed;
|
|
|
|
// 2. Calculate Potential Savings (Trim Projection)
|
|
nuint potentialSavings = 0;
|
|
if (free > 16)
|
|
{
|
|
int excess = free - 16;
|
|
potentialSavings = (nuint)excess * _currentSegmentSize;
|
|
}
|
|
|
|
return new PoolState
|
|
{
|
|
SegmentAlignment = alignment,
|
|
SegmentSize = _currentSegmentSize,
|
|
TotalReserved = _totalReserved,
|
|
TotalUsed = _totalUsed,
|
|
CurrentBase = basePtr,
|
|
CurrentOffset = _current->Offset,
|
|
BaseAligned = (basePtr & (alignment - 1)) == 0,
|
|
ActiveSegmentCount = active,
|
|
FreeSegmentCount = free,
|
|
TotalSegmentCount = total,
|
|
PaddingBytes = padding,
|
|
PotentialSavings = potentialSavings
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Generates a diagnostic report for the pool.
|
|
/// Thread-safe and produces no garbage.
|
|
/// </summary>
|
|
/// <returns>A formatted diagnostic string.</returns>
|
|
public string GetDiagnosticReport()
|
|
{
|
|
PoolState state = GetPoolState();
|
|
|
|
DiagnosticConfig config = new()
|
|
{
|
|
SegmentSize = _currentSegmentSize,
|
|
TotalReserved = _totalReserved
|
|
};
|
|
|
|
string suggestion = SegmentedPoolDiagnostics.GenerateSuggestions(state, config);
|
|
state = state with { Suggestion = suggestion };
|
|
|
|
return SegmentedPoolDiagnostics.GenerateReport(state);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to construct a <see cref="SegmentInfo"/> from raw segment data.
|
|
/// </summary>
|
|
/// <param name="segment">Pointer to the segment to inspect.</param>
|
|
/// <param name="current">Pointer to the currently active segment for comparison.</param>
|
|
/// <param name="index">The logical index of the segment in the list.</param>
|
|
/// <returns>A populated <see cref="SegmentInfo"/> struct.</returns>
|
|
private SegmentInfo CreateSegmentInfo(Segment* segment, Segment* current, int index)
|
|
{
|
|
nuint alignment = _segmentAlignment;
|
|
nuint ptr = (nuint)segment->Ptr;
|
|
bool isAligned = (ptr & (alignment - 1)) == 0;
|
|
|
|
return new SegmentInfo
|
|
{
|
|
Index = index,
|
|
BaseAddress = ptr,
|
|
UsedBytes = segment->Offset,
|
|
Size = segment->Size,
|
|
IsActive = (segment == current),
|
|
AlignmentRequirement = alignment,
|
|
IsAligned = isAligned
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets information about the currently active segment.
|
|
/// This is the primary diagnostic view for memory usage within the active segment.
|
|
/// </summary>
|
|
/// <returns>A <see cref="SegmentInfo"/> for the current segment, or null if disposed.</returns>
|
|
public SegmentInfo GetCurrentSegmentInfo()
|
|
{
|
|
ThrowIfDisposed();
|
|
lock (_lock)
|
|
{
|
|
return CreateSegmentInfo(_current, _current, 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of all segment details for deep diagnostics.
|
|
/// Includes both active and free segments.
|
|
/// </summary>
|
|
/// <returns>A list of <see cref="SegmentInfo"/> containing all segments.</returns>
|
|
public List<SegmentInfo> GetAllSegmentInfos()
|
|
{
|
|
ThrowIfDisposed();
|
|
lock (_lock)
|
|
{
|
|
var totalSegments = _activeSegments.Count + _freeSegments.Count;
|
|
var result = new List<SegmentInfo>(totalSegments);
|
|
|
|
int currentIndex = 0;
|
|
|
|
for (int i = 0; i < _activeSegments.Count; i++)
|
|
{
|
|
IntPtr ip = _activeSegments[i];
|
|
Segment* segment = (Segment*)ip;
|
|
|
|
result.Add(CreateSegmentInfo(segment, _current, currentIndex));
|
|
currentIndex++;
|
|
}
|
|
|
|
foreach (var ip in _freeSegments)
|
|
{
|
|
Segment* segment = (Segment*)ip;
|
|
result.Add(CreateSegmentInfo(segment, _current, currentIndex++));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
}
|