2 Commits

Author SHA1 Message Date
stdranges
b1a58ff77f fix: add solution as argument 2025-11-24 20:54:59 +00:00
stdranges
9b9184f03e feature: add dotnet.yaml 2025-11-24 20:50:13 +00:00
16 changed files with 725 additions and 2816 deletions

23
.github/workflows/dotnet.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
name: .NET
on: push
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore UnmangedMMU/UnmanagedMMU.sln
- name: Build
run: dotnet build UnmangedMMU/UnmanagedMMU.sln --no-restore
- name: Unit Testing
run: dotnet test UnmangedMMU/UnmanagedMMU.sln --no-build --verbosity normal

637
README.md
View File

@@ -1,103 +1,27 @@
# UnmanagedMMU <!-- omit from toc --> # UnmanagedMMU
UnmanagedMMU is a high-performance C# memory manager library that provides efficient unmanaged memory allocation. UnmanagedMMU is a high-performance C# memory manager library that provides efficient unmanaged memory allocation.
--- ---
## Table of Contents <!-- omit from toc --> ## Table of Contents
- [IMemoryHandle](#imemoryhandle) 1. [SegmentedPool](#segmentedpool)
- [IMemoryHandle (Untyped)](#imemoryhandle-untyped) - [Segments](#segments)
- [IMemoryHandle\<T\> (Typed)](#imemoryhandlet-typed) - [Allocation Strategy](#allocation-strategy)
- [SegmentedPool](#segmentedpool) - [Constructor](#segmentedpool-constructor)
- [Segments](#segments) - [Allocate](#segmentedpool-allocate)
- [SegmentAlignment](#segmentalignment) - [SetSegmentSize](#segmentedpool-set-segment-size)
- [Allocation Strategy](#allocation-strategy) - [ResetSegmentSize](#segmentedpool-reset-segment-size)
- [Constructor](#constructor) - [Reset](#segmentedpool-reset)
- [Allocation Methods](#allocation-methods) - [Trim](#segmentedpool-trim)
- [Allocate](#allocate) - [Dispose](#segmentedpool-dispose)
- [AllocateAligned](#allocatealigned)
- [Pool State Properties](#pool-state-properties)
- [CurrentSegmentSize](#currentsegmentsize)
- [TotalAllocatedBytes](#totalallocatedbytes)
- [TotalUsedBytes](#totalusedbytes)
- [ActiveSegmentCount](#activesegmentcount)
- [FreeSegmentCount](#freesegmentcount)
- [IsDisposed](#isdisposed)
- [Segment Size Management](#segment-size-management)
- [SetSegmentSize](#setsegmentsize)
- [ResetSegmentSize](#resetsegmentsize)
- [Reset and Trim](#reset-and-trim)
- [Reset](#reset)
- [Trim](#trim)
- [Diagnostics](#diagnostics)
- [GetPoolState](#getpoolstate)
- [GetDiagnosticReport](#getdiagnosticreport)
- [GetCurrentSegmentInfo](#getcurrentsegmentinfo)
- [GetAllSegmentInfos](#getallsegmentinfos)
- [Dispose](#dispose)
--- ---
## IMemoryHandle ## SegmentedPool
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<T> extends the non-generic interface to provide strong typing and implements IDisposable for automatic resource management. A `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.
---
### IMemoryHandle (Untyped)<a id="IMemoryHandle"></a>
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)<a id="IMemoryHandleT"></a>
The **IMemoryHandle\<T\>** interface extends [IMemoryHandle](#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:**
```csharp
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<a id="segmentedpool"></a>
`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. Once a segment is full, the pool automatically switches to a new segment, allowing fast, contiguous allocations without fragmentation.
Advantages of the `SegmentedPool`: Advantages of the `SegmentedPool`:
@@ -106,7 +30,6 @@ Advantages of the `SegmentedPool`:
- **Contiguous memory**: Reduces cache misses and improves data locality. - **Contiguous memory**: Reduces cache misses and improves data locality.
- **Thread-safe**: Supports concurrent allocation operations. - **Thread-safe**: Supports concurrent allocation operations.
- **Manual memory control**: Works outside of .NET GC, ideal for high-performance or low-latency scenarios. - **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.
--- ---
@@ -114,32 +37,16 @@ Advantages of the `SegmentedPool`:
A **Segment** is a contiguous block of unmanaged memory managed by the pool. Each segment contains: A **Segment** is a contiguous block of unmanaged memory managed by the pool. Each segment contains:
| Field | Type | Description | | Field | Type | Description |
| -------- | ------- | ------------------------------------------------------------------------------- | |-----------|-------|-------------|
| `Ptr` | `byte*` | Pointer to the start of the unmanaged memory block. | | `Ptr` | `byte*` | Pointer to the start of the unmanaged memory block. |
| `Offset` | `nuint` | Current allocation offset within the segment. Increases as memory is allocated. | | `Offset` | `nuint` | Current allocation offset within the segment. Increases as memory is allocated. |
| `Size` | `nuint` | Total size of the segment in bytes. | | `Size` | `nuint` | Total size of the segment in bytes. |
Segments are allocated automatically by the pool and should **never be modified outside the pool**. Segments are allocated automatically by the pool and should **never be modified outside the pool**.
--- ---
### SegmentAlignment<a id="segmentalignment"></a>
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 ### Allocation Strategy
The `SegmentedPool` uses a **bump allocator** strategy: The `SegmentedPool` uses a **bump allocator** strategy:
@@ -147,552 +54,184 @@ The `SegmentedPool` uses a **bump allocator** strategy:
1. Memory is allocated sequentially within the current [Segments](#segments). 1. Memory is allocated sequentially within the current [Segments](#segments).
2. `Offset` is incremented with each allocation. 2. `Offset` is incremented with each allocation.
3. When the current segment does not have enough space, the pool switches to a new [Segments](#segments) (either from the free pool or a freshly allocated one). 3. When the current segment does not have enough space, the pool switches to a new [Segments](#segments) (either from the free pool or a freshly allocated one).
4. Allocations are aligned according to the [SegmentAlignment](#segmentalignment) or a requested alignment.
This provides **O(1) allocation performance** for most operations. This provides **O(1) allocation performance** for most operations.
--- ---
### Constructor<a id="segmentedpool-constructor"></a> ### Constructor <a name="segmentedpool-constructor"></a>
Initializes a new instance of the SegmentedPool with the specified segment size, alignment, and number of pre-allocated [Segments](#segments). Initializes a new instance of the SegmentedPool with the specified segment size and number of pre-allocated [Segments](#segments).
#### Syntax <!-- omit from toc --> #### Syntax
```csharp ```csharp
// Default configuration SegmentedPool pool = new SegmentedPool(segmentSize: 4 * 1024 * 1024, initialSegments: 4);
SegmentedPool pool = new SegmentedPool();
// Custom parameters
SegmentedPool pool = new SegmentedPool(
segmentSize: 4 * 1024 * 1024,
segmentAlignment: SegmentAlignment.Aligned32,
initialSegments: 4,
zeroMemory: false
);
``` ```
#### Parameters<!-- omit from toc --> #### Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------- | ------- | ----------------------------------------------------------------------------------- |
| `segmentSize` | `nuint` | Size of each [Segment](#segments) in bytes. Optional. Defaults to 4 MiB (4 * 1024 * 1024 bytes). | | `segmentSize` | `nuint` | Size of each [Segment](#segments) in bytes. Optional. Defaults to 4 MiB (4 *1024* 1024 bytes). |
| `segmentAlignment` | `SegmentAlignment` | [Alignment](#segmentalignment) requirement for each allocated [Segment](#segments). Must be a power of 2. Optional. Defaults to **Aligned32**. | | `initialSegments` | `int` | Number of [Segments](#segments) to pre-allocate in the pool. Optional. Defaults to 4. |
| `initialSegments` | `int` | Number of [Segments](#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<!-- omit from toc --> #### Return value
Returns a new instance of the SegmentedPool with the specified [Segments](#segments) size and number of pre-allocated [Segments](#segments). Returns a new instance of the SegmentedPool with the specified [Segments](#segments) size and number of pre-allocated [Segments](#segments).
#### Exceptions<!-- omit from toc --> #### Remarks
| 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<!-- omit from toc -->
The SegmentedPool pre-allocates the specified number of [Segments](#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. The SegmentedPool pre-allocates the specified number of [Segments](#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. ### Allocate <a name="segmentedpool-allocate"></a>
Allocates a span of unmanaged memory for elements of type T from the pool.
#### Syntax
```csharp
Span<T> buffer = 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
A `Span<T>` representing the allocated memory. The span is valid until the pool is reset or disposed.
#### Remarks
Accessing the memory after calling Reset() or Dispose() is undefined behavior and may lead to crashes.
--- ---
### Allocation Methods<a id="segmentedpool-allocation-methods"></a> ### SetSegmentSize <a name="segmentedpool-set-segment-size"></a>
The SegmentedPool provides two allocation methods, both returning [IMemoryHandle\<T\>](#IMemoryHandleT) for safe memory management.
#### Allocate<a id="segmentedpool-allocate"></a>
Allocates a block of unmanaged memory of the specified count for elements of type `T` using the pool's default segment alignment.
##### Syntax<!-- omit from toc -->
```csharp
IMemoryHandle<T> handle = pool.Allocate<T>(count);
```
##### Parameters<!-- omit from toc -->
| Parameter | Type | Description |
| --------- | ----- | ---------------------------------------------------------------------- |
| `count` | `int` | Number of elements of type `T` to allocate. Must be greater than zero. |
##### Return value<!-- omit from toc -->
An [IMemoryHandle\<T\>](#IMemoryHandleT) representing the allocated memory. The handle is valid until either [Reset](#segmentedpool-reset) or [Dispose](#segmentedpool-dispose) is called on the pool.
##### Remarks<!-- omit from toc -->
- This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.
- Accessing the memory after [Reset](#segmentedpool-reset) or [Dispose](#segmentedpool-dispose) has been called is undefined behavior.
- Memory alignment is based on the pool's configured [SegmentAlignment](#segmentalignment).
##### Exceptions<!-- omit from toc -->
| 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<!-- omit from toc -->
```csharp
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<a id="segmentedpool-allocatealigned"></a>
Allocates a block of unmanaged memory with the specified alignment requirement. This is useful for SIMD or hardware-specific operations.
##### Syntax<!-- omit from toc -->
```csharp
IMemoryHandle<T> handle = pool.AllocateAligned<T>(count, alignment);
```
##### Parameters<!-- omit from toc -->
| Parameter | Type | Description |
| ----------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
| `count` | `int` | Number of elements of type `T` to allocate. Must be greater than zero. |
| `alignment` | `SegmentAlignment` | The [alignment](#segmentalignment) to align the allocation to inside the currently active [Segment](#segments). |
##### Return value<!-- omit from toc -->
An [IMemoryHandle\<T\>](#IMemoryHandleT) representing the allocated memory. The handle is valid until either [Reset](#segmentedpool-reset) or [Dispose](#segmentedpool-dispose) is called on the pool.
##### Remarks<!-- omit from toc -->
- This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.
- Accessing the memory after [Reset](#segmentedpool-reset) or [Dispose](#segmentedpool-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<!-- omit from toc -->
| 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<!-- omit from toc -->
```csharp
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<a id="segmentedpool-pool-state-properties"></a>
The SegmentedPool provides several read-only properties for monitoring pool state:
#### CurrentSegmentSize<a id="segmentedpool-currentsegmentsize"></a>
Gets the size, in bytes, used when allocating new [Segment](#segments) instances.
##### Syntax<!-- omit from toc -->
```csharp
nuint size = pool.CurrentSegmentSize;
```
##### Return value<!-- omit from toc -->
The size, in bytes, used when allocating new [Segment](#segments).
##### Remarks<!-- omit from toc -->
This reflects the most recently configured size and affects only future [segment](#segments) allocations.
---
#### TotalAllocatedBytes<a id="segmentedpool-totalallocatedbytes"></a>
Gets the total number of bytes that have currently been allocated from the system.
##### Syntax<!-- omit from toc -->
```csharp
nuint allocated = pool.TotalAllocatedBytes;
```
##### Return value<!-- omit from toc -->
The total number of bytes allocated from the system.
##### Remarks<!-- omit from toc -->
This includes memory for all active and free [Segments](#segments).
---
#### TotalUsedBytes<a id="segmentedpool-totalusedbytes"></a>
Gets the total number of bytes currently in use across all active [segments](#segments).
##### Syntax<!-- omit from toc -->
```csharp
nuint used = pool.TotalUsedBytes;
```
##### Return value<!-- omit from toc -->
The total number of bytes currently in use.
##### Remarks<!-- omit from toc -->
This represents the actual data bytes allocated, excluding alignment padding.
#### ActiveSegmentCount<a id="segmentedpool-activesegmentcount"></a>
Gets the number of [segments](#segments) currently in use in the pool.
##### Syntax<!-- omit from toc -->
```csharp
int count = pool.ActiveSegmentCount;
```
##### Return value<!-- omit from toc -->
The number of [segments](#segments) that are currently active and not yet returned to the free pool.
##### Remarks<!-- omit from toc -->
This property is thread-safe and reflects the current state of the pool.
---
#### FreeSegmentCount<a id="segmentedpool-freesegmentcount"></a>
Gets the number of free [segments](#segments) available for reuse in the pool.
##### Syntax<!-- omit from toc -->
```csharp
int count = pool.FreeSegmentCount;
```
##### Return value<!-- omit from toc -->
The number of [segments](#segments) that are currently free and available for reuse.
##### Remarks<!-- omit from toc -->
This property is thread-safe. A value of 0 indicates that the pool will need to allocate a new [segment](#segments) for the next allocation.
---
#### IsDisposed<a id="segmentedpool-isDisposed"></a>
Gets a value indicating whether this instance has been disposed.
##### Syntax<!-- omit from toc -->
```csharp
bool isDisposed = pool.IsDisposed;
```
##### Return value<!-- omit from toc -->
`true` if the pool has been disposed; otherwise `false`.
##### Remarks<!-- omit from toc -->
Once this value is true, any further calls to allocation or management methods will throw a `ObjectDisposedException`.
---
### Segment Size Management<a id="segmentedpool-segment-size-management"></a>
#### SetSegmentSize<a id="segmentedpool-setsegmentsize"></a>
Sets the [Segment](#segments) size to use for future allocations. Sets the [Segment](#segments) size to use for future allocations.
##### Syntax<!-- omit from toc --> #### Syntax
```csharp ```csharp
pool.SetSegmentSize(newSize); pool.SetSegmentSize(newSize);
``` ```
##### Parameters<!-- omit from toc --> #### Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ----- | ------------------------------------------- | | --------- | ----- | ------------------------------------------- |
| `newSize` | nuint | The new segment size in bytes. Must be > 0. | | `newSize` | nuint | The new segment size in bytes. Must be > 0. |
##### Return value<!-- omit from toc --> #### Return value
None. None.
##### Remarks<!-- omit from toc --> #### Remarks
This method will only affect the [Segment](#segments) size of future allocations; it does not modify existing [Segments](#segments). This method will only affect the [Segment](#segments) size of future allocations; it does not modify existing [Segments](#segments).
##### Exceptions<!-- omit from toc -->
| Exception | Condition |
| ----------------------------- | ------------------------------------- |
| `ArgumentOutOfRangeException` | Thrown if `newSize` is zero. |
| `ObjectDisposedException` | Thrown if the pool has been disposed. |
--- ---
#### ResetSegmentSize<a id="segmentedpool-resetsegmentsize"></a> ### ResetSegmentSize <a name="segmentedpool-reset-segment-size"></a>
Resets the [Segment](#segments) size for future allocations back to the default (4 MiB). Resets the [Segment](#segments) size for future allocations back to the default (4 MiB).
##### Syntax<!-- omit from toc --> #### Syntax
```csharp ```csharp
pool.ResetSegmentSize(); pool.ResetSegmentSize();
``` ```
##### Parameters<!-- omit from toc --> #### Parameters
None. None.
##### Return value<!-- omit from toc --> #### Return value
None. None.
##### Remarks<!-- omit from toc --> #### Remarks
This method will only affect the [Segment](#segments) size of future allocations; it does not modify existing [Segments](#segments). This method will only affect the [Segment](#segments) size of future allocations; it does not modify existing [Segments](#segments).
--- ---
### Reset and Trim<a id="segmentedpool-reset-and-trim"></a> ### Reset <a name="segmentedpool-reset"></a>
#### Reset<a id="segmentedpool-reset"></a>
Resets the pool, returning all active [Segments](#segments) to the free pool for reuse. Resets the pool, returning all active [Segments](#segments) to the free pool for reuse.
##### Syntax<!-- omit from toc --> #### Syntax
```csharp ```csharp
pool.Reset(trim: false); pool.Reset(trim: false);
``` ```
##### Parameters<!-- omit from toc --> #### Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ------ | ----------------------------------------------------------------------------------------- | | ----------- | ------- | ----------------------------------------------------------------------------- |
| `trim` | `bool` | Optional. If true, trims excess free [Segments](#segments) after reset. Defaults to false | | `trim` | `bool` | Optional. If true, trims excess free [Segments](#segments) after reset. Defaults to false |
##### Return value<!-- omit from toc --> #### Return value
None. None.
##### Remarks<!-- omit from toc --> #### Remarks
This method resets all active [Segments](#segments) offsets to zero. Additionally, no memory is freed unless trim is true; the pool retains free [Segments](#segments) for future allocations. This method resets all active [Segments](#segments) offsets to zero. Additionally, no memory is freed unless trim is true; the pool retains free [Segments](#segments) for future allocations.
--- ---
#### Trim<a id="segmentedpool-trim"></a> ### Trim <a name="segmentedpool-trim"></a>
Frees unused [Segments](#segments) in the free pool, reducing memory usage. Frees unused [Segments](#segments) in the free pool, reducing memory usage.
##### Syntax<!-- omit from toc --> #### Syntax
```csharp ```csharp
pool.Trim(minFreeSegments: 16); pool.Trim(minFreeSegments: 16);
``` ```
##### Parameters<!-- omit from toc --> #### Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | | ----------- | ------- | ----------------------------------------------------------------------------- |
| `minFreeSegments` | `int` | Minimum number of free [Segments](#segments) to retain. [Segments](#segments) beyond this count are released. Defaults to 16. | | `minFreeSegments` | `int` | Minimum number of free [Segments](#segments) to retain. [Segments](#segments) beyond this count are released. Defaults to 16. |
##### Return value<!-- omit from toc --> #### Return value
None. None.
##### Remarks<!-- omit from toc --> #### Remarks
This method releases unmanaged memory from [Segments](#segments) beyond the specified minimum. This is useful for helping to control the unmanaged memory footprint in long-running applications. This method releases unmanaged memory for excess [Segments](#segments) beyond the specified minimum. This is useful for helping to controlthe unmanaged memory footprint in long-running applications.
##### Exceptions<!-- omit from toc -->
| Exception | Condition |
| ----------------------------- | ---------------------------------------- |
| `ArgumentOutOfRangeException` | Thrown if `minFreeSegments` is negative. |
| `ObjectDisposedException` | Thrown if the pool has been disposed. |
--- ---
### Diagnostics<a id="segmentedpool-diagnostics"></a> ### Dispose <a name="segmentedpool-dispose"></a>
The SegmentedPool provides several diagnostic methods for monitoring pool health and efficiency.
#### GetPoolState<a id="segmentedpool-getpoolstate"></a>
Gets a snapshot of the current pool state for diagnostics.
##### Syntax<!-- omit from toc -->
```csharp
SegmentedPool.PoolState state = pool.GetPoolState();
```
##### Return value<!-- omit from toc -->
A `SegmentedPool.PoolState` struct containing current pool metrics.
##### Example<!-- omit from toc -->
```csharp
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<a id="segmentedpool-getdiagnosticreport"></a>
Generates a diagnostic report for the pool including actionable suggestions.
##### Syntax<!-- omit from toc -->
```csharp
string report = pool.GetDiagnosticReport();
```
##### Return value<!-- omit from toc -->
A formatted diagnostic string.
##### Example<!-- omit from toc -->
```csharp
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<a id="segmentedpool-getcurrentsegmentinfo"></a>
Gets information about the currently active segment. This is the primary diagnostic view for memory usage within the active segment.
##### Syntax<!-- omit from toc -->
```csharp
SegmentedPool.SegmentInfo info = pool.GetCurrentSegmentInfo();
```
##### Return value<!-- omit from toc -->
A `SegmentedPool.SegmentInfo` struct for the current segment.
---
#### GetAllSegmentInfos<a id="segmentedpool-getallsegmentinfos"></a>
Gets a list of all segment details for deep diagnostics. Includes both active and free segments.
##### Syntax<!-- omit from toc -->
```csharp
List<SegmentedPool.SegmentInfo> allSegments = pool.GetAllSegmentInfos();
```
##### Return value<!-- omit from toc -->
A list of `SegmentedPool.SegmentInfo` structs containing all segments.
---
### Dispose<a id="segmentedpool-dispose"></a>
Releases all unmanaged memory used by the pool and clears internal state. Releases all unmanaged memory used by the pool and clears internal state.
#### Syntax<!-- omit from toc --> #### Syntax
```csharp ```csharp
pool.Dispose(); pool.Dispose();
``` ```
#### Parameters<!-- omit from toc --> #### Parameters
None. None.
#### Return value<!-- omit from toc --> #### Return value
None. None.
#### Remarks<!-- omit from toc --> #### Remarks
This method frees all active and free [Segments](#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. This method frees all active and free [Segments](#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](https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception).

File diff suppressed because it is too large Load Diff

View File

@@ -3,36 +3,14 @@
namespace UnmanagedMMU.Allocators namespace UnmanagedMMU.Allocators
{ {
/// <summary> /// <summary>
/// Wrapper class around <see cref="NativeMemory.AlignedAlloc(nuint, nuint)"/> and <see cref="NativeMemory.AlignedFree(void*)"/>. /// Wrapper class around <see cref="NativeMemory.Alloc(nuint)"/> and <see cref="NativeMemory.Free(void*)"/>.
/// </summary> /// </summary>
internal sealed unsafe class DefaultUnmanagedAllocator : IUnmanagedAllocator internal sealed unsafe class DefaultUnmanagedAllocator : IUnmanagedAllocator
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void* Alloc(nuint size) public void* Alloc(nuint size) => NativeMemory.Alloc(size);
{
return NativeMemory.AlignedAlloc(size, 16);
}
/// <inheritdoc/> /// <inheritdoc/>
public void* AllocAligned(nuint size, nuint alignment) public void Free(void* ptr) => NativeMemory.Free(ptr);
{
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);
}
} }
} }

View File

@@ -1,11 +1,9 @@
using System.Runtime.InteropServices; namespace UnmanagedMMU.Allocators
namespace UnmanagedMMU.Allocators
{ {
/// <summary> /// <summary>
/// Interface that defines an Unmanaged allocator /// Interface that defines an Unmanaged allocator
/// </summary> /// </summary>
public unsafe interface IUnmanagedAllocator internal unsafe interface IUnmanagedAllocator
{ {
/// <summary> /// <summary>
/// Allocates an unmanaged memory block of the specified size. /// Allocates an unmanaged memory block of the specified size.
@@ -16,28 +14,10 @@ namespace UnmanagedMMU.Allocators
/// </returns> /// </returns>
void* Alloc(nuint size); 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> /// <summary>
/// Frees a previously allocated unmanaged memory block. /// Frees a previously allocated unmanaged memory block.
/// </summary> /// </summary>
/// <param name="ptr">A pointer to the beginning of the memory block to free.</param> /// <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); 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);
} }
} }

View File

@@ -1,16 +0,0 @@
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);
}
}

View File

@@ -1,170 +0,0 @@
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; }
}
}

View File

@@ -1,27 +0,0 @@
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; }
}
}

View File

@@ -1,34 +0,0 @@
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() { }
}
}

View File

@@ -1,15 +0,0 @@
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();
}
}

View File

@@ -1,126 +0,0 @@
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;
}
}
}

View File

@@ -1,21 +0,0 @@
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
}
}
}
}

View File

@@ -1,21 +0,0 @@
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;
}
}
}

View File

@@ -2,50 +2,9 @@
{ {
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using UnmanagedMMU.Allocators; 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> /// <summary>
/// Implementation of segmented Bump-Allocator. /// Implementation of segmented Bump-Allocator.
@@ -54,7 +13,7 @@
/// This ensures allocations are fast and contiguous within a segment. /// This ensures allocations are fast and contiguous within a segment.
/// Once a segment is full, a new one is automatically allocated /// Once a segment is full, a new one is automatically allocated
/// </summary> /// </summary>
public unsafe sealed class SegmentedPool : IDisposable, IUnmanagedMemoryOwner public unsafe sealed class SegmentedPool : IDisposable
{ {
/// <summary> /// <summary>
/// The default size for a <see cref="Segment"/> /// The default size for a <see cref="Segment"/>
@@ -66,11 +25,6 @@
/// </summary> /// </summary>
private nuint _currentSegmentSize; private nuint _currentSegmentSize;
/// <summary>
/// The alignment that each new <see cref="Segment"/> should be on
/// </summary>
private readonly nuint _segmentAlignment;
/// <summary> /// <summary>
/// Queue of free segments /// Queue of free segments
/// </summary> /// </summary>
@@ -82,12 +36,12 @@
private readonly List<IntPtr> _activeSegments = []; private readonly List<IntPtr> _activeSegments = [];
/// <summary> /// <summary>
/// Tracks the total bytes of memory reserved from the provided <see cref="IUnmanagedAllocator"/> /// Tracks the total amount of allocated bytes
/// </summary> /// </summary>
private nuint _totalReserved = 0; private nuint _totalAllocated = 0;
/// <summary> /// <summary>
/// Tracks the total amount of allocated bytes currently in use /// Tracks the total amount of allocated bytes current in use
/// </summary> /// </summary>
private nuint _totalUsed = 0; private nuint _totalUsed = 0;
@@ -97,7 +51,7 @@
private readonly Lock _lock = new(); private readonly Lock _lock = new();
/// <summary> /// <summary>
/// Indicates whether this <see cref="SegmentedPool"/> has been disposed. /// Indicates whether the <see cref="SegmentedPool"/> has been disposed.
/// </summary> /// </summary>
private volatile bool _disposed; private volatile bool _disposed;
@@ -106,16 +60,8 @@
/// </summary> /// </summary>
private Segment* _current; private Segment* _current;
/// <summary>
/// Allocator interface used for all underlying unmanaged memory operations.
/// </summary>
private readonly IUnmanagedAllocator _allocator; private readonly IUnmanagedAllocator _allocator;
/// <summary>
/// Indicates if allocations from the pool should be zeroed.
/// </summary>
private readonly bool _zeroMemory;
/// <summary> /// <summary>
/// Represents a memory segment in the <see cref="SegmentedPool"/>. /// Represents a memory segment in the <see cref="SegmentedPool"/>.
/// </summary> /// </summary>
@@ -147,152 +93,16 @@
} }
/// <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> /// <summary>
/// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count /// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count
/// </summary> /// </summary>
/// <param name="segmentSize">Size of each segment in bytes (default 4 MiB)</param> /// <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="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"> /// <exception cref="ArgumentException">
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1. /// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
/// </exception> /// </exception>
public SegmentedPool(nuint segmentSize = _defaultSegmentSize, SegmentAlignment segmentAlignment = SegmentAlignment.Aligned32, int initialSegments = 4, bool zeroMemory = false) public SegmentedPool(nuint segmentSize = _defaultSegmentSize, int initialSegments = 4)
: this(segmentSize, segmentAlignment, initialSegments, zeroMemory, new DefaultUnmanagedAllocator()) : this(segmentSize, initialSegments, new DefaultUnmanagedAllocator())
{ {
} }
@@ -300,30 +110,28 @@
/// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count /// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count
/// </summary> /// </summary>
/// <param name="segmentSize">Size of each segment in bytes (default 4 MiB)</param> /// <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="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> /// <param name="allocator">IUnmanagedAllocator instance that implements the allocator</param>
/// <exception cref="ArgumentException"> /// <exception cref="ArgumentException">
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1. /// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
/// </exception> /// </exception>
internal SegmentedPool(nuint segmentSize, SegmentAlignment segmentAlignment, int initialSegments, bool zeroMemory, IUnmanagedAllocator allocator) internal SegmentedPool(nuint segmentSize, int initialSegments, IUnmanagedAllocator allocator)
{ {
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(segmentSize); if (segmentSize == 0)
ArgumentOutOfRangeException.ThrowIfLessThan(initialSegments, 1); {
throw new ArgumentException("Segment size must be greater than zero.", nameof(segmentSize));
}
if (initialSegments < 1)
{
throw new ArgumentException("Initial segments count must be at least 1.", nameof(initialSegments));
}
_allocator = allocator; _allocator = allocator;
_currentSegmentSize = segmentSize; _currentSegmentSize = segmentSize;
_segmentAlignment = (nuint)segmentAlignment;
_zeroMemory = zeroMemory;
Segment* seg;
// Pre-allocate segments // Pre-allocate segments
for (int i = 0; i < initialSegments; i++) for (int i = 0; i < initialSegments; i++)
{ {
seg = AllocateNewSegment(_currentSegmentSize); _freeSegments.Push((IntPtr)AllocateNewSegment(_currentSegmentSize));
ZeroSegment(seg);
_freeSegments.Push((IntPtr)seg);
} }
@@ -337,13 +145,7 @@
/// <returns>The number of free segments.</returns> /// <returns>The number of free segments.</returns>
public int FreeSegmentCount public int FreeSegmentCount
{ {
get get { return _freeSegments.Count; }
{
lock (_lock)
{
return _freeSegments.Count;
}
}
} }
/// <summary> /// <summary>
@@ -352,13 +154,7 @@
/// <returns>The number of currently active Segments.</returns> /// <returns>The number of currently active Segments.</returns>
public int ActiveSegmentCount public int ActiveSegmentCount
{ {
get get { return _activeSegments.Count; }
{
lock (_lock)
{
return _activeSegments.Count;
}
}
} }
/// <summary> /// <summary>
@@ -367,13 +163,7 @@
/// <returns>The total number of bytes that have been allocated.</returns> /// <returns>The total number of bytes that have been allocated.</returns>
public nuint TotalAllocatedBytes public nuint TotalAllocatedBytes
{ {
get get { return _totalAllocated; }
{
lock (_lock)
{
return _totalReserved;
}
}
} }
/// <summary> /// <summary>
@@ -382,13 +172,7 @@
/// <returns>The total number of bytes that are in use.</returns> /// <returns>The total number of bytes that are in use.</returns>
public nuint TotalUsedBytes public nuint TotalUsedBytes
{ {
get get { return _totalUsed; }
{
lock (_lock)
{
return _totalUsed;
}
}
} }
/// <summary> /// <summary>
@@ -414,153 +198,6 @@
get { return _disposed; } 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> /// <summary>
/// Sets the current <see cref="Segment"/> size used for subsequent allocations. /// Sets the current <see cref="Segment"/> size used for subsequent allocations.
/// </summary> /// </summary>
@@ -599,15 +236,15 @@
} }
/// <summary> /// <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. /// Allocates a span of unmanaged memory of size <paramref name="count"/> for elements of type <typeparamref name="T"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam> /// <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="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> /// <returns>A <see cref="Span{T}"/> representing the allocated memory. The span is valid until the <see cref="SegmentedPool"/> is reset or disposed.</returns>
/// <remarks> /// <remarks>
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.</description></item> /// <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> /// <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> /// </list>
/// </remarks> /// </remarks>
/// <exception cref="ArgumentOutOfRangeException"> /// <exception cref="ArgumentOutOfRangeException">
@@ -616,43 +253,73 @@
/// <exception cref="OverflowException"> /// <exception cref="OverflowException">
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size. /// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
/// </exception> /// </exception>
public IMemoryHandle<T> Allocate<T>(int count) where T : unmanaged public Span<T> Allocate<T>(int count) where T : unmanaged
{ {
T* ptr = Alloc<T>(count); ThrowIfDisposed();
nuint byteLength = (nuint)count * (nuint)sizeof(T); if (count <= 0)
return new SegmentedMemoryHandle<T>(ptr, byteLength, this); {
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);
}
} }
/// <summary> /// <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. /// Switches to a new <see cref="Segment"/> when the current <see cref="Segment"/> is full.
/// </summary> /// </summary>
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam> /// <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>
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param> private void SwitchSegment(nuint requiredBytes)
/// <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; Segment* segment;
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); // Allocate fresh Segment if needed
nuint byteLength = (nuint)count * (nuint)sizeof(T); if (_freeSegments.Count == 0 || requiredBytes > _currentSegmentSize)
return new SegmentedMemoryHandle<T>(ptr, byteLength, this); {
segment = AllocateNewSegment(requiredBytes > _currentSegmentSize ? requiredBytes : _currentSegmentSize);
}
else
{
segment = (Segment*)_freeSegments.Pop();
segment->Offset = 0;
}
_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> /// <summary>
@@ -677,9 +344,9 @@
Segment* segment = (Segment*)ip; Segment* segment = (Segment*)ip;
// Free the unmanaged memory // Free the unmanaged memory
_allocator.FreeAligned(segment->Ptr, _segmentAlignment); _allocator.Free(segment->Ptr);
_totalReserved -= segment->Size; _totalAllocated -= segment->Size;
_allocator.FreeAligned(segment, 8); _allocator.Free(segment);
} }
} }
} }
@@ -698,9 +365,6 @@
Segment* segment = (Segment*)ip; Segment* segment = (Segment*)ip;
segment->Offset = 0; segment->Offset = 0;
_freeSegments.Push(ip); _freeSegments.Push(ip);
// Zero memory if requested
ZeroSegment(segment);
} }
_activeSegments.Clear(); _activeSegments.Clear();
@@ -716,10 +380,7 @@
// This should not be hit in normal circumstances as we always have _current // This should not be hit in normal circumstances as we always have _current
_current = AllocateNewSegment(_currentSegmentSize); _current = AllocateNewSegment(_currentSegmentSize);
_activeSegments.Add((IntPtr)_current); _activeSegments.Add((IntPtr)_current);
// Zero newly allocated segment if requested
ZeroSegment(_current);
} }
// Optionally trim excess free segments after reset // Optionally trim excess free segments after reset
if (trim) if (trim)
{ {
@@ -728,27 +389,6 @@
} }
} }
/// <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> /// <summary>
/// Releases all unmanaged memory allocated by the <see cref="SegmentedPool"/> and clears internal state. /// 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. /// After calling this method, the pool can no longer be used for allocations.
@@ -766,26 +406,26 @@
return; return;
} }
// Free active segments // Free active pages
foreach (var ip in _activeSegments) foreach (var ip in _activeSegments)
{ {
Segment* segment = (Segment*)ip; Segment* segment = (Segment*)ip;
_allocator.FreeAligned(segment->Ptr, _segmentAlignment); _allocator.Free(segment->Ptr);
_allocator.FreeAligned(segment, 8); _allocator.Free(segment);
} }
// Free free segments // Free free pages
foreach (var ip in _freeSegments) foreach (var ip in _freeSegments)
{ {
Segment* segment = (Segment*)ip; Segment* segment = (Segment*)ip;
_allocator.FreeAligned(segment->Ptr, _segmentAlignment); _allocator.Free(segment->Ptr);
_allocator.FreeAligned(segment, 8); _allocator.Free(segment);
} }
_activeSegments.Clear(); _activeSegments.Clear();
_freeSegments.Clear(); _freeSegments.Clear();
_current = null; _current = null;
_totalReserved = 0; _totalAllocated = 0;
_totalUsed = 0; _totalUsed = 0;
_disposed = true; _disposed = true;
} }
@@ -795,153 +435,11 @@
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="SegmentedPool"/> has already been disposed. /// Throws an <see cref="ObjectDisposedException"/> if the <see cref="SegmentedPool"/> has already been disposed.
/// </summary> /// </summary>
/// <exception cref="ObjectDisposedException"> /// <exception cref="ObjectDisposedException">
/// Thrown when this <see cref="SegmentedPool"/> instance is no longer valid for use. /// Thrown when this instance is no longer valid for use.
/// </exception> /// </exception>
private void ThrowIfDisposed() private void ThrowIfDisposed()
{ {
ObjectDisposedException.ThrowIf(_disposed, this); 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;
}
}
} }
} }

View File

@@ -5,9 +5,6 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Optional: Suppress warnings for undocumented members -->
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -1,516 +0,0 @@
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> (&gt; 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);
}
}
}