Refactor AllocationManager and enhance debug tracking

Refactored `AllocationManager` to introduce intrusive allocation tracking with `AllocationHeader` structs for debug mode. Added lightweight allocation counters for non-debug mode. Enhanced memory leak detection with detailed stack traces and `MemoryLeakException`.

Simplified `AllocationInfo` by removing the `Allocator` property. Updated `AllocationOption` enum to remove `UnTracked` and clarified documentation.

Improved unsafe collections (`UnsafeArray`, `UnsafeStack`, etc.) with strongly-typed enumerators and better compatibility with `IEnumerable<T>`. Enhanced `UnsafeStack` with a dedicated `Enumerator` struct and consistent constructor parameters.

Refactored `MemoryLeakException` to support detailed allocation info and improved stack trace formatting. Simplified `MemoryUtility` by removing redundant null checks.

Added unit tests for `AllocationManager`, `UnsafeArray`, and `UnsafeStack` to validate memory management and functionality. Updated `Program.cs` with new examples.

Cleaned up namespaces, removed redundant `using` directives, and improved XML documentation. Applied `MethodImplOptions.AggressiveInlining` to performance-critical methods.
This commit is contained in:
2025-11-06 01:28:43 +09:00
parent b914716225
commit fbe72e33f7
17 changed files with 606 additions and 174 deletions

View File

