UnmanagedMMU
UnmanagedMMU is a high-performance C# memory manager library that provides efficient unmanaged memory allocation.
Table of Contents
- IMemoryHandle
- SegmentedPool
IMemoryHandle
The UnmanagedMMU namespace provides two interface variations for memory handles. The non-generic IMemoryHandle is a thin wrapper around a pointer for untyped access. The generic IMemoryHandle extends the non-generic interface to provide strong typing and implements IDisposable for automatic resource management.
IMemoryHandle (Untyped)
The IMemoryHandle interface represents a non-generic memory handle. It provides access to the raw pointer and byte count, useful when the element type is unknown at compile time or when interfacing with low-level APIs.
Key Properties:
| Property | Type | Description |
|---|---|---|
Pointer |
void* |
A raw pointer to the start of the allocation. |
ByteCount |
nuint |
The total size of the allocation in bytes. |
IMemoryHandle<T> (Typed)
The IMemoryHandle<T> interface extends IMemoryHandle and provides strongly typed access to the underlying memory. It is the primary interface used with SegmentedPool allocations. This interface implements IDisposable.
Key Properties:
| Property | Type | Description |
|---|---|---|
Pointer |
T* |
A typed pointer to the start of the allocation. |
ByteCount |
nuint |
The total size of the allocation in bytes. |
Length |
nuint |
The number of T elements in the allocation (ByteCount / sizeof(T)). |
Example Usage:
using var pool = new SegmentedPool();
// Allocate memory for 100 integers
IMemoryHandle<int> handle = pool.Allocate<int>(100);
try
{
// Access via typed pointer
unsafe
{
for (int i = 0; i < handle.Length; i++)
{
handle.Pointer[i] = i;
}
}
}
finally
{
handle.Dispose();
}
SegmentedPool
SegmentedPool is a segmented bump allocator for unmanaged memory in C#. It pre-allocates memory in fixed-size segments and serves allocations sequentially within each segment.
Once a segment is full, the pool automatically switches to a new segment, allowing fast, contiguous allocations without fragmentation.
Advantages of the SegmentedPool:
- High performance: Allocation is simple pointer arithmetic.
- Contiguous memory: Reduces cache misses and improves data locality.
- Thread-safe: Supports concurrent allocation operations.
- Manual memory control: Works outside of .NET GC, ideal for high-performance or low-latency scenarios.
- Configurable alignment: Supports SIMD and hardware-specific alignment requirements.
Segments
A Segment is a contiguous block of unmanaged memory managed by the pool. Each segment contains:
| Field | Type | Description |
|---|---|---|
Ptr |
byte* |
Pointer to the start of the unmanaged memory block. |
Offset |
nuint |
Current allocation offset within the segment. Increases as memory is allocated. |
Size |
nuint |
Total size of the segment in bytes. |
Segments are allocated automatically by the pool and should never be modified outside the pool.
SegmentAlignment
The SegmentAlignment enum defines the alignment requirements for memory segments. Values are powers of 2 and reflect common hardware requirements (SIMD, cache lines, native pointer size).
| Value | Alignment | Description |
|---|---|---|
Aligned8 |
8 bytes | Minimum for 64-bit pointers and primitives (long, double). |
Aligned16 |
16 bytes | Required for Vector128 (SSE/NEON). Common default for general-purpose SIMD workloads. |
Aligned32 |
32 bytes | Required for Vector256 (AVX). Recommended default for SIMD-heavy applications. |
Aligned64 |
64 bytes | Matches standard CPU cache-line size. Ensures segment bases align to cache line boundaries, minimizing cache-line splits. |
Aligned128 |
128 bytes | Advanced optimization for specific cache-aware algorithms or AVX-512 contexts. |
Default: SegmentAlignment.Aligned32
Allocation Strategy
The SegmentedPool uses a bump allocator strategy:
- Memory is allocated sequentially within the current Segments.
Offsetis incremented with each allocation.- When the current segment does not have enough space, the pool switches to a new Segments (either from the free pool or a freshly allocated one).
- Allocations are aligned according to the SegmentAlignment or a requested alignment.
This provides O(1) allocation performance for most operations.
Constructor
Initializes a new instance of the SegmentedPool with the specified segment size, alignment, and number of pre-allocated Segments.
Syntax
// Default configuration
SegmentedPool pool = new SegmentedPool();
// Custom parameters
SegmentedPool pool = new SegmentedPool(
segmentSize: 4 * 1024 * 1024,
segmentAlignment: SegmentAlignment.Aligned32,
initialSegments: 4,
zeroMemory: false
);
Parameters
| Parameter | Type | Description |
|---|---|---|
segmentSize |
nuint |
Size of each Segment in bytes. Optional. Defaults to 4 MiB (4 * 1024 * 1024 bytes). |
segmentAlignment |
SegmentAlignment |
Alignment requirement for each allocated Segment. Must be a power of 2. Optional. Defaults to Aligned32. |
initialSegments |
int |
Number of Segments to pre-allocate in the pool. Optional. Defaults to 4. |
zeroMemory |
bool |
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. Optional. Defaults to false. |
Return value
Returns a new instance of the SegmentedPool with the specified Segments size and number of pre-allocated Segments.
Exceptions
| Exception | Condition |
|---|---|
ArgumentOutOfRangeException |
Thrown if segmentSize is zero or initialSegments is less than 1. |
ArgumentException |
Thrown if alignment is not a power of 2. |
Remarks
The SegmentedPool pre-allocates the specified number of Segments during construction, ensuring that the pool can immediately serve allocations without additional memory allocation overhead. The pool operates in unmanaged memory and bypasses the .NET garbage collector, so all memory must be manually released by calling Dispose() when the pool is no longer needed.
When zeroMemory is set to true, all memory returned from the pool is zero-initialized, which ensures that no residual data from previous allocations is exposed.
When zeroMemory is false, memory may contain leftover data from prior use, and it is the caller's responsibility to clear it if necessary to maintain data integrity or security.
Setting zeroMemory to true may have a slight performance cost due to the additional initialization step.
Allocation Methods
The SegmentedPool provides two allocation methods, both returning IMemoryHandle<T> for safe memory management.
Allocate
Allocates a block of unmanaged memory of the specified count for elements of type T using the pool's default segment alignment.
Syntax
IMemoryHandle<T> handle = pool.Allocate<T>(count);
Parameters
| Parameter | Type | Description |
|---|---|---|
count |
int |
Number of elements of type T to allocate. Must be greater than zero. |
Return value
An IMemoryHandle<T> representing the allocated memory. The handle is valid until either Reset or Dispose is called on the pool.
Remarks
- This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.
- Accessing the memory after Reset or Dispose has been called is undefined behavior.
- Memory alignment is based on the pool's configured SegmentAlignment.
Exceptions
| Exception | Condition |
|---|---|
ArgumentOutOfRangeException |
Thrown if count is less than or equal to zero. |
OverflowException |
Thrown if the total allocation size exceeds the maximum allowable size. |
ObjectDisposedException |
Thrown if the pool has been disposed. |
Example
using var pool = new SegmentedPool();
IMemoryHandle<byte> handle = pool.Allocate<byte>(100);
try
{
// Access memory through typed pointer
unsafe
{
for (int i = 0; i < handle.Length; i++)
{
handle.Pointer[i] = i;
}
}
}
finally
{
handle.Dispose(); // Returns memory to pool
}
AllocateAligned
Allocates a block of unmanaged memory with the specified alignment requirement. This is useful for SIMD or hardware-specific operations.
Syntax
IMemoryHandle<T> handle = pool.AllocateAligned<T>(count, alignment);
Parameters
| Parameter | Type | Description |
|---|---|---|
count |
int |
Number of elements of type T to allocate. Must be greater than zero. |
alignment |
SegmentAlignment |
The alignment to align the allocation to inside the currently active Segment. |
Return value
An IMemoryHandle<T> representing the allocated memory. The handle is valid until either Reset or Dispose is called on the pool.
Remarks
- This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.
- Accessing the memory after Reset or Dispose has been called is undefined behavior.
- The alignment requirement must be a power of 2.
- If the requested alignment is less than
sizeof(T), the type's natural size is used instead.
Exceptions
| Exception | Condition |
|---|---|
ArgumentOutOfRangeException |
Thrown if count is less than or equal to zero. |
OverflowException |
Thrown if the total allocation size exceeds the maximum allowable size. |
ObjectDisposedException |
Thrown if the pool has been disposed. |
ArgumentException |
Thrown if alignment is not a power of 2. |
Example
using var pool = new SegmentedPool(segmentAlignment: SegmentAlignment.Aligned32);
IMemoryHandle<UInt32> handle = pool.AllocateAligned<Vector256>(64, SegmentAlignment.Aligned32);
try
{
unsafe
{
// Perform SIMD operations on aligned memory
for (int i = 0; i < handle.Length; i++)
{
// Process aligned SIMD data
}
}
}
finally
{
handle.Dispose();
}
Pool State Properties
The SegmentedPool provides several read-only properties for monitoring pool state:
CurrentSegmentSize
Gets the size, in bytes, used when allocating new Segment instances.
Syntax
nuint size = pool.CurrentSegmentSize;
Return value
The size, in bytes, used when allocating new Segment.
Remarks
This reflects the most recently configured size and affects only future segment allocations.
TotalAllocatedBytes
Gets the total number of bytes that have currently been allocated from the system.
Syntax
nuint allocated = pool.TotalAllocatedBytes;
Return value
The total number of bytes allocated from the system.
Remarks
This includes memory for all active and free Segments.
TotalUsedBytes
Gets the total number of bytes currently in use across all active segments.
Syntax
nuint used = pool.TotalUsedBytes;
Return value
The total number of bytes currently in use.
Remarks
This represents the actual data bytes allocated, excluding alignment padding.
ActiveSegmentCount
Gets the number of segments currently in use in the pool.
Syntax
int count = pool.ActiveSegmentCount;
Return value
The number of segments that are currently active and not yet returned to the free pool.
Remarks
This property is thread-safe and reflects the current state of the pool.
FreeSegmentCount
Gets the number of free segments available for reuse in the pool.
Syntax
int count = pool.FreeSegmentCount;
Return value
The number of segments that are currently free and available for reuse.
Remarks
This property is thread-safe. A value of 0 indicates that the pool will need to allocate a new segment for the next allocation.
IsDisposed
Gets a value indicating whether this instance has been disposed.
Syntax
bool isDisposed = pool.IsDisposed;
Return value
true if the pool has been disposed; otherwise false.
Remarks
Once this value is true, any further calls to allocation or management methods will throw a ObjectDisposedException.
Segment Size Management
SetSegmentSize
Sets the Segment size to use for future allocations.
Syntax
pool.SetSegmentSize(newSize);
Parameters
| Parameter | Type | Description |
|---|---|---|
newSize |
nuint | The new segment size in bytes. Must be > 0. |
Return value
None.
Remarks
This method will only affect the Segment size of future allocations; it does not modify existing Segments.
Exceptions
| Exception | Condition |
|---|---|
ArgumentOutOfRangeException |
Thrown if newSize is zero. |
ObjectDisposedException |
Thrown if the pool has been disposed. |
ResetSegmentSize
Resets the Segment size for future allocations back to the default (4 MiB).
Syntax
pool.ResetSegmentSize();
Parameters
None.
Return value
None.
Remarks
This method will only affect the Segment size of future allocations; it does not modify existing Segments.
Reset and Trim
Reset
Resets the pool, returning all active Segments to the free pool for reuse.
Syntax
pool.Reset(trim: false);
Parameters
| Parameter | Type | Description |
|---|---|---|
trim |
bool |
Optional. If true, trims excess free Segments after reset. Defaults to false |
Return value
None.
Remarks
This method resets all active Segments offsets to zero. Additionally, no memory is freed unless trim is true; the pool retains free Segments for future allocations.
Trim
Frees unused Segments in the free pool, reducing memory usage.
Syntax
pool.Trim(minFreeSegments: 16);
Parameters
| Parameter | Type | Description |
|---|---|---|
minFreeSegments |
int |
Minimum number of free Segments to retain. Segments beyond this count are released. Defaults to 16. |
Return value
None.
Remarks
This method releases unmanaged memory from Segments beyond the specified minimum. This is useful for helping to control the unmanaged memory footprint in long-running applications.
Exceptions
| Exception | Condition |
|---|---|
ArgumentOutOfRangeException |
Thrown if minFreeSegments is negative. |
ObjectDisposedException |
Thrown if the pool has been disposed. |
Diagnostics
The SegmentedPool provides several diagnostic methods for monitoring pool health and efficiency.
GetPoolState
Gets a snapshot of the current pool state for diagnostics.
Syntax
SegmentedPool.PoolState state = pool.GetPoolState();
Return value
A SegmentedPool.PoolState struct containing current pool metrics.
Example
var state = pool.GetPoolState();
Console.WriteLine($"Active: {state.ActiveSegmentCount}, Free: {state.FreeSegmentCount}");
Console.WriteLine($"Efficiency: {(100.0 * state.TotalUsed / (state.TotalUsed + state.PaddingBytes)):F0}%");
GetDiagnosticReport
Generates a diagnostic report for the pool including actionable suggestions.
Syntax
string report = pool.GetDiagnosticReport();
Return value
A formatted diagnostic string.
Example
string report = pool.GetDiagnosticReport();
// Output:
// === SegmentedPool Diagnostics ===
// Configuration
// Segment Alignment: 32 bytes (Min Base Alignment)
// Segment Size: 4 MiB
// Segment Summary
// Total Segments: 5 (4 active, 1 free)
// Potential Savings: 0 (via Trim())
// Current Segment
// Base Address: 0x1a2b3c4d
// Offset: 1048576 bytes
// Base Alignment: OK (To 32 bytes)
// Memory Statistics
// Total Reserved: 16 MiB
// Total Used: 8 MiB
// Efficiency: 80% (Good)
// Padding Overhead: 2 MiB
// Allocation Breakdown
// Data Bytes: 8 MiB
// Alignment Padding: 2 MiB
// Total Segment Space: 10 MiB
// Action Required: Pool operating normally.
GetCurrentSegmentInfo
Gets information about the currently active segment. This is the primary diagnostic view for memory usage within the active segment.
Syntax
SegmentedPool.SegmentInfo info = pool.GetCurrentSegmentInfo();
Return value
A SegmentedPool.SegmentInfo struct for the current segment.
GetAllSegmentInfos
Gets a list of all segment details for deep diagnostics. Includes both active and free segments.
Syntax
List<SegmentedPool.SegmentInfo> allSegments = pool.GetAllSegmentInfos();
Return value
A list of SegmentedPool.SegmentInfo structs containing all segments.
Dispose
Releases all unmanaged memory used by the pool and clears internal state.
Syntax
pool.Dispose();
Parameters
None.
Return value
None.
Remarks
This method frees all active and free Segments so that after disposal, any memory allocated from the pool is invalid. This method is safe to call multiple times and calling any other public method after disposal is also safe.
After disposal, all allocations become invalid and any further operations will throw ObjectDisposedException.