Added SegmentedPool

This commit is contained in:
Jim
2025-11-04 01:00:03 +00:00
committed by 0xJ1M
parent e5d4a47962
commit 0ba3a9fef5
10 changed files with 1753 additions and 17 deletions

361
.gitignore vendored
View File

@@ -1,11 +1,20 @@
## A streamlined .gitignore for modern .NET projects ## Ignore Visual Studio temporary files, build results, and
## including temporary files, build results, and ## files generated by popular Visual Studio add-ons.
## files generated by popular .NET tools. If you are
## developing with Visual Studio, the VS .gitignore
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## has more thorough IDE-specific entries.
## ##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results # Build results
[Dd]ebug/ [Dd]ebug/
@@ -20,29 +29,17 @@ x86/
bld/ bld/
[Bb]in/ [Bb]in/
[Oo]bj/ [Oo]bj/
[Oo]ut/
[Ll]og/ [Ll]og/
[Ll]ogs/ [Ll]ogs/
# .NET Core # Visual Studio 2015/2017 cache/options directory
project.lock.json .vs/
project.fragment.lock.json # Uncomment if you have tasks that create the project's static files in wwwroot
artifacts/ #wwwroot/
# ASP.NET Scaffolding # Visual Studio 2017 auto generated files
ScaffoldingReadMe.txt Generated\ Files/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# Others
~$*
*~
CodeCoverage/
# MSBuild Binary and Structured Log
*.binlog
# MSTest test Results # MSTest test Results
[Tt]est[Rr]esult*/ [Tt]est[Rr]esult*/
@@ -52,3 +49,315 @@ CodeCoverage/
*.VisualState.xml *.VisualState.xml
TestResult.xml TestResult.xml
nunit-*.xml nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

239
README.md
View File

