From 48f2dce77858f283547378c3f379992459509a39 Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 3 Apr 2025 09:13:07 +0900 Subject: [PATCH] Enhance memory management and performance benchmarks Added a new configuration setting in `.editorconfig` to sort system directives last and increased the maximum line length to 400 characters. Added a new static class `MathUtilities` in `MathUtilities.cs` with a method `CeilPow2` for computing powers of two. Added a new benchmark class `CollectionBenchmark` in `CollectionBenchmark.cs` to measure performance of standard versus unsafe arrays. Added a new benchmark class `HashCodeBenchmark` in `HashCodeBenchmark.cs` to evaluate hash code generation performance. Added new utility methods in `UnsafeUtilities.cs` for memory allocation and deallocation, including `Malloc`, `AlignedAlloc`, `Realloc`, and `Free`. Added a new `AllocationType` enum in `AllocationType.cs` to specify memory allocation types. Changed the project file `Misaki.HighPerformance.Mathematics.csproj` to target .NET 9.0 and enable implicit usings and nullable reference types. Changed the `ParallelNoiseBenchmark` class in `ParallelNoiseBenchmark.cs` to improve memory allocation strategies and performance. Changed memory management in `Arena.cs` and `DynamicArena.cs` to use custom `Malloc` and `Free` functions. Changed the `IUnsafeCollection` interface in `IUnsafeCollection.cs` to include new methods for resizing collections and obtaining unsafe pointers. Changed the `UnsafeArray.cs` to improve management of unsafe arrays, including constructor and method updates. Changed the `UnsafeHashMap` and `UnsafeHashSet` classes to enhance performance and memory management. Changed the `UnsafeCollectionExtensions` class to provide additional methods for copying elements and converting collections. Changed the `ObjectPool` class in `ObjectPool.cs` to simplify cleanup and remove auto-cleanup functionality. Changed job scheduling and worker classes in `JobExtensions.cs` and `JobWorker.cs` to improve job scheduling in a thread pool. Removed commented-out code in `Program.cs` related to previous testing methods. Removed auto-cleanup functionality from the `ObjectPool` class. --- .editorconfig | 2 + Misaki.HighPerformance.Math/MathUtilities.cs | 19 + .../Misaki.HighPerformance.Mathematics.csproj | 9 + .../CollectionBenchmark.cs | 41 ++ .../HashCodeBenchmark.cs | 95 ++++ .../Misaki.HighPerformance.Test.csproj | 1 + .../ParallelNoiseBenchmark.cs | 20 +- Misaki.HighPerformance.Test/Program.cs | 42 +- .../Buffer/Arena .cs | 6 +- .../Buffer/DynamicArena.cs | 14 +- .../Collections/AllocationType.cs | 6 + .../Contracts/IUnsafeCollection.cs | 31 +- .../Collections/Services/AllocationManager.cs | 51 ++ .../Collections/UnsafeArray.cs | 94 ++-- .../Collections/UnsafeHashMap.cs | 205 ++++++++ .../Collections/UnsafeHashSet.cs | 122 +++++ .../Collections/UnsafeList.cs | 133 +++-- .../Collections/UnsafeQueue.cs | 121 ++++- .../Helpers/HashMapHelper.cs | 491 ++++++++++++++++++ .../Helpers/MemoryUtilities.cs | 72 ++- .../Helpers/UnsafeCollectionExtensions.cs | 101 +++- .../Helpers/UnsafeUtilities.cs | 17 +- .../Misaki.HighPerformance.Unsafe.csproj | 4 + Misaki.HighPerformance.sln | 6 + Misaki.HighPerformance/Buffer/ObjectPool.cs | 64 +-- Misaki.HighPerformance/Jobs/JobExtensions.cs | 12 +- Misaki.HighPerformance/Jobs/JobWorker.cs | 6 +- 27 files changed, 1557 insertions(+), 228 deletions(-) create mode 100644 .editorconfig create mode 100644 Misaki.HighPerformance.Math/MathUtilities.cs create mode 100644 Misaki.HighPerformance.Math/Misaki.HighPerformance.Mathematics.csproj create mode 100644 Misaki.HighPerformance.Test/CollectionBenchmark.cs create mode 100644 Misaki.HighPerformance.Test/HashCodeBenchmark.cs create mode 100644 Misaki.HighPerformance.Unsafe/Collections/Services/AllocationManager.cs create mode 100644 Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs create mode 100644 Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs create mode 100644 Misaki.HighPerformance.Unsafe/Helpers/HashMapHelper.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..85fd5ce --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +dotnet_sort_system_directives_first = false +max_line_length = 400 \ No newline at end of file diff --git a/Misaki.HighPerformance.Math/MathUtilities.cs b/Misaki.HighPerformance.Math/MathUtilities.cs new file mode 100644 index 0000000..90e96ce --- /dev/null +++ b/Misaki.HighPerformance.Math/MathUtilities.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace Misaki.HighPerformance.Mathematics; + +public static class MathUtilities +{ + /// Returns the smallest power of two that is greater than or equal to the specified number. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CeilPow2(int x) + { + x -= 1; + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + return x + 1; + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Math/Misaki.HighPerformance.Mathematics.csproj b/Misaki.HighPerformance.Math/Misaki.HighPerformance.Mathematics.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/Misaki.HighPerformance.Math/Misaki.HighPerformance.Mathematics.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/Misaki.HighPerformance.Test/CollectionBenchmark.cs b/Misaki.HighPerformance.Test/CollectionBenchmark.cs new file mode 100644 index 0000000..4169fee --- /dev/null +++ b/Misaki.HighPerformance.Test/CollectionBenchmark.cs @@ -0,0 +1,41 @@ +using BenchmarkDotNet.Attributes; +using Misaki.HighPerformance.Unsafe.Collections; +using Misaki.HighPerformance.Unsafe.Collections.Services; + +namespace Misaki.HighPerformance.Test; + +[MemoryDiagnoser] +public class CollectionBenchmark +{ + [Params(10, 100, 1000)] + public int count = 100; + + [Benchmark] + public void Array() + { + for (var i = 0; i < count; i++) + { + 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; + } + + foreach (var item in array) + { + Console.WriteLine(item); + } + + AllocationManager.Reset(); + } + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/HashCodeBenchmark.cs b/Misaki.HighPerformance.Test/HashCodeBenchmark.cs new file mode 100644 index 0000000..f4c1dc7 --- /dev/null +++ b/Misaki.HighPerformance.Test/HashCodeBenchmark.cs @@ -0,0 +1,95 @@ +using BenchmarkDotNet.Attributes; +using System.Numerics; + +namespace Misaki.HighPerformance.Test; + +public class HashCodeBenchmark +{ + private struct Component + { + public int Value; + public int Value2; + public float Value3; + public Guid Value4; + public Matrix4x4 Value5; + public Vector4 Value6; + } + + [Params(100, 1000, 10000)] + public int count; + + private Component _component = new Component() + { + Value = 0, + Value2 = 1, + Value3 = 2, + Value4 = Guid.NewGuid(), + Value5 = Matrix4x4.Identity, + Value6 = Vector4.One + }; + + private Dictionary _hashCache = new(); + //private UnsafeHashMap _hashMap = new(16); + + private bool _disposed; + + //~HashCodeBenchmark() + //{ + // Dispose(); + //} + + [Benchmark] + public void Hash() + { + for (var i = 0; i < count; i++) + { + _ = _component.GetHashCode(); + } + } + + [Benchmark] + public void HashWithCache() + { + for (var i = 0; i < count; i++) + { + var type = typeof(Component); + if (!_hashCache.TryGetValue(type, out var hash)) + { + hash = type.GetHashCode(); + _hashCache[type] = hash; + } + + _ = hash; + } + } + + //[Benchmark] + //public void HashWithUnsafeHashMap() + //{ + // for (var i = 0; i < count; i++) + // { + // var type = _component.GetType(); + // var guid = type.GUID; + // if (!_hashMap.TryGetValue(guid, out var hash)) + // { + // hash = type.GetHashCode(); + // _hashMap.Add(guid, hash); + // _hashMap.Test(ref _hashMap._hashMap); + // } + + // _ = hash; + // } + //} + + //public void Dispose() + //{ + // if (_disposed) + // { + // return; + // } + + // _hashMap.Dispose(); + + // GC.SuppressFinalize(this); + //} +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj b/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj index 8a86be8..ded352b 100644 --- a/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj +++ b/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj @@ -6,6 +6,7 @@ enable enable True + True diff --git a/Misaki.HighPerformance.Test/ParallelNoiseBenchmark.cs b/Misaki.HighPerformance.Test/ParallelNoiseBenchmark.cs index 75062a3..3aa8d73 100644 --- a/Misaki.HighPerformance.Test/ParallelNoiseBenchmark.cs +++ b/Misaki.HighPerformance.Test/ParallelNoiseBenchmark.cs @@ -21,12 +21,6 @@ public class ParallelNoiseBenchmark return x - MathF.Truncate(x); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float Lerp(float a, float b, float t) - { - return a + t * (b - a); - } - private static Vector2 GradientNoiseDirect(Vector2 uv) { uv.X %= 289; @@ -48,7 +42,7 @@ public class ParallelNoiseBenchmark var d11 = Vector2.Dot(GradientNoiseDirect(ip + new Vector2(1, 1)), fp - new Vector2(1, 1)); fp = fp * fp * fp * (fp * (fp * new Vector2(6.0f) - new Vector2(15.0f)) + new Vector2(10.0f)); - return Lerp(Lerp(d00, d10, fp.Y), Lerp(d01, d11, fp.Y), fp.X); + return float.Lerp(float.Lerp(d00, d10, fp.Y), float.Lerp(d01, d11, fp.Y), fp.X); } public void Execute(int index) @@ -65,9 +59,9 @@ public class ParallelNoiseBenchmark private const int _LENGTH = _WIDTH * _HEIGHT; [Benchmark] - public void JobSystem() + public static void JobSystem() { - using var buffers = new UnsafeArray(_LENGTH, AllocationType.UnInitialized); + using var buffers = new UnsafeArray(_LENGTH, Allocator.Persistent, AllocationType.UnInitialized); var job = new NoiseJob() { buffers = buffers, @@ -80,9 +74,9 @@ public class ParallelNoiseBenchmark } [Benchmark] - public void ParallelFor() + public static void ParallelFor() { - using var buffers = new UnsafeArray(_LENGTH, AllocationType.UnInitialized); + using var buffers = new UnsafeArray(_LENGTH, Allocator.Persistent, AllocationType.UnInitialized); Parallel.For(0, _LENGTH, i => { @@ -94,9 +88,9 @@ public class ParallelNoiseBenchmark } [Benchmark] - public void For() + public static void For() { - using var buffers = new UnsafeArray(_LENGTH, AllocationType.UnInitialized); + using var buffers = new UnsafeArray(_LENGTH, Allocator.Persistent, AllocationType.UnInitialized); for (var i = 0; i < _LENGTH; i++) { var x = i % _WIDTH; diff --git a/Misaki.HighPerformance.Test/Program.cs b/Misaki.HighPerformance.Test/Program.cs index 43dae1b..0b7b2e1 100644 --- a/Misaki.HighPerformance.Test/Program.cs +++ b/Misaki.HighPerformance.Test/Program.cs @@ -1,37 +1,7 @@ -//using BenchmarkDotNet.Running; -//using Misaki.HighPerformance.Test; +using Misaki.HighPerformance.Test; +using Misaki.HighPerformance.Unsafe.Collections.Services; -//BenchmarkRunner.Run(); - -using Misaki.HighPerformance.Unsafe.Collections; - -using var test = new UnsafeArray(10, AllocationType.UnInitialized); - -for (var i = 0; i < 10; i++) -{ - var t = new Test(); - t.buffers[0] = i; - test[i] = t; -} - -test.ReAlloc(20); - -for (var i = 0; i < 10; i++) -{ - Console.WriteLine(test[i].buffers[0]); -} - -struct Test : IDisposable -{ - public UnsafeArray buffers; - - public Test() - { - buffers = new UnsafeArray(1, AllocationType.UnInitialized); - } - - public void Dispose() - { - buffers.Dispose(); - } -} \ No newline at end of file +AllocationManager.Initialize(512_000); +var test = new CollectionBenchmark(); +test.UnsafeArray(); +AllocationManager.Dispose(); diff --git a/Misaki.HighPerformance.Unsafe/Buffer/Arena .cs b/Misaki.HighPerformance.Unsafe/Buffer/Arena .cs index ae908ae..e06de3d 100644 --- a/Misaki.HighPerformance.Unsafe/Buffer/Arena .cs +++ b/Misaki.HighPerformance.Unsafe/Buffer/Arena .cs @@ -8,7 +8,7 @@ namespace Misaki.HighPerformance.Unsafe.Buffer; /// public unsafe struct Arena : IDisposable { - private void* _buffer; + private byte* _buffer; private uint _size; private uint _offset; @@ -16,7 +16,7 @@ public unsafe struct Arena : IDisposable public Arena(uint size) { - _buffer = NativeMemory.Alloc(size); + _buffer = (byte*)Malloc(size); _size = size; _offset = 0; } @@ -70,7 +70,7 @@ public unsafe struct Arena : IDisposable public void Dispose() { - NativeMemory.Free(_buffer); + Free(_buffer); _buffer = null; _size = 0; diff --git a/Misaki.HighPerformance.Unsafe/Buffer/DynamicArena.cs b/Misaki.HighPerformance.Unsafe/Buffer/DynamicArena.cs index 2877f22..e276490 100644 --- a/Misaki.HighPerformance.Unsafe/Buffer/DynamicArena.cs +++ b/Misaki.HighPerformance.Unsafe/Buffer/DynamicArena.cs @@ -1,5 +1,4 @@ using Misaki.HighPerformance.Unsafe.Collections; -using System.Runtime.InteropServices; namespace Misaki.HighPerformance.Unsafe.Buffer; @@ -27,18 +26,17 @@ public unsafe struct DynamicArena : IDisposable public DynamicArena(uint initialSize) { _initialSize = initialSize; - _root = (ArenaNode*)NativeMemory.Alloc(SizeOf()); + _root = (ArenaNode*)Malloc(SizeOf()); _root->arena = new Arena(initialSize); _root->next = null; _current = _root; - _disposed = false; } private bool CreateNewNode(uint size) { try { - var newNode = (ArenaNode*)NativeMemory.Alloc(SizeOf()); + var newNode = (ArenaNode*)Malloc(SizeOf()); newNode->arena = new Arena(size); newNode->next = null; @@ -70,10 +68,14 @@ public unsafe struct DynamicArena : IDisposable { result = current->arena.Allocate(size, alignSize, allocationType); if (result != null) + { return result; + } if (current->next == null && !CreateNewNode(Math.Max(size, _initialSize))) + { return null; + } current = current->next; } @@ -107,14 +109,16 @@ public unsafe struct DynamicArena : IDisposable public void Dispose() { if (_disposed) + { return; + } var current = _root; while (current != null) { var next = current->next; current->arena.Dispose(); - NativeMemory.Free(current); + Free(current); current = next; } diff --git a/Misaki.HighPerformance.Unsafe/Collections/AllocationType.cs b/Misaki.HighPerformance.Unsafe/Collections/AllocationType.cs index eae23ef..eeac6e1 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/AllocationType.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/AllocationType.cs @@ -4,4 +4,10 @@ public enum AllocationType { UnInitialized, Clear +} + +public enum Allocator +{ + Temp, + Persistent } \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Collections/Contracts/IUnsafeCollection.cs b/Misaki.HighPerformance.Unsafe/Collections/Contracts/IUnsafeCollection.cs index 3230985..aebaadc 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/Contracts/IUnsafeCollection.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/Contracts/IUnsafeCollection.cs @@ -1,21 +1,38 @@ namespace Misaki.HighPerformance.Unsafe.Collections.Contracts; -public unsafe interface IUnsafeCollection : IDisposable +public unsafe interface IUnsafeCollection : IEnumerable, IDisposable where T : unmanaged { - public T* Buffer + /// + /// Gets the number of elements in a collection. The value is read-only. + /// + public int Count { get; } - public int Size + /// + /// Indicates whether the object has been created. Returns true if the object is created, otherwise false. + /// + public bool IsCreated { get; } - public ref T this[int index] { get; } - + /// + /// Removes all elements from the collection. The collection will be empty after this operation. + /// public void Clear(); - public void ReAlloc(int newSize); -} + /// + /// Changes the size of a collection or array to the specified value. + /// + /// Specifies the new size to which the collection or array should be adjusted. + public void Resize(int newSize); + + /// + /// Returns a pointer to an unmanaged memory location. This pointer can be used for low-level memory operations. + /// + /// The method returns a void pointer to the unsafe memory location. + public void* GetUnsafePtr(); +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Collections/Services/AllocationManager.cs b/Misaki.HighPerformance.Unsafe/Collections/Services/AllocationManager.cs new file mode 100644 index 0000000..e2e8339 --- /dev/null +++ b/Misaki.HighPerformance.Unsafe/Collections/Services/AllocationManager.cs @@ -0,0 +1,51 @@ +using Misaki.HighPerformance.Unsafe.Buffer; + +namespace Misaki.HighPerformance.Unsafe.Collections.Services; + +public static unsafe class AllocationManager +{ + private static DynamicArena _arena; + private static bool _initialized; + + private static readonly Lock _lock = new(); + + public static void Initialize(uint initialSize) + { + _arena = new DynamicArena(initialSize); + _initialized = true; + } + + public static T* Allocate(uint size, uint alignSize, Allocator allocator, AllocationType allocationType) + where T : unmanaged + { + if (!_initialized) + { + throw new InvalidOperationException("The AllocationManager has not been initialized."); + } + + lock (_lock) + { + return allocator switch + { + 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."), + }; + } + } + + public static void Reset(bool clear = false) + { + if (!_initialized) + { + throw new InvalidOperationException("The AllocationManager has not been initialized."); + } + + _arena.Reset(clear); + } + + public static void Dispose() + { + _arena.Dispose(); + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs index af6c46a..97cf129 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeArray.cs @@ -1,21 +1,25 @@ using Misaki.HighPerformance.Unsafe.Collections.Contracts; +using Misaki.HighPerformance.Unsafe.Collections.Services; using Misaki.HighPerformance.Unsafe.Helpers; using System.Collections; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Misaki.HighPerformance.Unsafe.Collections; -public unsafe struct UnsafeArray : IUnsafeCollection, IEnumerable +/// +/// A structure for managing an array of unmanaged types with unsafe memory operations. +/// +/// Represents a type that can be stored in an unmanaged memory context. +public unsafe struct UnsafeArray : IUnsafeCollection where T : unmanaged { private struct Enumerator : IEnumerator { - private UnsafeArray _collection; + private UnsafeArray* _collection; private int _index; private T _value; - public Enumerator(ref UnsafeArray collection) + public Enumerator(UnsafeArray* collection) { _collection = collection; _index = -1; @@ -26,9 +30,9 @@ public unsafe struct UnsafeArray : IUnsafeCollection, IEnumerable public bool MoveNext() { _index++; - if (_index < _collection.Size) + if (_index < _collection->_count) { - _value = UnsafeUtilities.ReadArrayElement(_collection.Buffer, _index); + _value = UnsafeUtilities.ReadArrayElement(_collection->_buffer, _index); return true; } @@ -42,22 +46,16 @@ public unsafe struct UnsafeArray : IUnsafeCollection, IEnumerable } // Let NativeArray indexer check for out of range. - public T Current + public readonly T Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return _value; - } + get => _value; } - object IEnumerator.Current + readonly object IEnumerator.Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return Current; - } + get => Current; } public void Dispose() @@ -66,10 +64,9 @@ public unsafe struct UnsafeArray : IUnsafeCollection, IEnumerable } private T* _buffer; - private int _size; + private int _count; - public readonly T* Buffer => _buffer; - public readonly int Size => _size; + public readonly int Count => _count; public readonly ref T this[int index] { @@ -77,14 +74,31 @@ public unsafe struct UnsafeArray : IUnsafeCollection, IEnumerable get => ref UnsafeUtilities.ReadArrayElementRef(_buffer, index); } - public IEnumerator GetEnumerator() => new Enumerator(ref this); + public readonly bool IsCreated + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer != null; + } + public IEnumerator GetEnumerator() => new Enumerator((UnsafeArray*)UnsafeUtilities.AddressOf(ref this)); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public UnsafeArray(int size, AllocationType allocationType) + /// + /// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation type. It + /// 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. + /// Thrown when the specified number of elements is less than or equal to zero. + public UnsafeArray(int count, Allocator allocator, AllocationType allocationType = AllocationType.UnInitialized) { - _size = size; - _buffer = (T*)NativeMemory.AlignedAlloc((nuint)(size * sizeof(T)), AlignOf()); + if (count <= 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Count must be greater than zero."); + } + + _buffer = AllocationManager.Allocate((uint)count, (uint)AlignOf(), allocator, allocationType); + _count = count; if (allocationType == AllocationType.Clear) { @@ -92,29 +106,47 @@ public unsafe struct UnsafeArray : IUnsafeCollection, IEnumerable } } - public void ReAlloc(int newSize) + /// + /// 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. + /// + /// 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. + public UnsafeArray(void* buffer, int count) { - if (newSize == _size) + _buffer = (T*)buffer; + _count = count; + } + + public void Resize(int newSize) + { + if (newSize == _count) { return; } - _buffer = (T*)NativeMemory.AlignedRealloc(_buffer, (nuint)(newSize * sizeof(T)), AlignOf()); - _size = newSize; + _buffer = (T*)AlignedRealloc(_buffer, (nuint)(newSize * sizeof(T)), AlignOf()); + _count = newSize; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Clear() + public readonly void Clear() { - MemClear(_buffer, (uint)(_size * sizeof(T))); + MemClear(_buffer, (uint)(_count * sizeof(T))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void* GetUnsafePtr() + { + return _buffer; } public void Dispose() { - NativeMemory.AlignedFree(_buffer); + AlignedFree(_buffer); _buffer = null; - _size = 0; + _count = 0; } } diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs new file mode 100644 index 0000000..69cd556 --- /dev/null +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashMap.cs @@ -0,0 +1,205 @@ +using Misaki.HighPerformance.Unsafe.Collections.Contracts; +using Misaki.HighPerformance.Unsafe.Helpers; +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Misaki.HighPerformance.Unsafe.Collections; + +public unsafe struct UnsafeHashMap : IUnsafeCollection> where TKey : unmanaged, IEquatable where TValue : unmanaged +{ + private struct Enumerator : IEnumerator> + { + internal HashMapHelper.Enumerator _enumerator; + + public Enumerator(HashMapHelper* data) + { + _enumerator = new HashMapHelper.Enumerator(data); + } + + /// + /// The current key-value pair. + /// + /// The current key-value pair. + public KeyValuePair Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _enumerator.GetCurrent(); + } + + /// + /// Gets the element at the current position of the enumerator in the container. + /// + object IEnumerator.Current => Current; + + /// + /// Advances the enumerator to the next key-value pair. + /// + /// True if is valid to read after the call. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => _enumerator.MoveNext(); + + /// + /// Resets the enumerator to its initial state. + /// + public void Reset() => _enumerator.Reset(); + + /// + /// Does nothing. + /// + public void Dispose() + { + } + } + + private HashMapHelper _hashMap; + + public readonly int Count => _hashMap.Count; + public readonly int Capacity => _hashMap.Capacity; + public readonly bool IsCreated => _hashMap.IsCreated; + + /// + /// Gets and sets values by key. + /// + /// Getting a key that is not present will throw. Setting a key that is not already present will add the key. + /// The key to look up. + /// The value associated with the key. + /// For getting, thrown if the key was not present. + public TValue this[TKey key] + { + get + { + if (!_hashMap.TryGetValue(key, out var result)) + { + throw new ArgumentException($"Key: {key} is not present."); + } + + return result; + } + + set + { + var idx = _hashMap.Find(key); + if (-1 != idx) + { + UnsafeUtilities.WriteArrayElement(_hashMap.Buffer, idx, value); + return; + } + + TryAdd(key, value); + } + } + + public IEnumerator> GetEnumerator() => new Enumerator((HashMapHelper*)UnsafeUtilities.AddressOf(ref _hashMap)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public UnsafeHashMap(int capacity) + { + _hashMap = new HashMapHelper(capacity, sizeof(TValue), HashMapHelper.MINIMAL_CAPACITY); + } + + /// + /// Adds a new key-value pair. + /// + /// If the key is already present, this method returns false without modifying the hash map. + /// The key to add. + /// The value to add. + /// True if the key-value pair was added. + public bool TryAdd(TKey key, TValue item) + { + var idx = _hashMap.TryAdd(key); + if (idx != -1) + { + UnsafeUtilities.WriteArrayElement(_hashMap.Buffer, idx, item); + return true; + } + + return false; + } + + /// + /// Adds a new key-value pair. + /// + /// If the key is already present, this method throws without modifying the hash map. + /// The key to add. + /// The value to add. + /// Thrown if the key was already present. + public void Add(TKey key, TValue item) + { + var result = TryAdd(key, item); + if (!result) + { + throw new ArgumentException($"An item with the same key has already been added: {key}"); + } + } + + /// + /// Returns the value associated with a key. + /// + /// The key to look up. + /// Outputs the value associated with the key. Outputs default if the key was not present. + /// True if the key was present. + public bool TryGetValue(TKey key, out TValue item) + { + return _hashMap.TryGetValue(key, out item); + } + + /// + /// Returns true if a given key is present in this hash map. + /// + /// The key to look up. + /// True if the key was present. + public bool ContainsKey(TKey key) + { + return -1 != _hashMap.Find(key); + } + + /// + /// Sets the capacity to match what it would be if it had been originally initialized with all its entries. + /// + public void TrimExcess() => _hashMap.TrimExcess(); + + public void Resize(int newSize) + { + _hashMap.Resize(newSize); + } + + public void Clear() + { + _hashMap.Clear(); + } + + /// + /// Retrieves an array of keys from the hash map. + /// + /// An array containing the keys stored in the hash map. + public UnsafeArray GetKeyArray(Allocator allocator) => _hashMap.GetKeyArray(allocator); + + /// + /// Retrieves an array of values from the underlying hash map. + /// + /// An UnsafeArray containing the values stored in the hash map. + public UnsafeArray GetValueArray(Allocator allocator) => _hashMap.GetValueArray(allocator); + + /// + /// Retrieves an array of key-value pairs from the hash map. The keys are of type TKey and the values are of type + /// TValue. + /// + /// Returns an UnsafeArray containing KeyValuePair objects. + public UnsafeArray> GetKeyValueArrays(Allocator allocator) => _hashMap.GetKeyValueArrays(allocator); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void* GetUnsafePtr() + { + return _hashMap.Buffer; + } + + public void Dispose() + { + _hashMap.Dispose(); + } + + public void Test(ref HashMapHelper t) + { + Console.WriteLine(t.Equals(_hashMap)); + } +} diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs new file mode 100644 index 0000000..50c81be --- /dev/null +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeHashSet.cs @@ -0,0 +1,122 @@ +using Misaki.HighPerformance.Unsafe.Collections.Contracts; +using Misaki.HighPerformance.Unsafe.Helpers; +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Misaki.HighPerformance.Unsafe.Collections; + +/// +/// A collection that provides fast, unsafe operations for managing a set of unmanaged types. It supports adding, +/// removing, and checking for values. +/// +/// Represents an unmanaged type that can be compared for equality. +public unsafe struct UnsafeHashSet : IUnsafeCollection, IEnumerable + where T : unmanaged, IEquatable +{ + private struct Enumerator : IEnumerator + { + internal HashMapHelper.Enumerator _enumerator; + + public Enumerator(HashMapHelper* hashMap) + { + _enumerator = new HashMapHelper.Enumerator(hashMap); + } + + public T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _enumerator.buffer->_keys[_enumerator.index]; + } + + object IEnumerator.Current => Current; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => _enumerator.MoveNext(); + + public void Reset() => _enumerator.Reset(); + + public readonly void Dispose() + { + } + } + + private HashMapHelper _hashMap; + + public readonly int Count => _hashMap.Count; + public readonly int Capacity => _hashMap.Capacity; + public readonly bool IsCreated => _hashMap.IsCreated; + + public IEnumerator GetEnumerator() => new Enumerator((HashMapHelper*)UnsafeUtilities.AddressOf(ref _hashMap)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public UnsafeHashSet(int capacity) + { + _hashMap = new HashMapHelper(capacity, 0, HashMapHelper.MINIMAL_CAPACITY); + } + + /// + /// Adds a new value (unless it is already present). + /// + /// The value to add. + /// True if the value was not already present. + public bool Add(T item) + { + return -1 != _hashMap.TryAdd(item); + } + + /// + /// Removes a particular value. + /// + /// The value to remove. + /// True if the value was present. + public bool Remove(T item) + { + return -1 != _hashMap.TryRemove(item); + } + + /// + /// Returns true if a particular value is present. + /// + /// The value to check for. + /// True if the value was present. + public bool Contains(T item) + { + return -1 != _hashMap.Find(item); + } + + /// + /// Sets the capacity to match what it would be if it had been originally initialized with all its entries. + /// + public void TrimExcess() => _hashMap.TrimExcess(); + + /// + /// Returns an array with a copy of this set's values (in no particular order). + /// + /// The allocator to use. + /// An array with a copy of the set's values. + public UnsafeArray ToNativeArray(Allocator allocator) + { + return _hashMap.GetKeyArray(allocator); + } + + public void Resize(int newSize) + { + _hashMap.Resize(newSize); + } + + public void Clear() + { + _hashMap.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void* GetUnsafePtr() + { + return _hashMap.Buffer; + } + + public void Dispose() + { + _hashMap.Dispose(); + } +} diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs index 3a6155f..92f1fe3 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeList.cs @@ -5,16 +5,20 @@ using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.Unsafe.Collections; -public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable +/// +/// A collection that allows for unsafe operations on a list of unmanaged types. +/// +/// Represents a type that can be stored in the collection, constrained to unmanaged types for performance and safety. +public unsafe struct UnsafeList : IUnsafeCollection where T : unmanaged { private struct Enumerator : IEnumerator { - private UnsafeList _collection; + private UnsafeList* _collection; private int _index; private T _value; - public Enumerator(ref UnsafeList collection) + public Enumerator(UnsafeList* collection) { _collection = collection; _index = -1; @@ -25,9 +29,9 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable public bool MoveNext() { _index++; - if (_index < _collection.Size) + if (_index < _collection->_count) { - _value = UnsafeUtilities.ReadArrayElement(_collection.Buffer, _index); + _value = UnsafeUtilities.ReadArrayElement(_collection->_array.GetUnsafePtr(), _index); return true; } @@ -88,9 +92,9 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable /// The value to be added to the collection. public void AddNoResize(T value) { - var idx = Interlocked.Increment(ref listData->_size) - 1; + var idx = Interlocked.Increment(ref listData->_count) - 1; listData->CheckNoResizeCapacity(idx, 1); - UnsafeUtilities.WriteArrayElement(listData->Buffer, idx, value); + UnsafeUtilities.WriteArrayElement(listData->_array.GetUnsafePtr(), idx, value); } /// @@ -100,19 +104,19 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable /// Indicates the number of elements to be added from the source data. public void AddRangeNoResize(T* ptr, int count) { - var idx = Interlocked.Add(ref listData->_size, count) - count; + var idx = Interlocked.Add(ref listData->_count, count) - count; listData->CheckNoResizeCapacity(idx, count); - MemCpy(listData->Buffer + idx, ptr, (uint)(count * sizeof(T))); + MemCpy(UnsafeUtilities.ReadArrayElementUnsafe(listData->_array.GetUnsafePtr(), idx), ptr, (uint)(count * sizeof(T))); } } private UnsafeArray _array; - private int _size; + private int _count; - public readonly T* Buffer => _array.Buffer; - public readonly int Size => _size; - public readonly int Capacity => _array.Size; + public readonly int Count => _count; + public readonly int Capacity => _array.Count; + public readonly bool IsCreated => _array.IsCreated; public readonly ref T this[int index] { @@ -120,17 +124,15 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable get => ref _array[index]; } - public IEnumerator GetEnumerator() => new Enumerator(ref this); - + public IEnumerator GetEnumerator() => new Enumerator((UnsafeList*)UnsafeUtilities.AddressOf(ref this)); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public ParallelWriter AsParallelWriter() => - new((UnsafeList*)UnsafeUtilities.AddressOf(ref this)); + public ParallelWriter AsParallelWriter() => new((UnsafeList*)UnsafeUtilities.AddressOf(ref this)); - public UnsafeList(int capacity, AllocationType allocationType) + public UnsafeList(int capacity, Allocator allocator, AllocationType allocationType = AllocationType.UnInitialized) { - _array = new UnsafeArray(capacity, allocationType); - _size = 0; + _array = new UnsafeArray(capacity, allocator, allocationType); + _count = 0; if (allocationType == AllocationType.Clear) { @@ -140,7 +142,7 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable private readonly void CheckNoResizeCapacity(int count) { - CheckNoResizeCapacity(count, Size); + CheckNoResizeCapacity(count, Count); } private readonly void CheckNoResizeCapacity(int index, int count) @@ -148,7 +150,7 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable if (index + count > Capacity) { throw new Exception( - $"AddNoResize assumes that list capacity is sufficient (Capacity {Capacity}, Size {Size}), requested count {count}!" + $"AddNoResize assumes that list capacity is sufficient (Capacity {Capacity}, Size {Count}), requested count {count}!" ); } } @@ -165,12 +167,12 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable throw new ArgumentOutOfRangeException($"Value for index {index} must be positive."); } - if (index > Size) + if (index > Count) { throw new ArgumentOutOfRangeException($"Value for index {index} is out of bounds."); } - if (index + count > Size) + if (index + count > Count) { throw new ArgumentOutOfRangeException($"Value for count {count} is out of bounds."); } @@ -178,37 +180,37 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable public void Add(T value) { - if (_size >= Capacity) + if (_count >= Capacity) { - ReAlloc(Capacity + (int)(Capacity * 0.5f)); + Resize(Capacity + (int)(Capacity * 0.5f)); } - UnsafeUtilities.WriteArrayElement(Buffer, _size, value); - _size++; + UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), _count, value); + _count++; } public void AddNoResize(T value) { CheckNoResizeCapacity(1); - UnsafeUtilities.WriteArrayElement(Buffer, _size, value); - _size++; + UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), _count, value); + _count++; } public void AddRange(Span values, int count) { - var newSize = _size + count; + var newSize = _count + count; if (newSize > Capacity) { - ReAlloc(Capacity + count); + Resize(Capacity + count); } fixed (T* ptr = values) { - MemCpy(_array.Buffer + _size, ptr, (uint)(count * sizeof(T))); + MemCpy(UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), _count), ptr, (uint)(count * sizeof(T))); } - _size += count; + _count += count; } public void AddRangeNoResize(ReadOnlySpan values) @@ -217,18 +219,18 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable fixed (T* ptr = values) { - MemCpy(_array.Buffer + _size, ptr, (uint)(values.Length * sizeof(T))); + MemCpy(UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), _count), ptr, (uint)(values.Length * sizeof(T))); } - _size += values.Length; + _count += values.Length; } public void AddRangeNoResize(T* ptr, int count) { CheckNoResizeCapacity(count); - MemCpy(_array.Buffer + _size, ptr, (uint)(count * sizeof(T))); - _size += count; + MemCpy(UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), _count), ptr, (uint)(count * sizeof(T))); + _count += count; } public void RemoveRange(int start, int length) @@ -240,13 +242,12 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable return; } - var copyFrom = Math.Min(start + length, _size); - MemCpy( - _array.Buffer + start, - _array.Buffer + copyFrom, - (uint)((_size - copyFrom) * sizeof(T)) + var copyFrom = Math.Min(start + length, _count); + MemCpy(UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), start), + UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), copyFrom), + (uint)((_count - copyFrom) * sizeof(T)) ); - _size -= length; + _count -= length; } public void RemoveAt(int index) @@ -263,13 +264,12 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable return; } - var copyFrom = Math.Min(_size - length, start + length); - MemCpy( - _array.Buffer + start, - _array.Buffer + copyFrom, - (uint)((_size - copyFrom) * sizeof(T)) + var copyFrom = Math.Min(_count - length, start + length); + MemCpy(UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), start), + UnsafeUtilities.ReadArrayElementUnsafe(_array.GetUnsafePtr(), copyFrom), + (uint)((_count - copyFrom) * sizeof(T)) ); - _size -= length; + _count -= length; } public void RemoveAtSwapBack(int index) @@ -277,26 +277,47 @@ public unsafe struct UnsafeList : IUnsafeCollection, IEnumerable RemoveRangeSwapBack(index, 1); } - public void ReAlloc(int newSize) + public void Resize(int newSize) { - _array.ReAlloc(newSize); + _array.Resize(newSize); - if (_size > newSize) + if (_count > newSize) { - _size = newSize; + _count = newSize; } } public void Clear() { _array.Clear(); - _size = 0; + _count = 0; + } + + /// + /// Returns a pointer to the underlying data of the array in an unsafe manner. This method is optimized for + /// performance. + /// + /// A pointer to the array's data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void* GetUnsafePtr() + { + return _array.GetUnsafePtr(); + } + + /// + /// Converts the current array to an UnsafeArray representation using its pointer and count. + /// + /// Returns a new UnsafeArray instance initialized with the array's unsafe pointer and its count. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly UnsafeArray AsUnsafeArray() + { + return new UnsafeArray(_array.GetUnsafePtr(), _count); } public void Dispose() { _array.Dispose(); - _size = 0; + _count = 0; } } diff --git a/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs b/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs index 117b122..d1b2a21 100644 --- a/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs +++ b/Misaki.HighPerformance.Unsafe/Collections/UnsafeQueue.cs @@ -1,20 +1,75 @@ 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; +/// +/// A structure that implements a queue using unmanaged types for efficient memory management. +/// +/// Represents the type of elements stored in the queue, which must be an unmanaged type for performance and safety. public unsafe struct UnsafeQueue : IUnsafeCollection where T : unmanaged { + private struct Enumerator : IEnumerator + { + private UnsafeQueue* _collection; + private int _index; + private T _value; + + public Enumerator(UnsafeQueue* collection) + { + _collection = collection; + _index = -1; + _value = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + _index++; + if (_index < _collection->_count) + { + _value = UnsafeUtilities.ReadArrayElement(_collection->_array.GetUnsafePtr(), _index); + return true; + } + + _value = default; + return false; + } + + public void Reset() + { + _index = -1; + } + + // Let NativeArray indexer check for out of range. + 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 _size; + private int _count; private int _offset; - public readonly T* Buffer => _array.Buffer; - public readonly int Size => _size; - public readonly int Capacity => _array.Size; + public readonly int Count => _count; + public readonly int Capacity => _array.Count; + public readonly bool IsCreated => _array.IsCreated; public readonly ref T this[int index] { @@ -22,10 +77,13 @@ public unsafe struct UnsafeQueue : IUnsafeCollection get => ref _array[index]; } - public UnsafeQueue(int capacity, AllocationType allocationType) + public IEnumerator GetEnumerator() => new Enumerator((UnsafeQueue*)UnsafeUtilities.AddressOf(ref this)); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public UnsafeQueue(int capacity, Allocator allocator, AllocationType allocationType = AllocationType.UnInitialized) { - _array = new UnsafeArray(capacity, allocationType); - _size = 0; + _array = new UnsafeArray(capacity, allocator, allocationType); + _count = 0; _offset = 0; if (allocationType == AllocationType.Clear) @@ -34,34 +92,49 @@ public unsafe struct UnsafeQueue : IUnsafeCollection } } + /// + /// Adds an element to the end of a collection, resizing if the current capacity is reached. The new element is + /// stored in a circular buffer. + /// + /// The item to be added to the collection. public void Enqueue(T value) { - if (_size >= Capacity) + if (_count >= Capacity) { - ReAlloc(Capacity + (int)(Capacity * 0.5f)); + Resize(Capacity + (int)(Capacity * 0.5f)); } - UnsafeUtilities.WriteArrayElement(Buffer, (_offset + _size) % Capacity, value); - _size++; + UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), (_offset + _count) % Capacity, value); + _count++; } + /// + /// Removes and returns the element at the front of the queue. If the queue is empty, an exception is thrown. + /// + /// The element that was removed from the front of the queue. + /// Thrown when attempting to dequeue from an empty queue. public T Dequeue() { - if (_size == 0) + if (_count == 0) { throw new InvalidOperationException("Queue is empty."); } - var value = UnsafeUtilities.ReadArrayElement(Buffer, _offset); + var value = UnsafeUtilities.ReadArrayElement(_array.GetUnsafePtr(), _offset); _offset = (_offset + 1) % Capacity; - _size--; + _count--; return value; } + /// + /// Attempts to remove and return an item from a collection. Returns a boolean indicating success or failure. + /// + /// 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) { - if (_size == 0) + if (_count == 0) { value = default; return false; @@ -71,27 +144,33 @@ public unsafe struct UnsafeQueue : IUnsafeCollection return true; } - public void ReAlloc(int newSize) + public void Resize(int newSize) { - _array.ReAlloc(newSize); + _array.Resize(newSize); - if (_size > newSize) + if (_count > newSize) { - _size = newSize; + _count = newSize; } } public void Clear() { _array.Clear(); - _size = 0; + _count = 0; _offset = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void* GetUnsafePtr() + { + return _array.GetUnsafePtr(); + } + public void Dispose() { _array.Dispose(); - _size = 0; + _count = 0; _offset = 0; } } diff --git a/Misaki.HighPerformance.Unsafe/Helpers/HashMapHelper.cs b/Misaki.HighPerformance.Unsafe/Helpers/HashMapHelper.cs new file mode 100644 index 0000000..e65a400 --- /dev/null +++ b/Misaki.HighPerformance.Unsafe/Helpers/HashMapHelper.cs @@ -0,0 +1,491 @@ +using Misaki.HighPerformance.Mathematics; +using Misaki.HighPerformance.Unsafe.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Misaki.HighPerformance.Unsafe.Helpers; + +public unsafe struct HashMapHelper : IDisposable + where TKey : unmanaged, IEquatable +{ + internal unsafe struct Enumerator + { + public HashMapHelper* buffer; + public int index; + public int bucketIndex; + public int nextIndex; + + public unsafe Enumerator(HashMapHelper* data) + { + buffer = data; + index = -1; + bucketIndex = 0; + nextIndex = -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + return buffer->MoveNext(ref bucketIndex, ref nextIndex, out index); + } + + public void Reset() + { + index = -1; + bucketIndex = 0; + nextIndex = -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public KeyValuePair GetCurrent() + where TValue : unmanaged + { + return new KeyValuePair(buffer->_keys[index], UnsafeUtilities.ReadArrayElement(buffer->_buffer, index)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TKey GetCurrentKey() + { + if (index != -1) + { + return buffer->_keys[index]; + } + + return default; + } + + public void Dispose() + { + } + } + + // This buffer has 4 parts: TValue, TKey, Next, Buckets. + private byte* _buffer; + + internal TKey* _keys; + internal int* _next; + internal int* _buckets; + + private int _count; + private int _capacity; + private int _bucketCapacity; + private int _allocatedIndex; + private int _firstFreeIndex; + + private readonly int _sizeOfTValue; + private readonly int _log2MinGrowth; + + public const int MINIMAL_CAPACITY = 64; + + public readonly byte* Buffer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer; + } + + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _count; + } + + public readonly int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _capacity; + } + + public readonly bool IsCreated + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer != null; + } + + public readonly bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !IsCreated || _count == 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly int CalcCapacityCeilPow2(int capacity) + { + capacity = Math.Max(Math.Max(1, _count), capacity); + var newCapacity = Math.Max(capacity, 1 << _log2MinGrowth); + var result = MathUtilities.CeilPow2(newCapacity); + + return result; + } + + private static int CalculateDataSize(int capacity, int bucketCapacity, int sizeOfTValue, out int outKeyOffset, out int outNextOffset, out int outBucketOffset) + { + var sizeOfTKey = sizeof(TKey); + var sizeOfInt = sizeof(int); + + var valuesSize = sizeOfTValue * capacity; + var keysSize = sizeOfTKey * capacity; + var nextSize = sizeOfInt * capacity; + var bucketSize = sizeOfInt * bucketCapacity; + var totalSize = valuesSize + keysSize + nextSize + bucketSize; + + outKeyOffset = 0 + valuesSize; + outNextOffset = outKeyOffset + keysSize; + outBucketOffset = outNextOffset + nextSize; + + return totalSize; + } + + public HashMapHelper(int capacity, int sizeOfTValue, uint minGrowth) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); + } + + if (sizeOfTValue <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeOfTValue), "Size of TValue must be greater than zero."); + } + + _capacity = CalcCapacityCeilPow2(capacity); + _bucketCapacity = _capacity * 2; + + var totalSize = CalculateDataSize(_capacity, _bucketCapacity, sizeOfTValue, + out var keyOffset, out var nextOffset, out var bucketOffset); + + _buffer = (byte*)Malloc((nuint)totalSize); + _keys = (TKey*)(_buffer + keyOffset); + _next = (int*)(_buffer + nextOffset); + _buckets = (int*)(_buffer + bucketOffset); + + Clear(); + + _sizeOfTValue = sizeOfTValue; + _log2MinGrowth = BitOperations.Log2(minGrowth); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly int GetBucket(in TKey key) + { + return (int)((uint)key.GetHashCode() & _bucketCapacity - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void CheckIndexOutOfBounds(int idx) + { + if ((uint)idx >= (uint)_capacity) + { + throw new InvalidOperationException($"Internal HashMap error. idx {idx}"); + } + } + + internal void ResizeExact(int newCapacity, int newBucketCapacity) + { + var totalSize = CalculateDataSize(newCapacity, newBucketCapacity, _sizeOfTValue, + out var keyOffset, out var nextOffset, out var bucketOffset); + + var oldBuffer = _buffer; + var oldKeys = _keys; + var oldNext = _next; + var oldBuckets = _buckets; + var oldBucketCapacity = _bucketCapacity; + + _buffer = (byte*)Malloc((nuint)totalSize); + _keys = (TKey*)(_buffer + keyOffset); + _next = (int*)(_buffer + nextOffset); + _buckets = (int*)(_buffer + bucketOffset); + _capacity = newCapacity; + _bucketCapacity = newBucketCapacity; + + Clear(); + + for (int i = 0, num = oldBucketCapacity; i < num; ++i) + { + for (var idx = oldBuckets[i]; idx != -1; idx = oldNext[idx]) + { + var newIdx = TryAdd(oldKeys[idx]); + MemCpy(_buffer + _sizeOfTValue * newIdx, oldBuffer + _sizeOfTValue * idx, (nuint)_sizeOfTValue); + } + } + + Free(oldBuffer); + } + + internal void Resize(int newCapacity) + { + newCapacity = Math.Max(newCapacity, _count); + var newBucketCapacity = MathUtilities.CeilPow2(newCapacity * 2); + + if (_capacity == newCapacity && _bucketCapacity == newBucketCapacity) + { + return; + } + + ResizeExact(newCapacity, newBucketCapacity); + } + + public void TrimExcess() + { + var capacity = CalcCapacityCeilPow2(_count); + ResizeExact(capacity, capacity * 2); + } + + public int Find(TKey key) + { + if (_allocatedIndex <= 0) + { + return -1; + } + + // First find the slot based on the hash + var bucket = GetBucket(key); + var entryIdx = _buckets[bucket]; + + if ((uint)entryIdx < (uint)_capacity) + { + var nextPtrs = _next; + var test = UnsafeUtilities.ReadArrayElement(_keys, entryIdx); + var test2 = UnsafeUtilities.ReadArrayElement(_next, 0); + while (!UnsafeUtilities.ReadArrayElement(_keys, entryIdx).Equals(key)) + { + entryIdx = nextPtrs[entryIdx]; + if ((uint)entryIdx >= (uint)_capacity) + { + return -1; + } + } + + return entryIdx; + } + + return -1; + } + + public int TryAdd(in TKey key) + { + if (Find(key) != -1) + { + return -1; + } + + // Allocate an entry from the free list + int idx; + int* next; + + if (_allocatedIndex >= _capacity && _firstFreeIndex < 0) + { + var newCap = Math.Min(MINIMAL_CAPACITY, CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth))); + Resize(newCap); + } + + idx = _firstFreeIndex; + + if (idx >= 0) + { + _firstFreeIndex = _next[idx]; + } + else + { + idx = _allocatedIndex++; + } + + CheckIndexOutOfBounds(idx); + + UnsafeUtilities.WriteArrayElement(_keys, idx, key); + var bucket = GetBucket(key); + + // Add the index to the hash-map + next = _next; + next[idx] = _buckets[bucket]; + _buckets[bucket] = idx; + _count++; + + return idx; + } + + public int TryRemove(TKey key) + { + if (_capacity == 0) + { + return -1; + } + + var removed = 0; + + // First find the slot based on the hash + var bucket = GetBucket(key); + + var prevEntry = -1; + var entryIdx = _buckets[bucket]; + + while (entryIdx >= 0 && entryIdx < _capacity) + { + if (UnsafeUtilities.ReadArrayElement(_keys, entryIdx).Equals(key)) + { + ++removed; + + // Found matching element, remove it + if (prevEntry < 0) + { + _buckets[bucket] = _next[entryIdx]; + } + else + { + _next[prevEntry] = _next[entryIdx]; + } + + // And free the index + var nextIdx = _next[entryIdx]; + _next[entryIdx] = _firstFreeIndex; + _firstFreeIndex = entryIdx; + entryIdx = nextIdx; + + break; + } + else + { + prevEntry = entryIdx; + entryIdx = _next[entryIdx]; + } + } + + _count -= removed; + return 0 != removed ? removed : -1; + } + + public bool TryGetValue(TKey key, out TValue item) + where TValue : unmanaged + { + var idx = Find(key); + + if (idx != -1) + { + item = UnsafeUtilities.ReadArrayElement(_buffer, idx); + return true; + } + + item = default; + return false; + } + + public bool MoveNextSearch(ref int bucketIndex, ref int nextIndex, out int index) + { + for (int i = bucketIndex, num = _bucketCapacity; i < num; ++i) + { + var idx = _buckets[i]; + + if (idx != -1) + { + index = idx; + bucketIndex = i + 1; + nextIndex = _next[idx]; + + return true; + } + } + + index = -1; + bucketIndex = _bucketCapacity; + nextIndex = -1; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext(ref int bucketIndex, ref int nextIndex, out int index) + { + if (nextIndex != -1) + { + index = nextIndex; + nextIndex = _next[nextIndex]; + return true; + } + + return MoveNextSearch(ref bucketIndex, ref nextIndex, out index); + } + + internal UnsafeArray GetKeyArray(Allocator allocator) + { + var result = new UnsafeArray(_count, allocator, AllocationType.UnInitialized); + + for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; i++) + { + var bucket = _buckets[i]; + + while (bucket != -1) + { + result[count++] = UnsafeUtilities.ReadArrayElement(_keys, bucket); + bucket = _next[bucket]; + } + } + + return result; + } + + internal UnsafeArray GetValueArray(Allocator allocator) + where TValue : unmanaged + { + var result = new UnsafeArray(_count, allocator, AllocationType.UnInitialized); + + for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; ++i) + { + var bucket = _buckets[i]; + + while (bucket != -1) + { + result[count++] = UnsafeUtilities.ReadArrayElement(_buffer, bucket); + bucket = _next[bucket]; + } + } + + return result; + } + + public UnsafeArray> GetKeyValueArrays(Allocator allocator) + where TValue : unmanaged + { + var result = new UnsafeArray>(_count, allocator, AllocationType.UnInitialized); + + for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; i++) + { + var bucket = _buckets[i]; + + while (bucket != -1) + { + result[count] = new(UnsafeUtilities.ReadArrayElement(_keys, bucket), + UnsafeUtilities.ReadArrayElement(_buffer, bucket)); + + count++; + bucket = _next[bucket]; + } + } + + return result; + } + + public void Clear() + { + MemSet(_buckets, 0xff, (nuint)_bucketCapacity * sizeof(int)); + MemSet(_next, 0xff, (nuint)_capacity * sizeof(int)); + + _count = 0; + _firstFreeIndex = -1; + _allocatedIndex = 0; + } + + public void Dispose() + { + if (IsCreated) + { + Free(_buffer); + + _buffer = null; + _keys = null; + _next = null; + _buckets = null; + + _count = 0; + _capacity = 0; + _bucketCapacity = 0; + } + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Helpers/MemoryUtilities.cs b/Misaki.HighPerformance.Unsafe/Helpers/MemoryUtilities.cs index 39c9de0..4509fc0 100644 --- a/Misaki.HighPerformance.Unsafe/Helpers/MemoryUtilities.cs +++ b/Misaki.HighPerformance.Unsafe/Helpers/MemoryUtilities.cs @@ -13,6 +13,76 @@ public static unsafe class MemoryUtilities public T data; } + /// + /// Allocates a block of memory of the specified size in bytes. + /// + /// Specifies the number of bytes to allocate in memory. + /// Returns a pointer to the allocated memory block. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Malloc(nuint size) + { + return NativeMemory.Alloc(size); + } + + /// + /// Allocates a block of memory with a specified size and alignment. + /// + /// Specifies the total number of bytes to allocate for the memory block. + /// Defines the required alignment for the allocated memory address. + /// Returns a pointer to the allocated memory block. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AlignedAlloc(nuint size, nuint alignment) + { + return NativeMemory.AlignedAlloc(size, alignment); + } + + /// + /// Resizes a previously allocated memory block to a new size. It returns a pointer to the reallocated memory. + /// + /// The pointer to the memory block that needs to be resized. + /// The new size for the memory block after resizing. + /// A pointer to the reallocated memory block, or null if the operation fails. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Realloc(void* ptr, nuint size) + { + return NativeMemory.Realloc(ptr, size); + } + + /// + /// Reallocates memory to a specified size with a given alignment. It returns a pointer to the newly allocated + /// memory. + /// + /// The pointer to the existing memory block that needs to be reallocated. + /// The new size for the memory allocation. + /// The required alignment for the new memory allocation. + /// A pointer to the reallocated memory block, or null if the allocation fails. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* AlignedRealloc(void* ptr, nuint size, nuint alignment) + { + return NativeMemory.AlignedRealloc(ptr, size, alignment); + } + + /// + /// Releases the allocated memory pointed to by the given pointer. This helps in managing memory usage effectively. + /// + /// The pointer to the memory block that needs to be freed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Free(void* ptr) + { + NativeMemory.Free(ptr); + } + + /// + /// Releases memory that was allocated with alignment requirements. It ensures proper deallocation of aligned memory + /// blocks. + /// + /// The pointer to the memory block that needs to be freed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AlignedFree(void* ptr) + { + NativeMemory.AlignedFree(ptr); + } + /// /// Clears a block of memory by setting it to zero. It initializes a specified number of bytes at a given memory /// address. @@ -32,7 +102,7 @@ public static unsafe class MemoryUtilities /// The number of bytes to set to the specified value. /// The byte value to which the memory block will be initialized. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void MemSet(void* ptr, nuint size, byte value) + public static void MemSet(void* ptr, byte value, nuint size) { NativeMemory.Fill(ptr, size, value); } diff --git a/Misaki.HighPerformance.Unsafe/Helpers/UnsafeCollectionExtensions.cs b/Misaki.HighPerformance.Unsafe/Helpers/UnsafeCollectionExtensions.cs index 2a42eb3..0bc2fd4 100644 --- a/Misaki.HighPerformance.Unsafe/Helpers/UnsafeCollectionExtensions.cs +++ b/Misaki.HighPerformance.Unsafe/Helpers/UnsafeCollectionExtensions.cs @@ -2,6 +2,10 @@ namespace Misaki.HighPerformance.Unsafe.Helpers; +/// +/// Provides extension methods for copying elements between unsafe collections and spans, converting collections to +/// arrays or lists, and searching for values. +/// public unsafe static class UnsafeCollectionExtensions { /// @@ -13,14 +17,37 @@ public unsafe static class UnsafeCollectionExtensions /// Thrown when the sizes of the source collection and destination span do not match. public static void CopyTo(this IUnsafeCollection source, Span destination) where T : unmanaged { - if (source.Size > destination.Length) + if (source.Count > destination.Length) { throw new ArgumentException("Source collection is larger than the destination span."); } fixed (T* ptr = destination) { - SystemUnsfae.CopyBlock(ptr, source.Buffer, (uint)(source.Size * sizeof(T))); + SystemUnsfae.CopyBlock(ptr, source.GetUnsafePtr(), (uint)(source.Count * sizeof(T))); + } + } + + /// + /// Copies a range of elements from a source collection to a destination span, ensuring both are adequately sized. + /// + /// Specifies the type of elements being copied, which must be a value type. + /// The collection from which elements are copied. + /// The span where the elements will be copied to. + /// The starting index in the source collection for the copy operation. + /// The starting index in the destination span where the elements will be placed. + /// The number of elements to copy from the source to the destination. + /// Thrown when the specified range exceeds the bounds of the source collection or destination span. + public static void CopyTo(this IUnsafeCollection source, Span destination, int sourceIndex, int destinationIndex, int length) where T : unmanaged + { + if (sourceIndex + length > source.Count || destinationIndex + length > destination.Length) + { + throw new ArgumentException("Source collection or destination span is too small for the specified range."); + } + + fixed (T* ptr = destination) + { + SystemUnsfae.CopyBlock(ptr + destinationIndex, (byte*)source.GetUnsafePtr() + (sourceIndex * sizeof(T)), (uint)(length * sizeof(T))); } } @@ -33,14 +60,37 @@ public unsafe static class UnsafeCollectionExtensions /// Thrown when the source span and destination collection have different sizes. public static void CopyFrom(this IUnsafeCollection destination, Span source) where T : unmanaged { - if (destination.Size > source.Length) + if (destination.Count > source.Length) { throw new ArgumentException("Destination collection is larger than the source span."); } fixed (T* ptr = source) { - SystemUnsfae.CopyBlock(destination.Buffer, ptr, (uint)(source.Length * sizeof(T))); + SystemUnsfae.CopyBlock(destination.GetUnsafePtr(), ptr, (uint)(source.Length * sizeof(T))); + } + } + + /// + /// Copies a specified range of elements from a source span to a destination collection. + /// + /// Represents the type of elements being copied, which must be unmanaged. + /// The collection where elements will be copied to. + /// The span containing the elements to be copied. + /// The starting index in the source span from which to begin copying. + /// The starting index in the destination collection where the elements will be placed. + /// The number of elements to copy from the source span to the destination collection. + /// Thrown when the specified range exceeds the bounds of the source span or destination collection. + public static void CopyFrom(this IUnsafeCollection destination, Span source, int sourceIndex, int destinationIndex, int length) where T : unmanaged + { + if (sourceIndex + length > source.Length || destinationIndex + length > destination.Count) + { + throw new ArgumentException("Source span or destination collection is too small for the specified range."); + } + + fixed (T* ptr = source) + { + SystemUnsfae.CopyBlock((byte*)destination.GetUnsafePtr() + (destinationIndex * sizeof(T)), ptr + sourceIndex, (uint)(length * sizeof(T))); } } @@ -52,10 +102,10 @@ public unsafe static class UnsafeCollectionExtensions /// A new collection containing the elements from the UnsafeCollection. public static T[] ToArray(this IUnsafeCollection source) where T : unmanaged { - var array = new T[source.Size]; + var array = new T[source.Count]; fixed (T* ptr = array) { - SystemUnsfae.CopyBlock(ptr, source.Buffer, (uint)(source.Size * sizeof(T))); + SystemUnsfae.CopyBlock(ptr, source.GetUnsafePtr(), (uint)(source.Count * sizeof(T))); } return array; @@ -69,10 +119,10 @@ public unsafe static class UnsafeCollectionExtensions /// A list containing the elements from the specified unmanaged collection. public static List ToList(this IUnsafeCollection source) where T : unmanaged { - var list = new List(source.Size); + var list = new List(source.Count); fixed (T* ptr = list.ToArray()) { - SystemUnsfae.CopyBlock(ptr, source.Buffer, (uint)(source.Size * sizeof(T))); + SystemUnsfae.CopyBlock(ptr, source.GetUnsafePtr(), (uint)(source.Count * sizeof(T))); } return list; } @@ -85,6 +135,39 @@ public unsafe static class UnsafeCollectionExtensions /// A Span that provides a view over the elements of the UnsafeCollection. public static Span AsSpan(this IUnsafeCollection source) where T : unmanaged { - return new(source.Buffer, source.Size); + return new(source.GetUnsafePtr(), source.Count); + } + + /// + /// Finds the index of a specified value in a collection. Returns -1 if the value is not found. + /// + /// The type of elements in the collection, which must support equality comparison. + /// The collection to search for the specified value. + /// The value to locate within the collection. + /// Outputs the index of the found value or -1 if not found. + public static void IndexOf(this IUnsafeCollection source, T value, out int index) where T : unmanaged, IEquatable + { + for (var i = 0; i < source.Count; i++) + { + if (UnsafeUtilities.ReadArrayElement(source.GetUnsafePtr(), i).Equals(value)) + { + index = i; + return; + } + } + index = -1; + } + + /// + /// Checks if a specified value exists within an unsafe collection of unmanaged types. + /// + /// Represents a type that is unmanaged and supports equality comparison. + /// The collection being searched for the specified value. + /// The value being searched for within the collection. + /// Returns true if the value is found; otherwise, returns false. + public static bool Conations(this IUnsafeCollection source, T value) where T : unmanaged, IEquatable + { + source.IndexOf(value, out var index); + return index != -1; } } \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Helpers/UnsafeUtilities.cs b/Misaki.HighPerformance.Unsafe/Helpers/UnsafeUtilities.cs index 77d21bc..48411c6 100644 --- a/Misaki.HighPerformance.Unsafe/Helpers/UnsafeUtilities.cs +++ b/Misaki.HighPerformance.Unsafe/Helpers/UnsafeUtilities.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using Misaki.HighPerformance.Unsafe.Collections; +using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.Unsafe.Helpers; @@ -79,4 +80,18 @@ public static unsafe class UnsafeUtilities { *ReadArrayElementUnsafe(ptr, index) = value; } + + /// + /// Converts an UnsafeArray of one unmanaged type to another unmanaged type without copying the elements. + /// + /// Represents the type of elements in the input array. + /// Represents the type of elements in the output array. + /// The input array containing elements of the specified input type. + /// An UnsafeArray containing elements of the specified output type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static UnsafeArray CastArray(UnsafeArray array) + where TIn : unmanaged where TOut : unmanaged + { + return new UnsafeArray(array.GetUnsafePtr(), array.Count * sizeof(TIn) / sizeof(TOut)); + } } \ No newline at end of file diff --git a/Misaki.HighPerformance.Unsafe/Misaki.HighPerformance.Unsafe.csproj b/Misaki.HighPerformance.Unsafe/Misaki.HighPerformance.Unsafe.csproj index 9373d81..42b3bd0 100644 --- a/Misaki.HighPerformance.Unsafe/Misaki.HighPerformance.Unsafe.csproj +++ b/Misaki.HighPerformance.Unsafe/Misaki.HighPerformance.Unsafe.csproj @@ -15,4 +15,8 @@ True + + + + diff --git a/Misaki.HighPerformance.sln b/Misaki.HighPerformance.sln index 1ed6d0a..a1d7bbd 100644 --- a/Misaki.HighPerformance.sln +++ b/Misaki.HighPerformance.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Misaki.HighPerformance.Unsa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Misaki.HighPerformance.Test", "Misaki.HighPerformance.Test\Misaki.HighPerformance.Test.csproj", "{90EFF5B8-22CD-4B6A-83AB-48E0E97610EA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Misaki.HighPerformance.Mathematics", "Misaki.HighPerformance.Math\Misaki.HighPerformance.Mathematics.csproj", "{861F9574-2063-4B54-8D6A-AA32B26B6583}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {90EFF5B8-22CD-4B6A-83AB-48E0E97610EA}.Debug|Any CPU.Build.0 = Debug|Any CPU {90EFF5B8-22CD-4B6A-83AB-48E0E97610EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {90EFF5B8-22CD-4B6A-83AB-48E0E97610EA}.Release|Any CPU.Build.0 = Release|Any CPU + {861F9574-2063-4B54-8D6A-AA32B26B6583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {861F9574-2063-4B54-8D6A-AA32B26B6583}.Debug|Any CPU.Build.0 = Debug|Any CPU + {861F9574-2063-4B54-8D6A-AA32B26B6583}.Release|Any CPU.ActiveCfg = Release|Any CPU + {861F9574-2063-4B54-8D6A-AA32B26B6583}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Misaki.HighPerformance/Buffer/ObjectPool.cs b/Misaki.HighPerformance/Buffer/ObjectPool.cs index fd0d2b2..289a191 100644 --- a/Misaki.HighPerformance/Buffer/ObjectPool.cs +++ b/Misaki.HighPerformance/Buffer/ObjectPool.cs @@ -8,9 +8,6 @@ namespace Misaki.HighPerformance.Buffer private readonly Func _factory; private readonly ConcurrentQueue _objects = new(); - private readonly bool _autoCleanup; - private readonly int _autoCleanupInterval; - private bool _disposed; public uint InitialSize @@ -24,13 +21,10 @@ namespace Misaki.HighPerformance.Buffer private set; } - public ObjectPool(Func factory, uint initialSize = uint.MinValue, uint maxSize = uint.MaxValue, bool autoCleanup = false, int autoCleanupInterval = 1000 * 60 * 5) + public ObjectPool(Func factory, uint initialSize = uint.MinValue, uint maxSize = uint.MaxValue) { _factory = factory; - _autoCleanup = autoCleanup; - _autoCleanupInterval = autoCleanupInterval; - InitialSize = initialSize; MaxSize = maxSize; @@ -41,40 +35,11 @@ namespace Misaki.HighPerformance.Buffer _objects.Enqueue(_factory()); } } - - SetupAutoCleanup(); } - private void PoolCleanup() + ~ObjectPool() { - foreach (var obj in _objects) - { - if (obj is IDisposable disposable) - { - disposable.Dispose(); - } - } - - _objects.Clear(); - - GC.Collect(); - } - - private void SetupAutoCleanup() - { - if (!_autoCleanup) - { - return; - } - - Task.Run(async () => - { - while (true) - { - await Task.Delay(_autoCleanupInterval); - PoolCleanup(); - } - }); + Dispose(); } public bool TryRent([MaybeNullWhen(false)] out T obj) @@ -100,11 +65,32 @@ namespace Misaki.HighPerformance.Buffer } } + public void Reset() + { + foreach (var obj in _objects) + { + if (obj is IDisposable disposable) + { + disposable.Dispose(); + } + } + + _objects.Clear(); + GC.Collect(); + } + public void Dispose() { - PoolCleanup(); + if (_disposed) + { + return; + } + + Reset(); _disposed = true; + + GC.SuppressFinalize(this); } } } diff --git a/Misaki.HighPerformance/Jobs/JobExtensions.cs b/Misaki.HighPerformance/Jobs/JobExtensions.cs index 7afbd7a..9e92e79 100644 --- a/Misaki.HighPerformance/Jobs/JobExtensions.cs +++ b/Misaki.HighPerformance/Jobs/JobExtensions.cs @@ -2,7 +2,8 @@ public static class JobExtensions { - public static JobHandle Schedule(this T job, bool preferLocal = false) where T : struct, IJob + public static JobHandle Schedule(this T job, bool preferLocal = false) + where T : struct, IJob { var handle = new JobHandle(1); var worker = new JobWorker(job, handle); @@ -11,7 +12,8 @@ public static class JobExtensions return handle; } - public static JobHandle Schedule(this T job, ReadOnlySpan dependencies, bool preferLocal = false) where T : struct, IJob + public static JobHandle Schedule(this T job, ReadOnlySpan dependencies, bool preferLocal = false) + where T : struct, IJob { foreach (var dependency in dependencies) { @@ -21,7 +23,8 @@ public static class JobExtensions return job.Schedule(preferLocal); } - public static JobHandle Schedule(this T job, int length, int batchCount, bool preferLocal = false) where T : struct, IJobParallelFor + public static JobHandle Schedule(this T job, int length, int batchCount, bool preferLocal = false) + where T : struct, IJobParallelFor { var batchSize = (length + batchCount - 1) / batchCount; var handle = new JobHandle(batchCount); @@ -37,7 +40,8 @@ public static class JobExtensions return handle; } - public static JobHandle Schedule(this T job, int length, int batchCount, ReadOnlySpan dependencies, bool preferLocal = false) where T : struct, IJobParallelFor + public static JobHandle Schedule(this T job, int length, int batchCount, ReadOnlySpan dependencies, bool preferLocal = false) + where T : struct, IJobParallelFor { foreach (var dependency in dependencies) { diff --git a/Misaki.HighPerformance/Jobs/JobWorker.cs b/Misaki.HighPerformance/Jobs/JobWorker.cs index 6ab6fa0..8a1839d 100644 --- a/Misaki.HighPerformance/Jobs/JobWorker.cs +++ b/Misaki.HighPerformance/Jobs/JobWorker.cs @@ -1,6 +1,7 @@ namespace Misaki.HighPerformance.Jobs; -internal readonly struct JobWorker(T job, JobHandle handle) : IThreadPoolWorkItem where T : struct, IJob +internal readonly struct JobWorker(T job, JobHandle handle) : IThreadPoolWorkItem + where T : struct, IJob { public void Execute() { @@ -9,7 +10,8 @@ internal readonly struct JobWorker(T job, JobHandle handle) : IThreadPoolWork } } -internal readonly struct ParallelJobWorker(T job, JobHandle handle, int start, int end) : IThreadPoolWorkItem where T : struct, IJobParallelFor +internal readonly struct ParallelJobWorker(T job, JobHandle handle, int start, int end) : IThreadPoolWorkItem + where T : struct, IJobParallelFor { public void Execute() {