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.
This commit is contained in:
2026-03-18 19:26:16 +09:00
parent 9cee32aa83
commit 7326c83948
12 changed files with 425 additions and 64 deletions

View File

@@ -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<IntPtr> s_allocations;
private static ConcurrentSlotMap<IntPtr> s_allocations = null!;
public static readonly MemoryHandle MagicHandle = new MemoryHandle(int.MinValue, int.MinValue);
@@ -287,14 +361,12 @@ public static unsafe class AllocationManager
/// </summary>
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<IntPtr>(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
/// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception>
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
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param>
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
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param>
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
/// <param name="ptr">A pointer to the memory block to be registered. The pointer must reference a valid, allocated memory region.</param>
/// <returns>A MemoryHandle representing the registered allocation.</returns>
[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
/// </summary>
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;
}
}

View File

@@ -25,4 +25,8 @@ public enum Allocator : byte
/// Allocator for persistent allocations. Allocations are not automatically released after use.
/// </summary>
Persistent,
/// <summary>
/// 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.
/// </summary>
FreeList
}

View File

@@ -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
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This is thread safe.
/// </remarks>
/// <param name="size">Specifies the amount of memory to allocate in bytes.</param>
/// <param name="alignment">Defines the alignment requirement for the allocated memory.</param>
/// <param name="allocationOption">The option when allocating memory.</param>
@@ -81,17 +83,11 @@ public unsafe struct Arena : IDisposable
}
/// <summary>
/// Resets the arena, optionally clearing the allocated memory.
/// Resets the arena.
/// </summary>
/// <param name="clear">If true, the allocated memory will be cleared; otherwise, it will not be cleared.</param>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
if (_buffer == null)
{
throw new ObjectDisposedException(nameof(DynamicArena));
}
_offset = 0;
}

View File

@@ -3,8 +3,7 @@ using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 128)]
public unsafe struct DynamicArena : IDisposable
@@ -97,6 +96,9 @@ public unsafe struct DynamicArena : IDisposable
/// <summary>
/// Allocates a block of memory with specified size and alignment. Creates a new arena if current one is full.
/// </summary>
/// <remarks>
/// This is thread safe.
/// </remarks>
/// <param name="size">Size of the memory block to allocate in bytes.</param>
/// <param name="alignment">Alignment requirement for the memory block.</param>
/// <returns>Pointer to the allocated memory block.</returns>
@@ -132,10 +134,8 @@ public unsafe struct DynamicArena : IDisposable
}
/// <summary>
/// Resets all arenas in the chain, optionally clearing their memory.
/// Resets all arenas in the chain.
/// </summary>
/// <param name="clear">If true, memory will be cleared during reset.</param>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void Reset()
{
var current = _root;

View File

@@ -4,8 +4,7 @@ using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// 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.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FreeList : IDisposable
@@ -94,11 +93,6 @@ public unsafe struct FreeList : IDisposable
/// </summary>
public readonly nuint Alignment => _alignment;
/// <summary>
/// Gets whether the allocator has been disposed.
/// </summary>
public readonly bool IsDisposed => _disposed != 0;
/// <summary>
/// Gets the chunk size used by this allocator.
/// </summary>
@@ -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
/// <summary>
/// Allocates a memory block of the specified size.
/// </summary>
/// <remarks>
/// This is thread safe.
/// </remarks>
[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
/// <summary>
/// Frees a previously allocated memory block.
/// </summary>
/// <remarks>
/// This is thread safe.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Free(void* ptr)
{

View File

@@ -221,6 +221,11 @@ public unsafe partial struct Stack : IDisposable
public void Dispose()
{
if (_buffer == null)
{
return;
}
Free(_buffer);
_buffer = null;

View File

@@ -0,0 +1,119 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// A thread-safe memory management structure that reserves a large virtual address space and commits physical memory on demand as allocations are made.
/// </summary>
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.");
}
}
/// <summary>
/// Allocates a block of memory of the specified size and alignment, using the given allocation options.
/// </summary>
/// <remarks>
/// This is thread safe.
/// </remarks>
/// <param name="size">The number of bytes to allocate. Must be greater than zero and less than or equal to the reserved capacity.</param>
/// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two.</param>
/// <param name="allocationOption">The allocation options that control allocation behavior.</param>
/// <returns>A pointer to the allocated memory block if the allocation succeeds, otherwise null.</returns>
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;
}
/// <summary>
/// Resets the arena.
/// </summary>
[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;
}
}
}