Replaced the deprecated Allocator API with the new AllocationHandle API across the codebase. Updated constructors, methods, and tests to use AllocationHandle for memory management. Marked Allocator-based methods as [Obsolete] and provided alternatives. Added OwnershipTransferAnalyzer to detect ownership transfer issues and introduced OwnershipTransferAttribute for marking parameters. Enhanced DefensiveCopyAnalyzer with additional checks for readonly and ValueType instances. Refactored internal memory management in AllocationManager and updated benchmarks, utilities, and documentation to reflect the changes. BREAKING CHANGE: Deprecated Allocator API in favor of AllocationHandle. Updated constructors and methods to use AllocationHandle. Users must migrate to the new API.
483 lines
15 KiB
C#
483 lines
15 KiB
C#
#if MHP_ENABLE_SAFETY_CHECKS
|
|
using Misaki.HighPerformance.Collections;
|
|
#endif
|
|
using System.Diagnostics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.Versioning;
|
|
|
|
namespace Misaki.HighPerformance.LowLevel.Buffer;
|
|
|
|
/// <summary>
|
|
/// Holds information about a memory allocation.
|
|
/// </summary>
|
|
public readonly struct AllocationInfo
|
|
{
|
|
/// <summary>
|
|
/// Gets the address of the allocated memory block.
|
|
/// </summary>
|
|
public IntPtr Address
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the newSize of the allocation in bytes.
|
|
/// </summary>
|
|
public nuint Size
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
#if MHP_ENABLE_STACKTRACE
|
|
/// <summary>
|
|
/// Gets the stack trace at the time of allocation for debugging purposes.
|
|
/// </summary>
|
|
public StackTrace? StackTrace
|
|
{
|
|
get; init;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public readonly struct AllocationManagerInitOpts
|
|
{
|
|
public nuint ArenaCapacity
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public nuint StackCapacity
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public nuint FreeListChunkSize
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public nuint FreeListDefaultAlignment
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public int FreeListConcurrencyLevel
|
|
{
|
|
get; init;
|
|
}
|
|
|
|
public static AllocationManagerInitOpts Default => new AllocationManagerInitOpts
|
|
{
|
|
ArenaCapacity = 1024 * 1024 * 1024, // 1 GB
|
|
StackCapacity = 16 * 1024 * 1024, // 16 MB per thread
|
|
FreeListChunkSize = 64 * 1024 * 1024,
|
|
FreeListDefaultAlignment = 8,
|
|
FreeListConcurrencyLevel = Environment.ProcessorCount
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides memory allocation management for native memory allocations, with support for tracking,
|
|
/// debugging, and custom allocation strategies.
|
|
/// </summary>
|
|
public static unsafe class AllocationManager
|
|
{
|
|
internal 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
|
|
};
|
|
}
|
|
|
|
private static void* Allocate(void* _, nuint size, nuint alignment, AllocationOption allocationOption)
|
|
{
|
|
var ptr = AlignedAlloc(size, alignment);
|
|
if (ptr == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (allocationOption.HasFlag(AllocationOption.Clear))
|
|
{
|
|
MemClear(ptr, size);
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
private static void* Reallocate(void* _, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
|
|
{
|
|
var newPtr = AlignedRealloc(ptr, newSize, alignment);
|
|
if (newPtr == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (allocationOption.HasFlag(AllocationOption.Clear) && newSize > oldSize)
|
|
{
|
|
var offset = (byte*)newPtr + oldSize;
|
|
var clearSize = newSize - oldSize;
|
|
MemClear(offset, clearSize);
|
|
}
|
|
|
|
return newPtr;
|
|
}
|
|
|
|
private static void Free(void* _, void* ptr)
|
|
{
|
|
AlignedFree(ptr);
|
|
}
|
|
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
private static bool IsValid(void* _, MemoryHandle handle)
|
|
{
|
|
return ContainsAllocation(handle);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
internal static MemoryPool<VirtualArena, VirtualArena.CreationOptions> s_arenaAllocator;
|
|
internal static MemoryPool<FreeList, FreeList.CreationOptions> s_freeListAllocator;
|
|
internal static HeapAllocator* s_pHeapAllocator;
|
|
|
|
[ThreadStatic]
|
|
private static MemoryPool<VirtualStack, VirtualStack.CreationOptions> t_stackAllocator;
|
|
|
|
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
private static ConcurrentSlotMap<AllocationInfo> s_allocations = null!;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Gets the number of live tracked heap allocations. Always returns 0 if MHP_ENABLE_SAFETY_CHECKS is disabled.
|
|
/// </summary>
|
|
public static int LiveAllocationCount =>
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
s_allocations.Count;
|
|
#else
|
|
0;
|
|
#endif
|
|
|
|
private static volatile bool s_initialized;
|
|
|
|
private static nuint s_threadLocalStackSize;
|
|
private static readonly SpinLock s_stackLocker = new SpinLock(false);
|
|
private static VirtualStack** s_ppStack;
|
|
private static int s_ppStackCount;
|
|
private static int s_ppStackCapacity;
|
|
|
|
private static void EnsureThreadLocalStackInitialize()
|
|
{
|
|
if (Unsafe.IsNullRef(ref t_stackAllocator.Allocator))
|
|
{
|
|
t_stackAllocator = new MemoryPool<VirtualStack, VirtualStack.CreationOptions>(new VirtualStack.CreationOptions
|
|
{
|
|
reserveCapacity = s_threadLocalStackSize
|
|
});
|
|
|
|
var token = false;
|
|
try
|
|
{
|
|
s_stackLocker.Enter(ref token);
|
|
if (s_ppStack == null)
|
|
{
|
|
s_ppStack = (VirtualStack**)Malloc((nuint)(sizeof(VirtualStack*) * Environment.ProcessorCount));
|
|
s_ppStackCapacity = Environment.ProcessorCount;
|
|
}
|
|
|
|
if (s_ppStackCount >= s_ppStackCapacity)
|
|
{
|
|
var pOld = s_ppStack;
|
|
var newCapacity = s_ppStackCapacity * 2;
|
|
var pNew = (VirtualStack**)Realloc(pOld, (nuint)(sizeof(VirtualStack*) * newCapacity));
|
|
|
|
s_ppStack = pNew;
|
|
s_ppStackCapacity = newCapacity;
|
|
}
|
|
|
|
s_ppStack[s_ppStackCount] = (VirtualStack*)Unsafe.AsPointer(ref t_stackAllocator.Allocator);
|
|
var test = s_ppStack[s_ppStackCount];
|
|
s_ppStackCount++;
|
|
}
|
|
finally
|
|
{
|
|
if (token)
|
|
{
|
|
s_stackLocker.Exit();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Initialize(AllocationManagerInitOpts opts)
|
|
{
|
|
if (s_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
s_allocations = new ConcurrentSlotMap<AllocationInfo>(256);
|
|
#endif
|
|
|
|
s_arenaAllocator = new MemoryPool<VirtualArena, VirtualArena.CreationOptions>(new VirtualArena.CreationOptions
|
|
{
|
|
reserveCapacity = opts.ArenaCapacity
|
|
});
|
|
|
|
s_freeListAllocator = new MemoryPool<FreeList, FreeList.CreationOptions>(new FreeList.CreationOptions
|
|
{
|
|
alignment = opts.FreeListDefaultAlignment,
|
|
chunkSize = opts.FreeListChunkSize,
|
|
maxConcurrencyLevel = opts.FreeListConcurrencyLevel
|
|
});
|
|
|
|
s_pHeapAllocator = (HeapAllocator*)Malloc((nuint)(sizeof(HeapAllocator)));
|
|
s_pHeapAllocator->Init();
|
|
|
|
s_threadLocalStackSize = opts.StackCapacity;
|
|
|
|
s_initialized = 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)]
|
|
[Obsolete("Use AllocationHandle instead.")]
|
|
public static AllocationHandle GetAllocationHandle(Allocator allocator)
|
|
{
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
|
|
return allocator switch
|
|
{
|
|
Allocator.Temp => s_arenaAllocator.AllocationHandle,
|
|
Allocator.Persistent => s_pHeapAllocator->Handle,
|
|
Allocator.FreeList => s_freeListAllocator.AllocationHandle,
|
|
_ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the temporary memory allocator, clearing all allocated memory.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void ResetTempAllocator()
|
|
{
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
s_arenaAllocator.Allocator.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 VirtualStack.Scope CreateStackScope()
|
|
{
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
|
|
EnsureThreadLocalStackInitialize();
|
|
return t_stackAllocator.Allocator.CreateScope(t_stackAllocator.AllocationHandle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a memory allocation and returns a handle that can be used to manage or reference the allocated memory.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Always returns an invalid handle if MHP_ENABLE_SAFETY_CHECKS is disabled.
|
|
/// </remarks>
|
|
/// <param name="ptr">A pointer to the memory block to be registered. The pointer must reference a valid, allocated memory region.</param>
|
|
/// <param name="size">The newSize of the memory block to be registered.</param>
|
|
/// <returns>A MemoryHandle representing the registered allocation.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static MemoryHandle AddAllocation(void* ptr, nuint size)
|
|
{
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
|
|
var info = new AllocationInfo
|
|
{
|
|
Address = (IntPtr)ptr,
|
|
Size = size,
|
|
#if MHP_ENABLE_STACKTRACE
|
|
StackTrace = new StackTrace(1, true)
|
|
#endif
|
|
};
|
|
|
|
var id = s_allocations.Add(info, out var generation);
|
|
return new MemoryHandle(id, generation);
|
|
#else
|
|
return MemoryHandle.Invalid;
|
|
#endif
|
|
}
|
|
|
|
public static void UpdateAllocation(MemoryHandle handle, void* newPtr, nuint newSize)
|
|
{
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
|
|
if (newPtr == null)
|
|
{
|
|
s_allocations.Remove(handle.ID, handle.Generation);
|
|
}
|
|
else
|
|
{
|
|
|
|
if (s_allocations.TryGetElement(handle.ID, handle.Generation, out var oldInfo))
|
|
{
|
|
var newInfo = oldInfo with
|
|
{
|
|
Address = (IntPtr)newPtr,
|
|
Size = newSize
|
|
};
|
|
|
|
s_allocations.UpdateElement(handle.ID, handle.Generation, newInfo);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the memory allocation associated with the specified handle.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Always returns false if debug layer is disabled.
|
|
/// </remarks>
|
|
/// <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)
|
|
{
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
return s_allocations.Remove(handle.ID, handle.Generation, out var info);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to retrieve the memory allocation pointer associated with the specified handle.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Always returns false if debug layer is disabled, and the output pointer will be set to <see cref="IntPtr.Zero"/>.
|
|
/// </remarks>
|
|
/// <param name="handle">The memory handle identifying the allocation to retrieve allocation.</param>
|
|
/// <param name="info">When this method returns, contains the allocation information associated with the specified handle, if the allocation was found; otherwise, an uninitialized value.</param>
|
|
/// <returns>true if the allocation was found and <paramref name="info"/> contains valid allocation information; otherwise, false.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool TryGetAllocation(MemoryHandle handle, out AllocationInfo info)
|
|
{
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
return s_allocations.TryGetElement(handle.ID, handle.Generation, out info);
|
|
#else
|
|
info = default;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
/// <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.
|
|
/// Always returns false if debug layer is disabled.
|
|
/// </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 MHP_ENABLE_SAFETY_CHECKS
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
return s_allocations.Contains(handle.ID, handle.Generation);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total newSize of all currently tracked allocations.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Always returns 0 if MHP_ENABLE_SAFETY_CHECKS is disabled.
|
|
/// </remarks>
|
|
/// <returns>The total newSize of all currently tracked allocations.</returns>
|
|
public static nuint GetTotalAllocatedMemory()
|
|
{
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
Debug.Assert(s_initialized, "AllocationManager is not initialized.");
|
|
|
|
nuint total = 0;
|
|
foreach (var allocation in s_allocations)
|
|
{
|
|
total += allocation.Size;
|
|
}
|
|
|
|
return total;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes of the AllocationManager, freeing all allocated memory and resources.
|
|
/// </summary>
|
|
public static void Dispose()
|
|
{
|
|
if (!s_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
s_initialized = false;
|
|
|
|
#if MHP_ENABLE_SAFETY_CHECKS
|
|
if (s_allocations.Count > 0)
|
|
{
|
|
throw new MemoryLeakException(s_allocations);
|
|
}
|
|
#endif
|
|
|
|
s_arenaAllocator.Dispose();
|
|
s_freeListAllocator.Dispose();
|
|
|
|
if (s_ppStack != null)
|
|
{
|
|
for (var i = 0; i < s_ppStackCount; i++)
|
|
{
|
|
var pStack = s_ppStack[i];
|
|
if (pStack != null)
|
|
{
|
|
pStack->Dispose();
|
|
}
|
|
}
|
|
|
|
Free(s_ppStack);
|
|
s_ppStack = null;
|
|
}
|
|
|
|
if (s_pHeapAllocator != null)
|
|
{
|
|
Free(s_pHeapAllocator);
|
|
s_pHeapAllocator = null;
|
|
}
|
|
}
|
|
}
|