feat(buffer)!: refactor allocators to use MemoryPool<T>

Refactor memory allocation system to use generic MemoryPool<TAllocator, TOpts> for arena, stack, and free list allocators, replacing custom allocator structs. Introduce MemoryBlock as a safer, more robust replacement for UnTypedArray. Improve thread safety, safety checks, and documentation. Reorder and clarify Allocator enum. Add comprehensive unit tests for all allocators and pointer assertion utilities. Update project to enable safety checks in Debug builds. Remove obsolete interfaces and ensure consistent deallocation with MemoryUtility.Free.

BREAKING CHANGE: Custom allocator structs are removed and replaced with MemoryPool-based abstraction. UnTypedArray is replaced by MemoryBlock. Allocator enum order and semantics are changed. Public API changes may require code updates.
This commit is contained in:
2026-04-04 19:24:02 +09:00
parent 208e1aa975
commit 28e921c48d
18 changed files with 1284 additions and 505 deletions

View File

@@ -22,6 +22,13 @@ jobs:
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Run tests
# Run all test projects in the repository. If any test fails, this step will exit non-zero
# and subsequent packaging/publish steps will not run.
run: |
echo "Running all tests..."
dotnet test --configuration Release --no-restore
- name: Check, Build, and Pack Projects - name: Check, Build, and Pack Projects
env: env:
NUGET_API_KEY: ${{ secrets.NUGET_TOKEN }} NUGET_API_KEY: ${{ secrets.NUGET_TOKEN }}

View File

