Added UnsafeMultiHashMap
This commit is contained in:
724
Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
Normal file
724
Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
Normal file
@@ -0,0 +1,724 @@
|
||||
using Misaki.HighPerformance.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
/// <summary>
|
||||
/// Holds information about a memory allocation.
|
||||
/// </summary>
|
||||
public readonly struct AllocationInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the size of the allocation in bytes.
|
||||
/// </summary>
|
||||
public nuint Size
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the stack trace at the time of allocation for debugging purposes.
|
||||
/// </summary>
|
||||
public StackTrace StackTrace
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides memory allocation management for native memory allocations, with support for tracking,
|
||||
/// debugging, and custom allocation strategies.
|
||||
/// </summary>
|
||||
public static unsafe class AllocationManager
|
||||
{
|
||||
// === Intrusive allocation tracking (enabled when debug layer is on) ===
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AllocationHeader
|
||||
{
|
||||
public AllocationHeader* prev;
|
||||
public AllocationHeader* next;
|
||||
public void* basePtr; // pointer returned by underlying allocator
|
||||
public nuint userSize; // requested size from the user
|
||||
public GCHandle stackHandle; // GCHandle to managed StackTrace
|
||||
}
|
||||
|
||||
private struct ArenaAllocator : IAllocator, IDisposable
|
||||
{
|
||||
private const int _ARENA_MAGIC_ID = -3941029;
|
||||
|
||||
private DynamicArena _arena;
|
||||
private AllocationHandle _handle;
|
||||
private int _currentTick;
|
||||
|
||||
public readonly AllocationHandle Handle => _handle;
|
||||
public readonly int CurrentTick => _currentTick;
|
||||
|
||||
public void Init(uint initialSize)
|
||||
{
|
||||
_arena = new DynamicArena(initialSize);
|
||||
_handle = new AllocationHandle
|
||||
{
|
||||
State = Unsafe.AsPointer(ref this),
|
||||
Alloc = &Allocate,
|
||||
Realloc = &Reallocate,
|
||||
Free = null,
|
||||
IsValid = &IsValid
|
||||
};
|
||||
_currentTick = 0;
|
||||
}
|
||||
|
||||
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
|
||||
{
|
||||
var selfPtr = (ArenaAllocator*)instance;
|
||||
var ptr = selfPtr->_arena.Allocate(size, alignment, allocationOption);
|
||||
if (ptr == null)
|
||||
{
|
||||
*pHandle = MemoryHandle.Invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
*pHandle = new MemoryHandle(_ARENA_MAGIC_ID, selfPtr->_currentTick);
|
||||
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 = (ArenaAllocator*)instance;
|
||||
var newPtr = selfPtr->_arena.Allocate(newSize, alignment, allocationOption);
|
||||
if (newPtr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
|
||||
|
||||
*pHandle = new MemoryHandle(_ARENA_MAGIC_ID, selfPtr->_currentTick);
|
||||
return newPtr;
|
||||
}
|
||||
|
||||
private static bool IsValid(void* instance, MemoryHandle handle)
|
||||
{
|
||||
var selfPtr = (ArenaAllocator*)instance;
|
||||
return handle.id == _ARENA_MAGIC_ID && handle.generation == selfPtr->_currentTick;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_arena.Reset();
|
||||
_currentTick++;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_arena.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private struct HeapAllocator : IAllocator
|
||||
{
|
||||
private AllocationHandle _handle;
|
||||
|
||||
public readonly AllocationHandle Handle => _handle;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_handle = new AllocationHandle
|
||||
{
|
||||
State = null,
|
||||
Alloc = &Allocate,
|
||||
Realloc = &Reallocate,
|
||||
Free = &Free,
|
||||
IsValid = &IsValid
|
||||
};
|
||||
}
|
||||
|
||||
private static void* Allocate(void* _, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
|
||||
{
|
||||
return HeapAlloc(size, alignment, allocationOption, pHandle);
|
||||
}
|
||||
|
||||
private static void* Reallocate(void* _, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
|
||||
{
|
||||
if (ptr == null)
|
||||
{
|
||||
return Allocate(null, newSize, alignment, allocationOption, pHandle);
|
||||
}
|
||||
|
||||
MemoryHandle newHandle;
|
||||
var newPtr = HeapAlloc(newSize, alignment, allocationOption, &newHandle);
|
||||
if (newPtr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
|
||||
HeapFree(ptr, *pHandle);
|
||||
|
||||
*pHandle = newHandle;
|
||||
return newPtr;
|
||||
}
|
||||
|
||||
private static void Free(void* _, void* ptr, MemoryHandle handle)
|
||||
{
|
||||
HeapFree(ptr, handle);
|
||||
}
|
||||
|
||||
private static bool IsValid(void* _, MemoryHandle handle)
|
||||
{
|
||||
return ContainsAllocation(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private struct StackAllocator : IAllocator, IDisposable
|
||||
{
|
||||
private const int _STACK_MAGIC_ID = -6843541;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Stack s_stack;
|
||||
private AllocationHandle _handle;
|
||||
|
||||
public readonly AllocationHandle Handle => _handle;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_handle = new AllocationHandle
|
||||
{
|
||||
State = Unsafe.AsPointer(ref this),
|
||||
Alloc = &Allocate,
|
||||
Realloc = &Reallocate,
|
||||
Free = null,
|
||||
IsValid = &IsValid
|
||||
};
|
||||
}
|
||||
|
||||
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
|
||||
{
|
||||
var ptr = s_stack.Allocate(size, alignment, allocationOption);
|
||||
if (ptr == null)
|
||||
{
|
||||
*pHandle = MemoryHandle.Invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
|
||||
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);
|
||||
}
|
||||
|
||||
// Optimize for last allocation. Set offset directly.
|
||||
var oldBase = s_stack.Buffer + s_stack.Offset - oldSize;
|
||||
if (ptr == oldBase)
|
||||
{
|
||||
if (newSize > oldSize)
|
||||
{
|
||||
var diff = newSize - oldSize;
|
||||
s_stack.Offset += diff;
|
||||
if (allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
MemClear(s_stack.Buffer + s_stack.Offset - diff, diff);
|
||||
}
|
||||
}
|
||||
|
||||
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
var newPtr = s_stack.Allocate(newSize, alignment, allocationOption);
|
||||
if (newPtr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
|
||||
|
||||
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
|
||||
return newPtr;
|
||||
}
|
||||
|
||||
private static bool IsValid(void* instance, MemoryHandle handle)
|
||||
{
|
||||
return handle.id == _STACK_MAGIC_ID && handle.generation <= (int)s_stack.Offset;
|
||||
}
|
||||
|
||||
public static Stack.Scope CreateScope(StackAllocator* pSelf)
|
||||
{
|
||||
return s_stack.CreateScope(pSelf->_handle);
|
||||
}
|
||||
|
||||
public readonly void Dispose()
|
||||
{
|
||||
Stack.DisposeAll();
|
||||
}
|
||||
}
|
||||
|
||||
private const uint _DEFAULT_MEMORY_POOL_SIZE = 1024 * 1024; // 1 MB
|
||||
|
||||
private static readonly ArenaAllocator* s_pArenaAllocator;
|
||||
private static readonly HeapAllocator* s_pHeapAllocator;
|
||||
private static readonly StackAllocator* s_pStackAllocator;
|
||||
|
||||
private static bool s_debugLayer;
|
||||
private static bool s_disposed;
|
||||
|
||||
private static AllocationHeader* s_pLiveHead;
|
||||
private static SpinLock s_liveLock;
|
||||
|
||||
private static readonly ConcurrentSlotMap<IntPtr> s_allocations;
|
||||
|
||||
public static readonly MemoryHandle MagicHandle = new MemoryHandle(int.MinValue, int.MinValue);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of live persistent heap allocations when the debug layer is disabled.
|
||||
/// </summary>
|
||||
public static int LiveAllocationCount => s_allocations.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the debug layer is currently enabled.
|
||||
/// </summary>
|
||||
public static bool IsDebugLayerEnabled => s_debugLayer;
|
||||
|
||||
|
||||
static AllocationManager()
|
||||
{
|
||||
var allocatorTotalSize = (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator));
|
||||
var basePtr = NativeMemory.Alloc(allocatorTotalSize);
|
||||
s_pArenaAllocator = (ArenaAllocator*)basePtr;
|
||||
s_pHeapAllocator = (HeapAllocator*)((byte*)basePtr + (nuint)sizeof(ArenaAllocator));
|
||||
s_pStackAllocator = (StackAllocator*)((byte*)basePtr + (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator)));
|
||||
|
||||
s_liveLock = new SpinLock(false);
|
||||
|
||||
s_allocations = new ConcurrentSlotMap<IntPtr>(256);
|
||||
|
||||
s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE);
|
||||
s_pHeapAllocator->Init();
|
||||
s_pStackAllocator->Init();
|
||||
|
||||
s_pLiveHead = null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static byte* AlignUp(byte* p, nuint alignment)
|
||||
{
|
||||
var a = alignment == 0 ? (nuint)IntPtr.Size : alignment;
|
||||
return (byte*)(((nuint)p + (a - 1)) & ~(a - 1));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static GCHandle HeaderGetHandle(AllocationHeader* header)
|
||||
{
|
||||
return header->stackHandle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void HeaderSetHandle(AllocationHeader* header, GCHandle handle)
|
||||
{
|
||||
header->stackHandle = handle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void HeaderFreeHandle(AllocationHeader* header)
|
||||
{
|
||||
if (header->stackHandle.IsAllocated)
|
||||
{
|
||||
header->stackHandle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void LinkHeader(AllocationHeader* header)
|
||||
{
|
||||
var taken = false;
|
||||
try
|
||||
{
|
||||
s_liveLock.Enter(ref taken);
|
||||
header->prev = null;
|
||||
header->next = s_pLiveHead;
|
||||
if (s_pLiveHead != null)
|
||||
{
|
||||
s_pLiveHead->prev = header;
|
||||
}
|
||||
s_pLiveHead = header;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (taken)
|
||||
{
|
||||
s_liveLock.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void UnlinkHeader(AllocationHeader* header)
|
||||
{
|
||||
var taken = false;
|
||||
try
|
||||
{
|
||||
s_liveLock.Enter(ref taken);
|
||||
var prev = header->prev;
|
||||
var next = header->next;
|
||||
|
||||
if (prev != null)
|
||||
{
|
||||
prev->next = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_pLiveHead = next;
|
||||
}
|
||||
|
||||
if (next != null)
|
||||
{
|
||||
next->prev = prev;
|
||||
}
|
||||
|
||||
header->prev = header->next = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (taken)
|
||||
{
|
||||
s_liveLock.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void* DebugAllocate(nuint size, nuint alignment)
|
||||
{
|
||||
// Over-allocate to fit header + alignment padding; we align the user pointer, header is placed just before it.
|
||||
var pad = alignment == 0 ? (nuint)IntPtr.Size : alignment;
|
||||
var total = size + (nuint)sizeof(AllocationHeader) + (pad - 1);
|
||||
|
||||
var basePtr = AlignedAlloc(total, pad);
|
||||
var user = AlignUp((byte*)basePtr + (nuint)sizeof(AllocationHeader), pad);
|
||||
var header = (AllocationHeader*)(user - (nuint)sizeof(AllocationHeader));
|
||||
|
||||
header->basePtr = basePtr;
|
||||
header->userSize = size;
|
||||
HeaderSetHandle(header, GCHandle.Alloc(new StackTrace(2, true)));
|
||||
|
||||
LinkHeader(header);
|
||||
return user;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void DebugFree(void* userPtr)
|
||||
{
|
||||
var header = (AllocationHeader*)((byte*)userPtr - (nuint)sizeof(AllocationHeader));
|
||||
UnlinkHeader(header);
|
||||
HeaderFreeHandle(header);
|
||||
AlignedFree(header->basePtr);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void* DebugReallocate(void* userPtr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
|
||||
{
|
||||
if (userPtr == null)
|
||||
{
|
||||
return DebugAllocate(newSize, alignment);
|
||||
}
|
||||
|
||||
var oldHeader = (AllocationHeader*)((byte*)userPtr - (nuint)sizeof(AllocationHeader));
|
||||
var handle = HeaderGetHandle(oldHeader); // preserve original allocation StackTrace
|
||||
|
||||
var pad = alignment == 0 ? (nuint)IntPtr.Size : alignment;
|
||||
var total = newSize + (nuint)sizeof(AllocationHeader) + (pad - 1);
|
||||
var newBase = AlignedAlloc(total, pad);
|
||||
var newUser = AlignUp((byte*)newBase + (nuint)sizeof(AllocationHeader), pad);
|
||||
var newHeader = (AllocationHeader*)(newUser - (nuint)sizeof(AllocationHeader));
|
||||
|
||||
newHeader->basePtr = newBase;
|
||||
newHeader->userSize = newSize;
|
||||
HeaderSetHandle(newHeader, handle); // transfer ownership to the new header
|
||||
|
||||
LinkHeader(newHeader);
|
||||
|
||||
// Mirror original behavior: copy newSize bytes
|
||||
MemCpy(newUser, userPtr, newSize);
|
||||
if (allocationOption.HasFlag(AllocationOption.Clear) && newSize > oldSize)
|
||||
{
|
||||
MemClear(newUser + oldSize, newSize - oldSize);
|
||||
}
|
||||
|
||||
// Unlink and free the old block (without freeing the StackTrace pHandle again)
|
||||
//oldHeader->stackHandle = GCHandle.FromIntPtr(0);
|
||||
UnlinkHeader(oldHeader);
|
||||
AlignedFree(oldHeader->basePtr);
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the debug layer, allowing additional diagnostic information to be collected.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void EnableDebugLayer()
|
||||
{
|
||||
// To avoid ambiguity between pointers allocated before/after enabling, this must be called
|
||||
// before any heap allocations are live.
|
||||
if (s_allocations.Count != 0)
|
||||
{
|
||||
throw new InvalidOperationException("EnableDebugLayer must be called before any allocations are active.");
|
||||
}
|
||||
|
||||
s_debugLayer = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a reference to the allocation pHandle for the specified allocator type.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator type for which to retrieve the allocation pHandle.</param>
|
||||
/// <returns>A reference to the allocation pHandle associated with the specified allocator type.</returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AllocationHandle GetAllocationHandle(Allocator allocator)
|
||||
{
|
||||
return allocator switch
|
||||
{
|
||||
Allocator.Temp => s_pArenaAllocator->Handle,
|
||||
Allocator.Persistent => s_pHeapAllocator->Handle,
|
||||
_ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of memory from the heap with the specified size and alignment, using the given allocation options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will allocate memory from the heap. If the debug layer is enabled, additional tracking information will be recorded.
|
||||
/// The memory handle is always tracked unless the <see cref="AllocationOption.Untrack"/> flag is specified.
|
||||
/// </remarks>
|
||||
/// <param name="size">The number of bytes to allocate. Must be greater than zero.</param>
|
||||
/// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two.</param>
|
||||
/// <param name="allocationOption">An optional set of flags that control allocation behavior, such as whether the memory should be cleared or
|
||||
/// tracked. The default is <see cref="AllocationOption.None"/>.</param>
|
||||
/// <returns>A pointer to the beginning of the allocated memory block.</returns>
|
||||
/// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception>
|
||||
public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
|
||||
{
|
||||
var isUntrack = allocationOption.HasFlag(AllocationOption.Untrack);
|
||||
|
||||
void* ptr;
|
||||
if (s_debugLayer && !isUntrack)
|
||||
{
|
||||
ptr = DebugAllocate(size, alignment);
|
||||
}
|
||||
else
|
||||
{
|
||||
ptr = AlignedAlloc(size, alignment);
|
||||
}
|
||||
|
||||
if (ptr == null)
|
||||
{
|
||||
*pHandle = MemoryHandle.Invalid;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
MemClear(ptr, size);
|
||||
}
|
||||
|
||||
if (isUntrack)
|
||||
{
|
||||
*pHandle = MagicHandle;
|
||||
}
|
||||
else
|
||||
{
|
||||
*pHandle = AddAllocation((IntPtr)ptr);
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases a block of unmanaged memory previously allocated by the heap allocator.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the memory block to be freed. The pointer must have been returned by a compatible heap allocation
|
||||
/// method and must not be null.</param>
|
||||
/// <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)
|
||||
{
|
||||
if (s_debugLayer && handle != MagicHandle)
|
||||
{
|
||||
DebugFree(ptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
AlignedFree(ptr);
|
||||
}
|
||||
|
||||
RemoveAllocation(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases a block of unmanaged memory previously allocated by the heap allocator.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
if (TryGetAllocation(handle, out var ptr))
|
||||
{
|
||||
HeapFree((void*)ptr, handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the temporary memory allocator, clearing all allocated memory.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ResetTempAllocator()
|
||||
{
|
||||
s_pArenaAllocator->Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new thread local stack scope for managing temporary allocations within the current context.
|
||||
/// </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()
|
||||
{
|
||||
return StackAllocator.CreateScope(s_pStackAllocator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a memory allocation and returns a handle that can be used to manage or reference the allocated memory.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
var id = s_allocations.Add(ptr, out var generation);
|
||||
return new MemoryHandle(id, generation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the memory allocation associated with the specified handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle representing the memory allocation to remove. The handle must be valid and previously allocated.</param>
|
||||
/// <returns>true if the allocation was successfully removed; otherwise, false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool RemoveAllocation(MemoryHandle handle)
|
||||
{
|
||||
return s_allocations.Remove(handle.id, handle.generation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the memory allocation pointer associated with the specified handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The memory handle identifying the allocation to retrieve allocation.</param>
|
||||
/// <param name="ptr">When this method returns, contains the pointer to the memory allocation if found; otherwise, <see cref="IntPtr.Zero"/>.</param>
|
||||
/// <returns>true if the allocation was found and <paramref name="ptr"/> contains a valid pointer; otherwise, false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool TryGetAllocation(MemoryHandle handle, out IntPtr ptr)
|
||||
{
|
||||
return s_allocations.TryGetElement(handle.id, handle.generation, out ptr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified memory handle refers to a currently tracked allocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This only validates the memory when you added the allocation via <see cref="AddAllocation(IntPtr)"/>.
|
||||
/// For validating memory from <see cref="AllocationHandle"/>, use <see cref="AllocationHandle.IsValid"/> instead.
|
||||
/// </remarks>
|
||||
/// <param name="handle">The memory handle to check for an associated allocation.</param>
|
||||
/// <returns>true if the allocation corresponding to the handle exists; otherwise, false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool ContainsAllocation(MemoryHandle handle)
|
||||
{
|
||||
if (handle == MagicHandle)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return s_allocations.Contains(handle.id, handle.generation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the AllocationManager, freeing all allocated memory and resources.
|
||||
/// </summary>
|
||||
public static void Dispose()
|
||||
{
|
||||
if (s_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// In debug mode, walk the intrusive list to surface any leaks.
|
||||
if (s_debugLayer)
|
||||
{
|
||||
var snapshot = new List<AllocationInfo>();
|
||||
var taken = false;
|
||||
try
|
||||
{
|
||||
s_liveLock.Enter(ref taken);
|
||||
if (s_pLiveHead != null)
|
||||
{
|
||||
snapshot.Capacity = 128;
|
||||
for (var p = s_pLiveHead; p != null; p = p->next)
|
||||
{
|
||||
var trace = (StackTrace)HeaderGetHandle(p).Target!;
|
||||
snapshot.Add(new AllocationInfo
|
||||
{
|
||||
Size = p->userSize,
|
||||
StackTrace = trace
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (taken)
|
||||
{
|
||||
s_liveLock.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
nuint unfreeBytes = 0u;
|
||||
foreach (var info in snapshot)
|
||||
{
|
||||
unfreeBytes += info.Size;
|
||||
}
|
||||
|
||||
if (unfreeBytes > 0u)
|
||||
{
|
||||
throw new MemoryLeakException(CollectionsMarshal.AsSpan(snapshot));
|
||||
}
|
||||
|
||||
Debug.Assert(LiveAllocationCount == 0);
|
||||
}
|
||||
else if (LiveAllocationCount != 0)
|
||||
{
|
||||
throw new MemoryLeakException($"Found {LiveAllocationCount} memory lakes! Please enable debug layer for more informations.");
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
NativeMemory.Free(s_pArenaAllocator);
|
||||
}
|
||||
|
||||
s_disposed = true;
|
||||
}
|
||||
}
|
||||
32
Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs
Normal file
32
Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
[Flags]
|
||||
public enum AllocationOption : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Default allocation option. Values are uninitialized.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// Clear the memory to zero upon allocation.
|
||||
/// </summary>
|
||||
Clear = 1 << 0,
|
||||
/// <summary>
|
||||
/// Specify that this memory allocation should not been tracked competly, which <see cref="AllocationManager"/> will not perform any safty check like use after free and leack detection.
|
||||
/// </summary>
|
||||
Untrack = 1 << 1,
|
||||
}
|
||||
|
||||
public enum Allocator : byte
|
||||
{
|
||||
// Make the first allocator as invalid because we don't want to user create a default collection without passing any parameters
|
||||
Invalid,
|
||||
/// <summary>
|
||||
/// Allocator for temporary allocations. Allocations are automatically released after use automatically.
|
||||
/// </summary>
|
||||
Temp,
|
||||
/// <summary>
|
||||
/// Allocator for persistent allocations. Allocations are not automatically released after use.
|
||||
/// </summary>
|
||||
Persistent,
|
||||
}
|
||||
109
Misaki.HighPerformance.LowLevel/Buffer/Arena.cs
Normal file
109
Misaki.HighPerformance.LowLevel/Buffer/Arena.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
/// <summary>
|
||||
/// 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 : IDisposable
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
private byte* _buffer;
|
||||
[FieldOffset(8)]
|
||||
private nuint _size;
|
||||
[FieldOffset(16)]
|
||||
private nuint _offset;
|
||||
|
||||
public Arena(nuint size)
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_buffer = (byte*)Malloc(size);
|
||||
_size = size;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A pointer to the allocated memory block or null if the allocation cannot be fulfilled.</returns>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
|
||||
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption)
|
||||
{
|
||||
if (_buffer == null)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(Arena));
|
||||
}
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((alignment & (alignment - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
|
||||
}
|
||||
|
||||
nuint currentOffset, newOffset, alignedOffset;
|
||||
|
||||
do
|
||||
{
|
||||
currentOffset = _offset;
|
||||
alignedOffset = (currentOffset + alignment - 1) & ~(alignment - 1);
|
||||
newOffset = alignedOffset + size;
|
||||
|
||||
if (newOffset > _size)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
} while (Interlocked.CompareExchange(ref _offset, newOffset, currentOffset) != currentOffset);
|
||||
|
||||
var ptr = _buffer + alignedOffset;
|
||||
if (allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
MemClear(ptr, size);
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the arena, optionally clearing the allocated memory.
|
||||
/// </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>
|
||||
public void Reset()
|
||||
{
|
||||
if (_buffer == null)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(DynamicArena));
|
||||
}
|
||||
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Free(_buffer);
|
||||
|
||||
_buffer = null;
|
||||
_size = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
}
|
||||
170
Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs
Normal file
170
Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
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.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 128)]
|
||||
public unsafe struct DynamicArena : IDisposable
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct ArenaNode
|
||||
{
|
||||
public Arena arena;
|
||||
public ArenaNode* next;
|
||||
}
|
||||
|
||||
[FieldOffset(0)]
|
||||
private ArenaNode* _root;
|
||||
[FieldOffset(8)]
|
||||
private ArenaNode* _current;
|
||||
[FieldOffset(16)]
|
||||
private uint _initialSize;
|
||||
|
||||
[FieldOffset(20)]
|
||||
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)
|
||||
{
|
||||
Initialize(initialSize);
|
||||
}
|
||||
|
||||
public void Initialize(uint initialSize)
|
||||
{
|
||||
if (_root != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_initialSize = initialSize;
|
||||
_root = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
|
||||
_root->arena = new Arena(initialSize);
|
||||
_root->next = null;
|
||||
_current = _root;
|
||||
|
||||
_nodeCreationLock = 0;
|
||||
}
|
||||
|
||||
private bool TryCreateNewNode(nuint size)
|
||||
{
|
||||
while (Interlocked.CompareExchange(ref _nodeCreationLock, 1, 0) != 0)
|
||||
{
|
||||
Thread.SpinWait(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var current = _current;
|
||||
if (current->next != null)
|
||||
{
|
||||
// Another thread created a node while we were waiting
|
||||
_current = current->next;
|
||||
return true;
|
||||
}
|
||||
|
||||
var newNode = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
|
||||
try
|
||||
{
|
||||
newNode->arena = new Arena(size);
|
||||
newNode->next = null;
|
||||
|
||||
// Atomically link the new node
|
||||
current->next = newNode;
|
||||
|
||||
// Update current pointer
|
||||
_current = newNode;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Free(newNode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the spinlock
|
||||
Interlocked.Exchange(ref _nodeCreationLock, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of memory with specified size and alignment. Creates a new arena if current one is full.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
|
||||
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption)
|
||||
{
|
||||
if (_root == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
void* result = null;
|
||||
var current = _current;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
result = current->arena.Allocate(size, alignment, allocationOption);
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (current->next == null && !TryCreateNewNode(Math.Max(size, _initialSize)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
_current = current;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all arenas in the chain, optionally clearing their memory.
|
||||
/// </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;
|
||||
while (current != null)
|
||||
{
|
||||
current->arena.Reset();
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
_current = _root;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var current = _root;
|
||||
while (current != null)
|
||||
{
|
||||
var next = current->next;
|
||||
current->arena.Dispose();
|
||||
Free(current);
|
||||
current = next;
|
||||
}
|
||||
|
||||
_root = null;
|
||||
_current = null;
|
||||
}
|
||||
}
|
||||
490
Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs
Normal file
490
Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs
Normal file
@@ -0,0 +1,490 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
/// <summary>
|
||||
/// A lock-free, thread-safe variable-size allocator that manages memory blocks of different sizes.
|
||||
/// Optimized for high-performance scenarios with frequent allocations and deallocations.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = 256)] // Cache line aligned to prevent false sharing
|
||||
public unsafe struct FreeList : IDisposable
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct FreeNode
|
||||
{
|
||||
public FreeNode* next;
|
||||
public nuint size;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MemoryChunk
|
||||
{
|
||||
public MemoryChunk* next;
|
||||
public byte* memory;
|
||||
public nuint size;
|
||||
public nuint used; // Amount of memory used in this chunk
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 32)]
|
||||
private struct SizeBucket
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public long freeCount; // Number of free blocks
|
||||
[FieldOffset(8)]
|
||||
public nint freeHead; // Free list head for this size
|
||||
[FieldOffset(16)]
|
||||
public nuint blockSize; // Fixed size for this bucket
|
||||
[FieldOffset(24)]
|
||||
public int creationLock;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 24)]
|
||||
private struct BlockHeader
|
||||
{
|
||||
// Ensure the size is fixed across x86 and x64
|
||||
[FieldOffset(0)]
|
||||
public MemoryChunk* ownerChunk;
|
||||
[FieldOffset(8)]
|
||||
public nuint blockSize;
|
||||
[FieldOffset(16)]
|
||||
public ulong magicNumber;
|
||||
}
|
||||
|
||||
private const int _MAX_BUCKETS = 16; // Number of size buckets
|
||||
private const nuint _MIN_BLOCK_SIZE = 16; // Minimum block size
|
||||
private const nuint _DEFAULT_CHUNK_SIZE = 64 * 1024; // 64KB chunks
|
||||
private const ulong _MAGIC_NUMBER = 0xDEADBEEFDEADBEEF; // For validating blocks
|
||||
|
||||
[FieldOffset(0)]
|
||||
private fixed byte _buckets[_MAX_BUCKETS * 32]; // SizeBucket array (32 bytes per bucket)
|
||||
|
||||
[FieldOffset(512)]
|
||||
private DynamicArena _chunkArena; // 128
|
||||
|
||||
[FieldOffset(640)]
|
||||
private MemoryChunk* _chunks; // 8
|
||||
|
||||
[FieldOffset(648)]
|
||||
private readonly nuint _chunkSize; // 8
|
||||
|
||||
[FieldOffset(656)]
|
||||
private readonly nuint _alignment; // 8
|
||||
|
||||
[FieldOffset(664)]
|
||||
private long _totalAllocatedBytes; // 8
|
||||
|
||||
[FieldOffset(672)]
|
||||
private long _totalFreeBytes; // 8
|
||||
|
||||
[FieldOffset(676)]
|
||||
private volatile int _disposed; // 4
|
||||
|
||||
[FieldOffset(680)]
|
||||
private volatile int _chunkCreationLock; // 4
|
||||
|
||||
/// <summary>
|
||||
/// Gets the alignment requirement for allocations.
|
||||
/// </summary>
|
||||
public readonly nuint Alignment => _alignment;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of allocated bytes.
|
||||
/// </summary>
|
||||
public readonly long TotalAllocatedBytes => Interlocked.Read(ref Unsafe.AsRef(in _totalAllocatedBytes));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of free bytes available.
|
||||
/// </summary>
|
||||
public readonly long TotalFreeBytes => Interlocked.Read(ref Unsafe.AsRef(in _totalFreeBytes));
|
||||
|
||||
/// <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>
|
||||
public readonly nuint ChunkSize => _chunkSize;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new variable-size FreeList allocator with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="alignment">Alignment requirement for blocks (must be power of 2).</param>
|
||||
/// <param name="chunkSize">Size of memory chunks to allocate (default: 64KB).</param>
|
||||
public FreeList(nuint alignment, nuint chunkSize = _DEFAULT_CHUNK_SIZE)
|
||||
{
|
||||
if (alignment == 0 || (alignment & (alignment - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentException("Alignment must be a power of 2", nameof(alignment));
|
||||
}
|
||||
|
||||
if (chunkSize < 1024)
|
||||
{
|
||||
throw new ArgumentException("Chunk size must be at least 1KB", nameof(chunkSize));
|
||||
}
|
||||
|
||||
_alignment = alignment;
|
||||
_chunkSize = chunkSize;
|
||||
_chunks = null;
|
||||
_totalAllocatedBytes = 0;
|
||||
_totalFreeBytes = 0;
|
||||
_disposed = 0;
|
||||
_chunkCreationLock = 0;
|
||||
|
||||
_chunkArena = new DynamicArena(1024);
|
||||
InitializeBuckets();
|
||||
}
|
||||
|
||||
private readonly void InitializeBuckets()
|
||||
{
|
||||
var buckets = GetBuckets();
|
||||
var size = _MIN_BLOCK_SIZE;
|
||||
|
||||
for (var i = 0; i < _MAX_BUCKETS; i++)
|
||||
{
|
||||
buckets[i].blockSize = size;
|
||||
buckets[i].freeHead = 0;
|
||||
buckets[i].freeCount = 0;
|
||||
size *= 2; // Exponential size increase
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly SizeBucket* GetBuckets()
|
||||
{
|
||||
fixed (byte* ptr = _buckets)
|
||||
{
|
||||
return (SizeBucket*)ptr;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly int FindBucket(nuint size)
|
||||
{
|
||||
var buckets = GetBuckets();
|
||||
|
||||
for (var i = 0; i < _MAX_BUCKETS; i++)
|
||||
{
|
||||
if (size <= buckets[i].blockSize)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // Size too large for buckets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a memory block of the specified size. Thread-safe using lock-free algorithms.
|
||||
/// </summary>
|
||||
/// <param name="size">Size of memory to allocate in bytes.</param>
|
||||
/// <param name="alignment">Alignment requirement (0 = use default).</param>
|
||||
/// <param name="allocationOption">Options for allocation (e.g., clear memory).</param>
|
||||
/// <returns>MemoryBlock containing allocated memory, or Invalid if allocation fails.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
if (_disposed != 0)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(FreeList));
|
||||
}
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (alignment == 0)
|
||||
{
|
||||
alignment = _alignment;
|
||||
}
|
||||
|
||||
if ((alignment & (alignment - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
|
||||
}
|
||||
|
||||
// Align size to alignment boundary
|
||||
var alignedSize = (size + alignment - 1) & ~(alignment - 1);
|
||||
alignedSize = Math.Max(alignedSize, _MIN_BLOCK_SIZE);
|
||||
|
||||
var totalSize = alignedSize + (nuint)sizeof(BlockHeader);
|
||||
|
||||
var bucketIndex = FindBucket(totalSize);
|
||||
void* ptr = null;
|
||||
|
||||
if (bucketIndex >= 0)
|
||||
{
|
||||
// Try to allocate from bucket
|
||||
ptr = TryPopFromBucket(bucketIndex);
|
||||
|
||||
if (ptr == null)
|
||||
{
|
||||
// Create new blocks for this bucket
|
||||
if (TryCreateBlocksForBucket(bucketIndex))
|
||||
{
|
||||
ptr = TryPopFromBucket(bucketIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ptr == null)
|
||||
{
|
||||
// Fallback to direct allocation from chunk
|
||||
ptr = AllocateFromChunk(totalSize, alignment);
|
||||
}
|
||||
|
||||
if (ptr != null)
|
||||
{
|
||||
var header = (BlockHeader*)ptr;
|
||||
Interlocked.Add(ref _totalAllocatedBytes, (long)header->blockSize);
|
||||
|
||||
var pUserData = (byte*)ptr + sizeof(BlockHeader);
|
||||
if (allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
MemClear(pUserData, alignedSize);
|
||||
}
|
||||
|
||||
return pUserData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees a previously allocated memory block. Thread-safe using lock-free algorithms.
|
||||
/// </summary>
|
||||
/// <param name="block">MemoryBlock to free.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Free(void* ptr)
|
||||
{
|
||||
if (_disposed != 0 || ptr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var blockStartPtr = (byte*)ptr - sizeof(BlockHeader);
|
||||
var header = (BlockHeader*)blockStartPtr;
|
||||
|
||||
if (header->magicNumber != _MAGIC_NUMBER)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chuck = header->ownerChunk;
|
||||
if (chuck == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bucketIndex = FindBucket(header->blockSize);
|
||||
if (bucketIndex >= 0)
|
||||
{
|
||||
PushToBucket(bucketIndex, blockStartPtr, header->blockSize);
|
||||
}
|
||||
|
||||
Interlocked.Add(ref _totalAllocatedBytes, -(long)header->blockSize);
|
||||
header->ownerChunk = null;
|
||||
header->blockSize = 0;
|
||||
header->magicNumber = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly void* TryPopFromBucket(int bucketIndex)
|
||||
{
|
||||
var buckets = GetBuckets();
|
||||
var bucket = &buckets[bucketIndex];
|
||||
|
||||
nint head, newHead;
|
||||
FreeNode* headPtr;
|
||||
|
||||
do
|
||||
{
|
||||
head = bucket->freeHead;
|
||||
if (head == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
headPtr = (FreeNode*)head;
|
||||
newHead = (nint)headPtr->next;
|
||||
|
||||
} while (Interlocked.CompareExchange(ref bucket->freeHead, newHead, head) != head);
|
||||
|
||||
Interlocked.Decrement(ref bucket->freeCount);
|
||||
return (void*)head;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly void PushToBucket(int bucketIndex, void* ptr, nuint size)
|
||||
{
|
||||
var buckets = GetBuckets();
|
||||
var bucket = &buckets[bucketIndex];
|
||||
var node = (FreeNode*)ptr;
|
||||
|
||||
node->size = size;
|
||||
|
||||
nint head;
|
||||
do
|
||||
{
|
||||
head = bucket->freeHead;
|
||||
node->next = (FreeNode*)head;
|
||||
|
||||
} while (Interlocked.CompareExchange(ref bucket->freeHead, (nint)node, head) != head);
|
||||
|
||||
Interlocked.Increment(ref bucket->freeCount);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void AssignBlockHeader(BlockHeader* header, MemoryChunk* ownerChunk, nuint blockSize)
|
||||
{
|
||||
header->ownerChunk = ownerChunk;
|
||||
header->blockSize = blockSize;
|
||||
header->magicNumber = _MAGIC_NUMBER;
|
||||
}
|
||||
|
||||
private bool TryCreateBlocksForBucket(int bucketIndex)
|
||||
{
|
||||
var buckets = GetBuckets();
|
||||
var bucket = &buckets[bucketIndex];
|
||||
|
||||
while (Interlocked.CompareExchange(ref bucket->creationLock, 1, 0) != 0)
|
||||
{
|
||||
Thread.SpinWait(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (bucket->freeHead != 0)
|
||||
{
|
||||
return true; // Another thread did the work for us!
|
||||
}
|
||||
|
||||
var blockSize = bucket->blockSize;
|
||||
var blocksToCreate = Math.Min(_chunkSize / blockSize, 256); // Limit number of blocks
|
||||
|
||||
if (blocksToCreate == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var totalSize = blocksToCreate * blockSize;
|
||||
var memory = (byte*)AlignedAlloc(totalSize, _alignment);
|
||||
if (memory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var chunk = (MemoryChunk*)_chunkArena.Allocate(SizeOf<MemoryChunk>(), AlignOf<MemoryChunk>(), AllocationOption.None);
|
||||
if (chunk == null)
|
||||
{
|
||||
AlignedFree(memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
chunk->memory = memory;
|
||||
chunk->size = totalSize;
|
||||
chunk->used = totalSize;
|
||||
chunk->next = _chunks;
|
||||
_chunks = chunk;
|
||||
|
||||
// Add all blocks to the bucket's free list
|
||||
for (nuint i = 0; i < blocksToCreate; i++)
|
||||
{
|
||||
var blockStartPtr = memory + (i * blockSize);
|
||||
|
||||
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, blockSize);
|
||||
PushToBucket(bucketIndex, blockStartPtr, blockSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref bucket->creationLock, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void* AllocateFromChunk(nuint size, nuint alignment)
|
||||
{
|
||||
while (Interlocked.CompareExchange(ref _chunkCreationLock, 1, 0) != 0)
|
||||
{
|
||||
Thread.SpinWait(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to find space in existing chunks first
|
||||
var chunk = _chunks;
|
||||
while (chunk != null)
|
||||
{
|
||||
var available = chunk->size - chunk->used;
|
||||
var alignedOffset = (chunk->used + alignment - 1) & ~(alignment - 1);
|
||||
var totalNeeded = alignedOffset - chunk->used + size;
|
||||
|
||||
if (totalNeeded <= available)
|
||||
{
|
||||
var blockStartPtr = chunk->memory + alignedOffset;
|
||||
|
||||
// Write the header and return the pointer WITH the header
|
||||
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, size);
|
||||
return blockStartPtr;
|
||||
}
|
||||
|
||||
chunk = chunk->next;
|
||||
}
|
||||
|
||||
// Create new chunk
|
||||
var newChunkSize = Math.Max(_chunkSize, size + alignment);
|
||||
var newMemory = (byte*)AlignedAlloc(newChunkSize, alignment);
|
||||
if (newMemory == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var newChunk = (MemoryChunk*)_chunkArena.Allocate(SizeOf<MemoryChunk>(), AlignOf<MemoryChunk>(), AllocationOption.None);
|
||||
if (newChunk == null)
|
||||
{
|
||||
AlignedFree(newMemory);
|
||||
return null;
|
||||
}
|
||||
|
||||
newChunk->memory = newMemory;
|
||||
newChunk->size = newChunkSize;
|
||||
newChunk->used = size;
|
||||
newChunk->next = _chunks;
|
||||
_chunks = newChunk;
|
||||
|
||||
// Write the header and return the pointer WITH the header
|
||||
AssignBlockHeader((BlockHeader*)newMemory, newChunk, size);
|
||||
return newMemory;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _chunkCreationLock, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
|
||||
{
|
||||
// Free all memory chunks
|
||||
var chunk = _chunks;
|
||||
while (chunk != null)
|
||||
{
|
||||
var next = chunk->next;
|
||||
AlignedFree(chunk->memory);
|
||||
chunk = next;
|
||||
}
|
||||
|
||||
_chunkArena.Dispose();
|
||||
|
||||
_chunks = null;
|
||||
_totalAllocatedBytes = 0;
|
||||
_totalFreeBytes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Misaki.HighPerformance.LowLevel/Buffer/IAllocator.cs
Normal file
111
Misaki.HighPerformance.LowLevel/Buffer/IAllocator.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
public readonly struct MemoryHandle : IEquatable<MemoryHandle>
|
||||
{
|
||||
public readonly int id;
|
||||
public readonly int generation;
|
||||
|
||||
public readonly static MemoryHandle Invalid = new(-1, -1);
|
||||
|
||||
public MemoryHandle(int id, int generation)
|
||||
{
|
||||
this.id = id;
|
||||
this.generation = generation;
|
||||
}
|
||||
|
||||
public bool Equals(MemoryHandle other)
|
||||
{
|
||||
return id == other.id && generation == other.generation;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is MemoryHandle other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return id ^ generation;
|
||||
}
|
||||
|
||||
public override string? ToString()
|
||||
{
|
||||
return $"MemoryHandle(Id: {id}, Generation: {generation})";
|
||||
}
|
||||
|
||||
public static bool operator ==(MemoryHandle left, MemoryHandle right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(MemoryHandle left, MemoryHandle right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A structure that encapsulates function pointers for memory allocation operations.
|
||||
/// </summary>
|
||||
public readonly unsafe struct AllocationHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a pointer to the state instance associated with this allocation handle.
|
||||
/// </summary>
|
||||
public void* State
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a function pointer for allocating memory.
|
||||
/// </summary>
|
||||
public AllocFunc Alloc
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a function pointer for reallocating memory.
|
||||
/// </summary>
|
||||
public ReallocFunc Realloc
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a function pointer for freeing allocated memory.
|
||||
/// </summary>
|
||||
public FreeFunc Free
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a function pointer for validating a memory handle.
|
||||
/// </summary>
|
||||
public IsValidFunc IsValid
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an state interface for managing memory allocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The state must be pined to a specific memory region.
|
||||
/// Otherwise the reference of the <see cref="AllocationHandle.State"/>, may become invalid and lead to undefined behavior.
|
||||
/// </remarks>
|
||||
public interface IAllocator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a reference to the allocation handle associated with this state.
|
||||
/// </summary>
|
||||
AllocationHandle Handle
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
77
Misaki.HighPerformance.LowLevel/Buffer/MemoryBlock.cs
Normal file
77
Misaki.HighPerformance.LowLevel/Buffer/MemoryBlock.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an allocated memory block with metadata.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly unsafe struct MemoryBlock
|
||||
{
|
||||
/// <summary>
|
||||
/// Pointer to the actual allocated memory.
|
||||
/// </summary>
|
||||
public void* Ptr
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The heap from which the memory was allocated.
|
||||
/// </summary>
|
||||
public void* Heap
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size of the allocated memory in bytes.
|
||||
/// </summary>
|
||||
public nuint Size
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alignment of the allocated memory.
|
||||
/// </summary>
|
||||
public nuint Alignment
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this memory block is valid.
|
||||
/// </summary>
|
||||
public readonly bool IsValid => Ptr != null && Size > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new MemoryBlock with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Pointer to the allocated memory.</param>
|
||||
/// <param name="size">Size of the allocated memory.</param>
|
||||
/// <param name="alignment">Alignment of the allocated memory.</param>
|
||||
public MemoryBlock(void* ptr, void* heap, nuint size, nuint alignment)
|
||||
{
|
||||
Ptr = ptr;
|
||||
Heap = heap;
|
||||
Size = size;
|
||||
Alignment = alignment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invalid MemoryBlock.
|
||||
/// </summary>
|
||||
public static MemoryBlock Invalid => new(null, null, 0, 0);
|
||||
|
||||
public Span<T> AsSpan<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
if (!IsValid)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create span from invalid MemoryBlock.");
|
||||
}
|
||||
|
||||
return new Span<T>(Ptr, (int)(Size / SizeOf<T>()));
|
||||
}
|
||||
}
|
||||
223
Misaki.HighPerformance.LowLevel/Buffer/Stack.cs
Normal file
223
Misaki.HighPerformance.LowLevel/Buffer/Stack.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
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++)
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
/// <remarks>This is not a thread-safe implementation.</remarks>
|
||||
public unsafe partial struct Stack : IDisposable
|
||||
{
|
||||
private const nuint _DEFAULT_SIZE = 1024 * 1024; // 1MB
|
||||
|
||||
public readonly ref struct Scope : IDisposable
|
||||
{
|
||||
private readonly Stack* _allocator;
|
||||
private readonly AllocationHandle _handle;
|
||||
private readonly nuint _originalOffset;
|
||||
|
||||
public readonly AllocationHandle AllocationHandle => _handle;
|
||||
|
||||
internal Scope(Stack* allocator, AllocationHandle handle)
|
||||
{
|
||||
_allocator = allocator;
|
||||
_handle = handle;
|
||||
_originalOffset = allocator->_offset;
|
||||
_allocator->_activeScopeCount++;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_allocator != null)
|
||||
{
|
||||
_allocator->_offset = _allocator->_offset > _originalOffset ? _originalOffset : _allocator->_offset;
|
||||
_allocator->_activeScopeCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte* _buffer;
|
||||
private nuint _size;
|
||||
private nuint _offset;
|
||||
private uint _activeScopeCount;
|
||||
|
||||
internal readonly byte* Buffer => _buffer;
|
||||
|
||||
public nuint Offset
|
||||
{
|
||||
readonly get => _offset;
|
||||
internal set => _offset = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the StackAllocator class with a buffer of the specified size.
|
||||
/// </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)
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
Free(_buffer);
|
||||
}
|
||||
|
||||
_buffer = (byte*)Malloc(size);
|
||||
_size = size;
|
||||
_offset = 0;
|
||||
_activeScopeCount = 0;
|
||||
|
||||
var token = false;
|
||||
try
|
||||
{
|
||||
s_locker.Enter(ref token);
|
||||
if (s_pStackBuffers == null)
|
||||
{
|
||||
s_pStackBuffers = (void**)Malloc((nuint)sizeof(void*) * 4u);
|
||||
s_stackCapacity = 4;
|
||||
}
|
||||
|
||||
if (s_stackCount >= s_stackCapacity)
|
||||
{
|
||||
var pOld = s_pStackBuffers;
|
||||
var newCapacity = s_stackCapacity * 2;
|
||||
var pNew = (void**)Realloc(pOld, (nuint)sizeof(void*) * (nuint)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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scope instance associated with the current stack context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The instance of <see cref="Stack"/> 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)
|
||||
{
|
||||
EnsureInitialize();
|
||||
return new Scope((Stack*)Unsafe.AsPointer(ref this), handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of memory of the specified size and alignment from the buffer.
|
||||
/// </summary>
|
||||
/// <param name="size">The number of bytes to allocate. Must be greater than zero and less than or equal to the remaining buffer size.</param>
|
||||
/// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two and greater than zero.</param>
|
||||
/// <param name="allocationOption">An option specifying additional allocation behavior, such as whether the allocated memory should be cleared. The
|
||||
/// default is <see cref="AllocationOption.None"/>.</param>
|
||||
/// <returns>A pointer to the beginning of the allocated memory block if successful; otherwise, <see langword="null"/> if
|
||||
/// there is insufficient space in the buffer.</returns>
|
||||
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
ThrowIfNoScope();
|
||||
|
||||
if (size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((alignment & (alignment - 1)) != 0)
|
||||
{
|
||||
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
|
||||
}
|
||||
|
||||
var alignedOffset = (_offset + alignment - 1) & ~(alignment - 1);
|
||||
|
||||
var newOffset = alignedOffset + size;
|
||||
|
||||
if (newOffset > _size)
|
||||
{
|
||||
throw new OutOfMemoryException("Insufficient memory in stack allocator.");
|
||||
}
|
||||
|
||||
var ptr = _buffer + alignedOffset;
|
||||
|
||||
_offset = newOffset;
|
||||
|
||||
if (allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
MemClear(ptr, size);
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the internal offset to its initial position.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Free(_buffer);
|
||||
|
||||
_buffer = null;
|
||||
_size = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user