From fbe72e33f749c03b83c50b356bfaa47c970f69eb Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 6 Nov 2025 01:28:43 +0900 Subject: [PATCH] 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`. 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. --- .../Buffer/AllocationManager.cs | 413 +++++++++++++----- .../Buffer/AllocationOption.cs | 21 +- .../Buffer/Stack.cs | 4 +- .../Collections/UnsafeArray.cs | 5 +- .../Collections/UnsafeHashMap.cs | 5 +- .../Collections/UnsafeHashSet.cs | 5 +- .../Collections/UnsafeList.cs | 5 +- .../Collections/UnsafeQueue.cs | 3 +- .../Collections/UnsafeSlotMap.cs | 4 +- .../Collections/UnsafeSparseSet.cs | 3 +- .../Collections/UnsafeStack.cs | 74 +++- .../{Exceptions => }/MemoryLeakException.cs | 29 +- .../Utilities/MemoryUtility.cs | 12 +- Misaki.HighPerformance.Test/Program.cs | 12 +- .../UnitTest/Buffer/TestAllocationManager.cs | 61 +++ .../UnitTest/Collections/TestUnsafeArray.cs | 56 +++ .../UnitTest/Collections/TestUnsafeStack.cs | 68 +++ 17 files changed, 606 insertions(+), 174 deletions(-) rename Misaki.HighPerformance.LowLevel/{Exceptions => }/MemoryLeakException.cs (64%) create mode 100644 Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs create mode 100644 Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs create mode 100644 Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs index f67b01b..f0ff442 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs @@ -1,6 +1,4 @@ -using Misaki.HighPerformance.LowLevel.Contracts; -using Misaki.HighPerformance.LowLevel.Exceptions; -using System.Collections.Concurrent; +using Misaki.HighPerformance.LowLevel.Contracts; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -20,14 +18,6 @@ public readonly unsafe struct AllocationInfo get; init; } - /// - /// Get the allocator used for the allocation. - /// - public void* Allocator - { - get; init; - } - /// /// Get the stack trace at the time of allocation for debugging purposes. /// @@ -43,6 +33,17 @@ public readonly unsafe struct AllocationInfo /// 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 DynamicArena _arena; @@ -111,49 +112,17 @@ public static unsafe class AllocationManager private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption) { - var ptr = AlignedAlloc(size, alignment); - - if (allocationOption.HasFlag(AllocationOption.Clear)) - { - MemClear(ptr, size); - } - - if (!allocationOption.HasFlag(AllocationOption.UnTracked)) - { - TrackAllocation(ptr, size, instance); - } - - return ptr; + return HeapAlloc(size, alignment, allocationOption); } private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption) { - var newPtr = AlignedRealloc(ptr, newSize, alignment); - - 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; + return HeapRealloc(ptr, oldSize, newSize, alignment, allocationOption); } private static void FreeBlock(void* instance, void* ptr) { - AlignedFree(ptr); - UntrackAllocation(ptr); + HeapFree(ptr); } } @@ -207,32 +176,193 @@ public static unsafe class AllocationManager private const uint _DEFAULT_MEMORY_POOL_SIZE = 512 * 1024; - private static readonly ArenaAllocator* s_arenaAllocator; - private static readonly HeapAllocator* s_persistentAllocator; - private static readonly StackAllocator* s_stackAllocator; + private static readonly ArenaAllocator* s_pArenaAllocator; + private static readonly HeapAllocator* s_pHeapAllocator; + private static readonly StackAllocator* s_pStackAllocator; private static bool s_debugLayer; - private static ConcurrentDictionary? 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() { - s_arenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator)); - s_persistentAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator)); - s_stackAllocator = (StackAllocator*)NativeMemory.Alloc((nuint)sizeof(StackAllocator)); + s_pArenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator)); + s_pHeapAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator)); + s_pStackAllocator = (StackAllocator*)NativeMemory.Alloc((nuint)sizeof(StackAllocator)); - s_arenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE); - s_persistentAllocator->Init(); - s_stackAllocator->Init(); + s_liveLock = new SpinLock(false); + + 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; + } + + /// + /// Gets the number of live persistent heap allocations when the debug layer is disabled. + /// + public static long LiveHeapAllocationCount => Interlocked.Read(ref s_activeHeapAllocations); + /// /// Enables the debug layer, allowing additional diagnostic information to be collected. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] 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_allocated ??= new(-1, 64); } /// @@ -247,80 +377,95 @@ public static unsafe class AllocationManager switch (allocator) { case Allocator.Temp: - return ref s_arenaAllocator->Handle; + return ref s_pArenaAllocator->Handle; case Allocator.Persistent: - return ref s_persistentAllocator->Handle; + return ref s_pHeapAllocator->Handle; case Allocator.Stack: - return ref s_stackAllocator->Handle; + return ref s_pStackAllocator->Handle; default: throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)); } } /// - /// 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. /// - /// The pointer to the allocated memory. - /// The size of the allocation in bytes. - /// The allocator used for the allocation. - /// The function pointer used to free the allocated memory. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void TrackAllocation(void* ptr, nuint allocationSize, void* allocator) + /// The number of bytes to allocate. Must be greater than zero. + /// The alignment, in bytes, for the allocated memory block. Must be a power of two. + /// An optional set of flags that control allocation behavior, such as whether the memory should be cleared or + /// tracked. The default is . + /// A pointer to the beginning of the allocated memory block. + /// Thrown if the allocation fails. + 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, - Allocator = allocator, - StackTrace = new StackTrace(true) - }; + MemClear(ptr, size); + } + + Interlocked.Increment(ref s_activeHeapAllocations); + return ptr; } /// - /// Updates the allocation tracking information by replacing the old pointer with a new pointer. - /// - /// A pointer to the previously allocated memory. If is not tracked, the method does nothing. - /// A pointer to the newly allocated memory. This pointer will replace in the allocation tracking. - /// The size, in bytes, of the new allocation. - /// A pointer to the allocator responsible for the new allocation. - /// A delegate or function pointer used to free the memory associated with the allocation. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UpdateAllocation(void* oldPtr, void* newPtr, nuint allocationSize, void* allocator) + /// Reallocates a block of memory to a new size and alignment, optionally clearing newly allocated memory and + /// applying allocation options. + /// \ + /// A pointer to the previously allocated memory block to be reallocated. Can be to allocate new memory. + /// The size, in bytes, of the memory block currently pointed to by . + /// The desired size, in bytes, for the reallocated memory block. + /// The required alignment, in bytes, for the reallocated memory block. Must be a power of two. + /// An optional set of flags that control allocation behavior, such as whether to clear newly allocated memory or + /// track the allocation. The default is . + /// A pointer to the reallocated memory block with the specified size and alignment. Returns + /// if the allocation fails. + 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 - if (s_allocated.Remove((nint)oldPtr, out var info)) + var newPtr = AlignedRealloc(ptr, newSize, alignment); + if (allocationOption.HasFlag(AllocationOption.Clear)) { - s_allocated[(nint)newPtr] = new AllocationInfo + if (newSize > oldSize) { - Size = allocationSize, - Allocator = allocator, - StackTrace = info.StackTrace - }; + MemClear((byte*)newPtr + oldSize, newSize - oldSize); + } } + + return newPtr; } /// - /// Removes the specified memory allocation from the tracking system. + /// Releases a block of unmanaged memory previously allocated by the heap allocator. /// - /// A pointer to the memory allocation to untrack. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void UntrackAllocation(void* ptr) + /// A pointer to the memory block to be freed. The pointer must have been returned by a compatible heap allocation + /// method and must not be null. + 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); } /// @@ -329,7 +474,7 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ResetTempAllocator() { - s_arenaAllocator->Reset(); + s_pArenaAllocator->Reset(); } /// @@ -347,36 +492,74 @@ public static unsafe class AllocationManager /// public static void Dispose() { - if (s_allocated != null) + if (s_disposed) { - nuint unfreeBytes = 0u; - foreach (var pair in s_allocated) + return; + } + + // In debug mode, walk the intrusive list to surface any leaks. + if (s_debugLayer) + { + var snapshot = new List(); + 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) { - throw new MemoryLeakException([.. s_allocated.Values]); + throw new MemoryLeakException(snapshot); } - - s_allocated.Clear(); } - - if (s_arenaAllocator != null) + else if (s_activeHeapAllocations != 0) { - s_arenaAllocator->Dispose(); - NativeMemory.Free(s_arenaAllocator); + throw new MemoryLeakException($"Found {s_activeHeapAllocations} memory lakes! Please enable debug layer for more informations."); } - 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; } } \ No newline at end of file diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs index 36e613e..73bf2f1 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs @@ -1,21 +1,16 @@ -namespace Misaki.HighPerformance.LowLevel.Buffer; +namespace Misaki.HighPerformance.LowLevel.Buffer; [Flags] public enum AllocationOption : byte { + /// + /// Default allocation option. Values are uninitialized. + /// None = 0, /// - /// Allocator for initialized memory. + /// Clear the memory to zero upon allocation. /// Clear = 1 << 0, - /// - /// Allocator for untracked memory. - /// - /// - /// Use this option carefully, as the allocation manager will not track the memory. - /// No warning will be given if the memory is not freed. - /// - UnTracked = 1 << 1, } 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 Invalid, /// - /// Allocator for temporary allocations. Allocations are released after use automatically. + /// Allocator for temporary allocations. Allocations are automatically released after use automatically. /// Temp, /// - /// Allocator for persistent allocations. Allocations are not released after use. + /// Allocator for persistent allocations. Allocations are not automatically released after use. /// Persistent, /// - /// 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. /// Stack } \ No newline at end of file diff --git a/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs b/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs index 2dc87d2..3f01c5d 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.LowLevel.Buffer; @@ -11,7 +11,7 @@ public unsafe struct Stack : IDisposable { 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 nuint _originalOffset; diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs index 9751430..9e73d3a 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs @@ -1,4 +1,4 @@ -using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; @@ -107,7 +107,8 @@ public unsafe struct UnsafeArray : IUnsafeCollection get => _buffer != null; } - public IEnumerator GetEnumerator() => new Enumerator((UnsafeArray*)UnsafeUtility.AddressOf(ref this)); + public Enumerator GetEnumerator() => new ((UnsafeArray*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs index 56ae1aa..01545ba 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs @@ -1,4 +1,4 @@ -using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; @@ -92,7 +92,8 @@ public unsafe struct UnsafeHashMap : IUnsafeCollection> GetEnumerator() => new Enumerator((HashMapHelper*)UnsafeUtility.AddressOf(ref _hashMap)); + public Enumerator GetEnumerator() => new((HashMapHelper*)UnsafeUtility.AddressOf(ref this)); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs index 11deb0d..0bf5b7e 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs @@ -1,4 +1,4 @@ -using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; @@ -48,7 +48,8 @@ public unsafe struct UnsafeHashSet : IUnsafeCollection, IEnumerable public readonly int Capacity => _hashMap.Capacity; public readonly bool IsCreated => _hashMap.IsCreated; - public IEnumerator GetEnumerator() => new Enumerator((HashMapHelper*)UnsafeUtility.AddressOf(ref _hashMap)); + public Enumerator GetEnumerator() => new((HashMapHelper*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs index 6f7c541..0534552 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs @@ -1,4 +1,4 @@ -using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; @@ -136,7 +136,8 @@ public unsafe struct UnsafeList : IUnsafeCollection get => ref _array[index]; } - public IEnumerator GetEnumerator() => new Enumerator((UnsafeList*)UnsafeUtility.AddressOf(ref this)); + public Enumerator GetEnumerator() => new ((UnsafeList*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs index d1a2c36..d0fbd91 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs @@ -80,7 +80,8 @@ public unsafe struct UnsafeQueue : IUnsafeCollection set => _array[index] = value; } - public IEnumerator GetEnumerator() => new Enumerator((UnsafeQueue*)UnsafeUtility.AddressOf(ref this)); + public Enumerator GetEnumerator() => new((UnsafeQueue*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs index 9a1a6e0..4ab075a 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs @@ -67,8 +67,8 @@ public unsafe struct UnsafeSlotMap : IUnsafeCollection public readonly bool IsCreated => _data.IsCreated && _freeSlots.IsCreated; - public IEnumerator GetEnumerator() => new Enumerator((UnsafeSlotMap*)UnsafeUtility.AddressOf(ref this)); - + public Enumerator GetEnumerator() => new((UnsafeSlotMap*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs index c109adb..387b2d2 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs @@ -80,7 +80,8 @@ public unsafe struct UnsafeSparseSet : IUnsafeCollection public readonly int Capacity => _capacity; public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated; - public IEnumerator GetEnumerator() => new Enumerator((UnsafeSparseSet*)UnsafeUtility.AddressOf(ref this)); + public Enumerator GetEnumerator() => new((UnsafeSparseSet*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs index 154a5bd..250f3cb 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs @@ -1,4 +1,4 @@ -using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; @@ -15,20 +15,64 @@ namespace Misaki.HighPerformance.LowLevel.Collections; public unsafe struct UnsafeStack : IUnsafeCollection where T : unmanaged { + public struct Enumerator : IEnumerator + { + private readonly UnsafeStack* _collection; + private int _index; + private T _value; + + public Enumerator(UnsafeStack* collection) + { + _collection = collection; + _index = collection->Count; + _value = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + _index--; + if (_index >= 0) + { + _value = UnsafeUtility.ReadArrayElement(_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 _array; private int _count; public readonly int Count => _count; public readonly bool IsCreated => _array.IsCreated; - public IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + public Enumerator GetEnumerator() => new((UnsafeStack*)UnsafeUtility.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Invalid constructor, use or instead. @@ -41,23 +85,23 @@ public unsafe struct UnsafeStack : IUnsafeCollection /// /// Initializes a new instance of the UnsafeStack class with the specified initial capacity and allocation options. /// - /// The number of elements the stack can initially hold. Must be greater than zero. + /// The number of elements the stack can initially hold. Must be greater than zero. /// A reference to an AllocationHandle used to manage the underlying memory allocation for the stack. /// Specifies additional options for memory allocation. The default is AllocationOption.None. - 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(initialCapacity, ref handle, allocationOption); + _array = new UnsafeArray(capacity, ref handle, allocationOption); } /// /// Initializes a new instance of the UnsafeStack class with the specified initial capacity, allocator, and /// allocation options. /// - /// The initial number of elements that the stack can hold. Must be greater than zero. + /// The initial number of elements that the stack can hold. Must be greater than zero. /// The allocator to use for memory management of the stack's storage. /// The allocation option that determines how memory is allocated for the stack. The default is AllocationOption.None. - public UnsafeStack(int initialCapacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) - : this(initialCapacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption) + public UnsafeStack(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) + : this(capacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption) { } diff --git a/Misaki.HighPerformance.LowLevel/Exceptions/MemoryLeakException.cs b/Misaki.HighPerformance.LowLevel/MemoryLeakException.cs similarity index 64% rename from Misaki.HighPerformance.LowLevel/Exceptions/MemoryLeakException.cs rename to Misaki.HighPerformance.LowLevel/MemoryLeakException.cs index de8a0bb..69a002d 100644 --- a/Misaki.HighPerformance.LowLevel/Exceptions/MemoryLeakException.cs +++ b/Misaki.HighPerformance.LowLevel/MemoryLeakException.cs @@ -1,15 +1,28 @@ -using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Buffer; using System.Diagnostics; using System.Text; -namespace Misaki.HighPerformance.LowLevel.Exceptions; +namespace Misaki.HighPerformance.LowLevel; /// /// An exception that is thrown when a memory leak is detected. /// /// An array of AllocationInfo containing details about the memory leaks. -public class MemoryLeakException(params AllocationInfo[] Infos) : Exception +public class MemoryLeakException : Exception { + private readonly IEnumerable? _infos; + private readonly string _message = string.Empty; + + public MemoryLeakException(IEnumerable infos) + { + _infos = infos; + } + + public MemoryLeakException(string message) + { + _message = message; + } + private static string GetMessage(StackTrace? stackTrace) { if (stackTrace == null) @@ -36,9 +49,15 @@ public class MemoryLeakException(params AllocationInfo[] Infos) : Exception { get { + if (_infos == null) + { + return _message; + } + var stringBuilder = new StringBuilder(); - stringBuilder.AppendLine($"Found {Infos.Length} memory lakes!"); - foreach (var info in Infos) + stringBuilder.AppendLine($"Found {_infos.Count()} memory lakes!"); + + foreach (var info in _infos) { stringBuilder.AppendLine(GetMessage(info.StackTrace)); } diff --git a/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs b/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs index 281c636..2d75aa0 100644 --- a/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs +++ b/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Misaki.HighPerformance.LowLevel.Utilities; @@ -69,11 +69,6 @@ public static unsafe partial class MemoryUtility [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Free(void* ptr) { - if (ptr == null) - { - return; - } - NativeMemory.Free(ptr); } @@ -85,11 +80,6 @@ public static unsafe partial class MemoryUtility [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AlignedFree(void* ptr) { - if (ptr == null) - { - return; - } - NativeMemory.AlignedFree(ptr); } diff --git a/Misaki.HighPerformance.Test/Program.cs b/Misaki.HighPerformance.Test/Program.cs index 13aaf49..42157eb 100644 --- a/Misaki.HighPerformance.Test/Program.cs +++ b/Misaki.HighPerformance.Test/Program.cs @@ -43,4 +43,14 @@ using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; -var array = new UnsafeArray(10, Allocator.Persistent); +//AllocationManager.EnableDebugLayer(); +//var array = new UnsafeArray(10, Allocator.Persistent); +//var array2 = new UnsafeArray(10, Allocator.Persistent); +//array.Dispose(); +//array2.Dispose(); +//AllocationManager.Dispose(); + +using (AllocationManager.CreateStackScope()) +{ + var arr = new UnsafeArray(10, Allocator.Stack); +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs b/Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs new file mode 100644 index 0000000..e8ed6f0 --- /dev/null +++ b/Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs @@ -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(10, Allocator.Persistent); + var array2 = new UnsafeArray(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(10, Allocator.Persistent); + var array2 = new UnsafeArray(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."); + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs new file mode 100644 index 0000000..dee7deb --- /dev/null +++ b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs @@ -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 _arr; + + [TestInitialize] + public void Initialize() + { + _arr = new UnsafeArray(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); + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs new file mode 100644 index 0000000..7c12e9b --- /dev/null +++ b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs @@ -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 _stack; + + [TestInitialize] + public void Initialize() + { + _stack = new UnsafeStack(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--; + } + } +}