feat(lowlevel): add VirtualStack, update allocators, docs

Introduce VirtualStack allocator, refactor memory management to use virtual memory stacks, and update documentation.

Added VirtualStack as a new stack allocator using virtual memory, replaced Stack with VirtualStack in allocation manager and related APIs, and updated TempJobAllocator to use VirtualArena. Introduced AllocationManagerInitOpts for allocator configuration. Replaced ENABLE_COLLECTION_CHECKS with ENABLE_SAFETY_CHECKS for safety checks. Removed Result.cs and updated project files and examples. Added comprehensive README files for all major packages and improved root documentation.

BREAKING CHANGE: Stack allocator replaced by VirtualStack; TempJobAllocator and AllocationManager initialization signatures changed; Result types removed.
This commit is contained in:
2026-03-19 15:38:23 +09:00
parent faf87953a3
commit 69b054e81d
24 changed files with 798 additions and 542 deletions

View File

@@ -1,6 +1,9 @@
using Misaki.HighPerformance.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
#if ENABLE_DEBUG_LAYER
using System.Runtime.InteropServices;
#endif
namespace Misaki.HighPerformance.LowLevel.Buffer;
@@ -26,6 +29,24 @@ public readonly struct AllocationInfo
}
}
public readonly struct AllocationManagerInitOpts
{
public nuint ArenaCapacity
{
get; init;
}
public nuint StackCapacity
{
get; init;
}
public int FreeListConcurrencyLevel
{
get; init;
}
}
/// <summary>
/// Provides memory allocation management for native memory allocations, with support for tracking,
/// debugging, and custom allocation strategies.
@@ -181,8 +202,13 @@ public static unsafe class AllocationManager
{
private const int _STACK_MAGIC_ID = -6843541;
private static void** s_pStackBuffers = null;
private static int s_stackCount = 0;
private static int s_stackCapacity = 0;
private static readonly SpinLock s_locker = new SpinLock(false);
[ThreadStatic]
private static Stack s_stack;
private static VirtualStack s_stack;
private AllocationHandle _handle;
public readonly AllocationHandle Handle => _handle;
@@ -199,8 +225,49 @@ public static unsafe class AllocationManager
};
}
private static void EnsureInitialize()
{
if (s_stack.Buffer == null)
{
s_stack = new VirtualStack(s_threadLocalStackDefaultSize);
var token = false;
try
{
s_locker.Enter(ref token);
if (s_pStackBuffers == null)
{
s_pStackBuffers = (void**)Malloc((nuint)(sizeof(void*) * Environment.ProcessorCount));
s_stackCapacity = Environment.ProcessorCount;
}
if (s_stackCount >= s_stackCapacity)
{
var pOld = s_pStackBuffers;
var newCapacity = s_stackCapacity * 2;
var pNew = (void**)Realloc(pOld, (nuint)(sizeof(void*) * newCapacity));
s_pStackBuffers = pNew;
s_stackCapacity = newCapacity;
}
s_pStackBuffers[s_stackCount] = s_stack.Buffer;
s_stackCount++;
}
finally
{
if (token)
{
s_locker.Exit();
}
}
}
}
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
EnsureInitialize();
var ptr = s_stack.Allocate(size, alignment, allocationOption);
if (ptr == null)
{
@@ -219,6 +286,8 @@ public static unsafe class AllocationManager
return Allocate(instance, newSize, alignment, allocationOption, pHandle);
}
EnsureInitialize();
// Optimize for last allocation. Set offset directly.
var oldBase = s_stack.Buffer + s_stack.Offset - oldSize;
if (ptr == oldBase)
@@ -254,14 +323,23 @@ public static unsafe class AllocationManager
return handle.id == _STACK_MAGIC_ID && handle.generation <= (int)s_stack.Offset;
}
public static Stack.Scope CreateScope(StackAllocator* pSelf)
public static VirtualStack.Scope CreateScope(StackAllocator* pSelf)
{
EnsureInitialize();
return s_stack.CreateScope(pSelf->_handle);
}
public readonly void Dispose()
{
Stack.DisposeAll();
if (s_pStackBuffers == null)
{
return;
}
for (var i = 0; i < s_stackCount; i++)
{
Free(s_pStackBuffers[i]);
}
}
}
@@ -345,8 +423,6 @@ public static unsafe class AllocationManager
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;
@@ -356,12 +432,16 @@ public static unsafe class AllocationManager
public static readonly MemoryHandle MagicHandle = new MemoryHandle(int.MinValue, int.MinValue);
private static bool s_initialized;
internal static nuint s_threadLocalStackDefaultSize;
/// <summary>
/// Gets the number of live persistent heap allocations when the debug layer is disabled.
/// Gets the number of live tracked heap allocations.
/// </summary>
public static int LiveAllocationCount => s_allocations.Count;
public static void Initialize(nuint arenaCapacity, int freeListConcurrencyLevel)
public static void Initialize(AllocationManagerInitOpts opts)
{
if (s_initialized)
{
@@ -382,10 +462,12 @@ public static unsafe class AllocationManager
s_pStackAllocator = (StackAllocator*)(ptr + sizeof(ArenaAllocator) + sizeof(HeapAllocator));
s_pFreeListAllocator = (FreeListAllocator*)(ptr + sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator));
s_pArenaAllocator->Init(arenaCapacity);
s_pArenaAllocator->Init(opts.ArenaCapacity);
s_pHeapAllocator->Init();
s_pStackAllocator->Init();
s_pFreeListAllocator->Init(freeListConcurrencyLevel);
s_pFreeListAllocator->Init(opts.FreeListConcurrencyLevel);
s_threadLocalStackDefaultSize = opts.StackCapacity;
s_initialized = true;
}
@@ -652,7 +734,7 @@ public static unsafe class AllocationManager
/// </summary>
/// <returns>A <see cref="Stack.Scope"/> instance representing the newly created stack scope. The scope must be disposed when no longer needed to release allocated resources.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Stack.Scope CreateStackScope()
public static VirtualStack.Scope CreateStackScope()
{
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
return StackAllocator.CreateScope(s_pStackAllocator);

View File

@@ -7,14 +7,14 @@ namespace Misaki.HighPerformance.LowLevel.Buffer;
/// A memory management structure that allocates and resets memory blocks with specified alignment.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 64)] // Cache line aligned to prevent false sharing
public unsafe struct Arena : IMemoryAllocator<Arena, Arena.CreateOptions>
public unsafe struct Arena : IMemoryAllocator<Arena, Arena.CreationOptions>
{
public struct CreateOptions
public struct CreationOptions
{
public nuint size;
}
public static Arena Create(in CreateOptions opts)
public static Arena Create(in CreationOptions opts)
{
return new Arena(opts.size);
}

View File

@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
@@ -30,27 +31,17 @@ public unsafe struct DynamicArena : IMemoryAllocator<DynamicArena, DynamicArena.
[FieldOffset(8)]
private ArenaNode* _current;
[FieldOffset(16)]
private uint _initialSize;
private readonly nuint _initialSize;
[FieldOffset(20)]
[FieldOffset(24)]
private volatile int _nodeCreationLock;
/// <summary>
/// Initializes a new instance of DynamicArena with the specified initial size.
/// </summary>
/// <param name="initialSize">The initial size in bytes for the first arena block.</param>
public DynamicArena(uint initialSize)
public DynamicArena(nuint initialSize)
{
Initialize(initialSize);
}
public void Initialize(uint initialSize)
{
if (_root != null)
{
return;
}
_initialSize = initialSize;
_root = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
_root->arena = new Arena(initialSize);
@@ -143,6 +134,7 @@ public unsafe struct DynamicArena : IMemoryAllocator<DynamicArena, DynamicArena.
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Free(void* ptr)
{
}

View File

@@ -38,8 +38,7 @@ public unsafe struct MemoryPool<T, TOpts> : IDisposable
return Allocate(pAllocator, newSize, alignment, allocationOption, pHandle);
}
MemoryHandle newHandle;
var newPtr = Allocate(pAllocator, newSize, alignment, allocationOption, &newHandle);
var newPtr = Allocate(pAllocator, newSize, alignment, allocationOption, pHandle);
if (newPtr == null)
{
return null;
@@ -48,7 +47,6 @@ public unsafe struct MemoryPool<T, TOpts> : IDisposable
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
Free(pAllocator, ptr, *pHandle);
*pHandle = newHandle;
return newPtr;
}

View File

@@ -1,29 +1,8 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
public unsafe partial struct Stack
{
private static void** s_pStackBuffers = null;
private static int s_stackCount = 0;
private static int s_stackCapacity = 0;
private static readonly SpinLock s_locker = new SpinLock(false);
public static void DisposeAll()
{
if (s_pStackBuffers == null)
{
return;
}
for (var i = 0; i < s_stackCount; i++)
{
MemoryUtility.Free(s_pStackBuffers[i]);
}
}
}
/// <summary>
/// Provides a stack-based memory allocator for unmanaged memory, enabling fast allocation and deallocation of memory
/// blocks within a preallocated buffer.
@@ -41,8 +20,6 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
return new Stack(opts.size);
}
private const nuint _DEFAULT_SIZE = 1024 * 1024; // 1MB
public readonly ref struct Scope : IDisposable
{
private readonly Stack* _allocator;
@@ -56,7 +33,9 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
_allocator = allocator;
_handle = handle;
_originalOffset = allocator->_offset;
#if ENABLE_SAFETY_CHECKS
_allocator->_activeScopeCount++;
#endif
}
public void Dispose()
@@ -64,7 +43,9 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
if (_allocator != null)
{
_allocator->_offset = _allocator->_offset > _originalOffset ? _originalOffset : _allocator->_offset;
#if ENABLE_SAFETY_CHECKS
_allocator->_activeScopeCount--;
#endif
}
}
}
@@ -72,14 +53,15 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
private byte* _buffer;
private nuint _size;
private nuint _offset;
#if ENABLE_SAFETY_CHECKS
private uint _activeScopeCount;
#endif
internal readonly byte* Buffer => _buffer;
public nuint Offset
internal nuint Offset
{
readonly get => _offset;
internal set => _offset = value;
set => _offset = value;
}
/// <summary>
@@ -87,81 +69,15 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
/// </summary>
/// <param name="size">The size, in bytes, of the memory buffer to allocate for stack-based allocations. Must be greater than zero.</param>
public Stack(nuint size)
{
if (size == 0)
{
throw new ArgumentException("Size must be greater than zero.", nameof(size));
}
Init(size);
}
private void Init(nuint size)
{
ArgumentOutOfRangeException.ThrowIfNegative(size);
if (_buffer != null)
{
Free(_buffer);
}
_buffer = (byte*)Malloc(size);
if (_buffer == null)
{
throw new OutOfMemoryException("Failed to allocate memory for the stack.");
}
_size = size;
_offset = 0;
#if ENABLE_SAFETY_CHECKS
_activeScopeCount = 0;
var token = false;
try
{
s_locker.Enter(ref token);
if (s_pStackBuffers == null)
{
s_pStackBuffers = (void**)Malloc((nuint)(sizeof(void*) * Environment.ProcessorCount));
s_stackCapacity = Environment.ProcessorCount;
}
if (s_stackCount >= s_stackCapacity)
{
var pOld = s_pStackBuffers;
var newCapacity = s_stackCapacity * 2;
var pNew = (void**)Realloc(pOld, (nuint)(sizeof(void*) * newCapacity));
s_pStackBuffers = pNew;
s_stackCapacity = newCapacity;
}
s_pStackBuffers[s_stackCount] = _buffer;
s_stackCount++;
}
finally
{
if (token)
{
s_locker.Exit();
}
}
}
private readonly void ThrowIfNoScope()
{
if (_activeScopeCount == 0)
{
throw new InvalidOperationException("Allocations can only be made within an active memory scope.");
}
}
private void EnsureInitialize()
{
if (_buffer == null || _size == 0)
{
Init(_DEFAULT_SIZE);
}
#endif
}
/// <summary>
@@ -174,7 +90,6 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Scope CreateScope(AllocationHandle handle)
{
EnsureInitialize();
return new Scope((Stack*)Unsafe.AsPointer(ref this), handle);
}
@@ -189,7 +104,12 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
/// there is insufficient space in the buffer.</returns>
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{
ThrowIfNoScope();
#if ENABLE_SAFETY_CHECKS
if (_activeScopeCount == 0)
{
throw new InvalidOperationException("Allocations can only be made within an active memory scope.");
}
#endif
if (size == 0)
{
@@ -202,7 +122,6 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
}
var alignedOffset = (_offset + alignment - 1) & ~(alignment - 1);
var newOffset = alignedOffset + size;
if (newOffset > _size)
@@ -222,6 +141,7 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Free(void* ptr)
{
}
@@ -229,6 +149,7 @@ public unsafe partial struct Stack : IMemoryAllocator<Stack, Stack.CreationOpts>
/// <summary>
/// Resets the internal offset to its initial position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_offset = 0;

View File

@@ -6,8 +6,17 @@ 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
public unsafe struct VirtualArena : IMemoryAllocator<VirtualArena, VirtualArena.CreationOptions>
{
public struct CreationOptions
{
public nuint reserveCapacity;
}
public static VirtualArena Create(in CreationOptions opts)
{
return new VirtualArena(opts.reserveCapacity);
}
private const nuint _PAGE_SIZE = 64 * 1024;
private byte* _baseAddress;
@@ -95,6 +104,11 @@ public unsafe struct VirtualArena
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Free(void* ptr)
{
}
/// <summary>
/// Resets the arena.
/// </summary>

View File

@@ -0,0 +1,180 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
public unsafe struct VirtualStack : IMemoryAllocator<VirtualStack, VirtualStack.CreationOpts>
{
private const nuint _PAGE_SIZE = 64 * 1024;
public struct CreationOpts
{
public nuint reserveCapacity;
}
public static VirtualStack Create(in CreationOpts opts)
{
return new VirtualStack(opts.reserveCapacity);
}
public readonly ref struct Scope : IDisposable
{
private readonly VirtualStack* _allocator;
private readonly AllocationHandle _handle;
private readonly nuint _originalOffset;
public readonly AllocationHandle AllocationHandle => _handle;
internal Scope(VirtualStack* allocator, AllocationHandle handle)
{
_allocator = allocator;
_handle = handle;
_originalOffset = allocator->_allocatedOffset;
#if ENABLE_SAFETY_CHECKS
_allocator->_activeScopeCount++;
#endif
}
public void Dispose()
{
if (_allocator != null)
{
_allocator->_allocatedOffset = _allocator->_allocatedOffset > _originalOffset ? _originalOffset : _allocator->_allocatedOffset;
#if ENABLE_SAFETY_CHECKS
_allocator->_activeScopeCount--;
#endif
}
}
}
private byte* _baseAddress;
private nuint _reserveCapacity;
private nuint _committedSize;
private nuint _allocatedOffset;
#if ENABLE_SAFETY_CHECKS
private uint _activeScopeCount;
#endif
internal readonly byte* Buffer => _baseAddress;
internal nuint Offset
{
readonly get => _allocatedOffset;
set => _allocatedOffset = value;
}
public VirtualStack(nuint reserveCapacity)
{
_reserveCapacity = (reserveCapacity + _PAGE_SIZE - 1) & ~(_PAGE_SIZE - 1);
_committedSize = 0;
_allocatedOffset = 0;
_baseAddress = (byte*)Mmap(null, _reserveCapacity, VirtualAllocationFlags.Reserve);
#if ENABLE_SAFETY_CHECKS
_activeScopeCount = 0;
#endif
}
/// <summary>
/// Creates a new scope instance associated with the current stack context.
/// </summary>
/// <remarks>
/// The instance of <see cref="VirtualStack"/> must be pinned or allocated on the native heap to ensure that the pointer remains valid for the lifetime of the scope.
/// </remarks>
/// <returns>A <see cref="Scope"/> object that represents a scope tied to this stack.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Scope CreateScope(AllocationHandle handle)
{
return new Scope((VirtualStack*)Unsafe.AsPointer(ref this), handle);
}
/// <summary>
/// Allocates a block of memory of the specified size and alignment.
/// </summary>
/// <remarks>
/// This is not thread-safe. It is designed for single-threaded or thread-local contexts.
/// </remarks>
public void* Allocate(nuint size, nuint alignment, AllocationOption option = AllocationOption.None)
{
#if ENABLE_SAFETY_CHECKS
if (_activeScopeCount == 0)
{
throw new InvalidOperationException("Allocations can only be made within an active memory scope.");
}
#endif
if (size == 0)
{
return null;
}
if ((alignment & (alignment - 1)) != 0)
{
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
}
// 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;
}
var userPtr = _baseAddress + alignedOffset;
_allocatedOffset = newAllocatedOffset;
if (option.HasFlag(AllocationOption.Clear))
{
MemClear(userPtr, size);
}
return userPtr;
}
/// <summary>
/// Resets the internal offset to its initial position, keeping the committed physical memory intact for future reuse.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_allocatedOffset = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Free(void* ptr)
{
}
public void Dispose()
{
if (_baseAddress != null)
{
Munmap(_baseAddress, _reserveCapacity);
_baseAddress = null;
_reserveCapacity = 0;
_committedSize = 0;
_allocatedOffset = 0;
}
}
}