@@ -1,2 +1,237 @@
# CSUnmangedMMU # UnmanagedMMU
A C# Memory manager that provides a Segmented Memory Pool and Persistent pool
UnmanagedMMU is a high-performance C# memory manager library that provides efficient unmanaged memory allocation.
---
## Table of Contents
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)
---
## SegmentedPool
A `SegmentedPool` is a **segmented bump allocator** for unmanaged memory in C#. It pre-allocates memory in **fixed-size segments** and serves allocations sequentially within each segment.
Once a segment is full, the pool automatically switches to a new segment, allowing fast, contiguous allocations without fragmentation.
Advantages of the `SegmentedPool`:
- **High performance**: Allocation is simple pointer arithmetic.
- **Contiguous memory**: Reduces cache misses and improves data locality.
- **Thread-safe**: Supports concurrent allocation operations.
- **Manual memory control**: Works outside of .NET GC, ideal for high-performance or low-latency scenarios.
---
### Segments
A **Segment** is a contiguous block of unmanaged memory managed by the pool. Each segment contains:
| Field | Type | Description |
|-----------|-------|-------------|
| `Ptr` | `byte*` | Pointer to the start of the unmanaged memory block. |
| `Offset` | `nuint` | Current allocation offset within the segment. Increases as memory is allocated. |
| `Size` | `nuint` | Total size of the segment in bytes. |
Segments are allocated automatically by the pool and should **never be modified outside the pool**.
---
### Allocation Strategy
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).
This provides **O(1) allocation performance** for most operations.
---
### Constructor <a name="segmentedpool-constructor"></a>
Initializes a new instance of the SegmentedPool with the specified segment size and number of pre-allocated [Segments](#segments).
#### Syntax
```csharp
SegmentedPool pool = new SegmentedPool(segmentSize: 4 * 1024 * 1024, initialSegments: 4);
```
#### Parameters
| 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
Returns a new instance of the SegmentedPool with the specified [Segments](#segments) size and number of pre-allocated [Segments](#segments).
#### 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.
---
### 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.
---
### SetSegmentSize <a name="segmentedpool-set-segment-size"></a>
Sets the [Segment](#segments) size to use for future allocations.
#### Syntax
```csharp
pool.SetSegmentSize(newSize);
```
#### Parameters
| Parameter | Type | Description |
| --------- | ----- | ------------------------------------------- |
| `newSize` | nuint | The new segment size in bytes. Must be > 0. |
#### Return value
None.
#### Remarks
This method will only affect the [Segment](#segments) size of future allocations; it does not modify existing [Segments](#segments).
---
### ResetSegmentSize <a name="segmentedpool-reset-segment-size"></a>
Resets the [Segment](#segments) size for future allocations back to the default (4 MiB).
#### Syntax
```csharp
pool.ResetSegmentSize();
```
#### Parameters
None.
#### Return value
None.
#### Remarks
This method will only affect the [Segment](#segments) size of future allocations; it does not modify existing [Segments](#segments).
---
### Reset <a name="segmentedpool-reset"></a>
Resets the pool, returning all active [Segments](#segments) to the free pool for reuse.
#### Syntax
```csharp
pool.Reset(trim: false);
```
#### Parameters
| Parameter | Type | Description |
| ----------- | ------- | ----------------------------------------------------------------------------- |
| `trim` | `bool` | Optional. If true, trims excess free [Segments](#segments) after reset. Defaults to false |
#### Return value
None.
#### 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.
---
### Trim <a name="segmentedpool-trim"></a>
Frees unused [Segments](#segments) in the free pool, reducing memory usage.
#### Syntax
```csharp
pool.Trim(minFreeSegments: 16);
```
#### Parameters
| 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
None.
#### Remarks
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.
---
### Dispose <a name="segmentedpool-dispose"></a>
Releases all unmanaged memory used by the pool and clears internal state.
#### Syntax
```csharp
pool.Dispose();
```
#### Parameters
None.
#### Return value
None.
#### 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.

View File

@@ -0,0 +1,636 @@
using System;
using System.Runtime.InteropServices;
using UnmanagedMMU;
using UnmanagedMMU.Allocators;
using Xunit;
namespace UnmanagedMMUTests
{
/// <summary>
/// UnmanagedAllocator that will fail
/// </summary>
public sealed unsafe class FailingAllocator : IUnmanagedAllocator
{
private readonly int _failAfter;
private int _allocCount;
/// <summary>
/// Initializes a new <see cref="SegmentedPool"/> wich fails after <paramref name="failAfterNSegmentsAllocated"/> bytes has been allocated
/// </summary>
/// <param name="failAfterNSegmentsAllocated">Indicates the number of allocations that are allowed to succssed, more allocations after this will fail </param>
public FailingAllocator(int failAfterNSegmentsAllocated = 0)
{
// each segment has two unmanaged allocs!
_failAfter = 2 * failAfterNSegmentsAllocated;
}
public void* Alloc(nuint size)
{
_allocCount++;
if (_allocCount > _failAfter)
{
throw new OutOfMemoryException("The allocator has failed!");
}
return NativeMemory.Alloc(size);
}
public void Free(void* ptr)
{
NativeMemory.Free(ptr);
}
}
public class SegmentedPoolTests
{
#region TestData
public struct TestMyStruct
{
public int A;
public double B;
}
// Example enum
public enum TestMyEnum : int
{
First,
Second
}
#endregion
private void AssertSpanIsNotEmptyAndHasNElements<T>(Span<T> span, int nElements) where T : unmanaged
{
Assert.False(span.IsEmpty);
Assert.Equal(nElements, span.Length);
}
/// <summary>
/// Test that an ArgumentException is raised if zero is given for SegmentSize
/// </summary>
[Fact]
public void ConstructorSegmentSizeZeroThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new SegmentedPool(segmentSize: 0));
Assert.Equal("Segment size must be greater than zero. (Parameter 'segmentSize')", ex.Message);
}
/// <summary>
/// Test that an ArgumentException is raised if initialSegment is 0
/// </summary>
[Fact]
public void ConstructorInitialSegmentCountLessThanOneThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new SegmentedPool(initialSegments: 0));
Assert.Equal("Initial segments count must be at least 1. (Parameter 'initialSegments')", ex.Message);
}
/// <summary>
/// Test that valid arguments create valid object
/// </summary>
[Fact]
public void ConstructorValidArgumentsIsValidObject()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.False(pool.IsDisposed);
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
Assert.Equal(initialSegments, (int)(pool.TotalAllocatedBytes / segmentSize));
Assert.Equal(1, pool.ActiveSegmentCount); // one is active
Assert.True(pool.TotalAllocatedBytes >= segmentSize * (nuint)initialSegments);
Assert.Equal(0u, pool.TotalUsedBytes);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that valid arguments but the allocation of the initial segments fails throws OutOfMemoryException
/// </summary>
[Fact]
public void ConstructorValidArgumentsButAllocationOfinitialSegmentsFailsThrowsOutOfMemoryException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
FailingAllocator failingAllocator = new FailingAllocator(failAfterNSegmentsAllocated: 1);
var ex = Assert.Throws<OutOfMemoryException>(() => new SegmentedPool(segmentSize, initialSegments, failingAllocator));
Assert.Equal("The allocator has failed!", ex.Message);
}
/// <summary>
/// Test that SetSegmentSize with the new size set to Zero throws ArgumentOutOfRangeException
/// </summary>
[Fact]
public void SetSegmentSizeWithSizeOfZeroThrowsArgumentOutOfRangeException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => pool.SetSegmentSize(0));
Assert.Equal("Segment size must be greater than zero. (Parameter 'newSize')", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that SetSegmentSize changes the allocated SegmentSize
/// </summary>
[Fact]
public void SetSegmentSizeChangesTheSegmentSize()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
nuint newSegmentSize = 4096;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
pool.SetSegmentSize(newSegmentSize);
Assert.NotEqual(segmentSize, pool.CurrentSegmentSize);
Assert.Equal(newSegmentSize, pool.CurrentSegmentSize);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that ResetSegmentSize changes the allocated SegmentSize to the default 4 MiB
/// </summary>
[Fact]
public void ReetSegmentSizeChangesTheSegmentSizeToDefault()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
nuint defaultSize = 4194304; // 4 MiB
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(segmentSize, pool.CurrentSegmentSize);
pool.ResetSegmentSize();
Assert.NotEqual(segmentSize, pool.CurrentSegmentSize);
Assert.Equal(defaultSize, pool.CurrentSegmentSize);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that SetSegmentSize called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void SetSegmentSizeValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.SetSegmentSize(4096));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that ResetSegmentSize called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void ResetSegmentSizeValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.ResetSegmentSize());
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T is successful
/// </summary>
[Fact]
public void AllocateValidCountAndForGenericTSucceeds()
{
AllocateAndAssert<sbyte>();
AllocateAndAssert<byte>();
AllocateAndAssert<short>();
AllocateAndAssert<ushort>();
AllocateAndAssert<int>();
AllocateAndAssert<uint>();
AllocateAndAssert<long>();
AllocateAndAssert<ulong>();
AllocateAndAssert<nint>();
AllocateAndAssert<nuint>();
AllocateAndAssert<char>();
AllocateAndAssert<float>();
AllocateAndAssert<double>();
AllocateAndAssert<decimal>();
AllocateAndAssert<bool>();
}
private void AllocateAndAssert<T>() where T : unmanaged
{
SegmentedPool pool = new SegmentedPool();
try
{
Span<T> span = pool.Allocate<T>(100);
AssertSpanIsNotEmptyAndHasNElements(span, 100);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate with an invalid count but valid unamanged T throws ArgumentOutOfRangeException
/// </summary>
[Fact]
public void AllocateInvalidCountThrowsArgumentOutOfRangeException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => pool.Allocate<int>(0));
Assert.Equal("Allocation count must be greater than zero. (Parameter 'count')", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T but the allocation is larger than the current segment is successful
/// </summary>
[Fact]
public void AllocateValidCountButAllocationIsLargerThanCurrentSegmentSucceeds()
{
// use a small segment on purpose
SegmentedPool pool = new SegmentedPool(segmentSize: 128);
try
{
Span<int> span = pool.Allocate<int>(100);
AssertSpanIsNotEmptyAndHasNElements(span, 100);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T but there are no free segments
/// </summary>
[Fact]
public void AllocateValidCountButNoFreeSegmentsSucceeds()
{
// use a small segment on purpose
SegmentedPool pool = new SegmentedPool(segmentSize: 128, initialSegments: 1);
try
{
Span<byte> spanb = pool.Allocate<byte>(128);
AssertSpanIsNotEmptyAndHasNElements(spanb, 128);
Assert.Equal(0, pool.FreeSegmentCount);
Span<byte> spanb2 = pool.Allocate<byte>(128);
AssertSpanIsNotEmptyAndHasNElements(spanb2, 128);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T but the allocation size exceeds the segment size then the segment is switched Segment
/// </summary>
[Fact]
public void AllocateValidCountWhenAllocationExceedsSegmentSizeSucceedsSwitchesSegment()
{
nuint segmentSize = 1024;
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
int count = 300;
Assert.Equal(1, pool.FreeSegmentCount);
Span<int> span = pool.Allocate<int>(count);
AssertSpanIsNotEmptyAndHasNElements(span, count);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(1, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate a valid count and unamanged T and the allocaiton
/// </summary>
[Fact]
public void AllocateValidCountWhenFreeSegmentAvailableReusesSegment()
{
nuint segmentSize = 1024;
int initialSegments = 2; // one current + one free
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
// Fill _current segment almost completely
int sizeToFillCurrent = (int)(segmentSize / sizeof(int));
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(1, pool.FreeSegmentCount);
Span<int> span1 = pool.Allocate<int>(sizeToFillCurrent - 1);
AssertSpanIsNotEmptyAndHasNElements(span1, sizeToFillCurrent - 1);
// triggers SwitchSegment
Span<int> span2 = pool.Allocate<int>(2);
AssertSpanIsNotEmptyAndHasNElements(span2, 2);
Assert.Equal(2, pool.ActiveSegmentCount);
Assert.Equal(0, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Allocate called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void AllocateValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.Allocate<int>(123));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim with the min Segment less than 0 throws ArgumentOutOfRangeException
/// </summary>
[Fact]
public void TrimInvalidArgumentThrowsArgumentOutOfRangeException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => pool.Trim(-123));
Assert.Equal("minFreeSegments ('-123') must be a non-negative value. (Parameter 'minFreeSegments')\r\nActual value was -123.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim does not modify the total number of free segments if the minFreeSegment variable is greater than the number of free Segments
/// </summary>
[Fact]
public void TrimWhenFreeSegmentCountLessThanMinSegmentsDoesNothing()
{
nuint segmentSize = 1024;
int initialSegments = 32;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(31, pool.FreeSegmentCount);
pool.Trim(128);
Assert.Equal(31, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim doesmodify the total number of free segments if the minFreeSegment variable is less than the number of free Segments
/// </summary>
[Fact]
public void TrimWhenFreeSegmentGreaterThanMinSegmentsTrimsFreeSegmentsToNewSize()
{
nuint segmentSize = 1024;
int initialSegments = 32;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Assert.Equal(31, pool.FreeSegmentCount);
pool.Trim(16);
Assert.Equal(16, pool.FreeSegmentCount);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Trim called with a valid argument but the SegmentedPool has already been disposed throws ObjectDisposedException
/// </summary>
[Fact]
public void TrimValidArgumentButAlreadyDisposedThrowsObjectDisposedException()
{
nuint segmentSize = 1024; // 1 KiB for the test
int initialSegments = 2;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
pool.Dispose();
Assert.True(pool.IsDisposed);
var ex = Assert.Throws<ObjectDisposedException>(() => pool.Trim(16));
Assert.Equal("Cannot access a disposed object.\r\nObject name: 'UnmanagedMMU.SegmentedPool'.", ex.Message);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Reset with trim false clears all active Segments and leaves the SegmentPool with a single active page
/// </summary>
[Fact]
public void ResetNoTrimClearsAllActivateSegments()
{
nuint segmentSize = 300; // 1 KiB for the test
int initialSegments = 8;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Span<byte> testSpan1 = pool.Allocate<byte>(256);
Span<byte> testSpan2 = pool.Allocate<byte>(256);
Span<byte> testSpan3 = pool.Allocate<byte>(256);
Span<byte> testSpan4 = pool.Allocate<byte>(256);
AssertSpanIsNotEmptyAndHasNElements(testSpan1, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan2, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan3, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan4, 256);
Assert.Equal(4, pool.ActiveSegmentCount);
Assert.Equal(4, pool.FreeSegmentCount);
Assert.Equal(4 * 256, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(8*300, (int)pool.TotalAllocatedBytes);
pool.Reset();
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(7, pool.FreeSegmentCount);
Assert.Equal(0, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(8 * 300, (int)pool.TotalAllocatedBytes);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
/// <summary>
/// Test that Reset with trim true clears all active Segments and leaves the SegmentPool with a single active page and trims the free sements to 16
/// </summary>
[Fact]
public void ResetWithTrimClearsAllActivateSegments()
{
nuint segmentSize = 300; // 1 KiB for the test
int initialSegments = 64;
SegmentedPool pool = new SegmentedPool(segmentSize, initialSegments);
try
{
Span<byte> testSpan1 = pool.Allocate<byte>(256);
Span<byte> testSpan2 = pool.Allocate<byte>(256);
Span<byte> testSpan3 = pool.Allocate<byte>(256);
Span<byte> testSpan4 = pool.Allocate<byte>(256);
AssertSpanIsNotEmptyAndHasNElements(testSpan1, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan2, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan3, 256);
AssertSpanIsNotEmptyAndHasNElements(testSpan4, 256);
Assert.Equal(4, pool.ActiveSegmentCount);
Assert.Equal(60, pool.FreeSegmentCount);
Assert.Equal(4 * 256, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(64 * 300, (int)pool.TotalAllocatedBytes);
pool.Reset(true);
Assert.Equal(1, pool.ActiveSegmentCount);
Assert.Equal(16, pool.FreeSegmentCount);
Assert.Equal(0, (int)pool.TotalUsedBytes);
Assert.Equal(300, (int)pool.CurrentSegmentSize);
Assert.Equal(17 * 300, (int)pool.TotalAllocatedBytes);
}
finally
{
pool.Dispose();
Assert.True(pool.IsDisposed);
}
}
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="18.0.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\UnmangedMMU\UnmanagedMMU.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using System.Runtime.InteropServices;
namespace UnmanagedMMU.Allocators
{
/// <summary>
/// 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) => NativeMemory.Alloc(size);
/// <inheritdoc/>
public void Free(void* ptr) => NativeMemory.Free(ptr);
}
}

View File

@@ -0,0 +1,23 @@
namespace UnmanagedMMU.Allocators
{
/// <summary>
/// Interface that defines an Unmanaged allocator
/// </summary>
internal unsafe interface IUnmanagedAllocator
{
/// <summary>
/// Allocates an unmanaged memory block of the specified size.
/// </summary>
/// <param name="size">The number of bytes to allocate.</param>
/// <returns>
/// A pointer to the beginning of the allocated memory block,.
/// </returns>
void* Alloc(nuint size);
/// <summary>
/// Frees a previously allocated unmanaged memory block.
/// </summary>
/// <param name="ptr">A pointer to the beginning of the memory block to free.</param>
void Free(void* ptr);
}
}

View File

@@ -0,0 +1,4 @@
// AssemblyInfo.cs (or any C# source file in your main project)
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("UnmanagedMMUTests")]

View File

@@ -0,0 +1,445 @@
namespace UnmanagedMMU
{
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using UnmanagedMMU.Allocators;
/// <summary>
/// Implementation of segmented Bump-Allocator.
///
/// This implementation manages fixed-sized unmanaged memory segments.
/// This ensures allocations are fast and contiguous within a segment.
/// Once a segment is full, a new one is automatically allocated
/// </summary>
public unsafe sealed class SegmentedPool : IDisposable
{
/// <summary>
/// The default size for a <see cref="Segment"/>
/// </summary>
private const nuint _defaultSegmentSize = 4194304; // 4 MiB
/// <summary>
/// The size that a new <see cref="Segment"/> should be
/// </summary>
private nuint _currentSegmentSize;
/// <summary>
/// Queue of free segments
/// </summary>
private readonly Stack<IntPtr> _freeSegments = new();
/// <summary>
/// List of active memory segments
/// </summary>
private readonly List<IntPtr> _activeSegments = [];
/// <summary>
/// Tracks the total amount of allocated bytes
/// </summary>
private nuint _totalAllocated = 0;
/// <summary>
/// Tracks the total amount of allocated bytes current in use
/// </summary>
private nuint _totalUsed = 0;
/// <summary>
/// Internal lock, ensures thread safety while maintaining a simple interface
/// </summary>
private readonly Lock _lock = new();
/// <summary>
/// Indicates whether the <see cref="SegmentedPool"/> has been disposed.
/// </summary>
private volatile bool _disposed;
/// <summary>
/// Pointer to the currently in use <see cref="Segment"/>
/// </summary>
private Segment* _current;
private readonly IUnmanagedAllocator _allocator;
/// <summary>
/// Represents a memory segment in the <see cref="SegmentedPool"/>.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><description><see cref="Ptr"/> Pointer to the start of the unmanaged memory block</description></item>
/// <item><description><see cref="Offset"/> indicates the current allocation position within the segment. Each new allocation advances this offset by the number of bytes allocated.</description></item>
/// <item><description><see cref="Size"/> is the total size, in bytes, of the segment.</description></item>
/// <item><description>Segments are managed internally by the <see cref="SegmentedPool"/> and should not be modified directly outside of the pool.</description></item>
/// <item><description>A <see cref="Segment"/> is a contiguous block of unmanaged memory from which allocations are served sequentially via bump allocation.</description></item>
/// </list>
/// </remarks>
private struct Segment
{
/// <summary>
/// Pointer to the start of the unmanaged memory block.
/// </summary>
public byte* Ptr;
/// <summary>
/// The current offset into the <see cref="Segment"/> where the next allocation will occur.
/// </summary>
public nuint Offset;
/// <summary>
/// Total size of the <see cref="Segment"/> in bytes.
/// </summary>
public nuint Size;
}
/// <summary>
/// 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="initialSegments">Number of segments to pre-allocate to the pool</param>
/// <exception cref="ArgumentException">
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
/// </exception>
public SegmentedPool(nuint segmentSize = _defaultSegmentSize, int initialSegments = 4)
: this(segmentSize, initialSegments, new DefaultUnmanagedAllocator())
{
}
/// <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="initialSegments">Number of segments to pre-allocate to the pool</param>
/// <param name="allocator">IUnmanagedAllocator instance that implements the allocator</param>
/// <exception cref="ArgumentException">
/// Thrown if <paramref name="segmentSize"/> is zero, or if <paramref name="initialSegments"/> is less than 1.
/// </exception>
internal SegmentedPool(nuint segmentSize, int initialSegments, IUnmanagedAllocator allocator)
{
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;
// Pre-allocate segments
for (int i = 0; i < initialSegments; i++)
{
_freeSegments.Push((IntPtr)AllocateNewSegment(_currentSegmentSize));
}
_current = (Segment*)_freeSegments.Pop();
_activeSegments.Add((IntPtr)_current);
}
/// <summary>
/// Gets the number of free segments available for use in the pool.
/// </summary>
/// <returns>The number of free segments.</returns>
public int FreeSegmentCount
{
get { return _freeSegments.Count; }
}
/// <summary>
/// Gets the number of segments currently in use in the pool.
/// </summary>
/// <returns>The number of currently active Segments.</returns>
public int ActiveSegmentCount
{
get { return _activeSegments.Count; }
}
/// <summary>
/// Returns the total number of bytes that have currently been allocated.
/// </summary>
/// <returns>The total number of bytes that have been allocated.</returns>
public nuint TotalAllocatedBytes
{
get { return _totalAllocated; }
}
/// <summary>
/// Gets the total number of bytes currently in use across all active segments.
/// </summary>
/// <returns>The total number of bytes that are in use.</returns>
public nuint TotalUsedBytes
{
get { return _totalUsed; }
}
/// <summary>
/// Gets the size, in bytes, used when allocating new <see cref="Segment"/> instances.
/// </summary>
/// <remarks>
/// This reflects the most recently configured size and affects only future <see cref="Segment"/> allocations.
/// </remarks>
public nuint CurrentSegmentSize
{
get { return _currentSegmentSize; }
}
/// <summary>
/// Gets a value indicating whether this <see cref="SegmentedPool"/> instance has been disposed.
/// </summary>
/// <returns>True, if the current <see cref="SegmentedPool"/> instance has been disposed of, false otherwise.</returns>
/// <remarks>
/// Once disposed, further calls to allocation or reset methods will throw <see cref="ObjectDisposedException"/>.
/// </remarks>
public bool IsDisposed
{
get { return _disposed; }
}
/// <summary>
/// Sets the current <see cref="Segment"/> size used for subsequent allocations.
/// </summary>
/// <param name="newSize">
/// The new segment size, in bytes, that will be used when allocating future <see cref="Segment"/> instances.
/// Must be greater than zero.
/// </param>
/// <remarks>
/// This does not affect any <see cref="Segment"/> that have already been allocated or are currently active.
/// Only new <see cref="Segment"/> created after calling this method will use the updated size.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="newSize"/> is zero.
/// </exception>
public void SetSegmentSize(nuint newSize)
{
ThrowIfDisposed();
if (newSize == 0)
{
throw new ArgumentOutOfRangeException(nameof(newSize), "Segment size must be greater than zero.");
}
_currentSegmentSize = newSize;
}
/// <summary>
/// Resets the current <see cref="Segment"/> size back to the default 4 MiB
/// </summary>
/// <remarks>
/// Future <see cref="Segment"/> allocations will revert to using this default size
/// Active and free <see cref="Segment"/> are not modified.
/// </remarks>
public void ResetSegmentSize()
{
ThrowIfDisposed();
_currentSegmentSize = _defaultSegmentSize;
}
/// <summary>
/// Allocates a 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="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 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>
public Span<T> Allocate<T>(int count) where T : unmanaged
{
ThrowIfDisposed();
if (count <= 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "Allocation count must be greater than zero.");
}
if ((nuint)count > nuint.MaxValue / (nuint)(sizeof(T)))
{
throw new OverflowException($"Requested allocation of {count} elements of type {typeof(T)} exceeds allowable maximum memory size.");
}
nuint bytes = (nuint)(count * sizeof(T));
lock (_lock)
{
// Enough space in current segment?
if (_current->Offset + bytes > _current->Size)
SwitchSegment(bytes);
T* ptr = (T*)(_current->Ptr + _current->Offset);
_current->Offset += bytes;
_totalUsed += bytes;
return new Span<T>(ptr, count);
}
}
/// <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;
}
_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>
/// Frees unused <see cref="Segment"/> in the free pool, reducing unmanaged memory usage.
/// </summary>
/// <param name="minFreeSegments"> Minimum number of free segments to retain for future allocations. Defaults to <c>16</c>. </param>
/// <remarks>
/// Segments beyond the retained count will have their unmanaged memory released back to the system.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="minFreeSegments"/> is negative.
/// </exception>
public void Trim(int minFreeSegments = 16)
{
ThrowIfDisposed();
ArgumentOutOfRangeException.ThrowIfNegative(minFreeSegments, nameof(minFreeSegments));
lock (_lock)
{
while (_freeSegments.Count > minFreeSegments)
{
var ip = _freeSegments.Pop();
Segment* segment = (Segment*)ip;
// Free the unmanaged memory
_allocator.Free(segment->Ptr);
_totalAllocated -= segment->Size;
_allocator.Free(segment);
}
}
}
/// <summary>
/// Resets the allocator, returning all active segments to the free pool.
/// </summary>
/// <param name="trim">If true, trims the <see cref="Segment"/> in the free pool. <see cref="Trim"/></param>
public void Reset(bool trim = false)
{
ThrowIfDisposed();
lock (_lock)
{
foreach (var ip in _activeSegments)
{
Segment* segment = (Segment*)ip;
segment->Offset = 0;
_freeSegments.Push(ip);
}
_activeSegments.Clear();
_totalUsed = 0;
if (_freeSegments.Count > 0)
{
_current = (Segment*)_freeSegments.Pop();
_activeSegments.Add((IntPtr)_current);
}
else
{
// This should not be hit in normal circumstances as we always have _current
_current = AllocateNewSegment(_currentSegmentSize);
_activeSegments.Add((IntPtr)_current);
}
// Optionally trim excess free segments after reset
if (trim)
{
Trim();
}
}
}
/// <summary>
/// Releases all unmanaged memory allocated by the <see cref="SegmentedPool"/> and clears internal state.
/// After calling this method, the pool can no longer be used for allocations.
/// </summary>
/// <remarks>
/// After disposal, all allocations become invalid and any further operations will throw <see cref="ObjectDisposedException"/>.
/// This method is thread-safe and may be called multiple times safely.
/// </remarks>
public void Dispose()
{
lock (_lock)
{
if (_disposed)
{
return;
}
// Free active pages
foreach (var ip in _activeSegments)
{
Segment* segment = (Segment*)ip;
_allocator.Free(segment->Ptr);
_allocator.Free(segment);
}
// Free free pages
foreach (var ip in _freeSegments)
{
Segment* segment = (Segment*)ip;
_allocator.Free(segment->Ptr);
_allocator.Free(segment);
}
_activeSegments.Clear();
_freeSegments.Clear();
_current = null;
_totalAllocated = 0;
_totalUsed = 0;
_disposed = true;
}
}
/// <summary>
/// Throws an <see cref="ObjectDisposedException"/> if the <see cref="SegmentedPool"/> has already been disposed.
/// </summary>
/// <exception cref="ObjectDisposedException">
/// Thrown when this instance is no longer valid for use.
/// </exception>
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36518.9
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnmanagedMMU", "UnmanagedMMU.csproj", "{AEC4A989-1403-4FD3-A0C1-F20000E7C1D9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnmanagedMMUTests", "..\UnmanagedMMUTests\UnmanagedMMUTests.csproj", "{FABDC6C0-CEC5-4DA0-BAF6-FBBFBDFF75D2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AEC4A989-1403-4FD3-A0C1-F20000E7C1D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEC4A989-1403-4FD3-A0C1-F20000E7C1D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEC4A989-1403-4FD3-A0C1-F20000E7C1D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEC4A989-1403-4FD3-A0C1-F20000E7C1D9}.Release|Any CPU.Build.0 = Release|Any CPU
{FABDC6C0-CEC5-4DA0-BAF6-FBBFBDFF75D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FABDC6C0-CEC5-4DA0-BAF6-FBBFBDFF75D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FABDC6C0-CEC5-4DA0-BAF6-FBBFBDFF75D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FABDC6C0-CEC5-4DA0-BAF6-FBBFBDFF75D2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2C250EB-D2A6-4245-8B9F-16D2FE7A3E0F}
EndGlobalSection
EndGlobal