@@ -1,7 +1,5 @@
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
using Misaki.HighPerformance.Collections; using Misaki.HighPerformance.Collections;
using System.Collections.Concurrent;
#endif #endif
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -52,6 +50,16 @@ public readonly struct AllocationManagerInitOpts
get; init; get; init;
} }
public nuint FreeListChunkSize
{
get; init;
}
public nuint FreeListDefaultAlignment
{
get; init;
}
public int FreeListConcurrencyLevel public int FreeListConcurrencyLevel
{ {
get; init; get; init;
@@ -61,6 +69,8 @@ public readonly struct AllocationManagerInitOpts
{ {
ArenaCapacity = 1024 * 1024 * 1024, // 1 GB ArenaCapacity = 1024 * 1024 * 1024, // 1 GB
StackCapacity = 16 * 1024 * 1024, // 16 MB per thread StackCapacity = 16 * 1024 * 1024, // 16 MB per thread
FreeListChunkSize = 64 * 1024 * 1024,
FreeListDefaultAlignment = 8,
FreeListConcurrencyLevel = Environment.ProcessorCount FreeListConcurrencyLevel = Environment.ProcessorCount
}; };
} }
@@ -71,93 +81,6 @@ public readonly struct AllocationManagerInitOpts
/// </summary> /// </summary>
public static unsafe class AllocationManager public static unsafe class AllocationManager
{ {
private struct ArenaAllocator : IAllocator, IDisposable
{
private const int _ARENA_MAGIC_ID = -3941029;
private VirtualArena _arena;
private int _currentTick;
private AllocationHandle _handle;
public readonly AllocationHandle Handle => _handle;
public readonly int CurrentTick => _currentTick;
public void Init(nuint capacity)
{
_arena = new VirtualArena(capacity);
_handle = new AllocationHandle
{
State = Unsafe.AsPointer(ref this),
Alloc = &Allocate,
Realloc = &Reallocate,
Free = null,
#if MHP_ENABLE_SAFETY_CHECKS
IsValid = &IsValid
#else
IsValid = null
#endif
};
_currentTick = 0;
}
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
var selfPtr = (ArenaAllocator*)instance;
var ptr = selfPtr->_arena.Allocate(size, alignment, allocationOption);
if (ptr == null)
{
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = MemoryHandle.Invalid;
#endif
return null;
}
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = new MemoryHandle(_ARENA_MAGIC_ID, selfPtr->_currentTick);
#endif
return ptr;
}
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
var selfPtr = (ArenaAllocator*)instance;
var newPtr = selfPtr->_arena.Reallocate(ptr, oldSize, newSize, alignment, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = new MemoryHandle(_ARENA_MAGIC_ID, selfPtr->_currentTick);
#endif
return newPtr;
}
#if MHP_ENABLE_SAFETY_CHECKS
private static bool IsValid(void* instance, MemoryHandle handle)
{
var selfPtr = (ArenaAllocator*)instance;
return handle.ID == _ARENA_MAGIC_ID && handle.Generation == selfPtr->_currentTick;
}
#endif
public void Reset()
{
_arena.Reset();
_currentTick++;
}
public void Dispose()
{
_arena.Dispose();
}
}
private struct HeapAllocator : IAllocator private struct HeapAllocator : IAllocator
{ {
private AllocationHandle _handle; private AllocationHandle _handle;
@@ -186,384 +109,6 @@ public static unsafe class AllocationManager
#endif #endif
) )
{ {
return HeapAlloc(size, alignment, allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, pHandle
#endif
);
}
private static void* Reallocate(void* _, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
if (ptr == null)
{
return Allocate(null, newSize, alignment, allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, pHandle
#endif
);
}
#if MHP_ENABLE_SAFETY_CHECKS
MemoryHandle newHandle;
#endif
var newPtr = HeapAlloc(newSize, alignment, allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, &newHandle
#endif
);
if (newPtr == null)
{
return null;
}
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
HeapFree(ptr
#if MHP_ENABLE_SAFETY_CHECKS
, *pHandle
#endif
);
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = newHandle;
#endif
return newPtr;
}
private static void Free(void* _, void* ptr
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle handle
#endif
)
{
HeapFree(ptr
#if MHP_ENABLE_SAFETY_CHECKS
, handle
#endif
);
}
#if MHP_ENABLE_SAFETY_CHECKS
private static bool IsValid(void* _, MemoryHandle handle)
{
return ContainsAllocation(handle);
}
#endif
}
private struct StackAllocator : IAllocator, IDisposable
{
private const int _STACK_MAGIC_ID = -6843541;
private static void** s_pStackBuffers = null;
private static int s_stackCount = 0;
private static int s_stackCapacity = 0;
private static readonly SpinLock s_locker = new SpinLock(false);
[ThreadStatic]
private static VirtualStack s_stack;
private AllocationHandle _handle;
public readonly AllocationHandle Handle => _handle;
public void Init()
{
_handle = new AllocationHandle
{
State = Unsafe.AsPointer(ref this),
Alloc = &Allocate,
Realloc = &Reallocate,
Free = null,
#if MHP_ENABLE_SAFETY_CHECKS
IsValid = &IsValid
#else
IsValid = null
#endif
};
}
private static void EnsureInitialize()
{
if (s_stack.Buffer == null)
{
s_stack = new VirtualStack(s_threadLocalStackDefaultSize);
var token = false;
try
{
s_locker.Enter(ref token);
if (s_pStackBuffers == null)
{
s_pStackBuffers = (void**)Malloc((nuint)(sizeof(void*) * Environment.ProcessorCount));
s_stackCapacity = Environment.ProcessorCount;
}
if (s_stackCount >= s_stackCapacity)
{
var pOld = s_pStackBuffers;
var newCapacity = s_stackCapacity * 2;
var pNew = (void**)Realloc(pOld, (nuint)(sizeof(void*) * newCapacity));
s_pStackBuffers = pNew;
s_stackCapacity = newCapacity;
}
s_pStackBuffers[s_stackCount] = s_stack.Buffer;
s_stackCount++;
}
finally
{
if (token)
{
s_locker.Exit();
}
}
}
}
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
EnsureInitialize();
var ptr = s_stack.Allocate(size, alignment, allocationOption);
if (ptr == null)
{
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = MemoryHandle.Invalid;
#endif
return null;
}
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
#endif
return ptr;
}
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
EnsureInitialize();
var newPtr = s_stack.Reallocate(ptr, oldSize, newSize, alignment, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
#endif
return newPtr;
}
#if MHP_ENABLE_SAFETY_CHECKS
private static bool IsValid(void* instance, MemoryHandle handle)
{
return handle.ID == _STACK_MAGIC_ID && handle.Generation <= (int)s_stack.Offset;
}
#endif
public static VirtualStack.Scope CreateScope(StackAllocator* pSelf)
{
EnsureInitialize();
return s_stack.CreateScope(pSelf->_handle);
}
public readonly void Dispose()
{
if (s_pStackBuffers == null)
{
return;
}
for (var i = 0; i < s_stackCount; i++)
{
Munmap(s_pStackBuffers[i], s_threadLocalStackDefaultSize);
}
}
}
private struct FreeListAllocator : IAllocator, IDisposable
{
private FreeList _freeList;
private AllocationHandle _handle;
public readonly AllocationHandle Handle => _handle;
public void Init(int concurrencyLevel)
{
_freeList = new FreeList(8, 64 * 1024, concurrencyLevel);
_handle = new AllocationHandle
{
State = Unsafe.AsPointer(ref this),
Alloc = &Allocate,
Realloc = &Reallocate,
Free = &Free,
#if MHP_ENABLE_SAFETY_CHECKS
IsValid = &IsValid
#else
IsValid = null
#endif
};
}
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
var selfPtr = (FreeListAllocator*)instance;
var ptr = selfPtr->_freeList.Allocate(size, alignment, allocationOption);
if (ptr == null)
{
return null;
}
#if MHP_ENABLE_SAFETY_CHECKS
*pHandle = AddAllocation(ptr, size);
#endif
return ptr;
}
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
var selfPtr = (FreeListAllocator*)instance;
var newPtr = selfPtr->_freeList.Reallocate(ptr, oldSize, newSize, alignment, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
RemoveAllocation(*pHandle);
*pHandle = AddAllocation(newPtr, newSize);
#endif
return newPtr;
}
#if MHP_ENABLE_SAFETY_CHECKS
private static bool IsValid(void* instance, MemoryHandle handle)
{
return ContainsAllocation(handle);
}
#endif
private static void Free(void* instance, void* ptr
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle handle
#endif
)
{
var selfPtr = (FreeListAllocator*)instance;
selfPtr->_freeList.Free(ptr);
#if MHP_ENABLE_SAFETY_CHECKS
RemoveAllocation(handle);
#endif
}
public void Dispose()
{
_freeList.Dispose();
}
}
private static ArenaAllocator* s_pArenaAllocator;
private static HeapAllocator* s_pHeapAllocator;
private static StackAllocator* s_pStackAllocator;
private static FreeListAllocator* s_pFreeListAllocator;
#if MHP_ENABLE_SAFETY_CHECKS
private static ConcurrentSlotMap<AllocationInfo> s_allocations = null!;
#endif
/// <summary>
/// Gets the number of live tracked heap allocations. Always returns 0 if MHP_ENABLE_SAFETY_CHECKS is disabled.
/// </summary>
public static int LiveAllocationCount =>
#if MHP_ENABLE_SAFETY_CHECKS
s_allocations.Count;
#else
0;
#endif
private static volatile bool s_initialized;
private static nuint s_threadLocalStackDefaultSize;
public static void Initialize(AllocationManagerInitOpts opts)
{
if (s_initialized)
{
return;
}
#if MHP_ENABLE_SAFETY_CHECKS
s_allocations = new ConcurrentSlotMap<AllocationInfo>(256);
#endif
var ptr = (byte*)Malloc((nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator) + sizeof(FreeListAllocator)));
s_pArenaAllocator = (ArenaAllocator*)ptr;
s_pHeapAllocator = (HeapAllocator*)(ptr + sizeof(ArenaAllocator));
s_pStackAllocator = (StackAllocator*)(ptr + sizeof(ArenaAllocator) + sizeof(HeapAllocator));
s_pFreeListAllocator = (FreeListAllocator*)(ptr + sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator));
s_pArenaAllocator->Init(opts.ArenaCapacity);
s_pHeapAllocator->Init();
s_pStackAllocator->Init();
s_pFreeListAllocator->Init(opts.FreeListConcurrencyLevel);
s_threadLocalStackDefaultSize = opts.StackCapacity;
s_initialized = true;
}
/// <summary>
/// Gets a reference to the allocation pHandle for the specified allocator type.
/// </summary>
/// <param name="allocator">The allocator type for which to retrieve the allocation pHandle.</param>
/// <returns>A reference to the allocation pHandle associated with the specified allocator type.</returns>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AllocationHandle GetAllocationHandle(Allocator allocator)
{
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
return allocator switch
{
Allocator.Temp => s_pArenaAllocator->Handle,
Allocator.Persistent => s_pHeapAllocator->Handle,
Allocator.FreeList => s_pFreeListAllocator->Handle,
_ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)),
};
}
/// <summary>
/// Allocates a block of memory from the heap with the specified newSize and alignment, using the given allocation options.
/// </summary>
/// <param name="size">The number of bytes to allocate. Must be greater than zero.</param>
/// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two.</param>
/// <param name="allocationOption">An optional set of flags that control allocation behavior, such as whether the memory should be cleared or
/// tracked. The default is <see cref="AllocationOption.None"/>.</param>
/// <returns>A pointer to the beginning of the allocated memory block.</returns>
/// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception>
public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle* pHandle
#endif
)
{
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
var ptr = AlignedAlloc(size, alignment); var ptr = AlignedAlloc(size, alignment);
if (ptr == null) if (ptr == null)
{ {
@@ -584,45 +129,189 @@ public static unsafe class AllocationManager
return ptr; return ptr;
} }
/// <summary> private static void* Reallocate(void* _, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption
/// Releases a block of unmanaged memory previously allocated by the heap allocator. #if MHP_ENABLE_SAFETY_CHECKS
/// </summary> , MemoryHandle* pHandle
/// <param name="ptr">A pointer to the memory block to be freed. The pointer must have been returned by a compatible heap allocation #endif
/// method and must not be null.</param> )
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param> {
public static void HeapFree(void* ptr var newPtr = AlignedRealloc(ptr, newSize, alignment);
#if MHP_ENABLE_SAFETY_CHECKS
if (ptr == null && newPtr != null)
{
AddAllocation(newPtr, newSize);
}
else
{
if (newPtr == null)
{
RemoveAllocation(*pHandle);
}
else
{
UpdateAllocation(*pHandle, newPtr, newSize);
}
}
#endif
if (newPtr == null)
{
return null;
}
if (allocationOption.HasFlag(AllocationOption.Clear) && newSize > oldSize)
{
var offset = (byte*)newPtr + oldSize;
var clearSize = newSize - oldSize;
MemClear(offset, clearSize);
}
return newPtr;
}
private static void Free(void* _, void* ptr
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
, MemoryHandle handle , MemoryHandle handle
#endif #endif
) )
{ {
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
AlignedFree(ptr); AlignedFree(ptr);
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
RemoveAllocation(handle); RemoveAllocation(handle);
#endif #endif
} }
/// <summary>
/// Releases a block of unmanaged memory previously allocated by the heap allocator.
/// </summary>
/// <remarks>
/// No ops when MHP_ENABLE_SAFETY_CHECKS is disabled, as we cannot fetch the allocation info from the handle to get the pointer to free.
/// </remarks>
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param>
public static void HeapFree(MemoryHandle handle)
{
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
Debug.Assert(s_initialized, "AllocationManager is not initialized."); private static bool IsValid(void* _, MemoryHandle handle)
if (TryGetAllocation(handle, out var info))
{ {
HeapFree((void*)info.Address, handle); return ContainsAllocation(handle);
} }
#endif #endif
// No-op when safety checks are disabled, as we cannot fetch the allocation info from the handle. }
private static MemoryPool<VirtualArena, VirtualArena.CreationOptions> s_arenaAllocator;
private static MemoryPool<FreeList, FreeList.CreationOptions> s_freeListAllocator;
[ThreadStatic]
private static MemoryPool<VirtualStack, VirtualStack.CreationOptions> t_stackAllocator;
private static HeapAllocator* s_pHeapAllocator;
#if MHP_ENABLE_SAFETY_CHECKS
private static ConcurrentSlotMap<AllocationInfo> s_allocations = null!;
#endif
/// <summary>
/// Gets the number of live tracked heap allocations. Always returns 0 if MHP_ENABLE_SAFETY_CHECKS is disabled.
/// </summary>
public static int LiveAllocationCount =>
#if MHP_ENABLE_SAFETY_CHECKS
s_allocations.Count;
#else
0;
#endif
private static volatile bool s_initialized;
private static nuint s_threadLocalStackSize;
private static readonly SpinLock s_stackLocker = new SpinLock(false);
private static VirtualStack** s_ppStack;
private static int s_ppStackCount;
private static int s_ppStackCapacity;
private static void EnsureThreadLocalStackInitialize()
{
if (Unsafe.IsNullRef(ref t_stackAllocator.Allocator))
{
t_stackAllocator = new MemoryPool<VirtualStack, VirtualStack.CreationOptions>(new VirtualStack.CreationOptions
{
reserveCapacity = s_threadLocalStackSize
});
var token = false;
try
{
s_stackLocker.Enter(ref token);
if (s_ppStack == null)
{
s_ppStack = (VirtualStack**)Malloc((nuint)(sizeof(VirtualStack*) * Environment.ProcessorCount));
s_ppStackCapacity = Environment.ProcessorCount;
}
if (s_ppStackCount >= s_ppStackCapacity)
{
var pOld = s_ppStack;
var newCapacity = s_ppStackCapacity * 2;
var pNew = (VirtualStack**)Realloc(pOld, (nuint)(sizeof(VirtualStack*) * newCapacity));
s_ppStack = pNew;
s_ppStackCapacity = newCapacity;
}
s_ppStack[s_ppStackCount] = (VirtualStack*)Unsafe.AsPointer(ref t_stackAllocator.Allocator);
var test = s_ppStack[s_ppStackCount];
s_ppStackCount++;
}
finally
{
if (token)
{
s_stackLocker.Exit();
}
}
}
}
public static void Initialize(AllocationManagerInitOpts opts)
{
if (s_initialized)
{
return;
}
#if MHP_ENABLE_SAFETY_CHECKS
s_allocations = new ConcurrentSlotMap<AllocationInfo>(256);
#endif
s_arenaAllocator = new MemoryPool<VirtualArena, VirtualArena.CreationOptions>(new VirtualArena.CreationOptions
{
reserveCapacity = opts.ArenaCapacity
});
s_freeListAllocator = new MemoryPool<FreeList, FreeList.CreationOptions>(new FreeList.CreationOptions
{
alignment = opts.FreeListDefaultAlignment,
chunkSize = opts.FreeListChunkSize,
maxConcurrencyLevel = opts.FreeListConcurrencyLevel
});
s_pHeapAllocator = (HeapAllocator*)Malloc((nuint)(sizeof(HeapAllocator)));
s_pHeapAllocator->Init();
s_threadLocalStackSize = opts.StackCapacity;
s_initialized = true;
}
/// <summary>
/// Gets a reference to the allocation pHandle for the specified allocator type.
/// </summary>
/// <param name="allocator">The allocator type for which to retrieve the allocation pHandle.</param>
/// <returns>A reference to the allocation pHandle associated with the specified allocator type.</returns>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AllocationHandle GetAllocationHandle(Allocator allocator)
{
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
return allocator switch
{
Allocator.Temp => s_arenaAllocator.AllocationHandle,
Allocator.Persistent => s_pHeapAllocator->Handle,
Allocator.FreeList => s_freeListAllocator.AllocationHandle,
_ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)),
};
} }
/// <summary> /// <summary>
@@ -632,7 +321,7 @@ public static unsafe class AllocationManager
public static void ResetTempAllocator() public static void ResetTempAllocator()
{ {
Debug.Assert(s_initialized, "AllocationManager is not initialized."); Debug.Assert(s_initialized, "AllocationManager is not initialized.");
s_pArenaAllocator->Reset(); s_arenaAllocator.Allocator.Reset();
} }
/// <summary> /// <summary>
@@ -643,7 +332,9 @@ public static unsafe class AllocationManager
public static VirtualStack.Scope CreateStackScope() public static VirtualStack.Scope CreateStackScope()
{ {
Debug.Assert(s_initialized, "AllocationManager is not initialized."); Debug.Assert(s_initialized, "AllocationManager is not initialized.");
return StackAllocator.CreateScope(s_pStackAllocator);
EnsureThreadLocalStackInitialize();
return t_stackAllocator.Allocator.CreateScope(t_stackAllocator.AllocationHandle);
} }
/// <summary> /// <summary>
@@ -677,6 +368,24 @@ public static unsafe class AllocationManager
#endif #endif
} }
public static void UpdateAllocation(MemoryHandle handle, void* newPtr, nuint newSize)
{
#if MHP_ENABLE_SAFETY_CHECKS
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
if (s_allocations.TryGetElement(handle.ID, handle.Generation, out var oldInfo))
{
var newInfo = oldInfo with
{
Address = (IntPtr)newPtr,
Size = newSize
};
s_allocations.UpdateElement(handle.ID, handle.Generation, newInfo);
}
#endif
}
/// <summary> /// <summary>
/// Removes the memory allocation associated with the specified handle. /// Removes the memory allocation associated with the specified handle.
/// </summary> /// </summary>
@@ -781,10 +490,29 @@ public static unsafe class AllocationManager
} }
#endif #endif
s_pArenaAllocator->Dispose(); s_arenaAllocator.Dispose();
s_pStackAllocator->Dispose(); s_freeListAllocator.Dispose();
s_pFreeListAllocator->Dispose();
Free(s_pArenaAllocator); if (s_ppStack != null)
{
for (var i = 0; i < s_ppStackCount; i++)
{
var pStack = s_ppStack[i];
if (pStack != null)
{
pStack->Dispose();
Free(pStack);
}
}
Free(s_ppStack);
s_ppStack = null;
}
if (s_pHeapAllocator != null)
{
Free(s_pHeapAllocator);
s_pHeapAllocator = null;
}
} }
} }