@@ -1,6 +1,4 @@
using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Exceptions;
using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -20,14 +18,6 @@ public readonly unsafe struct AllocationInfo
get; init; get; init;
} }
/// <summary>
/// Get the allocator used for the allocation.
/// </summary>
public void* Allocator
{
get; init;
}
/// <summary> /// <summary>
/// Get the stack trace at the time of allocation for debugging purposes. /// Get the stack trace at the time of allocation for debugging purposes.
/// </summary> /// </summary>
@@ -43,6 +33,17 @@ public readonly unsafe struct AllocationInfo
/// </summary> /// </summary>
public static unsafe class AllocationManager public static unsafe class AllocationManager
{ {
// === Intrusive allocation tracking (enabled when debug layer is on) ===
[StructLayout(LayoutKind.Sequential)]
private struct AllocationHeader
{
public AllocationHeader* prev;
public AllocationHeader* next;
public void* basePtr; // pointer returned by underlying allocator
public nuint userSize; // requested size from the user
public nint stackHandle; // GCHandle to managed StackTrace (stored as IntPtr)
}
private unsafe struct ArenaAllocator : IAllocator, IDisposable private unsafe struct ArenaAllocator : IAllocator, IDisposable
{ {
private DynamicArena _arena; private DynamicArena _arena;
@@ -111,49 +112,17 @@ public static unsafe class AllocationManager
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption) private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{ {
var ptr = AlignedAlloc(size, alignment); return HeapAlloc(size, alignment, allocationOption);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, size);
}
if (!allocationOption.HasFlag(AllocationOption.UnTracked))
{
TrackAllocation(ptr, size, instance);
}
return ptr;
} }
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption) private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{ {
var newPtr = AlignedRealloc(ptr, newSize, alignment); return HeapRealloc(ptr, oldSize, newSize, alignment, allocationOption);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
if (newSize > oldSize)
{
MemClear((byte*)newPtr + oldSize, newSize - oldSize);
}
}
if (!allocationOption.HasFlag(AllocationOption.UnTracked))
{
UpdateAllocation(ptr, newPtr, newSize, instance);
}
else
{
UntrackAllocation(ptr);
}
return newPtr;
} }
private static void FreeBlock(void* instance, void* ptr) private static void FreeBlock(void* instance, void* ptr)
{ {
AlignedFree(ptr); HeapFree(ptr);
UntrackAllocation(ptr);
} }
} }
@@ -207,32 +176,193 @@ public static unsafe class AllocationManager
private const uint _DEFAULT_MEMORY_POOL_SIZE = 512 * 1024; private const uint _DEFAULT_MEMORY_POOL_SIZE = 512 * 1024;
private static readonly ArenaAllocator* s_arenaAllocator; private static readonly ArenaAllocator* s_pArenaAllocator;
private static readonly HeapAllocator* s_persistentAllocator; private static readonly HeapAllocator* s_pHeapAllocator;
private static readonly StackAllocator* s_stackAllocator; private static readonly StackAllocator* s_pStackAllocator;
private static bool s_debugLayer; private static bool s_debugLayer;
private static ConcurrentDictionary<nint, AllocationInfo>? s_allocated; private static bool s_disposed;
private static AllocationHeader* s_liveHead;
private static SpinLock s_liveLock;
// Lightweight allocation counter for non-debug layer (no sizes, just count of live heap blocks)
private static long s_activeHeapAllocations;
static AllocationManager() static AllocationManager()
{ {
s_arenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator)); s_pArenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator));
s_persistentAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator)); s_pHeapAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator));
s_stackAllocator = (StackAllocator*)NativeMemory.Alloc((nuint)sizeof(StackAllocator)); s_pStackAllocator = (StackAllocator*)NativeMemory.Alloc((nuint)sizeof(StackAllocator));
s_arenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE); s_liveLock = new SpinLock(false);
s_persistentAllocator->Init();
s_stackAllocator->Init(); s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE);
s_pHeapAllocator->Init();
s_pStackAllocator->Init();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte* AlignUp(byte* p, nuint alignment)
{
var a = alignment == 0 ? (nuint)IntPtr.Size : alignment;
return (byte*)(((nuint)p + (a - 1)) & ~(a - 1));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static GCHandle HeaderGetHandle(AllocationHeader* header) => GCHandle.FromIntPtr(header->stackHandle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void HeaderSetHandle(AllocationHeader* header, GCHandle handle) => header->stackHandle = GCHandle.ToIntPtr(handle);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void HeaderFreeHandle(AllocationHeader* header)
{
if (header->stackHandle != 0)
{
GCHandle.FromIntPtr(header->stackHandle).Free();
header->stackHandle = 0;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void LinkHeader(AllocationHeader* header)
{
var taken = false;
try
{
s_liveLock.Enter(ref taken);
header->prev = null;
header->next = s_liveHead;
if (s_liveHead != null)
{
s_liveHead->prev = header;
}
s_liveHead = header;
}
finally
{
if (taken)
{
s_liveLock.Exit();
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UnlinkHeader(AllocationHeader* header)
{
var taken = false;
try
{
s_liveLock.Enter(ref taken);
var prev = header->prev;
var next = header->next;
if (prev != null)
prev->next = next;
else
s_liveHead = next;
if (next != null)
next->prev = prev;
header->prev = header->next = null;
}
finally
{
if (taken)
{
s_liveLock.Exit();
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void* DebugAllocate(nuint size, nuint alignment)
{
// Over-allocate to fit header + alignment padding; we align the user pointer, header is placed just before it.
var pad = alignment == 0 ? (nuint)IntPtr.Size : alignment;
var total = size + (nuint)sizeof(AllocationHeader) + (pad - 1);
var basePtr = AlignedAlloc(total, pad);
var user = AlignUp((byte*)basePtr + (nuint)sizeof(AllocationHeader), pad);
var header = (AllocationHeader*)(user - (nuint)sizeof(AllocationHeader));
header->basePtr = basePtr;
header->userSize = size;
HeaderSetHandle(header, GCHandle.Alloc(new StackTrace(2, true)));
LinkHeader(header);
return user;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DebugFree(void* userPtr)
{
var header = (AllocationHeader*)((byte*)userPtr - (nuint)sizeof(AllocationHeader));
UnlinkHeader(header);
HeaderFreeHandle(header);
AlignedFree(header->basePtr);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void* DebugReallocate(void* userPtr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{
if (userPtr == null)
{
return DebugAllocate(newSize, alignment);
}
var oldHeader = (AllocationHeader*)((byte*)userPtr - (nuint)sizeof(AllocationHeader));
var handle = HeaderGetHandle(oldHeader); // preserve original allocation StackTrace
var pad = alignment == 0 ? (nuint)IntPtr.Size : alignment;
var total = newSize + (nuint)sizeof(AllocationHeader) + (pad - 1);
var newBase = AlignedAlloc(total, pad);
var newUser = AlignUp((byte*)newBase + (nuint)sizeof(AllocationHeader), pad);
var newHeader = (AllocationHeader*)(newUser - (nuint)sizeof(AllocationHeader));
newHeader->basePtr = newBase;
newHeader->userSize = newSize;
HeaderSetHandle(newHeader, handle); // transfer ownership to the new header
LinkHeader(newHeader);
// Mirror original behavior: copy newSize bytes
MemCpy(newUser, userPtr, newSize);
if (allocationOption.HasFlag(AllocationOption.Clear) && newSize > oldSize)
{
MemClear((byte*)newUser + oldSize, newSize - oldSize);
}
// Unlink and free the old block (without freeing the StackTrace handle again)
oldHeader->stackHandle = 0;
UnlinkHeader(oldHeader);
AlignedFree(oldHeader->basePtr);
return newUser;
}
/// <summary>
/// Gets the number of live persistent heap allocations when the debug layer is disabled.
/// </summary>
public static long LiveHeapAllocationCount => Interlocked.Read(ref s_activeHeapAllocations);
/// <summary> /// <summary>
/// Enables the debug layer, allowing additional diagnostic information to be collected. /// Enables the debug layer, allowing additional diagnostic information to be collected.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnableDebugLayer() public static void EnableDebugLayer()
{ {
// To avoid ambiguity between pointers allocated before/after enabling, this must be called
// before any heap allocations are live.
if (Interlocked.Read(ref s_activeHeapAllocations) != 0)
{
throw new InvalidOperationException("EnableDebugLayer must be called before any heap allocations are active.");
}
s_debugLayer = true; s_debugLayer = true;
s_allocated ??= new(-1, 64);
} }
/// <summary> /// <summary>
@@ -247,80 +377,95 @@ public static unsafe class AllocationManager
switch (allocator) switch (allocator)
{ {
case Allocator.Temp: case Allocator.Temp:
return ref s_arenaAllocator->Handle; return ref s_pArenaAllocator->Handle;
case Allocator.Persistent: case Allocator.Persistent:
return ref s_persistentAllocator->Handle; return ref s_pHeapAllocator->Handle;
case Allocator.Stack: case Allocator.Stack:
return ref s_stackAllocator->Handle; return ref s_pStackAllocator->Handle;
default: default:
throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)); throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator));
} }
} }
/// <summary> /// <summary>
/// Tracks a memory allocation in the allocation manager. /// Allocates a block of memory from the heap with the specified size and alignment, using the given allocation
/// options.
/// </summary> /// </summary>
/// <param name="ptr">The pointer to the allocated memory.</param> /// <param name="size">The number of bytes to allocate. Must be greater than zero.</param>
/// <param name="allocationSize">The size of the allocation in bytes.</param> /// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two.</param>
/// <param name="allocator">The allocator used for the allocation.</param> /// <param name="allocationOption">An optional set of flags that control allocation behavior, such as whether the memory should be cleared or
/// <param name="freeFunc">The function pointer used to free the allocated memory.</param> /// tracked. The default is <see cref="AllocationOption.None"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] /// <returns>A pointer to the beginning of the allocated memory block.</returns>
public static void TrackAllocation(void* ptr, nuint allocationSize, void* allocator) /// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception>
public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{ {
if (!s_debugLayer || s_allocated == null || ptr == null) void* ptr;
if (s_debugLayer)
{ {
return; ptr = DebugAllocate(size, alignment);
}
else
{
ptr = AlignedAlloc(size, alignment);
} }
s_allocated[(nint)ptr] = new AllocationInfo if (allocationOption.HasFlag(AllocationOption.Clear))
{ {
Size = allocationSize, MemClear(ptr, size);
Allocator = allocator, }
StackTrace = new StackTrace(true)
}; Interlocked.Increment(ref s_activeHeapAllocations);
return ptr;
} }
/// <summary> /// <summary>
/// Updates the allocation tracking information by replacing the old pointer with a new pointer. /// Reallocates a block of memory to a new size and alignment, optionally clearing newly allocated memory and
/// </summary> /// applying allocation options.
/// <param name="oldPtr">A pointer to the previously allocated memory. If <paramref name="oldPtr"/> is not tracked, the method does nothing.</param> /// </summary>\
/// <param name="newPtr">A pointer to the newly allocated memory. This pointer will replace <paramref name="oldPtr"/> in the allocation tracking.</param> /// <param name="ptr">A pointer to the previously allocated memory block to be reallocated. Can be <see langword="null"/> to allocate new memory.</param>
/// <param name="allocationSize">The size, in bytes, of the new allocation.</param> /// <param name="oldSize">The size, in bytes, of the memory block currently pointed to by <paramref name="ptr"/>.</param>
/// <param name="allocator">A pointer to the allocator responsible for the new allocation.</param> /// <param name="newSize">The desired size, in bytes, for the reallocated memory block.</param>
/// <param name="freeFunc">A delegate or function pointer used to free the memory associated with the allocation.</param> /// <param name="alignment">The required alignment, in bytes, for the reallocated memory block. Must be a power of two.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] /// <param name="allocationOption">An optional set of flags that control allocation behavior, such as whether to clear newly allocated memory or
public static void UpdateAllocation(void* oldPtr, void* newPtr, nuint allocationSize, void* allocator) /// track the allocation. The default is <see cref="AllocationOption.None"/>.</param>
/// <returns>A pointer to the reallocated memory block with the specified size and alignment. Returns <see langword="null"/>
/// if the allocation fails.</returns>
public static void* HeapRealloc(void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{ {
if (!s_debugLayer || s_allocated == null || oldPtr == null || newPtr == null) if (s_debugLayer)
{ {
return; return DebugReallocate(ptr, oldSize, newSize, alignment, allocationOption);
} }
// If we don't find the allocation info, that means the oldPtr was not tracked var newPtr = AlignedRealloc(ptr, newSize, alignment);
if (s_allocated.Remove((nint)oldPtr, out var info)) if (allocationOption.HasFlag(AllocationOption.Clear))
{ {
s_allocated[(nint)newPtr] = new AllocationInfo if (newSize > oldSize)
{ {
Size = allocationSize, MemClear((byte*)newPtr + oldSize, newSize - oldSize);
Allocator = allocator, }
StackTrace = info.StackTrace
};
} }
return newPtr;
} }
/// <summary> /// <summary>
/// Removes the specified memory allocation from the tracking system. /// Releases a block of unmanaged memory previously allocated by the heap allocator.
/// </summary> /// </summary>
/// <param name="ptr">A pointer to the memory allocation to untrack.</param> /// <param name="ptr">A pointer to the memory block to be freed. The pointer must have been returned by a compatible heap allocation
[MethodImpl(MethodImplOptions.AggressiveInlining)] /// method and must not be null.</param>
public static void UntrackAllocation(void* ptr) public static void HeapFree(void* ptr)
{ {
if (s_allocated == null) if (s_debugLayer)
{ {
return; DebugFree(ptr);
}
else
{
AlignedFree(ptr);
} }
s_allocated.Remove((nint)ptr, out _); Interlocked.Decrement(ref s_activeHeapAllocations);
} }
/// <summary> /// <summary>
@@ -329,7 +474,7 @@ public static unsafe class AllocationManager
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ResetTempAllocator() public static void ResetTempAllocator()
{ {
s_arenaAllocator->Reset(); s_pArenaAllocator->Reset();
} }
/// <summary> /// <summary>
@@ -347,36 +492,74 @@ public static unsafe class AllocationManager
/// </summary> /// </summary>
public static void Dispose() public static void Dispose()
{ {
if (s_allocated != null) if (s_disposed)
{ {
nuint unfreeBytes = 0u; return;
foreach (var pair in s_allocated) }
// In debug mode, walk the intrusive list to surface any leaks.
if (s_debugLayer)
{
var snapshot = new List<AllocationInfo>();
var taken = false;
try
{ {
unfreeBytes += pair.Value.Size; s_liveLock.Enter(ref taken);
if (s_liveHead != null)
{
snapshot.Capacity = 128;
for (var p = s_liveHead; p != null; p = p->next)
{
var trace = (StackTrace)HeaderGetHandle(p).Target!;
snapshot.Add(new AllocationInfo
{
Size = p->userSize,
StackTrace = trace
});
}
}
}
finally
{
if (taken)
{
s_liveLock.Exit();
}
}
nuint unfreeBytes = 0u;
foreach (var info in snapshot)
{
unfreeBytes += info.Size;
} }
if (unfreeBytes > 0u) if (unfreeBytes > 0u)
{ {
throw new MemoryLeakException([.. s_allocated.Values]); throw new MemoryLeakException(snapshot);
} }
s_allocated.Clear();
} }
else if (s_activeHeapAllocations != 0)
if (s_arenaAllocator != null)
{ {
s_arenaAllocator->Dispose(); throw new MemoryLeakException($"Found {s_activeHeapAllocations} memory lakes! Please enable debug layer for more informations.");
NativeMemory.Free(s_arenaAllocator);
} }
if (s_stackAllocator != null) if (s_pArenaAllocator != null)
{ {
NativeMemory.Free(s_stackAllocator); s_pArenaAllocator->Dispose();
NativeMemory.Free(s_pArenaAllocator);
} }
if (s_persistentAllocator != null) if (s_pHeapAllocator != null)
{ {
NativeMemory.Free(s_persistentAllocator); NativeMemory.Free(s_pHeapAllocator);
} }
if (s_pStackAllocator != null)
{
NativeMemory.Free(s_pStackAllocator);
}
s_activeHeapAllocations = 0;
s_disposed = true;
} }
} }

View File

@@ -1,21 +1,16 @@
namespace Misaki.HighPerformance.LowLevel.Buffer; namespace Misaki.HighPerformance.LowLevel.Buffer;
[Flags] [Flags]
public enum AllocationOption : byte public enum AllocationOption : byte
{ {
/// <summary>
/// Default allocation option. Values are uninitialized.
/// </summary>
None = 0, None = 0,
/// <summary> /// <summary>
/// Allocator for initialized memory. /// Clear the memory to zero upon allocation.
/// </summary> /// </summary>
Clear = 1 << 0, Clear = 1 << 0,
/// <summary>
/// Allocator for untracked memory.
/// </summary>
/// <remarks>
/// Use this option carefully, as the allocation manager will not track the memory.
/// No warning will be given if the memory is not freed.
/// </remarks>
UnTracked = 1 << 1,
} }
public enum Allocator : byte public enum Allocator : byte
@@ -23,15 +18,15 @@ 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
Invalid, Invalid,
/// <summary> /// <summary>
/// Allocator for temporary allocations. Allocations are 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. Allocations are not released after use. /// Allocator for persistent allocations. Allocations are not automatically released after use.
/// </summary> /// </summary>
Persistent, Persistent,
/// <summary> /// <summary>
/// Allocator for stack allocations. Must have at least one active stack scope. Allocations are released when the stack scope is exited. /// Allocator for stack allocations. Must have at least one active stack scope. Allocations are automatically released when the stack scope is exited.
/// </summary> /// </summary>
Stack Stack
} }

View File

@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer; namespace Misaki.HighPerformance.LowLevel.Buffer;
@@ -11,7 +11,7 @@ public unsafe struct Stack : IDisposable
{ {
private const nuint _DEFAULT_SIZE = 1024 * 1024; // 1MB private const nuint _DEFAULT_SIZE = 1024 * 1024; // 1MB
public readonly struct Scope : IDisposable public readonly ref struct Scope : IDisposable
{ {
private readonly Stack* _allocator; private readonly Stack* _allocator;
private readonly nuint _originalOffset; private readonly nuint _originalOffset;

View File

@@ -1,4 +1,4 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
@@ -107,7 +107,8 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
get => _buffer != null; get => _buffer != null;
} }
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeArray<T>*)UnsafeUtility.AddressOf(ref this)); public Enumerator GetEnumerator() => new ((UnsafeArray<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
@@ -92,7 +92,8 @@ public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeCollection<KeyValuePai
} }
} }
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => new Enumerator((HashMapHelper<TKey>*)UnsafeUtility.AddressOf(ref _hashMap)); public Enumerator GetEnumerator() => new((HashMapHelper<TKey>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
@@ -48,7 +48,8 @@ public unsafe struct UnsafeHashSet<T> : IUnsafeCollection<T>, IEnumerable<T>
public readonly int Capacity => _hashMap.Capacity; public readonly int Capacity => _hashMap.Capacity;
public readonly bool IsCreated => _hashMap.IsCreated; public readonly bool IsCreated => _hashMap.IsCreated;
public IEnumerator<T> GetEnumerator() => new Enumerator((HashMapHelper<T>*)UnsafeUtility.AddressOf(ref _hashMap)); public Enumerator GetEnumerator() => new((HashMapHelper<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
@@ -136,7 +136,8 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
get => ref _array[index]; get => ref _array[index];
} }
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeList<T>*)UnsafeUtility.AddressOf(ref this)); public Enumerator GetEnumerator() => new ((UnsafeList<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -80,7 +80,8 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
set => _array[index] = value; set => _array[index] = value;
} }
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeQueue<T>*)UnsafeUtility.AddressOf(ref this)); public Enumerator GetEnumerator() => new((UnsafeQueue<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -67,8 +67,8 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
public readonly bool IsCreated => _data.IsCreated && _freeSlots.IsCreated; public readonly bool IsCreated => _data.IsCreated && _freeSlots.IsCreated;
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeSlotMap<T>*)UnsafeUtility.AddressOf(ref this)); public Enumerator GetEnumerator() => new((UnsafeSlotMap<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -80,7 +80,8 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
public readonly int Capacity => _capacity; public readonly int Capacity => _capacity;
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated; public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeSparseSet<T>*)UnsafeUtility.AddressOf(ref this)); public Enumerator GetEnumerator() => new((UnsafeSparseSet<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
@@ -15,20 +15,64 @@ namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeStack<T> : IUnsafeCollection<T> public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
where T : unmanaged where T : unmanaged
{ {
public struct Enumerator : IEnumerator<T>
{
private readonly UnsafeStack<T>* _collection;
private int _index;
private T _value;
public Enumerator(UnsafeStack<T>* collection)
{
_collection = collection;
_index = collection->Count;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index--;
if (_index >= 0)
{
_value = UnsafeUtility.ReadArrayElement<T>(_collection->_array.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
}
public void Reset()
{
_index = _collection->Count;
}
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value;
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Current;
}
public readonly void Dispose()
{
}
}
private UnsafeArray<T> _array; private UnsafeArray<T> _array;
private int _count; private int _count;
public readonly int Count => _count; public readonly int Count => _count;
public readonly bool IsCreated => _array.IsCreated; public readonly bool IsCreated => _array.IsCreated;
public IEnumerator<T> GetEnumerator() public Enumerator GetEnumerator() => new((UnsafeStack<T>*)UnsafeUtility.AddressOf(ref this));
{ IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
throw new NotImplementedException(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary> /// <summary>
/// Invalid constructor, use <see cref="UnsafeStack(int, Allocator, AllocationOption)"/> or <see cref="UnsafeStack(int, ref AllocationHandle, AllocationOption)"/> instead. /// Invalid constructor, use <see cref="UnsafeStack(int, Allocator, AllocationOption)"/> or <see cref="UnsafeStack(int, ref AllocationHandle, AllocationOption)"/> instead.
@@ -41,23 +85,23 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
/// <summary> /// <summary>
/// Initializes a new instance of the UnsafeStack class with the specified initial capacity and allocation options. /// Initializes a new instance of the UnsafeStack class with the specified initial capacity and allocation options.
/// </summary> /// </summary>
/// <param name="initialCapacity">The number of elements the stack can initially hold. Must be greater than zero.</param> /// <param name="capacity">The number of elements the stack can initially hold. Must be greater than zero.</param>
/// <param name="handle">A reference to an AllocationHandle used to manage the underlying memory allocation for the stack.</param> /// <param name="handle">A reference to an AllocationHandle used to manage the underlying memory allocation for the stack.</param>
/// <param name="allocationOption">Specifies additional options for memory allocation. The default is AllocationOption.None.</param> /// <param name="allocationOption">Specifies additional options for memory allocation. The default is AllocationOption.None.</param>
public UnsafeStack(int initialCapacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None) public UnsafeStack(int capacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{ {
_array = new UnsafeArray<T>(initialCapacity, ref handle, allocationOption); _array = new UnsafeArray<T>(capacity, ref handle, allocationOption);
} }
/// <summary> /// <summary>
/// Initializes a new instance of the UnsafeStack class with the specified initial capacity, allocator, and /// Initializes a new instance of the UnsafeStack class with the specified initial capacity, allocator, and
/// allocation options. /// allocation options.
/// </summary> /// </summary>
/// <param name="initialCapacity">The initial number of elements that the stack can hold. Must be greater than zero.</param> /// <param name="capacity">The initial number of elements that the stack can hold. Must be greater than zero.</param>
/// <param name="allocator">The allocator to use for memory management of the stack's storage.</param> /// <param name="allocator">The allocator to use for memory management of the stack's storage.</param>
/// <param name="allocationOption">The allocation option that determines how memory is allocated for the stack. The default is AllocationOption.None.</param> /// <param name="allocationOption">The allocation option that determines how memory is allocated for the stack. The default is AllocationOption.None.</param>
public UnsafeStack(int initialCapacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) public UnsafeStack(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(initialCapacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption) : this(capacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{ {
} }

View File

@@ -1,15 +1,28 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
namespace Misaki.HighPerformance.LowLevel.Exceptions; namespace Misaki.HighPerformance.LowLevel;
/// <summary> /// <summary>
/// An exception that is thrown when a memory leak is detected. /// An exception that is thrown when a memory leak is detected.
/// </summary> /// </summary>
/// <param name="Infos">An array of AllocationInfo containing details about the memory leaks.</param> /// <param name="Infos">An array of AllocationInfo containing details about the memory leaks.</param>
public class MemoryLeakException(params AllocationInfo[] Infos) : Exception public class MemoryLeakException : Exception
{ {
private readonly IEnumerable<AllocationInfo>? _infos;
private readonly string _message = string.Empty;
public MemoryLeakException(IEnumerable<AllocationInfo> infos)
{
_infos = infos;
}
public MemoryLeakException(string message)
{
_message = message;
}
private static string GetMessage(StackTrace? stackTrace) private static string GetMessage(StackTrace? stackTrace)
{ {
if (stackTrace == null) if (stackTrace == null)
@@ -36,9 +49,15 @@ public class MemoryLeakException(params AllocationInfo[] Infos) : Exception
{ {
get get
{ {
if (_infos == null)
{
return _message;
}
var stringBuilder = new StringBuilder(); var stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"Found {Infos.Length} memory lakes!"); stringBuilder.AppendLine($"Found {_infos.Count()} memory lakes!");
foreach (var info in Infos)
foreach (var info in _infos)
{ {
stringBuilder.AppendLine(GetMessage(info.StackTrace)); stringBuilder.AppendLine(GetMessage(info.StackTrace));
} }

View File

@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Utilities; namespace Misaki.HighPerformance.LowLevel.Utilities;
@@ -69,11 +69,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Free(void* ptr) public static void Free(void* ptr)
{ {
if (ptr == null)
{
return;
}
NativeMemory.Free(ptr); NativeMemory.Free(ptr);
} }
@@ -85,11 +80,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AlignedFree(void* ptr) public static void AlignedFree(void* ptr)
{ {
if (ptr == null)
{
return;
}
NativeMemory.AlignedFree(ptr); NativeMemory.AlignedFree(ptr);
} }

View File

@@ -43,4 +43,14 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
var array = new UnsafeArray<int>(10, Allocator.Persistent); //AllocationManager.EnableDebugLayer();
//var array = new UnsafeArray<int>(10, Allocator.Persistent);
//var array2 = new UnsafeArray<int>(10, Allocator.Persistent);
//array.Dispose();
//array2.Dispose();
//AllocationManager.Dispose();
using (AllocationManager.CreateStackScope())
{
var arr = new UnsafeArray<int>(10, Allocator.Stack);
}

View File

@@ -0,0 +1,61 @@
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
public class TestAllocationManager
{
[TestInitialize]
public void Initialize()
{
AllocationManager.EnableDebugLayer();
}
[TestMethod]
public void ShouldNotLeakTest()
{
try
{
var array = new UnsafeArray<int>(10, Allocator.Persistent);
var array2 = new UnsafeArray<int>(10, Allocator.Persistent);
array.Dispose();
array2.Dispose();
AllocationManager.Dispose();
}
finally
{
var leaks = AllocationManager.LiveHeapAllocationCount;
Assert.AreEqual(0, leaks);
}
}
[TestMethod]
public void ShouldLeakTest()
{
var array = new UnsafeArray<int>(10, Allocator.Persistent);
var array2 = new UnsafeArray<int>(10, Allocator.Persistent);
try
{
AllocationManager.Dispose();
}
catch (MemoryLeakException)
{
var leaks = AllocationManager.LiveHeapAllocationCount;
Assert.AreEqual(2, leaks);
return;
}
finally
{
array.Dispose();
array2.Dispose();
}
Assert.Fail("Expected MemoryLeakException was not thrown.");
}
}

View File

@@ -0,0 +1,56 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
[TestClass]
public class TestUnsafeArray
{
private UnsafeArray<int> _arr;
[TestInitialize]
public void Initialize()
{
_arr = new UnsafeArray<int>(16, Allocator.Persistent);
}
[TestCleanup]
public void Cleanup()
{
_arr.Dispose();
}
[TestMethod]
public void TestIndexAccess()
{
for (int i = 0; i < _arr.Count; i++)
{
_arr[i] = i * 10;
}
for (int i = 0; i < _arr.Count; i++)
{
Assert.AreEqual(i * 10, _arr[i]);
}
}
[TestMethod]
public void TestEnumeration()
{
_arr.Clear();
int expectedValue = 0;
foreach (var item in _arr)
{
Assert.AreEqual(expectedValue, item);
}
}
[TestMethod]
public void TestIsCreated()
{
Assert.IsTrue(_arr.IsCreated);
_arr.Dispose();
Assert.IsFalse(_arr.IsCreated);
}
}

View File

@@ -0,0 +1,68 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
[TestClass]
public class TestUnsafeStack
{
private UnsafeStack<int> _stack;
[TestInitialize]
public void Initialize()
{
_stack = new UnsafeStack<int>(16, Allocator.Persistent);
}
[TestCleanup]
public void Cleanup()
{
_stack.Dispose();
}
[TestMethod]
public void TestPushPop()
{
for (int i = 0; i < 10; i++)
{
_stack.Push(i);
}
Assert.AreEqual(10, _stack.Count);
for (int i = 9; i >= 0; i--)
{
int value = _stack.Pop();
Assert.AreEqual(i, value);
}
Assert.AreEqual(0, _stack.Count);
}
[TestMethod]
public void TestPeek()
{
_stack.Push(42);
int value = _stack.Peek();
Assert.AreEqual(42, value);
Assert.AreEqual(1, _stack.Count);
}
[TestMethod]
public void TestEnumeration()
{
for (int i = 0; i < 5; i++)
{
_stack.Push(i);
}
int expected = 4;
foreach (var item in _stack)
{
Assert.AreEqual(expected, item);
expected--;
}
}
}