From 7326c8394815e388b284546967d4f29106b06a08 Mon Sep 17 00:00:00 2001 From: Misaki Date: Wed, 18 Mar 2026 19:26:16 +0900 Subject: [PATCH] feat(allocator): add VirtualArena and FreeList allocators Introduce VirtualArena for large, thread-safe virtual memory allocation and FreeList allocator for efficient persistent allocations. Update AllocationManager to support new allocators, add cross-platform virtual memory utilities, and improve thread-safety and performance in existing allocators. Bump version to 1.5.0 and update project configuration. BREAKING CHANGE: AllocationManager initialization now requires explicit parameters for arena and FreeList capacities. Existing allocator usage may require code changes. --- .../Buffer/AllocationManager.cs | 158 ++++++++++++++---- .../Buffer/AllocationOption.cs | 4 + .../Buffer/Arena.cs | 18 +- .../Buffer/DynamicArena.cs | 10 +- .../Buffer/FreeList.cs | 23 +-- .../Buffer/Stack.cs | 5 + .../Buffer/VirtualArena.cs | 119 +++++++++++++ .../Collections/UnsafeList.cs | 9 + .../Misaki.HighPerformance.LowLevel.csproj | 3 +- .../Utilities/MemoryUtility.cs | 118 +++++++++++++ .../Misaki.HighPerformance.Test.csproj | 6 +- Misaki.HighPerformance.Test/Program.cs | 16 +- 12 files changed, 425 insertions(+), 64 deletions(-) create mode 100644 Misaki.HighPerformance.LowLevel/Buffer/VirtualArena.cs diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs index da426ae..6087162 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs @@ -1,7 +1,6 @@ using Misaki.HighPerformance.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Misaki.HighPerformance.LowLevel.Buffer; @@ -49,16 +48,16 @@ public static unsafe class AllocationManager { private const int _ARENA_MAGIC_ID = -3941029; - private DynamicArena _arena; - private AllocationHandle _handle; + private VirtualArena _arena; private int _currentTick; + private AllocationHandle _handle; public readonly AllocationHandle Handle => _handle; public readonly int CurrentTick => _currentTick; - public void Init(uint initialSize) + public void Init(nuint capacity) { - _arena = new DynamicArena(initialSize); + _arena = new VirtualArena(capacity); _handle = new AllocationHandle { State = Unsafe.AsPointer(ref this), @@ -67,6 +66,7 @@ public static unsafe class AllocationManager Free = null, IsValid = &IsValid }; + _currentTick = 0; } @@ -265,20 +265,94 @@ public static unsafe class AllocationManager } } - private const uint _DEFAULT_MEMORY_POOL_SIZE = 1024 * 1024; // 1 MB + private struct FreeListAllocator : IAllocator, IDisposable + { + private FreeList _freeList; + private AllocationHandle _handle; - private static readonly ArenaAllocator* s_pArenaAllocator; - private static readonly HeapAllocator* s_pHeapAllocator; - private static readonly StackAllocator* s_pStackAllocator; + public readonly AllocationHandle Handle => _handle; - private static bool s_disposed; + public void Init(int concurrencyLevel) + { + _freeList = new FreeList(8, 64 * 1024, concurrencyLevel); + _handle = new AllocationHandle + { + State = Unsafe.AsPointer(ref this), + Alloc = &Allocate, + Realloc = &Reallocate, + Free = &Free, + IsValid = &IsValid + }; + } + + private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle) + { + var selfPtr = (FreeListAllocator*)instance; + var ptr = selfPtr->_freeList.Allocate(size, alignment, allocationOption); + if (ptr == null) + { + *pHandle = MemoryHandle.Invalid; + return null; + } + + *pHandle = AddAllocation(ptr); + return ptr; + } + + private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle) + { + if (ptr == null) + { + return Allocate(instance, newSize, alignment, allocationOption, pHandle); + } + + var selfPtr = (FreeListAllocator*)instance; + var newPtr = selfPtr->_freeList.Allocate(newSize, alignment, allocationOption); + if (newPtr == null) + { + return null; + } + + MemCpy(newPtr, ptr, Math.Min(oldSize, newSize)); + + selfPtr->_freeList.Free(ptr); + RemoveAllocation(*pHandle); + + *pHandle = AddAllocation(newPtr); + return newPtr; + } + + private static bool IsValid(void* instance, MemoryHandle handle) + { + return ContainsAllocation(handle); + } + + private static void Free(void* instance, void* ptr, MemoryHandle handle) + { + var selfPtr = (FreeListAllocator*)instance; + selfPtr->_freeList.Free(ptr); + RemoveAllocation(handle); + } + + public void Dispose() + { + _freeList.Dispose(); + } + } + + private static ArenaAllocator* s_pArenaAllocator; + private static HeapAllocator* s_pHeapAllocator; + private static StackAllocator* s_pStackAllocator; + private static FreeListAllocator* s_pFreeListAllocator; + + private static bool s_initialized; #if ENABLE_DEBUG_LAYER private static SpinLock s_liveLock; private static AllocationHeader* s_pLiveHead; #endif - private static readonly ConcurrentSlotMap s_allocations; + private static ConcurrentSlotMap s_allocations = null!; public static readonly MemoryHandle MagicHandle = new MemoryHandle(int.MinValue, int.MinValue); @@ -287,14 +361,12 @@ public static unsafe class AllocationManager /// public static int LiveAllocationCount => s_allocations.Count; - static AllocationManager() + public static void Initialize(nuint arenaCapacity, int freeListConcurrencyLevel) { - var allocatorTotalSize = (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator)); - var basePtr = Malloc(allocatorTotalSize); - - s_pArenaAllocator = (ArenaAllocator*)basePtr; - s_pHeapAllocator = (HeapAllocator*)((byte*)basePtr + (nuint)sizeof(ArenaAllocator)); - s_pStackAllocator = (StackAllocator*)((byte*)basePtr + (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator))); + if (s_initialized) + { + return; + } #if ENABLE_DEBUG_LAYER s_liveLock = new SpinLock(false); @@ -303,9 +375,19 @@ public static unsafe class AllocationManager s_allocations = new ConcurrentSlotMap(256); - s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE); + var ptr = (byte*)Malloc((nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator) + sizeof(FreeListAllocator))); + + s_pArenaAllocator = (ArenaAllocator*)ptr; + s_pHeapAllocator = (HeapAllocator*)(ptr + sizeof(ArenaAllocator)); + s_pStackAllocator = (StackAllocator*)(ptr + sizeof(ArenaAllocator) + sizeof(HeapAllocator)); + s_pFreeListAllocator = (FreeListAllocator*)(ptr + sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator)); + + s_pArenaAllocator->Init(arenaCapacity); s_pHeapAllocator->Init(); s_pStackAllocator->Init(); + s_pFreeListAllocator->Init(freeListConcurrencyLevel); + + s_initialized = true; } #if ENABLE_DEBUG_LAYER @@ -472,10 +554,13 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static AllocationHandle GetAllocationHandle(Allocator allocator) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); + return allocator switch { Allocator.Temp => s_pArenaAllocator->Handle, Allocator.Persistent => s_pHeapAllocator->Handle, + Allocator.FreeList => s_pFreeListAllocator->Handle, _ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)), }; } @@ -491,6 +576,8 @@ public static unsafe class AllocationManager /// Thrown if the allocation fails. public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); + #if ENABLE_DEBUG_LAYER var ptr = DebugAllocate(size, alignment); #else @@ -508,7 +595,7 @@ public static unsafe class AllocationManager MemClear(ptr, size); } - *pHandle = AddAllocation((IntPtr)ptr); + *pHandle = AddAllocation(ptr); return ptr; } @@ -520,6 +607,8 @@ public static unsafe class AllocationManager /// The handle representing the memory allocation to free. The handle must be valid and previously allocated. public static void HeapFree(void* ptr, MemoryHandle handle) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); + #if ENABLE_DEBUG_LAYER if (handle != MagicHandle) { @@ -540,6 +629,8 @@ public static unsafe class AllocationManager /// The handle representing the memory allocation to free. The handle must be valid and previously allocated. public static void HeapFree(MemoryHandle handle) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); + if (TryGetAllocation(handle, out var ptr)) { HeapFree((void*)ptr, handle); @@ -552,6 +643,7 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ResetTempAllocator() { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); s_pArenaAllocator->Reset(); } @@ -562,6 +654,7 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Stack.Scope CreateStackScope() { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); return StackAllocator.CreateScope(s_pStackAllocator); } @@ -571,9 +664,11 @@ public static unsafe class AllocationManager /// A pointer to the memory block to be registered. The pointer must reference a valid, allocated memory region. /// A MemoryHandle representing the registered allocation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MemoryHandle AddAllocation(IntPtr ptr) + public static MemoryHandle AddAllocation(void* ptr) { - var id = s_allocations.Add(ptr, out var generation); + Debug.Assert(s_initialized, "AllocationManager is not initialized."); + + var id = s_allocations.Add((nint)ptr, out var generation); return new MemoryHandle(id, generation); } @@ -585,6 +680,7 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool RemoveAllocation(MemoryHandle handle) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); return s_allocations.Remove(handle.id, handle.generation); } @@ -597,6 +693,7 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryGetAllocation(MemoryHandle handle, out IntPtr ptr) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); return s_allocations.TryGetElement(handle.id, handle.generation, out ptr); } @@ -612,6 +709,8 @@ public static unsafe class AllocationManager [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ContainsAllocation(MemoryHandle handle) { + Debug.Assert(s_initialized, "AllocationManager is not initialized."); + if (handle == MagicHandle) { return true; @@ -625,7 +724,7 @@ public static unsafe class AllocationManager /// public static void Dispose() { - if (s_disposed) + if (!s_initialized) { return; } @@ -678,15 +777,12 @@ public static unsafe class AllocationManager } #endif - // NOTE: Arena allocator holds the base ptr for all allocators, heap and stack allocators do not own any memory themselves. - if (s_pArenaAllocator != null) - { - s_pArenaAllocator->Dispose(); - s_pStackAllocator->Dispose(); + s_pArenaAllocator->Dispose(); + s_pStackAllocator->Dispose(); + s_pFreeListAllocator->Dispose(); - NativeMemory.Free(s_pArenaAllocator); - } + Free(s_pArenaAllocator); - s_disposed = true; + s_initialized = false; } } diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs index 3acf6b4..6d08366 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs @@ -25,4 +25,8 @@ public enum Allocator : byte /// Allocator for persistent allocations. Allocations are not automatically released after use. /// Persistent, + /// + /// Allocator for persistent allocations using a free list. Allocations are not automatically released after use, but can be reused to reduce fragmentation and improve performance. + /// + FreeList } diff --git a/Misaki.HighPerformance.LowLevel/Buffer/Arena.cs b/Misaki.HighPerformance.LowLevel/Buffer/Arena.cs index 633f056..8188aab 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/Arena.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/Arena.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Misaki.HighPerformance.LowLevel.Buffer; @@ -30,10 +31,11 @@ public unsafe struct Arena : IDisposable } /// - /// Allocates a block of memory of a specified size with a given alignment. Returns a pointer to the allocated - /// memory or null if allocation fails. - /// You don't need to free the memory manually, it will be freed when the arena is disposed. + /// Allocates a block of memory of a specified size with a given alignment. /// + /// + /// This is thread safe. + /// /// Specifies the amount of memory to allocate in bytes. /// Defines the alignment requirement for the allocated memory. /// The option when allocating memory. @@ -81,17 +83,11 @@ public unsafe struct Arena : IDisposable } /// - /// Resets the arena, optionally clearing the allocated memory. + /// Resets the arena. /// - /// If true, the allocated memory will be cleared; otherwise, it will not be cleared. - /// Thrown if the arena has been disposed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { - if (_buffer == null) - { - throw new ObjectDisposedException(nameof(DynamicArena)); - } - _offset = 0; } diff --git a/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs b/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs index 8fc8c45..1d64735 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs @@ -3,8 +3,7 @@ using System.Runtime.InteropServices; namespace Misaki.HighPerformance.LowLevel.Buffer; /// -/// A dynamic memory management structure that automatically grows by creating linked arenas -/// when more space is needed. +/// A dynamic memory management structure that automatically grows by creating linked arenas when more space is needed. /// [StructLayout(LayoutKind.Explicit, Size = 128)] public unsafe struct DynamicArena : IDisposable @@ -97,6 +96,9 @@ public unsafe struct DynamicArena : IDisposable /// /// Allocates a block of memory with specified size and alignment. Creates a new arena if current one is full. /// + /// + /// This is thread safe. + /// /// Size of the memory block to allocate in bytes. /// Alignment requirement for the memory block. /// Pointer to the allocated memory block. @@ -132,10 +134,8 @@ public unsafe struct DynamicArena : IDisposable } /// - /// Resets all arenas in the chain, optionally clearing their memory. + /// Resets all arenas in the chain. /// - /// If true, memory will be cleared during reset. - /// Thrown if the arena has been disposed. public void Reset() { var current = _root; diff --git a/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs b/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs index 2ae8c32..4152b1d 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs @@ -4,8 +4,7 @@ using System.Runtime.InteropServices; namespace Misaki.HighPerformance.LowLevel.Buffer; /// -/// A thread-safe variable-size allocator that uses per-thread caches for the hot path and -/// a remote-free queue for cross-thread deallocation. +/// A variable-size allocator that uses per-thread caches for the hot path and a remote-free queue for cross-thread deallocation. /// [StructLayout(LayoutKind.Sequential)] public unsafe struct FreeList : IDisposable @@ -94,11 +93,6 @@ public unsafe struct FreeList : IDisposable /// public readonly nuint Alignment => _alignment; - /// - /// Gets whether the allocator has been disposed. - /// - public readonly bool IsDisposed => _disposed != 0; - /// /// Gets the chunk size used by this allocator. /// @@ -208,7 +202,7 @@ public unsafe struct FreeList : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly int FindBucket(nuint size) + private static int FindBucket(nuint size) { var blockSize = _MIN_BLOCK_SIZE; for (var i = 0; i < _MAX_BUCKETS; i++) @@ -240,7 +234,7 @@ public unsafe struct FreeList : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrainRemoteFrees(ThreadCache* cache) + private readonly void DrainRemoteFrees(ThreadCache* cache) { var head = (FreeNode*)Interlocked.Exchange(ref cache->remoteFreeHead, 0); while (head != null) @@ -320,7 +314,7 @@ public unsafe struct FreeList : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void* TryPopFromBucket(ThreadCache* cache, int cacheIndex, int bucketIndex) + private readonly void* TryPopFromBucket(ThreadCache* cache, int cacheIndex, int bucketIndex) { var buckets = GetBuckets(cache); var bucket = &buckets[bucketIndex]; @@ -338,7 +332,7 @@ public unsafe struct FreeList : IDisposable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PushToBucket(ThreadCache* cache, int bucketIndex, void* ptr, MemoryChunk* ownerChunk, nuint blockSize) + private readonly void PushToBucket(ThreadCache* cache, int bucketIndex, void* ptr, MemoryChunk* ownerChunk, nuint blockSize) { var buckets = GetBuckets(cache); var bucket = &buckets[bucketIndex]; @@ -490,6 +484,9 @@ public unsafe struct FreeList : IDisposable /// /// Allocates a memory block of the specified size. /// + /// + /// This is thread safe. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None) { @@ -548,6 +545,7 @@ public unsafe struct FreeList : IDisposable { ptr = AllocateFromChunk(cacheIndex, totalSize, alignment); } + if (ptr == null) { return null; @@ -576,6 +574,9 @@ public unsafe struct FreeList : IDisposable /// /// Frees a previously allocated memory block. /// + /// + /// This is thread safe. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Free(void* ptr) { diff --git a/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs b/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs index 91d8e70..50cecff 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs @@ -221,6 +221,11 @@ public unsafe partial struct Stack : IDisposable public void Dispose() { + if (_buffer == null) + { + return; + } + Free(_buffer); _buffer = null; diff --git a/Misaki.HighPerformance.LowLevel/Buffer/VirtualArena.cs b/Misaki.HighPerformance.LowLevel/Buffer/VirtualArena.cs new file mode 100644 index 0000000..d67d08f --- /dev/null +++ b/Misaki.HighPerformance.LowLevel/Buffer/VirtualArena.cs @@ -0,0 +1,119 @@ +using Misaki.HighPerformance.LowLevel.Utilities; +using System.Runtime.CompilerServices; + +namespace Misaki.HighPerformance.LowLevel.Buffer; + +/// +/// A thread-safe memory management structure that reserves a large virtual address space and commits physical memory on demand as allocations are made. +/// +public unsafe struct VirtualArena +{ + private const nuint _PAGE_SIZE = 64 * 1024; + + private byte* _baseAddress; + private nuint _reserveCapacity; + private nuint _committedSize; + private nuint _allocatedOffset; + + private volatile int _allocationLock; + + public VirtualArena(nuint reserveCapacity) + { + _reserveCapacity = (reserveCapacity + _PAGE_SIZE - 1) & ~(_PAGE_SIZE - 1); + _committedSize = 0; + _allocatedOffset = 0; + + _baseAddress = (byte*)Mmap(null, _reserveCapacity, VirtualAllocationFlags.Reserve); + + if (_baseAddress == null) + { + throw new OutOfMemoryException("Failed to reserve virtual address space."); + } + } + + /// + /// Allocates a block of memory of the specified size and alignment, using the given allocation options. + /// + /// + /// This is thread safe. + /// + /// The number of bytes to allocate. Must be greater than zero and less than or equal to the reserved capacity. + /// The alignment, in bytes, for the allocated memory block. Must be a power of two. + /// The allocation options that control allocation behavior. + /// A pointer to the allocated memory block if the allocation succeeds, otherwise null. + public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption) + { + while (Interlocked.CompareExchange(ref _allocationLock, 1, 0) != 0) + { + Thread.SpinWait(1); + } + + void* ptr; + + try + { + // Align the requested offset + var alignedOffset = (_allocatedOffset + alignment - 1) & ~(alignment - 1); + var newAllocatedOffset = alignedOffset + size; + + if (newAllocatedOffset > _reserveCapacity) + { + return null; // Out of reserved space + } + + if (newAllocatedOffset > _committedSize) + { + var sizeToCommit = newAllocatedOffset - _committedSize; + + // Align the commit size to the 64KB OS Page Size + sizeToCommit = (sizeToCommit + _PAGE_SIZE - 1) & ~(_PAGE_SIZE - 1); + + var commitAddress = _baseAddress + _committedSize; + var result = Mmap(commitAddress, sizeToCommit, VirtualAllocationFlags.Commit); + + if (result == null) + { + return null; // Out of physical RAM + } + + _committedSize += sizeToCommit; + } + + ptr = _baseAddress + alignedOffset; + _allocatedOffset = newAllocatedOffset; + } + finally + { + Interlocked.Exchange(ref _allocationLock, 0); + } + + if (allocationOption.HasFlag(AllocationOption.Clear)) + { + MemClear(ptr, size); + } + + return ptr; + } + + /// + /// Resets the arena. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _allocatedOffset = 0; + } + + public void Dispose() + { + if (_baseAddress != null) + { + Munmap(_baseAddress, _reserveCapacity); + + _baseAddress = null; + _reserveCapacity = 0; + _committedSize = 0; + _allocatedOffset = 0; + } + } +} diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs index 222850e..9c584b5 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs @@ -206,11 +206,15 @@ public unsafe struct UnsafeList : IUnsafeCollection { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Conditional("ENABLE_COLLECTION_CHECKS")] private readonly void CheckNoResizeCapacity(int count) { CheckNoResizeCapacity(count, Count); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Conditional("ENABLE_COLLECTION_CHECKS")] private readonly void CheckNoResizeCapacity(int index, int count) { if (index + count > Capacity) @@ -242,16 +246,19 @@ public unsafe struct UnsafeList : IUnsafeCollection } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Enumerator GetEnumerator() { return new((UnsafeList*)UnsafeUtility.AddressOf(ref this)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -266,6 +273,7 @@ public unsafe struct UnsafeList : IUnsafeCollection /// Otherwise the parallel reader will be invalid after the stack frame that creates the list is popped, even if the list's internal array is still valid. /// /// A instance that can be used to read items from the list in a thread-safe manner. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ParallelReader AsParallelReader() { return new((UnsafeList*)UnsafeUtility.AddressOf(ref this)); @@ -280,6 +288,7 @@ public unsafe struct UnsafeList : IUnsafeCollection /// Otherwise the parallel writer will be invalid after the stack frame that creates the list is popped, even if the list's internal array is still valid. /// /// A instance that can be used to add items to the list in a thread-safe manner. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ParallelWriter AsParallelWriter() { return new((UnsafeList*)UnsafeUtility.AddressOf(ref this)); diff --git a/Misaki.HighPerformance.LowLevel/Misaki.HighPerformance.LowLevel.csproj b/Misaki.HighPerformance.LowLevel/Misaki.HighPerformance.LowLevel.csproj index d2b03a5..df6d061 100644 --- a/Misaki.HighPerformance.LowLevel/Misaki.HighPerformance.LowLevel.csproj +++ b/Misaki.HighPerformance.LowLevel/Misaki.HighPerformance.LowLevel.csproj @@ -7,7 +7,7 @@ true true Misaki - 1.4.5 + 1.5.0 $(AssemblyVersion) https://git.personalnas.com/Misaki/Misaki.HighPerformance.git https://git.personalnas.com/Misaki/Misaki.HighPerformance.git @@ -17,7 +17,6 @@ True - $(DefineConstants);ENABLE_COLLECTION_CHECKS diff --git a/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs b/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs index 699288b..4a03ebc 100644 --- a/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs +++ b/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs @@ -1,10 +1,55 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Versioning; namespace Misaki.HighPerformance.LowLevel.Utilities; +[Flags] +public enum VirtualAllocationFlags +{ + Reserve = 1 << 0, + Commit = 1 << 1, +} + public static unsafe partial class MemoryUtility { + private const uint _MEM_COMMIT = 0x00001000; + private const uint _MEM_RESERVE = 0x00002000; + private const uint _MEM_RELEASE = 0x00008000; + private const uint _PAGE_READWRITE = 0x04; + + [SupportedOSPlatform("windows")] + [DllImport("kernel32.dll")] + private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect); + + [SupportedOSPlatform("windows")] + [DllImport("kernel32.dll")] + private static extern int VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType); + + private const int _PROT_NONE = 0x0; + private const int _PROT_READ = 0x1; + private const int _PROT_WRITE = 0x2; + private const int _MAP_PRIVATE = 0x02; + + // Note: MAP_ANONYMOUS varies by OS. Linux is 0x20, macOS is 0x1000. + private static int GetMapAnonymousFlag() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x1000 : 0x20; + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + [DllImport("libc")] + private static extern void* mmap(void* addr, nuint length, int prot, int flags, int fd, nint offset); + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + [DllImport("libc")] + private static extern int munmap(void* addr, nuint length); + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + [DllImport("libc")] + private static extern int mprotect(void* addr, nuint len, int prot); + + [StructLayout(LayoutKind.Sequential)] private struct AlignOfHelper where T : struct @@ -224,6 +269,79 @@ public static unsafe partial class MemoryUtility return span1.SequenceCompareTo(span2); } + public static void* Mmap(void* addr, nuint size, VirtualAllocationFlags flags) + { + if (OperatingSystem.IsWindows()) + { + var allocFlags = 0u; + var protect = _PAGE_READWRITE; + + if (flags.HasFlag(VirtualAllocationFlags.Reserve)) + { + allocFlags |= _MEM_RESERVE; + } + + if (flags.HasFlag(VirtualAllocationFlags.Commit)) + { + allocFlags |= _MEM_COMMIT; + } + + var ptr = VirtualAlloc(addr, size, allocFlags, protect); + if (ptr == null) + { + throw new OutOfMemoryException("Failed to allocate memory using MMap."); + } + + return ptr; + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + if (flags == VirtualAllocationFlags.Commit) + { + // POSIX commit changes protection of already-reserved memory + if (mprotect(addr, size, _PROT_READ | _PROT_WRITE) == -1) + { + throw new OutOfMemoryException("Failed to commit physical RAM via mprotect."); + } + + return addr; + } + + // Reserve or Reserve|Commit creates a new mapping + var prot = flags.HasFlag(VirtualAllocationFlags.Commit) ? _PROT_READ | _PROT_WRITE : _PROT_NONE; + var ptr = mmap(addr, size, prot, _MAP_PRIVATE | GetMapAnonymousFlag(), -1, 0); + + if (ptr == (void*)-1) + { + throw new OutOfMemoryException("Failed to allocate memory using MMap."); + } + + return ptr; + } + + throw new PlatformNotSupportedException("Mmap is not supported on this platform."); + } + + + public static bool Munmap(void* ptr, nuint size) + { + if (ptr == null) + { + return false; + } + + if (OperatingSystem.IsWindows()) + { + return VirtualFree(ptr, 0, _MEM_RELEASE) != 0; + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + return munmap(ptr, size) == 0; + } + + throw new PlatformNotSupportedException("Munmap is not supported on this platform."); + } + /// /// Calculates the size in bytes of a specified unmanaged type. /// diff --git a/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj b/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj index dc1fe0e..9549eb2 100644 --- a/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj +++ b/Misaki.HighPerformance.Test/Misaki.HighPerformance.Test.csproj @@ -9,10 +9,12 @@ - $(DefineConstants);ENABLE_COLLECTION_CHECKS + $(DefineConstants);ENABLE_COLLECTION_CHECKS;PLATFORM_WINDOWS - + + $(DefineConstants);PLATFORM_WINDOWS + diff --git a/Misaki.HighPerformance.Test/Program.cs b/Misaki.HighPerformance.Test/Program.cs index fcfa4f1..f85eb04 100644 --- a/Misaki.HighPerformance.Test/Program.cs +++ b/Misaki.HighPerformance.Test/Program.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Running; +using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.Mathematics.SPMD; @@ -10,7 +11,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); //var hashMap = new UnsafeHashMap(10, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent); //hashMap[0] = 5; //hashMap[1] = 6; @@ -39,4 +40,15 @@ BenchmarkRunner.Run(); // return ref Unsafe.NullRef(); // } -//} \ No newline at end of file +//} + +AllocationManager.Initialize(1024 * 1024 * 1024, 1); + +// Should be undefined or throw exception because AllocationManager does not initialized. +var arr = new UnsafeArray(1000, Allocator.FreeList); +for (int i = 0; i < arr.Length; i++) +{ + Console.WriteLine(arr[i]); +} +arr.Dispose(); +AllocationManager.Dispose();