Compare commits
1 Commits
main
...
fix/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30af98df8c |
637
README.md
637
README.md
@@ -1,103 +1,27 @@
|
||||
# UnmanagedMMU <!-- omit from toc -->
|
||||
# UnmanagedMMU
|
||||
|
||||
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)
|
||||
- [IMemoryHandle (Untyped)](#imemoryhandle-untyped)
|
||||
- [IMemoryHandle\<T\> (Typed)](#imemoryhandlet-typed)
|
||||
- [SegmentedPool](#segmentedpool)
|
||||
- [Segments](#segments)
|
||||
- [SegmentAlignment](#segmentalignment)
|
||||
- [Allocation Strategy](#allocation-strategy)
|
||||
- [Constructor](#constructor)
|
||||
- [Allocation Methods](#allocation-methods)
|
||||
- [Allocate](#allocate)
|
||||
- [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)
|
||||
1. [SegmentedPool](#segmentedpool)
|
||||
- [Segments](#segments)
|
||||
- [Allocation Strategy](#allocation-strategy)
|
||||
- [Constructor](#segmentedpool-constructor)
|
||||
- [Allocate](#segmentedpool-allocate)
|
||||
- [SetSegmentSize](#segmentedpool-set-segment-size)
|
||||
- [ResetSegmentSize](#segmentedpool-reset-segment-size)
|
||||
- [Reset](#segmentedpool-reset)
|
||||
- [Trim](#segmentedpool-trim)
|
||||
- [Dispose](#segmentedpool-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.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
Advantages of the `SegmentedPool`:
|
||||
@@ -106,7 +30,6 @@ Advantages of the `SegmentedPool`:
|
||||
- **Contiguous memory**: Reduces cache misses and improves data locality.
|
||||
- **Thread-safe**: Supports concurrent allocation operations.
|
||||
- **Manual memory control**: Works outside of .NET GC, ideal for high-performance or low-latency scenarios.
|
||||
- **Configurable alignment**: Supports SIMD and hardware-specific alignment requirements.
|
||||
|
||||
---
|
||||
|
||||
@@ -114,32 +37,16 @@ Advantages of the `SegmentedPool`:
|
||||
|
||||
A **Segment** is a contiguous block of unmanaged memory managed by the pool. Each segment contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------- | ------- | ------------------------------------------------------------------------------- |
|
||||
| `Ptr` | `byte*` | Pointer to the start of the unmanaged memory block. |
|
||||
| `Offset` | `nuint` | Current allocation offset within the segment. Increases as memory is allocated. |
|
||||
| `Size` | `nuint` | Total size of the segment in bytes. |
|
||||
| Field | Type | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `Ptr` | `byte*` | Pointer to the start of the unmanaged memory block. |
|
||||
| `Offset` | `nuint` | Current allocation offset within the segment. Increases as memory is allocated. |
|
||||
| `Size` | `nuint` | Total size of the segment in bytes. |
|
||||
|
||||
Segments are allocated automatically by the pool and should **never be modified outside the pool**.
|
||||
|
||||
---
|
||||
|
||||
### SegmentAlignment<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
|
||||
|
||||
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).
|
||||
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).
|
||||
4. Allocations are aligned according to the [SegmentAlignment](#segmentalignment) or a requested alignment.
|
||||
|
||||
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
|
||||
// Default configuration
|
||||
SegmentedPool pool = new SegmentedPool();
|
||||
|
||||
// Custom parameters
|
||||
SegmentedPool pool = new SegmentedPool(
|
||||
segmentSize: 4 * 1024 * 1024,
|
||||
segmentAlignment: SegmentAlignment.Aligned32,
|
||||
initialSegments: 4,
|
||||
zeroMemory: false
|
||||
);
|
||||
SegmentedPool pool = new SegmentedPool(segmentSize: 4 * 1024 * 1024, initialSegments: 4);
|
||||
```
|
||||
|
||||
#### Parameters<!-- omit from toc -->
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| Parameter | Type | Description |
|
||||
| ----------------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `segmentSize` | `nuint` | Size of each [Segment](#segments) in bytes. Optional. Defaults to 4 MiB (4 *1024* 1024 bytes). |
|
||||
| `initialSegments` | `int` | Number of [Segments](#segments) to pre-allocate in the pool. Optional. Defaults to 4. |
|
||||
|
||||
#### 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).
|
||||
|
||||
#### Exceptions<!-- omit from toc -->
|
||||
|
||||
| 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 -->
|
||||
#### Remarks
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
### SetSegmentSize <a name="segmentedpool-set-segment-size"></a>
|
||||
|
||||
Sets the [Segment](#segments) size to use for future allocations.
|
||||
|
||||
##### Syntax<!-- omit from toc -->
|
||||
#### Syntax
|
||||
|
||||
```csharp
|
||||
pool.SetSegmentSize(newSize);
|
||||
```
|
||||
|
||||
##### Parameters<!-- omit from toc -->
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ----- | ------------------------------------------- |
|
||||
| `newSize` | nuint | The new segment size in bytes. Must be > 0. |
|
||||
|
||||
##### Return value<!-- omit from toc -->
|
||||
#### Return value
|
||||
|
||||
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).
|
||||
|
||||
##### 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).
|
||||
|
||||
##### Syntax<!-- omit from toc -->
|
||||
#### Syntax
|
||||
|
||||
```csharp
|
||||
pool.ResetSegmentSize();
|
||||
```
|
||||
|
||||
##### Parameters<!-- omit from toc -->
|
||||
#### Parameters
|
||||
|
||||
None.
|
||||
|
||||
##### Return value<!-- omit from toc -->
|
||||
#### Return value
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
### Reset and Trim<a id="segmentedpool-reset-and-trim"></a>
|
||||
|
||||
#### Reset<a id="segmentedpool-reset"></a>
|
||||
### Reset <a name="segmentedpool-reset"></a>
|
||||
|
||||
Resets the pool, returning all active [Segments](#segments) to the free pool for reuse.
|
||||
|
||||
##### Syntax<!-- omit from toc -->
|
||||
#### Syntax
|
||||
|
||||
```csharp
|
||||
pool.Reset(trim: false);
|
||||
```
|
||||
|
||||
##### Parameters<!-- omit from toc -->
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------ | ----------------------------------------------------------------------------------------- |
|
||||
| `trim` | `bool` | Optional. If true, trims excess free [Segments](#segments) after reset. Defaults to false |
|
||||
| Parameter | Type | Description |
|
||||
| ----------- | ------- | ----------------------------------------------------------------------------- |
|
||||
| `trim` | `bool` | Optional. If true, trims excess free [Segments](#segments) after reset. Defaults to false |
|
||||
|
||||
##### Return value<!-- omit from toc -->
|
||||
#### Return value
|
||||
|
||||
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.
|
||||
|
||||
##### Syntax<!-- omit from toc -->
|
||||
#### Syntax
|
||||
|
||||
```csharp
|
||||
pool.Trim(minFreeSegments: 16);
|
||||
```
|
||||
|
||||
##### Parameters<!-- omit from toc -->
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `minFreeSegments` | `int` | Minimum number of free [Segments](#segments) to retain. [Segments](#segments) beyond this count are released. Defaults to 16. |
|
||||
| Parameter | Type | Description |
|
||||
| ----------- | ------- | ----------------------------------------------------------------------------- |
|
||||
| `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.
|
||||
|
||||
##### 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.
|
||||
|
||||
##### Exceptions<!-- omit from toc -->
|
||||
|
||||
| Exception | Condition |
|
||||
| ----------------------------- | ---------------------------------------- |
|
||||
| `ArgumentOutOfRangeException` | Thrown if `minFreeSegments` is negative. |
|
||||
| `ObjectDisposedException` | Thrown if the pool has been disposed. |
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### Diagnostics<a id="segmentedpool-diagnostics"></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>
|
||||
### Dispose <a name="segmentedpool-dispose"></a>
|
||||
|
||||
Releases all unmanaged memory used by the pool and clears internal state.
|
||||
|
||||
#### Syntax<!-- omit from toc -->
|
||||
#### Syntax
|
||||
|
||||
```csharp
|
||||
pool.Dispose();
|
||||
```
|
||||
|
||||
#### Parameters<!-- omit from toc -->
|
||||
#### Parameters
|
||||
|
||||
None.
|
||||
|
||||
#### Return value<!-- omit from toc -->
|
||||
#### Return value
|
||||
|
||||
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.
|
||||
|
||||
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
@@ -3,36 +3,14 @@
|
||||
namespace UnmanagedMMU.Allocators
|
||||
{
|
||||
/// <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>
|
||||
internal sealed unsafe class DefaultUnmanagedAllocator : IUnmanagedAllocator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void* Alloc(nuint size)
|
||||
{
|
||||
return NativeMemory.AlignedAlloc(size, 16);
|
||||
}
|
||||
public void* Alloc(nuint size) => NativeMemory.Alloc(size);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void* AllocAligned(nuint size, nuint alignment)
|
||||
{
|
||||
if (!((alignment & (alignment - 1)) == 0 && alignment > 0))
|
||||
{
|
||||
throw new ArgumentException("Alignment must be a power of 2.", nameof(alignment));
|
||||
}
|
||||
return NativeMemory.AlignedAlloc(size, alignment);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Free(void* ptr)
|
||||
{
|
||||
NativeMemory.Free(ptr);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void FreeAligned(void* ptr, nuint alignment = 0)
|
||||
{
|
||||
NativeMemory.AlignedFree(ptr);
|
||||
}
|
||||
public void Free(void* ptr) => NativeMemory.Free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnmanagedMMU.Allocators
|
||||
namespace UnmanagedMMU.Allocators
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that defines an Unmanaged allocator
|
||||
/// </summary>
|
||||
public unsafe interface IUnmanagedAllocator
|
||||
internal unsafe interface IUnmanagedAllocator
|
||||
{
|
||||
/// <summary>
|
||||
/// Allocates an unmanaged memory block of the specified size.
|
||||
@@ -16,28 +14,10 @@ namespace UnmanagedMMU.Allocators
|
||||
/// </returns>
|
||||
void* Alloc(nuint size);
|
||||
|
||||
/// <summary>
|
||||
/// Allocates an unmanaged memory block of the specified size with the requested alignment
|
||||
/// </summary>
|
||||
/// <param name="size">The number of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment, in bytes, of the block to allocate. This must be a power of <c>2</c></param>
|
||||
/// <returns></returns>
|
||||
void* AllocAligned(nuint size, nuint alignment);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated unmanaged memory block.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the beginning of the memory block to free.</param>
|
||||
/// <remarks>This method should only be called on with pointers allocated with <see cref="Alloc"/>.</remarks>
|
||||
void Free(void* ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated unmanaged aligned memory block
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the beginning of the memory block to free.</param>
|
||||
/// <param name="alignment">The alignment that the memory refered to by <paramref name="ptr"/> was aligned at (This parameter can be ignored if the underlying allocator does not need it)</param>
|
||||
/// <remarks>This method should only be called on with pointers allocated with <see cref="AllocAligned"/>.</remarks>
|
||||
void FreeAligned(void* ptr, nuint alignment = 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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() { }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,50 +2,9 @@
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using UnmanagedMMU.Allocators;
|
||||
using UnmanagedMMU.Diagnostics;
|
||||
using UnmanagedMMU.Handles;
|
||||
using UnmanagedMMU.Handles.Internal;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents configurable alignment requirements for memory segments and allocations.
|
||||
/// Values are powers of 2 and reflect common hardware requirements (SIMD, cache lines, native pointer size).
|
||||
/// </summary>
|
||||
public enum SegmentAlignment
|
||||
{
|
||||
/// <summary>
|
||||
/// 8-byte alignment. Minimum for 64-bit pointers and primitives (long, double).
|
||||
/// </summary>
|
||||
Aligned8 = 8,
|
||||
|
||||
/// <summary>
|
||||
/// 16-byte alignment. Required for Vector128 (SSE/NEON).
|
||||
/// Common default for general-purpose SIMD workloads.
|
||||
/// </summary>
|
||||
Aligned16 = 16,
|
||||
|
||||
/// <summary>
|
||||
/// 32-byte alignment. Required for Vector256 (AVX).
|
||||
/// Recommended default for SIMD-heavy applications.
|
||||
/// </summary>
|
||||
Aligned32 = 32,
|
||||
|
||||
/// <summary>
|
||||
/// 64-byte alignment. Matches standard CPU cache-line size.
|
||||
/// Ensures segment bases align to cache line boundaries, minimizing cache-line splits.
|
||||
/// </summary>
|
||||
Aligned64 = 64,
|
||||
|
||||
/// <summary>
|
||||
/// 128-byte alignment.
|
||||
/// Advanced optimization for specific cache-aware algorithms or AVX-512 contexts.
|
||||
/// </summary>
|
||||
Aligned128 = 128
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of segmented Bump-Allocator.
|
||||
@@ -54,7 +13,7 @@
|
||||
/// This ensures allocations are fast and contiguous within a segment.
|
||||
/// Once a segment is full, a new one is automatically allocated
|
||||
/// </summary>
|
||||
public unsafe sealed class SegmentedPool : IDisposable, IUnmanagedMemoryOwner
|
||||
public unsafe sealed class SegmentedPool : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The default size for a <see cref="Segment"/>
|
||||
@@ -66,11 +25,6 @@
|
||||
/// </summary>
|
||||
private nuint _currentSegmentSize;
|
||||
|
||||
/// <summary>
|
||||
/// The alignment that each new <see cref="Segment"/> should be on
|
||||
/// </summary>
|
||||
private readonly nuint _segmentAlignment;
|
||||
|
||||
/// <summary>
|
||||
/// Queue of free segments
|
||||
/// </summary>
|
||||
@@ -82,12 +36,12 @@
|
||||
private readonly List<IntPtr> _activeSegments = [];
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the total bytes of memory reserved from the provided <see cref="IUnmanagedAllocator"/>
|
||||
/// Tracks the total amount of allocated bytes
|
||||
/// </summary>
|
||||
private nuint _totalReserved = 0;
|
||||
private nuint _totalAllocated = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the total amount of allocated bytes currently in use
|
||||
/// Tracks the total amount of allocated bytes current in use
|
||||
/// </summary>
|
||||
private nuint _totalUsed = 0;
|
||||
|
||||
@@ -97,7 +51,7 @@
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this <see cref="SegmentedPool"/> has been disposed.
|
||||
/// Indicates whether the <see cref="SegmentedPool"/> has been disposed.
|
||||
/// </summary>
|
||||
private volatile bool _disposed;
|
||||
|
||||
@@ -106,16 +60,8 @@
|
||||
/// </summary>
|
||||
private Segment* _current;
|
||||
|
||||
/// <summary>
|
||||
/// Allocator interface used for all underlying unmanaged memory operations.
|
||||
/// </summary>
|
||||
private readonly IUnmanagedAllocator _allocator;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if allocations from the pool should be zeroed.
|
||||
/// </summary>
|
||||
private readonly bool _zeroMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a memory segment in the <see cref="SegmentedPool"/>.
|
||||
/// </summary>
|
||||
@@ -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>
|
||||
/// Initializes a new <see cref="SegmentedPool"/> with the specified default <paramref name="segmentSize"/> and <paramref name="initialSegments"/> count
|
||||
/// </summary>
|
||||
/// <param name="segmentSize">Size of each segment in bytes (default 4 MiB)</param>
|
||||
/// <param name="segmentAlignment">Alignment requirement for each allocated segment and . Must be a power of 2 (Default 32)</param>
|
||||
/// <param name="initialSegments">Number of segments to pre-allocate to the pool</param>
|
||||
/// <param name="zeroMemory">When true, memory returned from the pool is zero-initialized.
|
||||
/// When false, memory may contain previously used data and it is the caller's responsibility to clear it if required.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
|
||||
/// </exception>
|
||||
public SegmentedPool(nuint segmentSize = _defaultSegmentSize, SegmentAlignment segmentAlignment = SegmentAlignment.Aligned32, int initialSegments = 4, bool zeroMemory = false)
|
||||
: this(segmentSize, segmentAlignment, initialSegments, zeroMemory, new DefaultUnmanagedAllocator())
|
||||
public SegmentedPool(nuint segmentSize = _defaultSegmentSize, int initialSegments = 4)
|
||||
: 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
|
||||
/// </summary>
|
||||
/// <param name="segmentSize">Size of each segment in bytes (default 4 MiB)</param>
|
||||
/// <param name="segmentAlignment">Alignment requirement for each allocated segment. Must be a power of 2 (Default 32)</param>
|
||||
/// <param name="initialSegments">Number of segments to pre-allocate to the pool</param>
|
||||
/// <param name="zeroMemory">When true, memory returned from the pool is zero-initialized.
|
||||
/// When false, memory may contain previously used data and it is the caller's responsibility to clear it if required.</param>
|
||||
/// <param name="allocator">IUnmanagedAllocator instance that implements the allocator</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
|
||||
/// </exception>
|
||||
internal SegmentedPool(nuint segmentSize, SegmentAlignment segmentAlignment, int initialSegments, bool zeroMemory, IUnmanagedAllocator allocator)
|
||||
internal SegmentedPool(nuint segmentSize, int initialSegments, IUnmanagedAllocator allocator)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(segmentSize);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(initialSegments, 1);
|
||||
if (segmentSize == 0)
|
||||
{
|
||||
throw new ArgumentException("Segment size must be greater than zero.", nameof(segmentSize));
|
||||
}
|
||||
if (initialSegments < 1)
|
||||
{
|
||||
throw new ArgumentException("Initial segments count must be at least 1.", nameof(initialSegments));
|
||||
}
|
||||
_allocator = allocator;
|
||||
_currentSegmentSize = segmentSize;
|
||||
_segmentAlignment = (nuint)segmentAlignment;
|
||||
_zeroMemory = zeroMemory;
|
||||
|
||||
Segment* seg;
|
||||
// Pre-allocate segments
|
||||
for (int i = 0; i < initialSegments; i++)
|
||||
{
|
||||
seg = AllocateNewSegment(_currentSegmentSize);
|
||||
ZeroSegment(seg);
|
||||
_freeSegments.Push((IntPtr)seg);
|
||||
_freeSegments.Push((IntPtr)AllocateNewSegment(_currentSegmentSize));
|
||||
}
|
||||
|
||||
|
||||
@@ -337,13 +145,7 @@
|
||||
/// <returns>The number of free segments.</returns>
|
||||
public int FreeSegmentCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _freeSegments.Count;
|
||||
}
|
||||
}
|
||||
get { return _freeSegments.Count; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -352,13 +154,7 @@
|
||||
/// <returns>The number of currently active Segments.</returns>
|
||||
public int ActiveSegmentCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _activeSegments.Count;
|
||||
}
|
||||
}
|
||||
get { return _activeSegments.Count; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -367,13 +163,7 @@
|
||||
/// <returns>The total number of bytes that have been allocated.</returns>
|
||||
public nuint TotalAllocatedBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalReserved;
|
||||
}
|
||||
}
|
||||
get { return _totalAllocated; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -382,13 +172,7 @@
|
||||
/// <returns>The total number of bytes that are in use.</returns>
|
||||
public nuint TotalUsedBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalUsed;
|
||||
}
|
||||
}
|
||||
get { return _totalUsed; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -414,153 +198,6 @@
|
||||
get { return _disposed; }
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nuint AlignUp(nuint value, nuint alignment)
|
||||
{
|
||||
return (value + alignment - 1) & ~(alignment - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zeroes the memory <see cref="Segment"></see> if the <see cref="SegmentedPool"/> is configured to do so.
|
||||
/// Called whenever a segment becomes active for use (new or reused).
|
||||
/// </summary>
|
||||
/// <param name="segment">Pointer to the Segment struct to initialize.</param>
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ZeroSegment(Segment* segment)
|
||||
{
|
||||
if (_zeroMemory)
|
||||
{
|
||||
Unsafe.InitBlockUnaligned(segment->Ptr, 0, (uint)segment->Size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of unmanaged memory of size <paramref name="count"/> for elements of type <typeparamref name="T"/> and returns a pointer to the allocated memory.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam>
|
||||
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param>
|
||||
/// <returns>A <typeparamref name="T"/> pointer to the first element of the allocated unmanaged memory block. The memory is valid until the <see cref="SegmentedPool"/> is reset or disposed.</returns>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.</description></item>
|
||||
/// <item><description>Accessing the memory after <see cref="Reset"/> or <see cref="Dispose"/> has been called is undefined behavior and may lead to crashes.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown if <paramref name="count"/> is less than or equal to zero.
|
||||
/// </exception>
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
||||
/// </exception>
|
||||
private T* Alloc<T>(int count) where T : unmanaged
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
||||
if ((nuint)count > nuint.MaxValue / (nuint)(sizeof(T)))
|
||||
{
|
||||
throw new OverflowException($"Requested allocation of {count} elements of type {typeof(T)} exceeds allowable maximum memory size.");
|
||||
}
|
||||
nuint bytes = (nuint)(count * sizeof(T));
|
||||
nuint alignment = _segmentAlignment > (nuint)sizeof(T) ? _segmentAlignment : (nuint)sizeof(T);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
nuint currentPtr = (nuint)_current->Ptr + _current->Offset;
|
||||
nuint alignedPtr = AlignUp(currentPtr, alignment);
|
||||
nuint alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
||||
// Check space INCLUDING padding
|
||||
if (alignedOffset + bytes > _current->Size)
|
||||
{
|
||||
SwitchSegment(bytes);
|
||||
|
||||
// Recalcuate from new the base of the new segment
|
||||
currentPtr = (nuint)_current->Ptr;
|
||||
alignedPtr = AlignUp(currentPtr, alignment);
|
||||
alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
||||
}
|
||||
|
||||
T* ptr = (T*)(_current->Ptr + alignedOffset);
|
||||
_current->Offset = alignedOffset + bytes;
|
||||
_totalUsed += bytes;
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
|
||||
private T* AllocateWithAlignment<T>(int count, nuint alignment) where T : unmanaged
|
||||
{
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
|
||||
if ((nuint)count > nuint.MaxValue / (nuint)(sizeof(T)))
|
||||
{
|
||||
throw new OverflowException($"Requested allocation of {count} elements of type {typeof(T)} exceeds allowable maximum memory size.");
|
||||
}
|
||||
|
||||
nuint bytes = (nuint)(count * sizeof(T));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
nuint currentPtr = (nuint)_current->Ptr + _current->Offset;
|
||||
nuint alignedPtr = AlignUp(currentPtr, alignment);
|
||||
nuint alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
||||
|
||||
if (alignedOffset + bytes > _current->Size)
|
||||
{
|
||||
SwitchSegment(bytes);
|
||||
currentPtr = (nuint)_current->Ptr;
|
||||
alignedPtr = AlignUp(currentPtr, alignment);
|
||||
alignedOffset = alignedPtr - (nuint)_current->Ptr;
|
||||
}
|
||||
|
||||
T* ptr = (T*)(_current->Ptr + alignedOffset);
|
||||
_current->Offset = alignedOffset + bytes;
|
||||
_totalUsed += bytes;
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches to a new <see cref="Segment"/> when the current <see cref="Segment"/> is full.
|
||||
/// </summary>
|
||||
/// <param name="requiredBytes">The number of bytes required for the upcoming allocation. If the current <see cref="Segment"/> does not have enough free space, a new <see cref="Segment"/> will be used.</param>
|
||||
private void SwitchSegment(nuint requiredBytes)
|
||||
{
|
||||
Segment* segment;
|
||||
|
||||
// Allocate fresh Segment if needed
|
||||
if (_freeSegments.Count == 0 || requiredBytes > _currentSegmentSize)
|
||||
{
|
||||
segment = AllocateNewSegment(requiredBytes > _currentSegmentSize ? requiredBytes : _currentSegmentSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = (Segment*)_freeSegments.Pop();
|
||||
segment->Offset = 0;
|
||||
}
|
||||
ZeroSegment(segment);
|
||||
_activeSegments.Add((IntPtr)segment);
|
||||
_current = segment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a new <see cref="Segment"/>
|
||||
/// </summary>
|
||||
/// <param name="size"> Size, in bytes, for the new <see cref="Segment"/>. </param>
|
||||
/// <returns>A pointer to the newly allocated <see cref="Segment"/></returns>
|
||||
private Segment* AllocateNewSegment(nuint size)
|
||||
{
|
||||
byte* ptr = (byte*)_allocator.AllocAligned(size, _segmentAlignment);
|
||||
// Allocate metadata struct with its natural alignment (8 bytes for 64-bit nuint)
|
||||
Segment* segment = (Segment*)_allocator.AllocAligned((nuint)sizeof(Segment), 8);
|
||||
segment->Ptr = ptr;
|
||||
segment->Offset = 0;
|
||||
segment->Size = size;
|
||||
|
||||
_totalReserved += size;
|
||||
return segment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current <see cref="Segment"/> size used for subsequent allocations.
|
||||
/// </summary>
|
||||
@@ -599,15 +236,15 @@
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam>
|
||||
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param>
|
||||
/// <returns>A <see cref="IMemoryHandle{T}"/> representing the allocated memory. The handle is valid until either <see cref="Reset"/> or <see cref="Dispose"/> is called on this <see cref="SegmentedPool"/>.</returns>
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <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">
|
||||
@@ -616,43 +253,73 @@
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
||||
/// </exception>
|
||||
public IMemoryHandle<T> Allocate<T>(int count) where T : unmanaged
|
||||
public Span<T> Allocate<T>(int count) where T : unmanaged
|
||||
{
|
||||
T* ptr = Alloc<T>(count);
|
||||
nuint byteLength = (nuint)count * (nuint)sizeof(T);
|
||||
return new SegmentedMemoryHandle<T>(ptr, byteLength, this);
|
||||
ThrowIfDisposed();
|
||||
if (count <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), "Allocation count must be greater than zero.");
|
||||
}
|
||||
if ((nuint)count > nuint.MaxValue / (nuint)(sizeof(T)))
|
||||
{
|
||||
throw new OverflowException($"Requested allocation of {count} elements of type {typeof(T)} exceeds allowable maximum memory size.");
|
||||
}
|
||||
nuint bytes = (nuint)(count * sizeof(T));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Enough space in current segment?
|
||||
if (_current->Offset + bytes > _current->Size)
|
||||
SwitchSegment(bytes);
|
||||
|
||||
T* ptr = (T*)(_current->Ptr + _current->Offset);
|
||||
_current->Offset += bytes;
|
||||
_totalUsed += bytes;
|
||||
return new Span<T>(ptr, count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <typeparam name="T">The unmanaged value type to store in the allocated memory. Must be a struct or primitive type.</typeparam>
|
||||
/// <param name="count">The number of elements of type <typeparamref name="T"/> to allocate.</param>
|
||||
/// <param name="alignment">The alignment to aliign the allocation to inside of the currently active <see cref="Segment"/></param>
|
||||
/// <returns>A <see cref="IMemoryHandle{T}"/> representing the allocated memory. The handle is valid until either <see cref="Reset"/> or <see cref="Dispose"/> is called on this <see cref="SegmentedPool"/>.</returns>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>This allocation is performed in unmanaged memory and bypasses the .NET garbage collector.</description></item>
|
||||
/// <item><description>Accessing the memory after <see cref="Reset"/> or <see cref="Dispose"/> has been called is undefined behavior.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown if <paramref name="count"/> is less than or equal to zero.
|
||||
/// </exception>
|
||||
/// <exception cref="OverflowException">
|
||||
/// Thrown if the total allocation size (count * sizeof(T)) exceeds the maximum allowable size.
|
||||
/// </exception>
|
||||
public IMemoryHandle<T> AllocateAligned<T>(int count, SegmentAlignment alignment) where T : unmanaged
|
||||
/// <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)
|
||||
{
|
||||
nuint requestedAlignment = (nuint)alignment;
|
||||
nuint typeSize = (nuint)sizeof(T);
|
||||
// Ensure alignment is at least the size of T (never under-align for types)
|
||||
nuint effectiveAlignment = requestedAlignment < typeSize ? typeSize : requestedAlignment;
|
||||
Segment* segment;
|
||||
|
||||
T* ptr = AllocateWithAlignment<T>(count, effectiveAlignment);
|
||||
nuint byteLength = (nuint)count * (nuint)sizeof(T);
|
||||
return new SegmentedMemoryHandle<T>(ptr, byteLength, this);
|
||||
// Allocate fresh Segment if needed
|
||||
if (_freeSegments.Count == 0 || requiredBytes > _currentSegmentSize)
|
||||
{
|
||||
segment = AllocateNewSegment(requiredBytes > _currentSegmentSize ? requiredBytes : _currentSegmentSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = (Segment*)_freeSegments.Pop();
|
||||
segment->Offset = 0;
|
||||
}
|
||||
|
||||
_activeSegments.Add((IntPtr)segment);
|
||||
_current = segment;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -677,9 +344,9 @@
|
||||
Segment* segment = (Segment*)ip;
|
||||
|
||||
// Free the unmanaged memory
|
||||
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
||||
_totalReserved -= segment->Size;
|
||||
_allocator.FreeAligned(segment, 8);
|
||||
_allocator.Free(segment->Ptr);
|
||||
_totalAllocated -= segment->Size;
|
||||
_allocator.Free(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -698,9 +365,6 @@
|
||||
Segment* segment = (Segment*)ip;
|
||||
segment->Offset = 0;
|
||||
_freeSegments.Push(ip);
|
||||
|
||||
// Zero memory if requested
|
||||
ZeroSegment(segment);
|
||||
}
|
||||
|
||||
_activeSegments.Clear();
|
||||
@@ -716,10 +380,7 @@
|
||||
// This should not be hit in normal circumstances as we always have _current
|
||||
_current = AllocateNewSegment(_currentSegmentSize);
|
||||
_activeSegments.Add((IntPtr)_current);
|
||||
// Zero newly allocated segment if requested
|
||||
ZeroSegment(_current);
|
||||
}
|
||||
|
||||
// Optionally trim excess free segments after reset
|
||||
if (trim)
|
||||
{
|
||||
@@ -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>
|
||||
/// 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.
|
||||
@@ -766,26 +406,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Free active segments
|
||||
// Free active pages
|
||||
foreach (var ip in _activeSegments)
|
||||
{
|
||||
Segment* segment = (Segment*)ip;
|
||||
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
||||
_allocator.FreeAligned(segment, 8);
|
||||
_allocator.Free(segment->Ptr);
|
||||
_allocator.Free(segment);
|
||||
}
|
||||
|
||||
// Free free segments
|
||||
// Free free pages
|
||||
foreach (var ip in _freeSegments)
|
||||
{
|
||||
Segment* segment = (Segment*)ip;
|
||||
_allocator.FreeAligned(segment->Ptr, _segmentAlignment);
|
||||
_allocator.FreeAligned(segment, 8);
|
||||
_allocator.Free(segment->Ptr);
|
||||
_allocator.Free(segment);
|
||||
}
|
||||
|
||||
_activeSegments.Clear();
|
||||
_freeSegments.Clear();
|
||||
_current = null;
|
||||
_totalReserved = 0;
|
||||
_totalAllocated = 0;
|
||||
_totalUsed = 0;
|
||||
_disposed = true;
|
||||
}
|
||||
@@ -795,153 +435,11 @@
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="SegmentedPool"/> has already been disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">
|
||||
/// Thrown when this <see cref="SegmentedPool"/> instance is no longer valid for use.
|
||||
/// Thrown when this instance is no longer valid for use.
|
||||
/// </exception>
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a snapshot of the current pool state for diagnostics.
|
||||
/// Thread-safe and produces no garbage.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="PoolState"/> containing current pool metrics.</returns>
|
||||
public PoolState GetPoolState()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lock)
|
||||
{
|
||||
nuint alignment = _segmentAlignment;
|
||||
nuint basePtr = (nuint)_current->Ptr;
|
||||
int active = _activeSegments.Count;
|
||||
int free = _freeSegments.Count;
|
||||
int total = active + free;
|
||||
|
||||
// 1. Calculate Padding (Alignment Overhead) for Current Segment
|
||||
// This is dynamic: Offset tracks total bytes written, TotalUsed tracks actual allocation bytes
|
||||
nuint padding = _current->Offset - _totalUsed;
|
||||
|
||||
// 2. Calculate Potential Savings (Trim Projection)
|
||||
nuint potentialSavings = 0;
|
||||
if (free > 16)
|
||||
{
|
||||
int excess = free - 16;
|
||||
potentialSavings = (nuint)excess * _currentSegmentSize;
|
||||
}
|
||||
|
||||
return new PoolState
|
||||
{
|
||||
SegmentAlignment = alignment,
|
||||
SegmentSize = _currentSegmentSize,
|
||||
TotalReserved = _totalReserved,
|
||||
TotalUsed = _totalUsed,
|
||||
CurrentBase = basePtr,
|
||||
CurrentOffset = _current->Offset,
|
||||
BaseAligned = (basePtr & (alignment - 1)) == 0,
|
||||
ActiveSegmentCount = active,
|
||||
FreeSegmentCount = free,
|
||||
TotalSegmentCount = total,
|
||||
PaddingBytes = padding,
|
||||
PotentialSavings = potentialSavings
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generates a diagnostic report for the pool.
|
||||
/// Thread-safe and produces no garbage.
|
||||
/// </summary>
|
||||
/// <returns>A formatted diagnostic string.</returns>
|
||||
public string GetDiagnosticReport()
|
||||
{
|
||||
PoolState state = GetPoolState();
|
||||
|
||||
DiagnosticConfig config = new()
|
||||
{
|
||||
SegmentSize = _currentSegmentSize,
|
||||
TotalReserved = _totalReserved
|
||||
};
|
||||
|
||||
string suggestion = SegmentedPoolDiagnostics.GenerateSuggestions(state, config);
|
||||
state = state with { Suggestion = suggestion };
|
||||
|
||||
return SegmentedPoolDiagnostics.GenerateReport(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to construct a <see cref="SegmentInfo"/> from raw segment data.
|
||||
/// </summary>
|
||||
/// <param name="segment">Pointer to the segment to inspect.</param>
|
||||
/// <param name="current">Pointer to the currently active segment for comparison.</param>
|
||||
/// <param name="index">The logical index of the segment in the list.</param>
|
||||
/// <returns>A populated <see cref="SegmentInfo"/> struct.</returns>
|
||||
private SegmentInfo CreateSegmentInfo(Segment* segment, Segment* current, int index)
|
||||
{
|
||||
nuint alignment = _segmentAlignment;
|
||||
nuint ptr = (nuint)segment->Ptr;
|
||||
bool isAligned = (ptr & (alignment - 1)) == 0;
|
||||
|
||||
return new SegmentInfo
|
||||
{
|
||||
Index = index,
|
||||
BaseAddress = ptr,
|
||||
UsedBytes = segment->Offset,
|
||||
Size = segment->Size,
|
||||
IsActive = (segment == current),
|
||||
AlignmentRequirement = alignment,
|
||||
IsAligned = isAligned
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about the currently active segment.
|
||||
/// This is the primary diagnostic view for memory usage within the active segment.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="SegmentInfo"/> for the current segment, or null if disposed.</returns>
|
||||
public SegmentInfo GetCurrentSegmentInfo()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lock)
|
||||
{
|
||||
return CreateSegmentInfo(_current, _current, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all segment details for deep diagnostics.
|
||||
/// Includes both active and free segments.
|
||||
/// </summary>
|
||||
/// <returns>A list of <see cref="SegmentInfo"/> containing all segments.</returns>
|
||||
public List<SegmentInfo> GetAllSegmentInfos()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lock)
|
||||
{
|
||||
var totalSegments = _activeSegments.Count + _freeSegments.Count;
|
||||
var result = new List<SegmentInfo>(totalSegments);
|
||||
|
||||
int currentIndex = 0;
|
||||
|
||||
for (int i = 0; i < _activeSegments.Count; i++)
|
||||
{
|
||||
IntPtr ip = _activeSegments[i];
|
||||
Segment* segment = (Segment*)ip;
|
||||
|
||||
result.Add(CreateSegmentInfo(segment, _current, currentIndex));
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
foreach (var ip in _freeSegments)
|
||||
{
|
||||
Segment* segment = (Segment*)ip;
|
||||
result.Add(CreateSegmentInfo(segment, _current, currentIndex++));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- Optional: Suppress warnings for undocumented members -->
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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> (> 256 KB) use tolerance-based reuse
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public unsafe sealed class WorkspaceHeap : IDisposable, IUnmanagedMemoryOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum size, in bytes, for "small" allocations. Uses fixed-size buckets
|
||||
/// </summary>
|
||||
private const nuint _smallThreshold = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size, in bytes, for allocations considered "medium". Uses best-fit reuse
|
||||
/// </summary>
|
||||
private const nuint _mediumThreshold = 256 * 1024; // 256 KB
|
||||
|
||||
/// <summary>
|
||||
/// The maximum absolute number of bytes that may be wasted when reusing a large block for a "large" allocation.
|
||||
/// </summary>
|
||||
private const nuint _largeMaxWasteBytes = 256 * 1024; // 256 KB
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed size ratio when reusing a large allocation block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example, a value of <c>1.25</c> allows a block up to 25% larger than
|
||||
/// the requested size to be reused.
|
||||
/// </remarks>
|
||||
private const double _largeWasteRatioLimit = 1.25;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined bucket sizes used for small allocation reuse.
|
||||
/// </summary>
|
||||
private static readonly nuint[] _sizeClasses =
|
||||
{
|
||||
32, 64, 96, 128, 160, 192, 224, 256,
|
||||
288, 320, 352, 384, 416, 448, 480, 512,
|
||||
544, 576, 608, 640, 672, 704, 736, 768,
|
||||
800, 832, 864, 896, 928, 960, 992, 1024
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Allocator interface used for all underlying unmanaged memory operations.
|
||||
/// </summary>
|
||||
private readonly IUnmanagedAllocator _allocator;
|
||||
|
||||
/// <summary>
|
||||
/// Internal lock, ensures thread safety while maintaining a simple interface
|
||||
/// </summary>
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Free lists for small allocations, keyed by the bucket size, in bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a given bucket, the corresponding stack contains previously allocated blocks that are available to be used
|
||||
/// </remarks>
|
||||
private readonly Dictionary<nuint, Stack<IntPtr>> _smallFree = new();
|
||||
|
||||
/// <summary>
|
||||
/// Free lists for medium allocations keyed by the exactly allocated size, in bytes, and sorted for best-fit.
|
||||
/// </summary>
|
||||
private readonly SortedDictionary<nuint, Stack<IntPtr>> _mediumFree = new();
|
||||
|
||||
/// <summary>
|
||||
/// Free lists for large allocations keyed by the exactly allocated size, in bytes, sorted for tolerance-based reuse.
|
||||
/// </summary>
|
||||
private readonly SortedDictionary<nuint, Stack<IntPtr>> _largeFree = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the total bytes of memory reserved from the provided <see cref="IUnmanagedAllocator"/>.
|
||||
/// </summary>
|
||||
private nuint _totalReserved;
|
||||
|
||||
/// <summary>
|
||||
/// Total memory currently in use by active allocations, in bytes.
|
||||
/// </summary>
|
||||
private nuint _totalInUse;
|
||||
|
||||
/// <summary>
|
||||
/// Counts actual underlying OS allocations.
|
||||
/// </summary>
|
||||
private nuint _totalAllocations;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this <see cref="WorkspaceHeap"/> has been disposed.
|
||||
/// </summary>
|
||||
private volatile bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Internal header prepended to each allocation to track its size.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlockHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// Size of the allocation in bytes.
|
||||
/// </summary>
|
||||
public nuint Size;
|
||||
|
||||
/// <summary>
|
||||
/// Padding to ensure <see cref="BlockHeader"></see> is 32-byte aligned
|
||||
/// </summary>
|
||||
private readonly nuint _pad1;
|
||||
|
||||
/// <summary>
|
||||
/// Padding to ensure <see cref="BlockHeader"></see> is 32-byte aligned
|
||||
/// </summary>
|
||||
private readonly nuint _pad2;
|
||||
|
||||
/// <summary>
|
||||
/// Padding to ensure <see cref="BlockHeader"></see> is 32-byte aligned
|
||||
/// </summary>
|
||||
private readonly nuint _pad3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkspaceHeap"/>.
|
||||
/// </summary>
|
||||
public WorkspaceHeap()
|
||||
: this(new DefaultUnmanagedAllocator())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="WorkspaceHeap"/> using the specified <see cref="IUnmanagedAllocator"/>.
|
||||
/// </summary>
|
||||
/// <param name="allocator">Allocator implementing <see cref="IUnmanagedAllocator"/>.</param>
|
||||
internal WorkspaceHeap(IUnmanagedAllocator allocator)
|
||||
{
|
||||
_allocator = allocator;
|
||||
|
||||
// Initialize small-size buckets
|
||||
foreach (var size in _sizeClasses)
|
||||
_smallFree[size] = new Stack<IntPtr>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of bytes currently allocated from the underlying allocator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This includes both active allocations and freed blocks retained for reuse.
|
||||
/// </remarks>
|
||||
public nuint TotalReservedBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
return _totalReserved;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of bytes currently in use by active allocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value decreases when memory is freed and increases when new allocations occur.
|
||||
/// </remarks>
|
||||
public nuint TotalUsedBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalInUse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of allocation operations performed by this heap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This counts new underlying OS allocations, not reuse from free lists.
|
||||
/// Useful for performance diagnostics and testing reuse behavior.
|
||||
/// </remarks>
|
||||
public nuint TotalAllocationCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _totalAllocations;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the <see cref="WorkspaceHeap"/> has been disposed.
|
||||
/// </summary>
|
||||
public bool IsDisposed
|
||||
{
|
||||
get { return _disposed; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the small size bucket for a requested allocation.
|
||||
/// </summary>
|
||||
/// <param name="size">The allocation size to get the bucket size for</param>
|
||||
/// <returns>The small size bucket for the requested allocation</returns>
|
||||
private static nuint GetSizeClass(nuint size)
|
||||
{
|
||||
foreach (nuint s in _sizeClasses)
|
||||
{
|
||||
if (size <= s)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a new block from the underlying allocator including a header.
|
||||
/// </summary>
|
||||
/// <param name="payloadSize">Requested payload size in bytes.</param>
|
||||
/// <returns>
|
||||
/// Pointer to the allocated block (header included).
|
||||
/// </returns>
|
||||
private IntPtr AllocateNew(nuint payloadSize)
|
||||
{
|
||||
nuint total = payloadSize + (nuint)sizeof(BlockHeader);
|
||||
void* raw = _allocator.Alloc(total);
|
||||
_totalReserved += total;
|
||||
_totalAllocations++;
|
||||
|
||||
return (IntPtr)raw;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates unmanaged memory from the workspace heap.
|
||||
/// </summary>
|
||||
/// <param name="count">Number of elements <typeparamref name="T"/> to allocate.</param>
|
||||
/// <param name="zero">If true, memory is zero-initialized.</param>
|
||||
/// <returns> An <see cref="IMemoryHandle{T}"/> to the allocated memory.</returns>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if heap is disposed.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown if size is zero.</exception>
|
||||
public IMemoryHandle<T> Allocate<T>(int count, bool zero = false) where T : unmanaged
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
||||
ArgumentOutOfRangeException.ThrowIfZero(count);
|
||||
|
||||
if ((nuint)count > nuint.MaxValue / (nuint)(sizeof(T)))
|
||||
{
|
||||
throw new OverflowException($"Requested allocation of {count} elements of type {typeof(T)} exceeds allowable maximum memory size.");
|
||||
}
|
||||
|
||||
nuint size = (nuint)count * (nuint)sizeof(T);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
void* ptr = null;
|
||||
if (size <= _smallThreshold)
|
||||
{
|
||||
ptr = AllocateSmall(size, zero);
|
||||
}
|
||||
else if (size <= _mediumThreshold)
|
||||
{
|
||||
ptr = AllocateMedium(size, zero);
|
||||
}
|
||||
else
|
||||
{
|
||||
ptr = AllocateLarge(size, zero);
|
||||
}
|
||||
|
||||
return new PersistentMemoryHandle<T>((T*)ptr, size, this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Allocates a small-size block using bucketed free lists.</summary>
|
||||
private void* AllocateSmall(nuint size, bool zero)
|
||||
{
|
||||
nuint bucket = GetSizeClass(size);
|
||||
Stack<IntPtr> stack = _smallFree[bucket];
|
||||
|
||||
IntPtr block = stack.Count > 0
|
||||
? stack.Pop()
|
||||
: AllocateNew(bucket);
|
||||
|
||||
BlockHeader* header = (BlockHeader*)block;
|
||||
header->Size = bucket;
|
||||
|
||||
_totalInUse += bucket;
|
||||
|
||||
void* user = header + 1;
|
||||
|
||||
if (zero)
|
||||
Unsafe.InitBlockUnaligned(user, 0, (uint)bucket);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>Allocates a medium-size block using best-fit reuse.</summary>
|
||||
private void* AllocateMedium(nuint size, bool zero)
|
||||
{
|
||||
foreach (var kv in _mediumFree)
|
||||
{
|
||||
if (kv.Key >= size && kv.Value.Count > 0)
|
||||
{
|
||||
var block = kv.Value.Pop();
|
||||
var header = (BlockHeader*)block;
|
||||
_totalInUse += header->Size;
|
||||
|
||||
void* user = header + 1;
|
||||
if (zero)
|
||||
Unsafe.InitBlockUnaligned(user, 0, (uint)header->Size);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
var newBlock = AllocateNew(size);
|
||||
var newHeader = (BlockHeader*)newBlock;
|
||||
newHeader->Size = size;
|
||||
_totalInUse += size;
|
||||
|
||||
void* newUser = newHeader + 1;
|
||||
if (zero)
|
||||
{
|
||||
Unsafe.InitBlockUnaligned(newUser, 0, (uint)size);
|
||||
}
|
||||
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
/// <summary>Allocates a large block using tolerance-based reuse (smallest ≥ requested within waste bounds).</summary>
|
||||
private void* AllocateLarge(nuint size, bool zero)
|
||||
{
|
||||
foreach (var kv in _largeFree)
|
||||
{
|
||||
nuint blockSize = kv.Key;
|
||||
if (blockSize < size || kv.Value.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
nuint waste = blockSize - size;
|
||||
bool acceptable =
|
||||
waste <= _largeMaxWasteBytes ||
|
||||
((double)blockSize / size) <= _largeWasteRatioLimit;
|
||||
|
||||
if (!acceptable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var block = kv.Value.Pop();
|
||||
var header = (BlockHeader*)block;
|
||||
_totalInUse += header->Size;
|
||||
|
||||
void* user = header + 1;
|
||||
if (zero)
|
||||
{
|
||||
Unsafe.InitBlockUnaligned(user, 0, (uint)header->Size);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
var newBlock = AllocateNew(size);
|
||||
var newHeader = (BlockHeader*)newBlock;
|
||||
newHeader->Size = size;
|
||||
_totalInUse += size;
|
||||
|
||||
void* newUser = newHeader + 1;
|
||||
if (zero)
|
||||
{
|
||||
Unsafe.InitBlockUnaligned(newUser, 0, (uint)size);
|
||||
}
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated block, returning it to the appropriate free list.
|
||||
/// </summary>
|
||||
void IUnmanagedMemoryOwner.Free(IOwnedHandle handle)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (handle.Pointer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (handle.GetOwner() != this)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Attempted to free a handle from a different allocator pool.");
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var header = ((BlockHeader*)handle.Pointer) - 1;
|
||||
nuint size = header->Size;
|
||||
_totalInUse -= size;
|
||||
|
||||
if (size <= _smallThreshold)
|
||||
_smallFree[size].Push((IntPtr)header);
|
||||
else if (size <= _mediumThreshold)
|
||||
{
|
||||
if (!_mediumFree.TryGetValue(size, out var stack))
|
||||
_mediumFree[size] = stack = new Stack<IntPtr>();
|
||||
stack.Push((IntPtr)header);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_largeFree.TryGetValue(size, out var stack))
|
||||
_largeFree[size] = stack = new Stack<IntPtr>();
|
||||
stack.Push((IntPtr)header);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all unused blocks back to the underlying allocator.
|
||||
/// </summary>
|
||||
public void Prune()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
lock (_lock)
|
||||
{
|
||||
PruneDictionary(_smallFree);
|
||||
PruneDictionary(_mediumFree);
|
||||
PruneDictionary(_largeFree);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Helper to free all blocks in a dictionary of free stacks.</summary>
|
||||
private void PruneDictionary(IDictionary<nuint, Stack<IntPtr>> dict)
|
||||
{
|
||||
foreach (var kv in dict)
|
||||
{
|
||||
var stack = kv.Value;
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var block = stack.Pop();
|
||||
var header = (BlockHeader*)block;
|
||||
_allocator.Free((void*)block);
|
||||
_totalReserved -= (header->Size + (nuint)sizeof(BlockHeader));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all memory and marks the heap as disposed.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_totalInUse > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot dispose WorkspaceHeap while active allocations exist. " +
|
||||
"Dispose all handles returned from this heap before disposing the heap.");
|
||||
}
|
||||
Prune();
|
||||
// Reset stats
|
||||
_totalInUse = 0;
|
||||
_totalAllocations = 0;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="WorkspaceHeap"/> has already been disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">
|
||||
/// Thrown when this instance is no longer valid for use.
|
||||
/// </exception>
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user