View File

@@ -16,17 +16,20 @@ public enum AllocationOption : byte
public enum Allocator : byte public enum Allocator : byte
{ {
// Make the first allocator as invalid because we don't want to user create a default collection without passing any parameters // Make the first allocator as invalid because we don't want to user create a default collection without passing any parameters
/// <summary>
/// The invalid allocator. This value is reserved and should not be used for actual memory allocations. It can be used to indicate an uninitialized or invalid state in allocation scenarios.
/// </summary>
Invalid, Invalid,
/// <summary> /// <summary>
/// Allocator for temporary allocations. Allocations are automatically released after use automatically. /// Allocator for temporary allocations. Allocations are automatically released after use automatically.
/// </summary> /// </summary>
Temp, Temp,
/// <summary> /// <summary>
/// Allocator for persistent allocations using a free list. Allocations are not automatically released after use, but can be reused to reduce fragmentation, system call and improve performance.
/// </summary>
FreeList,
/// <summary>
/// Allocator for persistent allocations. Allocations are not automatically released after use. /// Allocator for persistent allocations. Allocations are not automatically released after use.
/// </summary> /// </summary>
Persistent, Persistent,
/// <summary>
/// Allocator for persistent allocations using a free list. Allocations are not automatically released after use, but can be reused to reduce fragmentation and improve performance.
/// </summary>
FreeList
} }

