Improved SegmentedPool with the introduction of handles and alignment (#1)
This commit is contained in:
@@ -3,14 +3,36 @@
|
||||
namespace UnmanagedMMU.Allocators
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper class around <see cref="NativeMemory.Alloc(nuint)"/> and <see cref="NativeMemory.Free(void*)"/>.
|
||||
/// Wrapper class around <see cref="NativeMemory.AlignedAlloc(nuint, nuint)"/> and <see cref="NativeMemory.AlignedFree(void*)"/>.
|
||||
/// </summary>
|
||||
internal sealed unsafe class DefaultUnmanagedAllocator : IUnmanagedAllocator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void* Alloc(nuint size) => NativeMemory.Alloc(size);
|
||||
public void* Alloc(nuint size)
|
||||
{
|
||||
return NativeMemory.AlignedAlloc(size, 16);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Free(void* ptr) => NativeMemory.Free(ptr);
|
||||
public void* AllocAligned(nuint size, nuint alignment)
|
||||
{
|
||||
if (!((alignment & (alignment - 1)) == 0 && alignment > 0))
|
||||
{
|
||||
throw new ArgumentException("Alignment must be a power of 2.", nameof(alignment));
|
||||
}
|
||||
return NativeMemory.AlignedAlloc(size, alignment);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Free(void* ptr)
|
||||
{
|
||||
NativeMemory.Free(ptr);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void FreeAligned(void* ptr, nuint alignment = 0)
|
||||
{
|
||||
NativeMemory.AlignedFree(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
namespace UnmanagedMMU.Allocators
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnmanagedMMU.Allocators
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that defines an Unmanaged allocator
|
||||
/// </summary>
|
||||
internal unsafe interface IUnmanagedAllocator
|
||||
public unsafe interface IUnmanagedAllocator
|
||||
{
|
||||
/// <summary>
|
||||
/// Allocates an unmanaged memory block of the specified size.
|
||||
@@ -14,10 +16,28 @@
|
||||
/// </returns>
|
||||
void* Alloc(nuint size);
|
||||
|
||||
/// <summary>
|
||||
/// Allocates an unmanaged memory block of the specified size with the requested alignment
|
||||
/// </summary>
|
||||
/// <param name="size">The number of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment, in bytes, of the block to allocate. This must be a power of <c>2</c></param>
|
||||
/// <returns></returns>
|
||||
void* AllocAligned(nuint size, nuint alignment);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated unmanaged memory block.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the beginning of the memory block to free.</param>
|
||||
/// <remarks>This method should only be called on with pointers allocated with <see cref="Alloc"/>.</remarks>
|
||||
void Free(void* ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated unmanaged aligned memory block
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the beginning of the memory block to free.</param>
|
||||
/// <param name="alignment">The alignment that the memory refered to by <paramref name="ptr"/> was aligned at (This parameter can be ignored if the underlying allocator does not need it)</param>
|
||||
/// <remarks>This method should only be called on with pointers allocated with <see cref="AllocAligned"/>.</remarks>
|
||||
void FreeAligned(void* ptr, nuint alignment = 0);
|
||||
}
|
||||
}
|
||||
|
||||
16
UnmangedMMU/Allocators/IUnmanagedMemoryOwner.cs
Normal file
16
UnmangedMMU/Allocators/IUnmanagedMemoryOwner.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
using UnmanagedMMU.Handles.Internal;
|
||||
|
||||
namespace UnmanagedMMU.Allocators
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that defines a mechanisim for the owner of an unmanaged memory allocation
|
||||
/// </summary>
|
||||
internal interface IUnmanagedMemoryOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Frees the allocated memory represented by <paramref name="handle"/> back to the owning <see cref="IUnmanagedMemoryOwner"/> instance
|
||||
/// </summary>
|
||||
void Free(IOwnedHandle handle);
|
||||
}
|
||||
}
|
||||
170
UnmangedMMU/Diagnostics/SegmentedPoolDiagnostics.cs
Normal file
170
UnmangedMMU/Diagnostics/SegmentedPoolDiagnostics.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using static UnmanagedMMU.SegmentedPool;
|
||||
|
||||
namespace UnmanagedMMU.Diagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Static helper class for generating diagnostics and suggestions for SegmentedPool.
|
||||
/// Separates report generation logic from the pool implementation.
|
||||
/// </summary>
|
||||
public static class SegmentedPoolDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a formatted report string including suggestions.
|
||||
/// </summary>
|
||||
public static string GenerateReport(PoolState state)
|
||||
{
|
||||
string status = state.BaseAligned ? "OK" : "FAIL";
|
||||
string segmentStatus = $"{state.ActiveSegmentCount} active, {state.FreeSegmentCount} free";
|
||||
|
||||
double efficiency = (state.TotalUsed + state.PaddingBytes) > 0
|
||||
? (100.0 * state.TotalUsed / (state.TotalUsed + state.PaddingBytes))
|
||||
: 100.0;
|
||||
|
||||
string efficiencyLabel = GetEfficiencyLabel(efficiency, state.SegmentAlignment);
|
||||
|
||||
string suggestionLine = state.Suggestion;
|
||||
if (!string.IsNullOrEmpty(suggestionLine))
|
||||
{
|
||||
suggestionLine = $"\n {suggestionLine}";
|
||||
}
|
||||
|
||||
return $"=== SegmentedPool Diagnostics ===\n" +
|
||||
$" Configuration\n" +
|
||||
$" Segment Alignment: {state.SegmentAlignment} bytes (Min Base Alignment)\n" +
|
||||
$" Segment Size: {FormatBytes(state.SegmentSize)}\n" +
|
||||
$" Segment Summary\n" +
|
||||
$" Total Segments: {state.TotalSegmentCount} ({segmentStatus})\n" +
|
||||
$" Potential Savings: {FormatBytes(state.PotentialSavings)} (via Trim())\n" +
|
||||
$" Current Segment\n" +
|
||||
$" Base Address: 0x{(nuint)state.CurrentBase:x}\n" +
|
||||
$" Offset: {state.CurrentOffset} bytes\n" +
|
||||
$" Base Alignment: {status} (To {state.SegmentAlignment} bytes)\n" +
|
||||
$" Memory Statistics\n" +
|
||||
$" Total Reserved: {FormatBytes(state.TotalReserved)}\n" +
|
||||
$" Total Used: {FormatBytes(state.TotalUsed)}\n" +
|
||||
$" Efficiency: {efficiency:F0}% ({efficiencyLabel})\n" +
|
||||
$" Padding Overhead: {FormatBytes(state.PaddingBytes)}\n" +
|
||||
$" Allocation Breakdown\n" +
|
||||
$" Data Bytes: {FormatBytes(state.TotalUsed)}\n" +
|
||||
$" Alignment Padding: {FormatBytes(state.PaddingBytes)}\n" +
|
||||
$" Total Segment Space: {FormatBytes(state.CurrentOffset)}\n" +
|
||||
$" Action Required:{suggestionLine}";
|
||||
}
|
||||
|
||||
private static string GetEfficiencyLabel(double efficiency, nuint segmentAlignment)
|
||||
{
|
||||
int threshold = segmentAlignment switch
|
||||
{
|
||||
8 => 50,
|
||||
16 => 60,
|
||||
32 => 75,
|
||||
64 => 85,
|
||||
_ => 90
|
||||
};
|
||||
|
||||
if (efficiency >= 100) return "Perfect";
|
||||
if (efficiency >= threshold) return "Good";
|
||||
return "High Overhead";
|
||||
}
|
||||
|
||||
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>
|
||||
/// Generates actionable suggestions based on pool metrics.
|
||||
/// Comprehensive coverage of all pool states.
|
||||
/// </summary>
|
||||
public static string GenerateSuggestions(PoolState state, DiagnosticConfig config)
|
||||
{
|
||||
// === CRITICAL ISSUES ===
|
||||
|
||||
// 1. Base alignment broken (should never happen)
|
||||
if (!state.BaseAligned)
|
||||
{
|
||||
return "CRITICAL: Segment base not aligned to configured boundary. This indicates a memory management bug.";
|
||||
}
|
||||
|
||||
// === HIGH PRIORITY ===
|
||||
|
||||
// 2. Pool exhausted (Next alloc blocks)
|
||||
if (state.FreeSegmentCount == 0)
|
||||
{
|
||||
return "INFO: No free segments available. Next allocation will block. Call Reset() to recycle segments or increase initialSegments.";
|
||||
}
|
||||
|
||||
// 3. Significant Memory Waste (Trim Opportunity)
|
||||
if (state.PotentialSavings > 0)
|
||||
{
|
||||
double wasteRatio = (double)state.PotentialSavings / state.TotalReserved;
|
||||
if (wasteRatio > 0.50)
|
||||
{
|
||||
return $"ACTION: {state.FreeSegmentCount - 16} excess segments can be freed. Calling Trim() will recover {FormatBytes(state.PotentialSavings)} ({(wasteRatio * 100):F0}% of reserved memory).";
|
||||
}
|
||||
if (state.PotentialSavings > 1024 * 1024)
|
||||
{
|
||||
return $"ACTION: Excess free segments ({state.FreeSegmentCount}). Calling Trim() will recover {FormatBytes(state.PotentialSavings)}. Current usage: {FormatBytes(state.TotalUsed)} of {FormatBytes(state.TotalReserved)}. Efficiency: {(state.TotalUsed / state.TotalReserved) * 100:F0}%.";
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Efficiency Issues (Alignment Mismatch)
|
||||
double efficiency = (state.TotalUsed + state.PaddingBytes) > 0
|
||||
? (100.0 * state.TotalUsed / (state.TotalUsed + state.PaddingBytes))
|
||||
: 100.0;
|
||||
|
||||
int threshold = state.SegmentAlignment switch
|
||||
{
|
||||
8 => 50,
|
||||
16 => 60,
|
||||
32 => 75,
|
||||
64 => 85,
|
||||
_ => 90
|
||||
};
|
||||
|
||||
if (efficiency < threshold)
|
||||
{
|
||||
if (state.SegmentAlignment <= 32)
|
||||
{
|
||||
return $"Suggestion: Current alignment ({state.SegmentAlignment}B) is low. Increase to 32B for better efficiency.";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Suggestion: Verify allocation alignment matches segment base ({state.SegmentAlignment}B). Efficiency is {efficiency:F0}%.";
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Segment Nearly Full
|
||||
if (config.SegmentSize > 0 && state.CurrentOffset > config.SegmentSize * 0.90)
|
||||
{
|
||||
return "INFO: Current segment nearly full. Consider Reset() to reuse this segment instead of allocating a new one.";
|
||||
}
|
||||
|
||||
// === LOW PRIORITY / OPTIONAL ===
|
||||
|
||||
// 6. Low Utilization (Memory Bloat)
|
||||
if (state.TotalReserved > 16 * 1024 * 1024)
|
||||
{
|
||||
double usageRatio = (double)state.TotalUsed / state.TotalReserved;
|
||||
if (usageRatio < 0.10)
|
||||
{
|
||||
return $"INFO: Low memory utilization ({usageRatio:P0}). Consider Reset() or Trim() to reduce footprint.";
|
||||
}
|
||||
}
|
||||
|
||||
return "Pool operating normally.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration details passed to diagnostics for context-aware suggestions.
|
||||
/// </summary>
|
||||
public readonly struct DiagnosticConfig
|
||||
{
|
||||
public nuint SegmentSize { get; init; }
|
||||
public nuint TotalReserved { get; init; }
|
||||
}
|
||||
}
|
||||
27
UnmangedMMU/Handles/IMemoryHandle.cs
Normal file
27
UnmangedMMU/Handles/IMemoryHandle.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnmanagedMMU.Allocators;
|
||||
|
||||
namespace UnmanagedMMU.Handles
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Interface that represents an untyped handle to unmanaged memory.
|
||||
/// </summary>
|
||||
public unsafe interface IMemoryHandle
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pointer to the underlying unmanaged memory block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned pointer is valid only for the lifetime of the allocator
|
||||
/// that created this <see cref="IMemoryHandle"/>.
|
||||
/// </remarks>
|
||||
void* Pointer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of bytes in the unmanaged memory block
|
||||
/// represented by this <see cref="IMemoryHandle"/>.
|
||||
/// </summary>
|
||||
nuint ByteCount { get; }
|
||||
}
|
||||
}
|
||||
34
UnmangedMMU/Handles/IMemoryHandleT.cs
Normal file
34
UnmangedMMU/Handles/IMemoryHandleT.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using UnmanagedMMU.Allocators;
|
||||
|
||||
namespace UnmanagedMMU.Handles
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Interface that represents a typed handle to unmanaged memory.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// The unmanaged element type stored in the underlying memory block.
|
||||
/// </typeparam>
|
||||
public unsafe interface IMemoryHandle<T> : IMemoryHandle, IDisposable where T : unmanaged
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Gets the typed <typeparamref name="T"/> pointer to the underlying unmanaged memory block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pointer becomes invalid after the handle is disposed.
|
||||
/// </remarks>
|
||||
new T* Pointer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of elements contained in this handle.
|
||||
/// </summary>
|
||||
nuint Length { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the underlying unmanaged memory block held by this handle back to the owning <see cref="IUnmanagedMemoryOwner"/> instance
|
||||
/// </summary>
|
||||
new void Dispose() { }
|
||||
}
|
||||
}
|
||||
15
UnmangedMMU/Handles/Internal/IOwnedHandle.cs
Normal file
15
UnmangedMMU/Handles/Internal/IOwnedHandle.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using UnmanagedMMU.Allocators;
|
||||
|
||||
namespace UnmanagedMMU.Handles.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface defining an interface for handle ownership semantics
|
||||
/// </summary>
|
||||
internal interface IOwnedHandle: IMemoryHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the <see cref="IUnmanagedMemoryOwner"/> that created owns this <see cref="IOwnedHandle"/>
|
||||
/// </summary>
|
||||
IUnmanagedMemoryOwner GetOwner();
|
||||
}
|
||||
}
|
||||
126
UnmangedMMU/Handles/MemoryHandleBase.cs
Normal file
126
UnmangedMMU/Handles/MemoryHandleBase.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.Diagnostics;
|
||||
using UnmanagedMMU.Allocators;
|
||||
using UnmanagedMMU.Handles.Internal;
|
||||
|
||||
namespace UnmanagedMMU.Handles
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base implementation for typed unmanaged memory handles.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// The unmanaged element type stored in the underlying memory block.
|
||||
/// </typeparam>
|
||||
/// <remarks>
|
||||
/// This class encapsulates the raw pointer and byte length of an unmanaged
|
||||
/// memory allocation and provides typed access to that memory.
|
||||
///
|
||||
/// Memory lifetime is controlled by the originating allocator; this type
|
||||
/// does not own or free the underlying memory.
|
||||
/// </remarks>
|
||||
internal unsafe abstract class MemoryHandleBase<T> : IMemoryHandle<T>, IOwnedHandle where T : unmanaged
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IUnmanagedMemoryOwner"/> that owns this <see cref="MemoryHandleBase{T}"/> handle
|
||||
/// </summary>
|
||||
private readonly IUnmanagedMemoryOwner _owner;
|
||||
|
||||
/// <summary>
|
||||
/// The raw pointer to the unmanaged memory block
|
||||
/// </summary>
|
||||
private readonly void* _ptr;
|
||||
|
||||
/// <summary>
|
||||
/// The size of the unmanaged memory block in bytes
|
||||
/// </summary>
|
||||
private readonly nuint _bytelen;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this <see cref="MemoryHandleBase{T}"/> has been disposed.
|
||||
/// </summary>
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="MemoryHandleBase{T}"/> instnace
|
||||
/// </summary>
|
||||
/// <param name="ptr">Pointer to the allocated unmanaged memory</param>
|
||||
/// <param name="byteLength">The size of the unallocated memory block in bytes</param>
|
||||
/// <param name="owner">The <see cref="IUnmanagedMemoryOwner"/> that owns the <see cref="MemoryHandleBase{T}"/> handle being created</param>
|
||||
protected MemoryHandleBase(void* ptr, nuint byteLength, IUnmanagedMemoryOwner owner)
|
||||
{
|
||||
// Defensive check
|
||||
Debug.Assert(ptr != null, message: "BUG CHECK: E_INVALID_MEMORY_HANDLE");
|
||||
_ptr = ptr;
|
||||
_bytelen = byteLength;
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pointer to the unmanaged memory block
|
||||
/// </summary>
|
||||
public virtual void* Pointer
|
||||
{
|
||||
get { return _ptr; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the typed pointer to the unmanged memory block
|
||||
/// </summary>
|
||||
T* IMemoryHandle<T>.Pointer
|
||||
{
|
||||
get { return (T*)_ptr; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size in bytes of the unmanaged memory block
|
||||
/// </summary>
|
||||
public nuint ByteCount
|
||||
{
|
||||
get { return _bytelen; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of elements of type <typeparamref name="T"/>
|
||||
/// contained in the unmanaged memory block.
|
||||
/// </summary>
|
||||
public nuint Length
|
||||
{
|
||||
get { return _bytelen / (nuint)sizeof(T); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources held by the <see cref="MemoryHandleBase{T}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is idempotent and safe to call multiple times. It ensures the underlying
|
||||
/// memory is cleaned up according to the implementation in <see cref="OnDispose()"/>.
|
||||
/// After disposal, the handle is invalid for further use.
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
OnDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override in derived classes to define lifecycle behavior.
|
||||
/// This is an abstract method to ensure cleanup logic is always implemented.
|
||||
/// </summary>
|
||||
protected abstract void OnDispose();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="IUnmanagedMemoryOwner"/> that created owns this <see cref="MemoryHandleBase{T}"/> instance
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IUnmanagedMemoryOwner"/> that created owns this <see cref="MemoryHandleBase{T}"/> instance</returns>
|
||||
public IUnmanagedMemoryOwner GetOwner()
|
||||
{
|
||||
return _owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
UnmangedMMU/Handles/PersistentMemoryHandle.cs
Normal file
21
UnmangedMMU/Handles/PersistentMemoryHandle.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using UnmanagedMMU.Allocators;
|
||||
|
||||
namespace UnmanagedMMU.Handles
|
||||
{
|
||||
internal sealed unsafe class PersistentMemoryHandle<T> : MemoryHandleBase<T> where T : unmanaged
|
||||
{
|
||||
|
||||
public PersistentMemoryHandle(void* ptr, nuint byteLength, IUnmanagedMemoryOwner owner) : base(ptr, byteLength, owner)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnDispose()
|
||||
{
|
||||
if (Pointer != null)
|
||||
{
|
||||
GetOwner().Free(this);
|
||||
// No need to set _ptr = null here; MemoryHandleBase._disposed flag prevents double-free
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
UnmangedMMU/Handles/SegmentedMemoryHandle.cs
Normal file
21
UnmangedMMU/Handles/SegmentedMemoryHandle.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnmanagedMMU.Allocators;
|
||||
|
||||
namespace UnmanagedMMU.Handles
|
||||
{
|
||||
internal sealed unsafe class SegmentedMemoryHandle<T> : MemoryHandleBase<T> where T : unmanaged
|
||||
{
|
||||
public SegmentedMemoryHandle(void* ptr, nuint byteLength, IUnmanagedMemoryOwner owner) : base(ptr, byteLength, owner)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnDispose()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,50 @@
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
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.
|
||||
@@ -13,7 +54,7 @@
|
||||
/// 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
|
||||
public unsafe sealed class SegmentedPool : IDisposable, IUnmanagedMemoryOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The default size for a <see cref="Segment"/>
|
||||
@@ -25,6 +66,11 @@
|
||||
/// </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>
|
||||
@@ -36,12 +82,12 @@
|
||||
private readonly List<IntPtr> _activeSegments = [];
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the total amount of allocated bytes
|
||||
/// Tracks the total bytes of memory reserved from the provided <see cref="IUnmanagedAllocator"/>
|
||||
/// </summary>
|
||||
private nuint _totalAllocated = 0;
|
||||
private nuint _totalReserved = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the total amount of allocated bytes current in use
|
||||
/// Tracks the total amount of allocated bytes currently in use
|
||||
/// </summary>
|
||||
private nuint _totalUsed = 0;
|
||||
|
||||
@@ -51,7 +97,7 @@
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the <see cref="SegmentedPool"/> has been disposed.
|
||||
/// Indicates whether this <see cref="SegmentedPool"/> has been disposed.
|
||||
/// </summary>
|
||||
private volatile bool _disposed;
|
||||
|
||||
@@ -60,8 +106,16 @@
|
||||
/// </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>
|
||||
@@ -93,16 +147,152 @@
|
||||
}
|
||||
|
||||
|
||||
/// <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, int initialSegments = 4)
|
||||
: this(segmentSize, initialSegments, new DefaultUnmanagedAllocator())
|
||||
public SegmentedPool(nuint segmentSize = _defaultSegmentSize, SegmentAlignment segmentAlignment = SegmentAlignment.Aligned32, int initialSegments = 4, bool zeroMemory = false)
|
||||
: this(segmentSize, segmentAlignment, initialSegments, zeroMemory, new DefaultUnmanagedAllocator())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -110,28 +300,30 @@
|
||||
/// 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, int initialSegments, IUnmanagedAllocator allocator)
|
||||
internal SegmentedPool(nuint segmentSize, SegmentAlignment segmentAlignment, int initialSegments, bool zeroMemory, 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));
|
||||
}
|
||||
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++)
|
||||
{
|
||||
_freeSegments.Push((IntPtr)AllocateNewSegment(_currentSegmentSize));
|
||||
seg = AllocateNewSegment(_currentSegmentSize);
|
||||
ZeroSegment(seg);
|
||||
_freeSegments.Push((IntPtr)seg);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +337,13 @@
|
||||
/// <returns>The number of free segments.</returns>
|
||||
public int FreeSegmentCount
|
||||
{
|
||||
get { return _freeSegments.Count; }
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _freeSegments.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,7 +352,13 @@
|
||||
/// <returns>The number of currently active Segments.</returns>
|
||||
public int ActiveSegmentCount
|
||||
{
|
||||
get { return _activeSegments.Count; }
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _activeSegments.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -163,7 +367,13 @@
|
||||
/// <returns>The total number of bytes that have been allocated.</returns>
|
||||
public nuint TotalAllocatedBytes
|
||||
{
|
||||
get { return _totalAllocated; }
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalReserved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -172,7 +382,13 @@
|
||||
/// <returns>The total number of bytes that are in use.</returns>
|
||||
public nuint TotalUsedBytes
|
||||
{
|
||||
get { return _totalUsed; }
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -198,6 +414,153 @@
|
||||
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>
|
||||
@@ -236,15 +599,15 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a span of unmanaged memory of size <paramref name="count"/> for elements of type <typeparamref name="T"/>.
|
||||
/// 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="Span{T}"/> representing the allocated memory. The span is valid until the <see cref="SegmentedPool"/> is reset or disposed.</returns>
|
||||
/// <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 and may lead to crashes.</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">
|
||||
@@ -253,73 +616,43 @@
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
||||
/// </exception>
|
||||
public Span<T> Allocate<T>(int count) where T : unmanaged
|
||||
public IMemoryHandle<T> Allocate<T>(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<T>(ptr, count);
|
||||
}
|
||||
T* ptr = Alloc<T>(count);
|
||||
nuint byteLength = (nuint)count * (nuint)sizeof(T);
|
||||
return new SegmentedMemoryHandle<T>(ptr, byteLength, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches to a new <see cref="Segment"/> when the current <see cref="Segment"/> is full.
|
||||
/// 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>
|
||||
/// <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)
|
||||
/// <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
|
||||
{
|
||||
Segment* segment;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
T* ptr = AllocateWithAlignment<T>(count, effectiveAlignment);
|
||||
nuint byteLength = (nuint)count * (nuint)sizeof(T);
|
||||
return new SegmentedMemoryHandle<T>(ptr, byteLength, this);
|
||||
|
||||
_activeSegments.Add((IntPtr)segment);
|
||||
_current = segment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a new <see cref="Segment"/>
|
||||
/// </summary>
|
||||
/// <param name="size">
|
||||
/// Optional size, in bytes, for the new <see cref="Segment"/>.
|
||||
/// If <c>null</c>, the default <see cref="Segment"/> size (<see cref="_defaultSegmentSize"/>) is used.
|
||||
/// </param>
|
||||
/// <returns>A pointer to the newly allocated <see cref="Segment"/></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -344,9 +677,9 @@
|
||||
Segment* segment = (Segment*)ip;
|
||||
|
||||
// Free the unmanaged memory
|
||||
_allocator.Free(segment->Ptr);
|
||||
_totalAllocated -= segment->Size;
|
||||
_allocator.Free(segment);
|
||||
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
||||
_totalReserved -= segment->Size;
|
||||
_allocator.FreeAligned(segment, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,6 +698,9 @@
|
||||
Segment* segment = (Segment*)ip;
|
||||
segment->Offset = 0;
|
||||
_freeSegments.Push(ip);
|
||||
|
||||
// Zero memory if requested
|
||||
ZeroSegment(segment);
|
||||
}
|
||||
|
||||
_activeSegments.Clear();
|
||||
@@ -380,7 +716,10 @@
|
||||
// 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)
|
||||
{
|
||||
@@ -389,6 +728,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -406,26 +766,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Free active pages
|
||||
// Free active segments
|
||||
foreach (var ip in _activeSegments)
|
||||
{
|
||||
Segment* segment = (Segment*)ip;
|
||||
_allocator.Free(segment->Ptr);
|
||||
_allocator.Free(segment);
|
||||
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
||||
_allocator.FreeAligned(segment, 8);
|
||||
}
|
||||
|
||||
// Free free pages
|
||||
// Free free segments
|
||||
foreach (var ip in _freeSegments)
|
||||
{
|
||||
Segment* segment = (Segment*)ip;
|
||||
_allocator.Free(segment->Ptr);
|
||||
_allocator.Free(segment);
|
||||
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
||||
_allocator.FreeAligned(segment, 8);
|
||||
}
|
||||
|
||||
_activeSegments.Clear();
|
||||
_freeSegments.Clear();
|
||||
_current = null;
|
||||
_totalAllocated = 0;
|
||||
_totalReserved = 0;
|
||||
_totalUsed = 0;
|
||||
_disposed = true;
|
||||
}
|
||||
@@ -435,11 +795,153 @@
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="SegmentedPool"/> has already been disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">
|
||||
/// Thrown when this instance is no longer valid for use.
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- Optional: Suppress warnings for undocumented members -->
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
516
UnmangedMMU/WorkspaceHeap.cs
Normal file
516
UnmangedMMU/WorkspaceHeap.cs
Normal file
@@ -0,0 +1,516 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an unmanaged heap for long-lived allocations with reuse.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="WorkspaceHeap"/> minimizes calls to the underlying allocator by retaining
|
||||
/// freed blocks in size-segregated free lists and reusing them when possible.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Allocation strategy:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>
|
||||
/// <b>Small allocations</b> (≤ 1 KB) use fixed size buckets
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <b>Medium allocations</b> (≤ 256 KB) use best-fit reuse
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <b>Large allocations</b> (> 256 KB) use tolerance-based reuse
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public unsafe sealed class WorkspaceHeap : IDisposable, IUnmanagedMemoryOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum size, in bytes, for "small" allocations. Uses fixed-size buckets
|
||||
/// </summary>
|
||||
private const nuint _smallThreshold = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size, in bytes, for allocations considered "medium". Uses best-fit reuse
|
||||
/// </summary>
|
||||
private const nuint _mediumThreshold = 256 * 1024; // 256 KB
|
||||
|
||||
/// <summary>
|
||||
/// The maximum absolute number of bytes that may be wasted when reusing a large block for a "large" allocation.
|
||||
/// </summary>
|
||||
private const nuint _largeMaxWasteBytes = 256 * 1024; // 256 KB
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed size ratio when reusing a large allocation block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example, a value of <c>1.25</c> allows a block up to 25% larger than
|
||||
/// the requested size to be reused.
|
||||
/// </remarks>
|
||||
private const double _largeWasteRatioLimit = 1.25;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined bucket sizes used for small allocation reuse.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Allocator interface used for all underlying unmanaged memory operations.
|
||||
/// </summary>
|
||||
private readonly IUnmanagedAllocator _allocator;
|
||||
|
||||
/// <summary>
|
||||
/// Internal lock, ensures thread safety while maintaining a simple interface
|
||||
/// </summary>
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Free lists for small allocations, keyed by the bucket size, in bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a given bucket, the corresponding stack contains previously allocated blocks that are available to be used
|
||||
/// </remarks>
|
||||
private readonly Dictionary<nuint, Stack<IntPtr>> _smallFree = new();
|
||||
|
||||
/// <summary>
|
||||
/// Free lists for medium allocations keyed by the exactly allocated size, in bytes, and sorted for best-fit.
|
||||
/// </summary>
|
||||
private readonly SortedDictionary<nuint, Stack<IntPtr>> _mediumFree = new();
|
||||
|
||||
/// <summary>
|
||||
/// Free lists for large allocations keyed by the exactly allocated size, in bytes, sorted for tolerance-based reuse.
|
||||
/// </summary>
|
||||
private readonly SortedDictionary<nuint, Stack<IntPtr>> _largeFree = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the total bytes of memory reserved from the provided <see cref="IUnmanagedAllocator"/>.
|
||||
/// </summary>
|
||||
private nuint _totalReserved;
|
||||
|
||||
/// <summary>
|
||||
/// Total memory currently in use by active allocations, in bytes.
|
||||
/// </summary>
|
||||
private nuint _totalInUse;
|
||||
|
||||
/// <summary>
|
||||
/// Counts actual underlying OS allocations.
|
||||
/// </summary>
|
||||
private nuint _totalAllocations;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this <see cref="WorkspaceHeap"/> has been disposed.
|
||||
/// </summary>
|
||||
private volatile bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Internal header prepended to each allocation to track its size.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlockHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// Size of the allocation in bytes.
|
||||
/// </summary>
|
||||
public nuint Size;
|
||||
|
||||
/// <summary>
|
||||
/// Padding to ensure <see cref="BlockHeader"></see> is 32-byte aligned
|
||||
/// </summary>
|
||||
private readonly nuint _pad1;
|
||||
|
||||
/// <summary>
|
||||
/// Padding to ensure <see cref="BlockHeader"></see> is 32-byte aligned
|
||||
/// </summary>
|
||||
private readonly nuint _pad2;
|
||||
|
||||
/// <summary>
|
||||
/// Padding to ensure <see cref="BlockHeader"></see> is 32-byte aligned
|
||||
/// </summary>
|
||||
private readonly nuint _pad3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkspaceHeap"/>.
|
||||
/// </summary>
|
||||
public WorkspaceHeap()
|
||||
: this(new DefaultUnmanagedAllocator())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkspaceHeap"/> using the specified <see cref="IUnmanagedAllocator"/>.
|
||||
/// </summary>
|
||||
/// <param name="allocator">Allocator implementing <see cref="IUnmanagedAllocator"/>.</param>
|
||||
internal WorkspaceHeap(IUnmanagedAllocator allocator)
|
||||
{
|
||||
_allocator = allocator;
|
||||
|
||||
// Initialize small-size buckets
|
||||
foreach (var size in _sizeClasses)
|
||||
_smallFree[size] = new Stack<IntPtr>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of bytes currently allocated from the underlying allocator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This includes both active allocations and freed blocks retained for reuse.
|
||||
/// </remarks>
|
||||
public nuint TotalReservedBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
return _totalReserved;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of bytes currently in use by active allocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value decreases when memory is freed and increases when new allocations occur.
|
||||
/// </remarks>
|
||||
public nuint TotalUsedBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalInUse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of allocation operations performed by this heap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This counts new underlying OS allocations, not reuse from free lists.
|
||||
/// Useful for performance diagnostics and testing reuse behavior.
|
||||
/// </remarks>
|
||||
public nuint TotalAllocationCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalAllocations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the <see cref="WorkspaceHeap"/> has been disposed.
|
||||
/// </summary>
|
||||
public bool IsDisposed
|
||||
{
|
||||
get { return _disposed; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the small size bucket for a requested allocation.
|
||||
/// </summary>
|
||||
/// <param name="size">The allocation size to get the bucket size for</param>
|
||||
/// <returns>The small size bucket for the requested allocation</returns>
|
||||
private static nuint GetSizeClass(nuint size)
|
||||
{
|
||||
foreach (nuint s in _sizeClasses)
|
||||
{
|
||||
if (size <= s)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a new block from the underlying allocator including a header.
|
||||
/// </summary>
|
||||
/// <param name="payloadSize">Requested payload size in bytes.</param>
|
||||
/// <returns>
|
||||
/// Pointer to the allocated block (header included).
|
||||
/// </returns>
|
||||
private IntPtr AllocateNew(nuint payloadSize)
|
||||
{
|
||||
nuint total = payloadSize + (nuint)sizeof(BlockHeader);
|
||||
void* raw = _allocator.Alloc(total);
|
||||
_totalReserved += total;
|
||||
_totalAllocations++;
|
||||
|
||||
return (IntPtr)raw;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates unmanaged memory from the workspace heap.
|
||||
/// </summary>
|
||||
/// <param name="count">Number of elements <typeparamref name="T"/> to allocate.</param>
|
||||
/// <param name="zero">If true, memory is zero-initialized.</param>
|
||||
/// <returns> An <see cref="IMemoryHandle{T}"/> to the allocated memory.</returns>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if heap is disposed.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown if size is zero.</exception>
|
||||
public IMemoryHandle<T> Allocate<T>(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>((T*)ptr, size, this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Allocates a small-size block using bucketed free lists.</summary>
|
||||
private void* AllocateSmall(nuint size, bool zero)
|
||||
{
|
||||
nuint bucket = GetSizeClass(size);
|
||||
Stack<IntPtr> 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;
|
||||
}
|
||||
|
||||
/// <summary>Allocates a medium-size block using best-fit reuse.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Allocates a large block using tolerance-based reuse (smallest ≥ requested within waste bounds).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated block, returning it to the appropriate free list.
|
||||
/// </summary>
|
||||
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<IntPtr>();
|
||||
stack.Push((IntPtr)header);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_largeFree.TryGetValue(size, out var stack))
|
||||
_largeFree[size] = stack = new Stack<IntPtr>();
|
||||
stack.Push((IntPtr)header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all unused blocks back to the underlying allocator.
|
||||
/// </summary>
|
||||
public void Prune()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lock)
|
||||
{
|
||||
PruneDictionary(_smallFree);
|
||||
PruneDictionary(_mediumFree);
|
||||
PruneDictionary(_largeFree);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Helper to free all blocks in a dictionary of free stacks.</summary>
|
||||
private void PruneDictionary(IDictionary<nuint, Stack<IntPtr>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all memory and marks the heap as disposed.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="WorkspaceHeap"/> has already been disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">
|
||||
/// Thrown when this instance is no longer valid for use.
|
||||
/// </exception>
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user