From 1e00f4eb25a4000b4c52023dfddbd372edc179dd Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 3 Apr 2025 15:47:43 +0900 Subject: [PATCH] Enhance memory management and data structures Updated `CollectionBenchmark` for setup/cleanup methods, streamlined benchmarking in `Program.cs`, and improved documentation in `AllocationOption` and `Allocator` enums. Made `Enumerator` structs public in several collections and clarified constructor parameters. Introduced a new `UnsafeStack` struct for stack operations. Enhanced `AllocationManager` with better memory tracking and management, ensuring proper allocation and disposal. --- .../CollectionBenchmark.cs | 32 +++--- Misaki.HighPerformance.Test/Program.cs | 9 +- .../Collections/AllocationOption.cs | 28 ++++- .../Collections/UnsafeArray.cs | 16 +-- .../Collections/UnsafeHashMap.cs | 2 +- .../Collections/UnsafeHashSet.cs | 2 +- .../Collections/UnsafeList.cs | 8 +- .../Collections/UnsafeQueue.cs | 11 +- .../Collections/UnsafeStack.cs | 104 ++++++++++++++++++ .../Services/AllocationManager.cs | 98 +++++++++++++---- 10 files changed, 232 insertions(+), 78 deletions(-) create mode 100644 Misaki.HighPerformance.Unsafe/Collections/UnsafeStack.cs diff --git a/Misaki.HighPerformance.Test/CollectionBenchmark.cs b/Misaki.HighPerformance.Test/CollectionBenchmark.cs index 9f2f76b..0b625a8 100644 --- a/Misaki.HighPerformance.Test/CollectionBenchmark.cs +++ b/Misaki.HighPerformance.Test/CollectionBenchmark.cs @@ -10,32 +10,28 @@ public class CollectionBenchmark [Params(10, 100, 1000)] public int count = 100; + [GlobalSetup] + public void Setup() + { + AllocationManager.Initialize(512_000); + } + [Benchmark] public void Array() { - for (var i = 0; i < count; i++) - { - var array = new int[count]; - } + var array = new int[count]; } [Benchmark] public void UnsafeArray() { - for (var i = 0; i < count; i++) - { - var array = new UnsafeArray(count, Allocator.Temp); - for (var j = 0; j < count; j++) - { - array[j] = j; - } + var array = new UnsafeArray(count, Allocator.Temp); + AllocationManager.Reset(); + } - foreach (var item in array) - { - Console.WriteLine(item); - } - - AllocationManager.Reset(); - } + [GlobalCleanup] + public void Cleanup() + { + AllocationManager.Dispose(); } } \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/Program.cs b/Misaki.HighPerformance.Test/Program.cs index c8e4300..8ed5009 100644 --- a/Misaki.HighPerformance.Test/Program.cs +++ b/Misaki.HighPerformance.Test/Program.cs @@ -1,7 +1,4 @@ -using Misaki.HighPerformance.Test; -using Misaki.HighPerformance.Unsafe.Services; +using BenchmarkDotNet.Running; +using Misaki.HighPerformance.Test; -AllocationManager.Initialize(512_000); -var test = new CollectionBenchmark(); -test.UnsafeArray(); -AllocationManager.Dispose(); +BenchmarkRunner.Run(); diff --git a/Misaki.HighPerformance.Unsafe/Collections/AllocationOption.cs b/Misaki.HighPerformance.Unsafe/Collections/AllocationOption.cs index 829ff23..d2f35a1 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/AllocationOption.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/AllocationOption.cs @@ -2,14 +2,32 @@ public enum AllocationOption : byte { + /// + /// Allocator for uninitialized memory + /// UnInitialized, - Clear + /// + /// Allocator for initialized memory. + /// + Clear, + /// + /// 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 } -public enum Allocator: byte +public enum Allocator : byte { // Make the first allocator as invalid because we don't want to user create a defualt collection without passing any parameters - Invalid = 0, - Temp = 1, - Persistent = 2, + Invalid, + /// + /// Allocator for temporary allocations. Allocations are cleared after use. + /// + Temp, + /// + /// Allocator for persistent allocations. Allocations are not cleared after use. + /// + Persistent, } \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs index 9e955bc..d74a402 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs @@ -13,7 +13,7 @@ namespace Misaki.HighPerformance.Unsafe.Collections; public unsafe struct UnsafeArray : IUnsafeCollection where T : unmanaged { - private struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private UnsafeArray* _collection; private int _index; @@ -90,30 +90,30 @@ public unsafe struct UnsafeArray : IUnsafeCollection /// allocates memory and optionally clears it. /// /// Specifies the number of elements to allocate in the array, which must be greater than zero. - /// Determines how the allocated memory should be initialized, either uninitialized or cleared. + /// Specifies the allocator to use for memory allocation, which determines the memory management strategy. + /// Determines how the allocated memory should be initialized, either uninitialized or cleared. /// Thrown when the specified number of elements is less than or equal to zero. - public UnsafeArray(int count, Allocator allocator, AllocationOption allocationType = AllocationOption.UnInitialized) + public UnsafeArray(int count, Allocator allocator, AllocationOption allocationOption = AllocationOption.UnInitialized) { if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count), "Count must be greater than zero."); } - _buffer = AllocationManager.Allocate((uint)count, (uint)AlignOf(), allocator, allocationType); + _buffer = AllocationManager.Allocate((uint)count, (uint)AlignOf(), allocator, allocationOption); _count = count; - if (allocationType == AllocationOption.Clear) + if (allocationOption == AllocationOption.Clear) { Clear(); } } /// - /// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. The count is adjusted based on - /// the size of the type T. + /// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. /// /// A pointer to the memory location that holds the elements of the array. - /// The total size of the data in bytes, which is divided by the size of type T to determine the number of elements. + /// The total size of the data. public UnsafeArray(void* buffer, int count) { _buffer = (T*)buffer; diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs index 468d6d4..42c2d4e 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs @@ -7,7 +7,7 @@ namespace Misaki.HighPerformance.Unsafe.Collections; public unsafe struct UnsafeHashMap : IUnsafeCollection> where TKey : unmanaged, IEquatable where TValue : unmanaged { - private struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { internal HashMapHelper.Enumerator _enumerator; diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs index 610d7d3..37eded8 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs @@ -13,7 +13,7 @@ namespace Misaki.HighPerformance.Unsafe.Collections; public unsafe struct UnsafeHashSet : IUnsafeCollection, IEnumerable where T : unmanaged, IEquatable { - private struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { internal HashMapHelper.Enumerator _enumerator; diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs index 09aa534..a4e80ce 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs @@ -12,7 +12,7 @@ namespace Misaki.HighPerformance.Unsafe.Collections; public unsafe struct UnsafeList : IUnsafeCollection where T : unmanaged { - private struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private UnsafeList* _collection; private int _index; @@ -132,12 +132,6 @@ public unsafe struct UnsafeList : IUnsafeCollection public UnsafeList(int capacity, Allocator allocator, AllocationOption allocationType = AllocationOption.UnInitialized) { _array = new UnsafeArray(capacity, allocator, allocationType); - _count = 0; - - if (allocationType == AllocationOption.Clear) - { - Clear(); - } } private readonly void CheckNoResizeCapacity(int count) diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs index a13ce5d..c8fe4e3 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs @@ -13,7 +13,7 @@ namespace Misaki.HighPerformance.Unsafe.Collections; public unsafe struct UnsafeQueue : IUnsafeCollection where T : unmanaged { - private struct Enumerator : IEnumerator + public struct Enumerator : IEnumerator { private UnsafeQueue* _collection; private int _index; @@ -83,13 +83,6 @@ public unsafe struct UnsafeQueue : IUnsafeCollection public UnsafeQueue(int capacity, Allocator allocator, AllocationOption allocationType = AllocationOption.UnInitialized) { _array = new UnsafeArray(capacity, allocator, allocationType); - _count = 0; - _offset = 0; - - if (allocationType == AllocationOption.Clear) - { - Clear(); - } } /// @@ -132,7 +125,7 @@ public unsafe struct UnsafeQueue : IUnsafeCollection /// /// The output variable that will hold the dequeued item if the operation is successful. /// True if an item was successfully dequeued, otherwise false. - public bool TryDequeue([MaybeNullWhen(false)] out T value) + public bool TryDequeue(out T value) { if (_count == 0) { diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeStack.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeStack.cs new file mode 100644 index 0000000..71fce08 --- /dev/null +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeStack.cs @@ -0,0 +1,104 @@ +using Misaki.HighPerformance.Unsafe.Collections.Contracts; +using Misaki.HighPerformance.Unsafe.Helpers; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Misaki.HighPerformance.Unsafe.Collections; + +public unsafe struct UnsafeStack : IUnsafeCollection + where T : unmanaged +{ + 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 UnsafeStack(int initialSize, Allocator allocator, AllocationOption allocationOption = AllocationOption.UnInitialized) + { + _array = new UnsafeArray(initialSize, allocator, allocationOption); + } + + public void Push(T value) + { + if (_count >= _array.Count) + { + Resize(_array.Count + (int)(_array.Count * 0.5f)); + } + + UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), _count, value); + _count++; + } + + public T Pop() + { + if (_count == 0) + { + throw new InvalidOperationException("Stack is empty."); + } + + _count--; + return _array[_count]; + } + + public bool TryPop(out T value) + { + if (_count == 0) + { + value = default; + return false; + } + + _count--; + value = _array[_count]; + return true; + } + + public readonly T Peek() + { + if (_count == 0) + { + throw new InvalidOperationException("Stack is empty."); + } + + return _array[_count - 1]; + } + + public void Resize(int newSize) + { + _array.Resize(newSize); + + if (_count > newSize) + { + _count = newSize; + } + } + + public void Clear() + { + _array.Clear(); + _count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void* GetUnsafePtr() + { + return _array.GetUnsafePtr(); + } + + public void Dispose() + { + _array.Dispose(); + _count = 0; + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Services/AllocationManager.cs b/Misaki.HighPerformance.Unsafe/Services/AllocationManager.cs index da05851..77aa79e 100644 --- a/Misaki.HighPerformance.Unsafe/Services/AllocationManager.cs +++ b/Misaki.HighPerformance.Unsafe/Services/AllocationManager.cs @@ -1,47 +1,81 @@ using Misaki.HighPerformance.Unsafe.Buffer; using Misaki.HighPerformance.Unsafe.Collections; +using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.Unsafe.Services; public static unsafe class AllocationManager { + private readonly struct AllocationInfo(void* ptr, nuint size) + { + public readonly void* ptr = ptr; + public readonly nuint size = size; + } + private static DynamicArena _arena; private static bool _initialized; + private static UnsafeQueue _allocated; + private static readonly Lock _lock = new(); - public static void Initialize(uint initialSize) - { - _arena = new DynamicArena(initialSize); - _initialized = true; - } - - internal static T* Allocate(uint size, uint alignSize, Allocator allocator, AllocationOption allocationType) - where T : unmanaged + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void VerifyInitialization() { if (!_initialized) { throw new InvalidOperationException("The AllocationManager has not been initialized."); } + } + + /// + /// Initializes the AllocationManager with a specified initial size for the memory arena. + /// + /// The initial size in bytes for the memory arena. + public static void Initialize(uint initialSize) + { + if (_initialized || initialSize <= 0) + { + return; + } + + _arena = new DynamicArena(initialSize); + _allocated = new UnsafeQueue(16, Allocator.Persistent); + + _initialized = true; + } + + internal static T* Allocate(uint size, uint alignSize, Allocator allocator, AllocationOption allocationOption) + where T : unmanaged + { + if (allocationOption == AllocationOption.UnTracked) + { + return (T*)AlignedAlloc(size, alignSize); + } + + VerifyInitialization(); lock (_lock) { - return allocator switch + switch (allocator) { - Allocator.Temp => (T*)_arena.Allocate(size * (uint)sizeof(T), alignSize, allocationType), - Allocator.Persistent => (T*)AlignedAlloc((nuint)(size * sizeof(T)), alignSize), - _ => throw new ArgumentOutOfRangeException(nameof(allocator), "Invalid allocator type."), - }; + case Allocator.Temp: + return (T*)_arena.Allocate(size * (uint)sizeof(T), alignSize, allocationOption); + + case Allocator.Persistent: + var allocationSize = size * (nuint)sizeof(T); + var buffer = (T*)AlignedAlloc(allocationSize, alignSize); + _allocated.Enqueue(new AllocationInfo(buffer, allocationSize)); + return buffer; + + default: + throw new ArgumentOutOfRangeException(nameof(allocator), "Invalid allocator type."); + } } } internal static void Free(void* ptr, Allocator allocator) { - if (!_initialized) - { - throw new InvalidOperationException("The AllocationManager has not been initialized."); - } - lock (_lock) { if (allocator == Allocator.Persistent) @@ -51,18 +85,36 @@ public static unsafe class AllocationManager } } + /// + /// Resets the memory arena, optionally clearing the allocated memory. + /// + /// If true, the allocated memory will be cleared; otherwise, it will not be cleared. public static void Reset(bool clear = false) { - if (!_initialized) - { - throw new InvalidOperationException("The AllocationManager has not been initialized."); - } - + VerifyInitialization(); _arena.Reset(clear); } + /// + /// Disposes of the AllocationManager, freeing all allocated memory and resources. + /// + /// Thrown if there are still allocated buffers that have not been freed. public static void Dispose() { _arena.Dispose(); + + nuint unfreedBytes = 0u; + while (_allocated.TryDequeue(out var allocationInfo)) + { + unfreedBytes += allocationInfo.size; + AlignedFree(allocationInfo.ptr); + } + + _allocated.Dispose(); + + if (unfreedBytes > 0u) + { + throw new InvalidOperationException($"There are still {unfreedBytes} bytes allocated buffers. Please free them before disposing."); + } } } \ No newline at end of file