View File

@@ -1,3 +1,4 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -158,7 +159,7 @@ public unsafe struct Arena : IMemoryAllocator<Arena, Arena.CreationOptions>
return; return;
} }
Free(_buffer); MemoryUtility.Free(_buffer);
_buffer = null; _buffer = null;
_size = 0; _size = 0;

View File

@@ -1,3 +1,4 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -187,7 +188,7 @@ public unsafe struct DynamicArena : IMemoryAllocator<DynamicArena, DynamicArena.
{ {
var next = current->next; var next = current->next;
current->arena.Dispose(); current->arena.Dispose();
Free(current); MemoryUtility.Free(current);
current = next; current = next;
} }

View File

@@ -1,3 +1,4 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -728,13 +729,13 @@ public unsafe struct FreeList : IMemoryAllocator<FreeList, FreeList.CreationOpti
if (_caches != null) if (_caches != null)
{ {
Free(_caches); MemoryUtility.Free(_caches);
_caches = null; _caches = null;
} }
if (_instanceId != null) if (_instanceId != null)
{ {
Free(_instanceId); MemoryUtility.Free(_instanceId);
_instanceId = null; _instanceId = null;
} }

View File

@@ -1,12 +1,10 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections; namespace Misaki.HighPerformance.LowLevel.Buffer;
public unsafe struct UnTypedArray : IUnTypedCollection public unsafe struct MemoryBlock : IDisposable
{ {
private void* _buffer; private void* _buffer;
private nuint _size; private nuint _size;
@@ -42,20 +40,14 @@ public unsafe struct UnTypedArray : IUnTypedCollection
} }
} }
/// <summary> public MemoryBlock()
/// Constructs an UnsafeArray with a default size of 1 and uses the Persistent allocator. : this(0, 0, Allocator.Invalid)
/// </summary>
public UnTypedArray()
: this(0, 8, Allocator.Invalid)
{ {
} }
public UnTypedArray(nuint size, nuint alignment, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None) public MemoryBlock(nuint size, nuint alignment, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{ {
if (size <= 0) ArgumentOutOfRangeException.ThrowIfNegative(size);
{
throw new ArgumentOutOfRangeException(nameof(size), "Count must be greater than zero.");
}
if (handle.Alloc == null) if (handle.Alloc == null)
{ {
@@ -65,7 +57,7 @@ public unsafe struct UnTypedArray : IUnTypedCollection
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
MemoryHandle memHandle; MemoryHandle memHandle;
#endif #endif
var buff = handle.Alloc(handle.State, size, alignment, allocationOption _buffer = handle.Alloc(handle.State, size, alignment, allocationOption
#if MHP_ENABLE_SAFETY_CHECKS #if MHP_ENABLE_SAFETY_CHECKS
, &memHandle , &memHandle
#endif #endif
@@ -86,7 +78,7 @@ public unsafe struct UnTypedArray : IUnTypedCollection
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param> /// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
/// <param name="allocationOption">Determines how the memory should be allocated.</param> /// <param name="allocationOption">Determines how the memory should be allocated.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
public UnTypedArray(nuint size, nuint alignment, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) public MemoryBlock(nuint size, nuint alignment, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(size, alignment, AllocationManager.GetAllocationHandle(allocator), allocationOption) : this(size, alignment, AllocationManager.GetAllocationHandle(allocator), allocationOption)
{ {
} }
@@ -101,7 +93,7 @@ public unsafe struct UnTypedArray : IUnTypedCollection
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed. /// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing. /// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
/// </remarks> /// </remarks>
public UnTypedArray(void* buffer, uint size) public MemoryBlock(void* buffer, uint size)
{ {
_buffer = buffer; _buffer = buffer;
_size = size; _size = size;
@@ -111,6 +103,13 @@ public unsafe struct UnTypedArray : IUnTypedCollection
public readonly ref T GetElementAt<T>(nuint index) public readonly ref T GetElementAt<T>(nuint index)
where T : unmanaged where T : unmanaged
{ {
#if MHP_ENABLE_SAFETY_CHECKS
if (index * (uint)sizeof(T) >= _size)
{
throw new IndexOutOfRangeException($"Index {index} is out of range for collection of size {_size / (uint)sizeof(T)}.");
}
#endif
return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index); return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index);
} }
@@ -245,10 +244,12 @@ public unsafe struct UnTypedArray : IUnTypedCollection
/// <summary> /// <summary>
/// Converts into a Span for efficient memory access. /// Converts into a Span for efficient memory access.
/// </summary> /// </summary>
/// <returns>A Span that provides a view over the elements of the UnsafeCollection.</returns> /// <returns>A <see cref="Span{T}"/> that provides a view over the elements of the UnsafeCollection.</returns>
public readonly Span<byte> AsSpan<C>() public readonly Span<T> AsSpan<T>()
where T : unmanaged
{ {
return new(_buffer, (int)_size); Debug.Assert(_size % (uint)sizeof(T) == 0, "The size of the collection must be a multiple of the size of the element type.");
return new Span<T>(_buffer, (int)_size / sizeof(T));
} }
/// <summary> /// <summary>
@@ -257,15 +258,16 @@ public unsafe struct UnTypedArray : IUnTypedCollection
/// <param name="start">The zero-based index of the first element in the collection to include in the span. Must be greater than or equal to zero and less than the number of elements in the collection.</param> /// <param name="start">The zero-based index of the first element in the collection to include in the span. Must be greater than or equal to zero and less than the number of elements in the collection.</param>
/// <param name="length">The number of elements to include in the span. Must be greater than or equal to zero and the range defined by /// <param name="length">The number of elements to include in the span. Must be greater than or equal to zero and the range defined by
/// <paramref name="start"/> and <paramref name="length"/> must not exceed the bounds of the collection.</param> /// <paramref name="start"/> and <paramref name="length"/> must not exceed the bounds of the collection.</param>
/// <returns>A <see cref="Span{byte}"/> representing the specified region of the collection.</returns> /// <returns>A <see cref="Span{T}"/> representing the specified region of the collection.</returns>
public readonly Span<byte> AsSpan(int start, int length) public readonly Span<T> AsSpan<T>(int start, int length)
where T : unmanaged
{ {
if (start < 0 || length < 0 || (nuint)(start + length) > _size) if (start < 0 || length < 0 || (nuint)(start + length) * (nuint)sizeof(T) > _size)
{ {
throw new ArgumentOutOfRangeException(nameof(start), "The specified range is out of bounds of the collection."); throw new ArgumentOutOfRangeException(nameof(start), "The specified range is out of bounds of the collection.");
} }
return new Span<byte>((byte*)_buffer + start, length); return new Span<T>((T*)_buffer + start, length);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -278,6 +280,7 @@ public unsafe struct UnTypedArray : IUnTypedCollection
{ {
return; return;
} }
var message = "The UnTypedArray is not created or already disposed."; var message = "The UnTypedArray is not created or already disposed.";
#if MHP_ENABLE_STACKTRACE #if MHP_ENABLE_STACKTRACE
var stackTrace = new StackTrace(1, true); var stackTrace = new StackTrace(1, true);

View File

@@ -1,20 +1,21 @@
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer; namespace Misaki.HighPerformance.LowLevel.Buffer;
public unsafe struct MemoryPool<T, TOpts> : IDisposable public unsafe struct MemoryPool<TAllocator, TOpts> : IDisposable
where T : unmanaged, IMemoryAllocator<T, TOpts> where TAllocator : unmanaged, IMemoryAllocator<TAllocator, TOpts>
{ {
private T* _pAllocator; private TAllocator* _pAllocator;
private AllocationHandle _allocationHandle; private AllocationHandle _allocationHandle;
public readonly ref T Allocator => ref *_pAllocator; public readonly ref TAllocator Allocator => ref Unsafe.AsRef<TAllocator>(_pAllocator);
public readonly AllocationHandle AllocationHandle => _allocationHandle; public readonly AllocationHandle AllocationHandle => _allocationHandle;
public MemoryPool(in TOpts opts) public MemoryPool(in TOpts opts)
{ {
_pAllocator = (T*)Malloc((nuint)sizeof(T)); _pAllocator = (TAllocator*)Malloc((nuint)sizeof(TAllocator));
*_pAllocator = T.Create(opts); *_pAllocator = TAllocator.Create(opts);
_allocationHandle = new AllocationHandle _allocationHandle = new AllocationHandle
{ {
@@ -32,7 +33,14 @@ public unsafe struct MemoryPool<T, TOpts> : IDisposable
#endif #endif
) )
{ {
return ((T*)pAllocator)->Allocate(size, alignment, allocationOption); var ptr = ((TAllocator*)pAllocator)->Allocate(size, alignment, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
if (ptr != null)
{
*pHandle = AllocationManager.AddAllocation(ptr, size);
}
#endif
return ptr;
} }
private static void* Reallocate(void* pAllocator, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption private static void* Reallocate(void* pAllocator, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption
@@ -41,7 +49,25 @@ public unsafe struct MemoryPool<T, TOpts> : IDisposable
#endif #endif
) )
{ {
return ((T*)pAllocator)->Reallocate(ptr, oldSize, newSize, alignment, allocationOption); var newPtr = ((TAllocator*)pAllocator)->Reallocate(ptr, oldSize, newSize, alignment, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
if (ptr == null && newPtr != null)
{
*pHandle = AllocationManager.AddAllocation(newPtr, newSize);
}
else
{
if (newPtr == null)
{
AllocationManager.RemoveAllocation(*pHandle);
}
else
{
AllocationManager.UpdateAllocation(*pHandle, newPtr, newSize);
}
}
#endif
return newPtr;
} }
private static void Free(void* pAllocator, void* ptr private static void Free(void* pAllocator, void* ptr
@@ -50,7 +76,10 @@ public unsafe struct MemoryPool<T, TOpts> : IDisposable
#endif #endif
) )
{ {
((T*)pAllocator)->Free(ptr); ((TAllocator*)pAllocator)->Free(ptr);
#if MHP_ENABLE_SAFETY_CHECKS
AllocationManager.RemoveAllocation(handle);
#endif
} }
public void Dispose() public void Dispose()

View File

@@ -1,3 +1,4 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections; using System.Collections;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -197,7 +198,7 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOptio
return; return;
} }
Free(_buffer); MemoryUtility.Free(_buffer);
_buffer = null; _buffer = null;
_size = 0; _size = 0;

View File

@@ -83,6 +83,12 @@ public unsafe struct VirtualStack : IMemoryAllocator<VirtualStack, VirtualStack.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Scope CreateScope(AllocationHandle handle) public Scope CreateScope(AllocationHandle handle)
{ {
#if MHP_ENABLE_SAFETY_CHECKS
if (_baseAddress == null)
{
throw new InvalidOperationException("Allocator must be initialized before creating a scope.");
}
#endif
return new Scope((VirtualStack*)Unsafe.AsPointer(ref this), handle); return new Scope((VirtualStack*)Unsafe.AsPointer(ref this), handle);
} }
@@ -151,6 +157,13 @@ public unsafe struct VirtualStack : IMemoryAllocator<VirtualStack, VirtualStack.
public void* Reallocate(void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption) public void* Reallocate(void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{ {
#if MHP_ENABLE_SAFETY_CHECKS
if (_activeScopeCount == 0)
{
throw new InvalidOperationException("Allocations can only be made within an active memory scope.");
}
#endif
if (_baseAddress == null) if (_baseAddress == null)
{ {
return null; return null;
@@ -193,6 +206,8 @@ public unsafe struct VirtualStack : IMemoryAllocator<VirtualStack, VirtualStack.
{ {
MemClear(_baseAddress + _allocatedOffset - diff, diff); MemClear(_baseAddress + _allocatedOffset - diff, diff);
} }
return ptr;
} }
var newPtr = Allocate(newSize, alignment, allocationOption); var newPtr = Allocate(newSize, alignment, allocationOption);

View File

@@ -79,17 +79,3 @@ public interface IUnsafeHashCollection<T> : IEnumerable<T>, IDisposable
/// <param name="option">Specifies allocation options that may affect how memory is managed during the resize operation.</param> /// <param name="option">Specifies allocation options that may affect how memory is managed during the resize operation.</param>
void Resize(int newSize, AllocationOption option); void Resize(int newSize, AllocationOption option);
} }
public interface IUnTypedCollection : IUnsafeCollection
{
/// <summary>
/// The total size of the buffer in bytes.
/// </summary>
nuint Size
{
get;
}
ref T GetElementAt<T>(nuint index)
where T : unmanaged;
}

View File

@@ -17,7 +17,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<!--<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_STACKTRACE</DefineConstants>--> <DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@@ -0,0 +1,85 @@
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
[DoNotParallelize]
public class TestAllocationManager
{
[TestMethod]
public void PersistentAllocationTest()
{
var ptr1 = new MemoryBlock(1024, 8, Allocator.Persistent);
var ptr2 = new MemoryBlock(2048, 8, Allocator.Persistent);
Assert.IsTrue(ptr1.IsCreated);
Assert.IsTrue(ptr2.IsCreated);
ptr1.Dispose();
ptr2.Dispose();
Assert.IsFalse(ptr1.IsCreated);
Assert.IsFalse(ptr2.IsCreated);
}
[TestMethod]
public void TempAllocationTest()
{
var ptr1 = new MemoryBlock(1024, 8, Allocator.Temp);
var ptr2 = new MemoryBlock(2048, 8, Allocator.Temp);
Assert.IsTrue(ptr1.IsCreated);
Assert.IsTrue(ptr2.IsCreated);
ptr1.Dispose();
ptr2.Dispose();
Assert.IsFalse(ptr1.IsCreated);
Assert.IsFalse(ptr2.IsCreated);
}
[TestMethod]
public void FreeListAllocationTest()
{
var ptr1 = new MemoryBlock(1024, 8, Allocator.FreeList);
var ptr2 = new MemoryBlock(2048, 8, Allocator.FreeList);
Assert.IsTrue(ptr1.IsCreated);
Assert.IsTrue(ptr2.IsCreated);
ptr1.Dispose();
ptr2.Dispose();
Assert.IsFalse(ptr1.IsCreated);
Assert.IsFalse(ptr2.IsCreated);
}
[TestMethod]
public unsafe void StackAllocationTest()
{
var thread = new Thread(() =>
{
var scope = AllocationManager.CreateStackScope();
var ptr1 = new MemoryBlock(1024, 8, scope.AllocationHandle);
Assert.IsTrue(ptr1.IsCreated);
Assert.AreEqual(1024u, ((VirtualStack*)scope.AllocationHandle.State)->Allocated);
ptr1.Dispose();
scope.Dispose();
Assert.AreEqual(0u, ((VirtualStack*)scope.AllocationHandle.State)->Allocated);
});
thread.Start();
var scope = AllocationManager.CreateStackScope();
var ptr2 = new MemoryBlock(1024, 8, scope.AllocationHandle);
Assert.IsTrue(ptr2.IsCreated);
Assert.AreEqual(1024u, ((VirtualStack*)scope.AllocationHandle.State)->Allocated);
ptr2.Dispose();
scope.Dispose();
Assert.AreEqual(0u, ((VirtualStack*)scope.AllocationHandle.State)->Allocated);
}
}

View File

@@ -0,0 +1,358 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
public unsafe class TestArena
{
[TestMethod]
public void VirtualArena_AllocateReallocateResetDispose_Works()
{
var arena = new VirtualArena(100_000);
try
{
Assert.IsGreaterThanOrEqualTo(100_000u, arena.Reserved);
Assert.AreEqual(0u, (uint)arena.Allocated);
var p1 = (byte*)arena.Allocate(64, 16, AllocationOption.Clear);
Assert.IsTrue(p1 != null);
Assert.AreEqual((nuint)0u, ((nuint)p1) & 15u);
for (var i = 0; i < 64; i++)
{
Assert.AreEqual(0, p1[i]);
p1[i] = 0x5A;
}
var sameOnShrink = arena.Reallocate(p1, 64, 32, 16, AllocationOption.None);
Assert.AreEqual((nint)p1, (nint)sameOnShrink);
var p2 = (byte*)arena.Allocate(32, 8, AllocationOption.None);
Assert.IsTrue(p2 != null);
for (var i = 0; i < 64; i++)
{
p1[i] = (byte)(i + 1);
}
var moved = (byte*)arena.Reallocate(p1, 64, 128, 16, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 64; i++)
{
Assert.AreEqual((byte)(i + 1), moved[i]);
}
arena.Reset();
Assert.AreEqual(0u, (uint)arena.Allocated);
var pAfterReset = (byte*)arena.Allocate(32, 8, AllocationOption.None);
Assert.AreEqual((nint)arena.Buffer, (nint)pAfterReset);
}
finally
{
arena.Dispose();
}
Assert.IsTrue(arena.Buffer == null);
Assert.AreEqual(0u, (uint)arena.Reserved);
Assert.IsNull(arena.Allocate(16, 8, AllocationOption.None));
// Double dispose should be safe
arena.Dispose();
}
[TestMethod]
public void VirtualArena_ReallocateTailExtendsInPlace_AndClearsAdditionalMemory()
{
var arena = new VirtualArena(128 * 1024);
try
{
var ptr = (byte*)arena.Allocate(32, 8, AllocationOption.None);
Assert.IsNotNull(ptr);
for (var i = 0; i < 32; i++)
ptr[i] = 0x33;
var grown = (byte*)arena.Reallocate(ptr, 32, 96, 8, AllocationOption.Clear);
Assert.AreEqual((nint)ptr, (nint)grown);
for (var i = 0; i < 32; i++)
Assert.AreEqual(0x33, grown[i]);
for (var i = 32; i < 96; i++)
Assert.AreEqual(0, grown[i]);
}
finally
{
arena.Dispose();
}
}
[TestMethod]
public void VirtualArena_MultiThreadedAllocation_IsThreadSafe()
{
var mem = (VirtualArena*)NativeMemory.AllocZeroed((nuint)sizeof(VirtualArena));
*mem = new VirtualArena(8 * 1024 * 1024);
try
{
const int threadCount = 8;
const int iterations = 2000;
var ptrs = new ConcurrentDictionary<nint, byte>();
var errors = new ConcurrentQueue<Exception>();
var shared = (IntPtr)mem;
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
try
{
var arena = (VirtualArena*)shared;
for (var j = 0; j < iterations; j++)
{
var p = (byte*)arena->Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p);
Assert.AreEqual((nuint)0, ((nuint)p) & 7);
Assert.IsTrue(ptrs.TryAdd((nint)p, 0));
}
}
catch (Exception ex)
{
errors.Enqueue(ex);
}
});
}
foreach (var t in threads)
t.Start();
foreach (var t in threads)
t.Join();
if (errors.TryDequeue(out var err))
{
throw err;
}
Assert.AreEqual(threadCount * iterations, ptrs.Count);
}
finally
{
mem->Dispose();
NativeMemory.Free(mem);
}
}
[TestMethod]
public void Arena_AllocateReallocateResetDispose_Works()
{
var arena = new Arena(1024);
try
{
var p1 = (byte*)arena.Allocate(32, 8, AllocationOption.Clear);
Assert.IsNotNull(p1);
for (var i = 0; i < 32; i++)
Assert.AreEqual(0, p1[i]);
for (var i = 0; i < 32; i++)
p1[i] = 0x42;
var same = (byte*)arena.Reallocate(p1, 32, 64, 8, AllocationOption.Clear);
Assert.AreEqual((nint)p1, (nint)same);
for (var i = 0; i < 32; i++)
Assert.AreEqual(0x42, same[i]);
for (var i = 32; i < 64; i++)
Assert.AreEqual(0, same[i]);
var p2 = (byte*)arena.Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p2);
for (var i = 0; i < 64; i++)
same[i] = (byte)(255 - i);
var moved = (byte*)arena.Reallocate(same, 64, 96, 8, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 64; i++)
Assert.AreEqual((byte)(255 - i), moved[i]);
arena.Reset();
Assert.AreEqual(0u, (uint)arena.Offset);
var firstAfterReset = (byte*)arena.Allocate(8, 8, AllocationOption.None);
Assert.AreEqual((nint)arena.Buffer, (nint)firstAfterReset);
}
finally
{
arena.Dispose();
}
Assert.IsTrue(arena.Buffer == null);
Assert.AreEqual(0u, (uint)arena.Size);
Assert.IsNull(arena.Allocate(8, 8, AllocationOption.None));
arena.Dispose();
}
[TestMethod]
public void Arena_InvalidAlignment_Throws()
{
var arena = new Arena(256);
try
{
Assert.ThrowsExactly<ArgumentException>(() => arena.Allocate(8, 3, AllocationOption.None));
}
finally
{
arena.Dispose();
}
}
[TestMethod]
public void Arena_MultiThreadedAllocation_IsThreadSafe()
{
var mem = (Arena*)NativeMemory.AllocZeroed((nuint)sizeof(Arena));
*mem = new Arena(4 * 1024 * 1024);
try
{
const int threadCount = 8;
const int iterations = 2000;
var ptrs = new ConcurrentDictionary<nint, byte>();
var errors = new ConcurrentQueue<Exception>();
var shared = (IntPtr)mem;
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
try
{
var arena = (Arena*)shared;
for (var j = 0; j < iterations; j++)
{
var p = (byte*)arena->Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p);
Assert.AreEqual((nuint)0, ((nuint)p) & 7);
Assert.IsTrue(ptrs.TryAdd((nint)p, 0));
}
}
catch (Exception ex)
{
errors.Enqueue(ex);
}
});
}
foreach (var t in threads)
t.Start();
foreach (var t in threads)
t.Join();
if (errors.TryDequeue(out var err))
{
throw err;
}
Assert.AreEqual(threadCount * iterations, ptrs.Count);
}
finally
{
mem->Dispose();
NativeMemory.Free(mem);
}
}
[TestMethod]
public void DynamicArena_GrowthReallocateResetDispose_Works()
{
var arena = new DynamicArena(64);
try
{
var p1 = (byte*)arena.Allocate(48, 8, AllocationOption.None);
Assert.IsNotNull(p1);
for (var i = 0; i < 48; i++)
p1[i] = (byte)(i + 1);
// Force growth to another node
var p2 = (byte*)arena.Allocate(128, 8, AllocationOption.Clear);
Assert.IsNotNull(p2);
Assert.AreNotEqual((nint)p1, (nint)p2);
for (var i = 0; i < 128; i++)
Assert.AreEqual(0, p2[i]);
var moved = (byte*)arena.Reallocate(p1, 48, 96, 8, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 48; i++)
Assert.AreEqual((byte)(i + 1), moved[i]);
arena.Reset();
var firstAfterReset = (byte*)arena.Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(firstAfterReset);
Assert.AreEqual((nint)p1, (nint)firstAfterReset);
}
finally
{
arena.Dispose();
}
Assert.IsNull(arena.Allocate(8, 8, AllocationOption.None));
arena.Dispose();
}
[TestMethod]
public void DynamicArena_MultiThreadedAllocation_IsThreadSafe()
{
var mem = (DynamicArena*)NativeMemory.AllocZeroed((nuint)sizeof(DynamicArena));
*mem = new DynamicArena(256);
try
{
const int threadCount = 8;
const int iterations = 1000;
var ptrs = new ConcurrentDictionary<nint, byte>();
var errors = new ConcurrentQueue<Exception>();
var shared = (IntPtr)mem;
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
try
{
var arena = (DynamicArena*)shared;
for (var j = 0; j < iterations; j++)
{
var p = (byte*)arena->Allocate(24, 8, AllocationOption.None);
Assert.IsNotNull(p);
Assert.AreEqual((nuint)0, ((nuint)p) & 7);
Assert.IsTrue(ptrs.TryAdd((nint)p, 0));
}
}
catch (Exception ex)
{
errors.Enqueue(ex);
}
});
}
foreach (var t in threads)
t.Start();
foreach (var t in threads)
t.Join();
if (errors.TryDequeue(out var err))
{
throw err;
}
Assert.AreEqual(threadCount * iterations, ptrs.Count);
}
finally
{
mem->Dispose();
NativeMemory.Free(mem);
}
}
}

View File

@@ -15,9 +15,9 @@ public unsafe class TestFreeList
var p2 = freeList.Allocate(32, 8); var p2 = freeList.Allocate(32, 8);
var p3 = freeList.Allocate(64, 8); var p3 = freeList.Allocate(64, 8);
Assert.IsTrue(p1 != null); Assert.IsNotNull(p1);
Assert.IsTrue(p2 != null); Assert.IsNotNull(p2);
Assert.IsTrue(p3 != null); Assert.IsNotNull(p3);
// Free them // Free them
freeList.Free(p1); freeList.Free(p1);
@@ -28,8 +28,8 @@ public unsafe class TestFreeList
var p4 = freeList.Allocate(16, 8); var p4 = freeList.Allocate(16, 8);
var p5 = freeList.Allocate(32, 8); var p5 = freeList.Allocate(32, 8);
Assert.IsTrue(p4 != null); Assert.IsNotNull(p4);
Assert.IsTrue(p5 != null); Assert.IsNotNull(p5);
freeList.Free(p4); freeList.Free(p4);
freeList.Free(p5); freeList.Free(p5);
@@ -50,7 +50,7 @@ public unsafe class TestFreeList
for (var j = 0; j < iterations; j++) for (var j = 0; j < iterations; j++)
{ {
var ptr = freeList.Allocate(16, 8); var ptr = freeList.Allocate(16, 8);
Assert.IsTrue(ptr != null); Assert.IsNotNull(ptr);
freeList.Free(ptr); freeList.Free(ptr);
} }
}); });
@@ -83,7 +83,7 @@ public unsafe class TestFreeList
for (var j = 0; j < iterations; j++) for (var j = 0; j < iterations; j++)
{ {
var ptr = freeList.Allocate(32, 8); var ptr = freeList.Allocate(32, 8);
Assert.IsTrue(ptr != null); Assert.IsNotNull(ptr);
queue.Enqueue((IntPtr)ptr); queue.Enqueue((IntPtr)ptr);
} }
}); });
@@ -132,7 +132,7 @@ public unsafe class TestFreeList
threads[i] = new Thread(() => threads[i] = new Thread(() =>
{ {
var ptr = freeList.Allocate(16, 8); var ptr = freeList.Allocate(16, 8);
Assert.IsTrue(ptr != null); Assert.IsNotNull(ptr);
freeList.Free(ptr); freeList.Free(ptr);
}); });
} }
@@ -151,8 +151,30 @@ public unsafe class TestFreeList
// Allocate larger than default chunk size // Allocate larger than default chunk size
nuint largeSize = 2048; nuint largeSize = 2048;
var ptr = freeList.Allocate(largeSize, 8); var ptr = freeList.Allocate(largeSize, 8);
Assert.IsTrue(ptr != null); Assert.IsNotNull(ptr);
freeList.Free(ptr); freeList.Free(ptr);
} }
[TestMethod]
public void ZeroSizeAllocation_ReturnsNull()
{
using var freeList = new FreeList(8, 1024);
Assert.IsNull(freeList.Allocate(0, 8));
}
[TestMethod]
public void InvalidAlignment_Throws()
{
using var freeList = new FreeList(8, 1024);
Assert.ThrowsExactly<ArgumentException>(() => freeList.Allocate(16, 3));
}
[TestMethod]
public void DoubleDispose_IsSafe()
{
var freeList = new FreeList(8, 1024);
freeList.Dispose();
freeList.Dispose(); // Should not throw
}
} }

View File

@@ -0,0 +1,249 @@
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
public unsafe class TestStack
{
[TestMethod]
public void Stack_ScopeAllocateReallocateResetDispose_Works()
{
var stack = new Stack(256 * 1024);
try
{
var handle = default(AllocationHandle);
using (stack.CreateScope(handle))
{
var p1 = (byte*)stack.Allocate(32, 8, AllocationOption.Clear);
Assert.IsNotNull(p1);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0, p1[i]);
}
for (var i = 0; i < 32; i++)
{
p1[i] = 0x11;
}
var grown = (byte*)stack.Reallocate(p1, 32, 96, 8, AllocationOption.Clear);
Assert.AreEqual((nint)p1, (nint)grown);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0x11, grown[i]);
}
for (var i = 32; i < 96; i++)
{
Assert.AreEqual(0, grown[i]);
}
}
Assert.AreEqual(0u, (uint)stack.Offset);
stack.Reset();
Assert.AreEqual(0u, (uint)stack.Offset);
}
finally
{
stack.Dispose();
}
Assert.IsNull(stack.Buffer);
}
[TestMethod]
public void Stack_NestedScopes_RewindToPreviousOffsets()
{
var stack = new Stack(128 * 1024);
try
{
var handle = default(AllocationHandle);
using (stack.CreateScope(handle))
{
var pOuter = stack.Allocate(32, 8, AllocationOption.None);
Assert.IsNotNull(pOuter);
var outerOffset = stack.Offset;
using (stack.CreateScope(handle))
{
var pInner = stack.Allocate(128, 8, AllocationOption.None);
Assert.IsNotNull(pInner);
Assert.IsTrue(stack.Offset > outerOffset);
}
Assert.AreEqual(outerOffset, stack.Offset);
}
Assert.AreEqual(0u, (uint)stack.Offset);
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void Stack_MultipleAllocations_Works()
{
var stack = new Stack(1024 * 1024);
try
{
using (stack.CreateScope(default))
{
var p1 = (byte*)stack.Allocate(32, 8, AllocationOption.Clear);
Assert.IsNotNull(p1);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0, p1[i]);
}
for (var i = 0; i < 32; i++)
{
p1[i] = 0x42;
}
var same = (byte*)stack.Reallocate(p1, 32, 64, 8, AllocationOption.Clear);
Assert.AreEqual((nint)p1, (nint)same);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0x42, same[i]);
}
for (var i = 32; i < 64; i++)
{
Assert.AreEqual(0, same[i]);
}
var p2 = (byte*)stack.Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p2);
for (var i = 0; i < 64; i++)
{
same[i] = (byte)(255 - i);
}
var moved = (byte*)stack.Reallocate(same, 64, 96, 8, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 64; i++)
{
Assert.AreEqual((byte)(255 - i), moved[i]);
}
stack.Reset();
Assert.AreEqual(0u, (uint)stack.Offset);
var firstAfterReset = (byte*)stack.Allocate(8, 8, AllocationOption.None);
Assert.AreEqual((nint)stack.Buffer, (nint)firstAfterReset);
}
}
finally
{
stack.Dispose();
}
Assert.IsNull(stack.Buffer);
}
[TestMethod]
public void Stack_AllocationFailsOutsideScope()
{
var stack = new Stack(128 * 1024);
try
{
Assert.ThrowsExactly<InvalidOperationException>(() => stack.Allocate(32, 8, AllocationOption.Clear));
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void Stack_InvalidAlignment_Throws()
{
var stack = new Stack(128 * 1024);
try
{
using (stack.CreateScope(default))
{
Assert.ThrowsExactly<ArgumentException>(() => stack.Allocate(32, 3, AllocationOption.None));
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void Stack_ZeroSizeAllocation_ReturnsNull()
{
var stack = new Stack(128 * 1024);
try
{
using (stack.CreateScope(default))
{
Assert.IsNull(stack.Allocate(0, 8, AllocationOption.None));
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void Stack_AllocationExceedsCapacity_Throws()
{
var stack = new Stack(1024);
try
{
using (stack.CreateScope(default))
{
Assert.ThrowsExactly<OutOfMemoryException>(() => stack.Allocate(2048, 8, AllocationOption.None));
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void Stack_ReallocateSmallerSize_ReturnsSamePointer()
{
var stack = new Stack(128 * 1024);
try
{
using (stack.CreateScope(default))
{
var p = (byte*)stack.Allocate(64, 8);
Assert.IsNotNull(p);
for (var i = 0; i < 32; i++)
{
p[i] = (byte)(i + 1);
}
var shrunk = (byte*)stack.Reallocate(p, 64, 32, 8, AllocationOption.None);
Assert.AreEqual((nint)p, (nint)shrunk);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual((byte)(i + 1), shrunk[i]);
}
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void Stack_DoubleDispose_IsSafe()
{
var stack = new Stack(128 * 1024);
stack.Dispose();
stack.Dispose(); // Should not throw
}
}

View File

@@ -0,0 +1,268 @@
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
public unsafe class TestVirtualStack
{
[TestMethod]
public void VirtualStack_ScopeAllocateReallocateResetDispose_Works()
{
var stack = new VirtualStack(256 * 1024);
try
{
var handle = default(AllocationHandle);
using (stack.CreateScope(handle))
{
var p1 = (byte*)stack.Allocate(32, 8, AllocationOption.Clear);
Assert.IsNotNull(p1);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0, p1[i]);
}
for (var i = 0; i < 32; i++)
{
p1[i] = 0x11;
}
var grown = (byte*)stack.Reallocate(p1, 32, 96, 8, AllocationOption.Clear);
Assert.AreEqual((nint)p1, (nint)grown);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0x11, grown[i]);
}
for (var i = 32; i < 96; i++)
{
Assert.AreEqual(0, grown[i]);
}
}
Assert.AreEqual(0u, (uint)stack.Allocated);
stack.Reset();
Assert.AreEqual(0u, (uint)stack.Allocated);
}
finally
{
stack.Dispose();
}
Assert.IsTrue(stack.Buffer == null);
}
[TestMethod]
public void VirtualStack_NestedScopes_RewindToPreviousOffsets()
{
var stack = new VirtualStack(128 * 1024);
try
{
var handle = default(AllocationHandle);
using (stack.CreateScope(handle))
{
var pOuter = stack.Allocate(32, 8, AllocationOption.None);
Assert.IsNotNull(pOuter);
var outerOffset = stack.Allocated;
using (stack.CreateScope(handle))
{
var pInner = stack.Allocate(128, 8, AllocationOption.None);
Assert.IsNotNull(pInner);
Assert.IsTrue(stack.Allocated > outerOffset);
}
Assert.AreEqual(outerOffset, stack.Allocated);
}
Assert.AreEqual(0u, (uint)stack.Allocated);
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void VirtualStack_MultipleAllocations_Works()
{
var stack = new VirtualStack(1024 * 1024);
try
{
var handle = default(AllocationHandle);
using (stack.CreateScope(handle))
{
var p1 = (byte*)stack.Allocate(32, 8, AllocationOption.Clear);
Assert.IsNotNull(p1);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0, p1[i]);
}
for (var i = 0; i < 32; i++)
{
p1[i] = 0x42;
}
var same = (byte*)stack.Reallocate(p1, 32, 64, 8, AllocationOption.Clear);
Assert.AreEqual((nint)p1, (nint)same);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual(0x42, same[i]);
}
for (var i = 32; i < 64; i++)
{
Assert.AreEqual(0, same[i]);
}
var p2 = (byte*)stack.Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p2);
for (var i = 0; i < 64; i++)
{
same[i] = (byte)(255 - i);
}
var moved = (byte*)stack.Reallocate(same, 64, 96, 8, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 64; i++)
{
Assert.AreEqual((byte)(255 - i), moved[i]);
}
stack.Reset();
Assert.AreEqual(0u, (uint)stack.Allocated);
var firstAfterReset = (byte*)stack.Allocate(8, 8, AllocationOption.None);
Assert.AreEqual((nint)stack.Buffer, (nint)firstAfterReset);
}
}
finally
{
stack.Dispose();
}
Assert.IsNull(stack.Buffer);
}
[TestMethod]
public void VirtualStack_AllocationFailsOutsideScope()
{
var stack = new VirtualStack(128 * 1024);
try
{
Assert.ThrowsExactly<InvalidOperationException>(() => stack.Allocate(32, 8, AllocationOption.Clear));
stack.Dispose();
Assert.IsTrue(stack.Buffer == null);
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void VirtualStack_InvalidAlignment_Throws()
{
var stack = new VirtualStack(128 * 1024);
try
{
using (stack.CreateScope(default))
{
Assert.ThrowsExactly<ArgumentException>(() => stack.Allocate(32, 3, AllocationOption.None));
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void VirtualStack_ZeroSizeAllocation_ReturnsNull()
{
var stack = new VirtualStack(128 * 1024);
try
{
using (stack.CreateScope(default))
{
Assert.IsNull(stack.Allocate(0, 8, AllocationOption.None));
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void VirtualStack_AllocationExceedsReservedCapacity_ReturnsNull()
{
var stack = new VirtualStack(1024);
try
{
using (stack.CreateScope(default))
{
Assert.IsNull(stack.Allocate(128 * 1024, 8, AllocationOption.None));
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void VirtualStack_ReallocateSmallerSize_ReturnsSamePointer()
{
var stack = new VirtualStack(128 * 1024);
try
{
using (stack.CreateScope(default))
{
var p = (byte*)stack.Allocate(64, 8);
Assert.IsNotNull(p);
for (var i = 0; i < 32; i++)
{
p[i] = (byte)(i + 1);
}
var shrunk = (byte*)stack.Reallocate(p, 64, 32, 8, AllocationOption.None);
Assert.AreEqual((nint)p, (nint)shrunk);
for (var i = 0; i < 32; i++)
{
Assert.AreEqual((byte)(i + 1), shrunk[i]);
}
}
}
finally
{
stack.Dispose();
}
}
[TestMethod]
public void VirtualStack_AllocationRequiresCommitment()
{
var stack = new VirtualStack(128 * 1024);
try
{
var handle = default(AllocationHandle);
using (stack.CreateScope(handle))
{
Assert.AreEqual(0u, (uint)stack.Committed);
var p = (byte*)stack.Allocate(64, 8);
Assert.IsNotNull(p);
Assert.IsTrue(stack.Committed >= 64);
}
}
finally
{
stack.Dispose();
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace Misaki.HighPerformance.Test.UnitTest;
internal static class Utility
{
extension(Assert)
{
public unsafe static void IsNull(void* ptr, string message = "Expected pointer to be null.", [CallerArgumentExpression(nameof(ptr))] string conditionExpression = "")
{
Assert.IsTrue(ptr == null, message, conditionExpression);
}
public unsafe static void IsNotNull(void* ptr, string message = "Expected pointer to be not null.", [CallerArgumentExpression(nameof(ptr))] string conditionExpression = "")
{
Assert.IsTrue(ptr != null, message, conditionExpression);
}
}
}