feat(allocator): add per-thread caches to FreeList

Refactored FreeList allocator to use per-thread caches for improved scalability and performance, with configurable max concurrency and overflow cache. AllocationManager debug layer is now compile-time via ENABLE_DEBUG_LAYER. MemoryUtility methods no longer catch exceptions. Argument validation standardized with ThrowIfNegative. JobScheduler passes maxConcurrencyLevel to allocator. CollectionUtility's GetElementUnsafe returns mutable ref. AssemblyVersion incremented. Added comprehensive FreeList unit tests. Improved robustness and error handling in allocation classes.

BREAKING CHANGE: Debug layer APIs removed; FreeList allocator interface changed for thread cache support.
This commit is contained in:
2026-03-17 20:58:31 +09:00
parent 7ffe8bc0d1
commit 9cee32aa83
15 changed files with 772 additions and 482 deletions

View File

@@ -270,14 +270,15 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
/// <param name="threadCount">The number of worker threads to create. If less than 1, at least one thread will be created.</param> /// <param name="threadCount">The number of worker threads to create. If less than 1, at least one thread will be created.</param>
public JobScheduler(int threadCount) public JobScheduler(int threadCount)
{ {
_jobDataAllocator = new(8); var workerCount = Math.Max(1, threadCount);
_jobDataAllocator = new(8, maxConcurrencyLevel: workerCount + 1);
_jobInfoPool = new(); _jobInfoPool = new();
_jobQueue = new(); _jobQueue = new();
_workSignal = new(0); _workSignal = new(0);
_cts = new(); _cts = new();
var workerCount = Math.Max(1, threadCount);
_workerThreads = new WorkerThread[workerCount]; _workerThreads = new WorkerThread[workerCount];
for (var i = 0; i < workerCount; i++) for (var i = 0; i < workerCount; i++)

View File

@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>1.5.1</AssemblyVersion> <AssemblyVersion>1.5.2</AssemblyVersion>
<Version>$(AssemblyVersion)</Version> <Version>$(AssemblyVersion)</Version>
<Authors>Misaki</Authors> <Authors>Misaki</Authors>
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl> <PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>

View File

@@ -33,16 +33,17 @@ public readonly struct AllocationInfo
/// </summary> /// </summary>
public static unsafe class AllocationManager public static unsafe class AllocationManager
{ {
// === Intrusive allocation tracking (enabled when debug layer is on) === #if ENABLE_DEBUG_LAYER
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
private struct AllocationHeader private struct AllocationHeader
{ {
public AllocationHeader* prev; public AllocationHeader* prev;
public AllocationHeader* next; public AllocationHeader* next;
public void* basePtr; // pointer returned by underlying allocator public void* basePtr; // pointer returned by underlying allocator
public nuint userSize; // requested size from the user public nuint userSize; // requested size from the user
public GCHandle stackHandle; // GCHandle to managed StackTrace public GCHandle stackHandle; // GCHandle to managed StackTrace
} }
#endif
private struct ArenaAllocator : IAllocator, IDisposable private struct ArenaAllocator : IAllocator, IDisposable
{ {
@@ -270,11 +271,12 @@ public static unsafe class AllocationManager
private static readonly HeapAllocator* s_pHeapAllocator; private static readonly HeapAllocator* s_pHeapAllocator;
private static readonly StackAllocator* s_pStackAllocator; private static readonly StackAllocator* s_pStackAllocator;
private static bool s_debugLayer;
private static bool s_disposed; private static bool s_disposed;
private static AllocationHeader* s_pLiveHead; #if ENABLE_DEBUG_LAYER
private static SpinLock s_liveLock; private static SpinLock s_liveLock;
private static AllocationHeader* s_pLiveHead;
#endif
private static readonly ConcurrentSlotMap<IntPtr> s_allocations; private static readonly ConcurrentSlotMap<IntPtr> s_allocations;
@@ -285,31 +287,28 @@ public static unsafe class AllocationManager
/// </summary> /// </summary>
public static int LiveAllocationCount => s_allocations.Count; 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() static AllocationManager()
{ {
var allocatorTotalSize = (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator)); var allocatorTotalSize = (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator));
var basePtr = NativeMemory.Alloc(allocatorTotalSize); var basePtr = Malloc(allocatorTotalSize);
s_pArenaAllocator = (ArenaAllocator*)basePtr; s_pArenaAllocator = (ArenaAllocator*)basePtr;
s_pHeapAllocator = (HeapAllocator*)((byte*)basePtr + (nuint)sizeof(ArenaAllocator)); s_pHeapAllocator = (HeapAllocator*)((byte*)basePtr + (nuint)sizeof(ArenaAllocator));
s_pStackAllocator = (StackAllocator*)((byte*)basePtr + (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator))); s_pStackAllocator = (StackAllocator*)((byte*)basePtr + (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator)));
#if ENABLE_DEBUG_LAYER
s_liveLock = new SpinLock(false); s_liveLock = new SpinLock(false);
s_pLiveHead = null;
#endif
s_allocations = new ConcurrentSlotMap<IntPtr>(256); s_allocations = new ConcurrentSlotMap<IntPtr>(256);
s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE); s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE);
s_pHeapAllocator->Init(); s_pHeapAllocator->Init();
s_pStackAllocator->Init(); s_pStackAllocator->Init();
s_pLiveHead = null;
} }
#if ENABLE_DEBUG_LAYER
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte* AlignUp(byte* p, nuint alignment) private static byte* AlignUp(byte* p, nuint alignment)
{ {
@@ -462,22 +461,7 @@ public static unsafe class AllocationManager
return newUser; return newUser;
} }
#endif
/// <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> /// <summary>
/// Gets a reference to the allocation pHandle for the specified allocator type. /// Gets a reference to the allocation pHandle for the specified allocator type.
@@ -499,10 +483,6 @@ public static unsafe class AllocationManager
/// <summary> /// <summary>
/// Allocates a block of memory from the heap with the specified size and alignment, using the given allocation options. /// Allocates a block of memory from the heap with the specified size and alignment, using the given allocation options.
/// </summary> /// </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="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="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 /// <param name="allocationOption">An optional set of flags that control allocation behavior, such as whether the memory should be cleared or
@@ -511,17 +491,11 @@ public static unsafe class AllocationManager
/// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception> /// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception>
public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle) public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{ {
var isUntrack = allocationOption.HasFlag(AllocationOption.Untrack); #if ENABLE_DEBUG_LAYER
var ptr = DebugAllocate(size, alignment);
void* ptr; #else
if (s_debugLayer && !isUntrack) var ptr = AlignedAlloc(size, alignment);
{ #endif
ptr = DebugAllocate(size, alignment);
}
else
{
ptr = AlignedAlloc(size, alignment);
}
if (ptr == null) if (ptr == null)
{ {
@@ -534,15 +508,7 @@ public static unsafe class AllocationManager
MemClear(ptr, size); MemClear(ptr, size);
} }
if (isUntrack) *pHandle = AddAllocation((IntPtr)ptr);
{
*pHandle = MagicHandle;
}
else
{
*pHandle = AddAllocation((IntPtr)ptr);
}
return ptr; return ptr;
} }
@@ -554,11 +520,13 @@ public static unsafe class AllocationManager
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param> /// <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) public static void HeapFree(void* ptr, MemoryHandle handle)
{ {
if (s_debugLayer && handle != MagicHandle) #if ENABLE_DEBUG_LAYER
if (handle != MagicHandle)
{ {
DebugFree(ptr); DebugFree(ptr);
} }
else else
#endif
{ {
AlignedFree(ptr); AlignedFree(ptr);
} }
@@ -662,53 +630,53 @@ public static unsafe class AllocationManager
return; return;
} }
#if ENABLE_DEBUG_LAYER
// In debug mode, walk the intrusive list to surface any leaks. // In debug mode, walk the intrusive list to surface any leaks.
if (s_debugLayer) var snapshot = new List<AllocationInfo>();
var taken = false;
try
{ {
var snapshot = new List<AllocationInfo>(); s_liveLock.Enter(ref taken);
var taken = false; if (s_pLiveHead != null)
try
{ {
s_liveLock.Enter(ref taken); snapshot.Capacity = 128;
if (s_pLiveHead != null) for (var p = s_pLiveHead; p != null; p = p->next)
{ {
snapshot.Capacity = 128; var trace = (StackTrace)HeaderGetHandle(p).Target!;
for (var p = s_pLiveHead; p != null; p = p->next) snapshot.Add(new AllocationInfo
{ {
var trace = (StackTrace)HeaderGetHandle(p).Target!; Size = p->userSize,
snapshot.Add(new AllocationInfo StackTrace = trace
{ });
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) 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."); throw new MemoryLeakException($"Found {LiveAllocationCount} memory lakes! Please enable debug layer for more informations.");
} }
#endif
// NOTE: Arena allocator holds the base ptr for all allocators, heap and stack allocators do not own any memory themselves. // NOTE: Arena allocator holds the base ptr for all allocators, heap and stack allocators do not own any memory themselves.
if (s_pArenaAllocator != null) if (s_pArenaAllocator != null)

View File

@@ -10,11 +10,7 @@ public enum AllocationOption : byte
/// <summary> /// <summary>
/// Clear the memory to zero upon allocation. /// Clear the memory to zero upon allocation.
/// </summary> /// </summary>
Clear = 1 << 0, 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 public enum Allocator : byte

View File

@@ -1,4 +1,4 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer; namespace Misaki.HighPerformance.LowLevel.Buffer;
@@ -17,6 +17,8 @@ public unsafe struct Arena : IDisposable
public Arena(nuint size) public Arena(nuint size)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(size);
if (_buffer != null) if (_buffer != null)
{ {
return; return;

View File

@@ -1,4 +1,4 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer; namespace Misaki.HighPerformance.LowLevel.Buffer;

View File

@@ -4,17 +4,19 @@ using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer; namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary> /// <summary>
/// A lock-free, thread-safe variable-size allocator that manages memory blocks of different sizes. /// A thread-safe variable-size allocator that uses per-thread caches for the hot path and
/// Optimized for high-performance scenarios with frequent allocations and deallocations. /// a remote-free queue for cross-thread deallocation.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Explicit, Size = 256)] // Cache line aligned to prevent false sharing [StructLayout(LayoutKind.Sequential)]
public unsafe struct FreeList : IDisposable public unsafe struct FreeList : IDisposable
{ {
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
private struct FreeNode private struct FreeNode
{ {
public FreeNode* next; public FreeNode* next;
public nuint size; public MemoryChunk* ownerChunk;
public nuint blockSize;
public int bucketIndex;
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@@ -23,81 +25,75 @@ public unsafe struct FreeList : IDisposable
public MemoryChunk* next; public MemoryChunk* next;
public byte* memory; public byte* memory;
public nuint size; public nuint size;
public nuint used; // Amount of memory used in this chunk public nuint used;
} }
[StructLayout(LayoutKind.Explicit, Size = 32)] [StructLayout(LayoutKind.Explicit, Size = 32)]
private struct SizeBucket private struct SizeBucket
{ {
[FieldOffset(0)] [FieldOffset(0)]
public long freeCount; // Number of free blocks public long freeCount;
[FieldOffset(8)] [FieldOffset(8)]
public nint freeHead; // Free list head for this size public nint freeHead;
[FieldOffset(16)] [FieldOffset(16)]
public nuint blockSize; // Fixed size for this bucket public nuint blockSize;
[FieldOffset(24)] [FieldOffset(24)]
public int creationLock; public int creationLock;
} }
[StructLayout(LayoutKind.Explicit, Size = 24)] [StructLayout(LayoutKind.Sequential)]
private struct ThreadCache
{
public fixed byte buckets[_MAX_BUCKETS * 32];
public nint remoteFreeHead;
public int threadId;
public int active;
}
[StructLayout(LayoutKind.Explicit, Size = 32)]
private struct BlockHeader private struct BlockHeader
{ {
// Ensure the size is fixed across x86 and x64
[FieldOffset(0)] [FieldOffset(0)]
public MemoryChunk* ownerChunk; public MemoryChunk* ownerChunk;
[FieldOffset(8)] [FieldOffset(8)]
public nuint blockSize; public nuint blockSize;
[FieldOffset(16)] [FieldOffset(16)]
public ulong magicNumber; public ulong magicNumber;
[FieldOffset(24)]
public int ownerCacheIndex;
} }
private const int _MAX_BUCKETS = 16; // Number of size buckets private const int _MAX_BUCKETS = 16;
private const nuint _MIN_BLOCK_SIZE = 16; // Minimum block size private const int _DEFAULT_MAX_CONCURRENCY_LEVEL = 16;
private const nuint _DEFAULT_CHUNK_SIZE = 64 * 1024; // 64KB chunks private const int _OVERFLOW_CACHE_INDEX = 0;
private const ulong _MAGIC_NUMBER = 0xDEADBEEFDEADBEEF; // For validating blocks private const nuint _MIN_BLOCK_SIZE = 16;
private const nuint _DEFAULT_CHUNK_SIZE = 64 * 1024;
private const ulong _MAGIC_NUMBER = 0xDEADBEEFDEADBEEF;
[FieldOffset(0)] [ThreadStatic]
private fixed byte _buckets[_MAX_BUCKETS * 32]; // SizeBucket array (32 bytes per bucket) private static int t_cacheIndex;
[FieldOffset(512)] [ThreadStatic]
private DynamicArena _chunkArena; // 128 private static void* t_ownerId;
[FieldOffset(640)] private void* _instanceId;
private MemoryChunk* _chunks; // 8 private ThreadCache** _caches;
private DynamicArena _chunkArena;
[FieldOffset(648)] private MemoryChunk* _chunks;
private readonly nuint _chunkSize; // 8 private readonly nuint _chunkSize;
private readonly nuint _alignment;
[FieldOffset(656)] private readonly int _maxConcurrencyLevel;
private readonly nuint _alignment; // 8 private int _cacheCount;
private volatile int _disposed;
[FieldOffset(664)] private volatile int _chunkCreationLock;
private long _totalAllocatedBytes; // 8 private volatile int _cacheRegistrationLock;
private volatile int _overflowLock;
[FieldOffset(672)]
private long _totalFreeBytes; // 8
[FieldOffset(676)]
private volatile int _disposed; // 4
[FieldOffset(680)]
private volatile int _chunkCreationLock; // 4
/// <summary> /// <summary>
/// Gets the alignment requirement for allocations. /// Gets the alignment requirement for allocations.
/// </summary> /// </summary>
public readonly nuint Alignment => _alignment; 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> /// <summary>
/// Gets whether the allocator has been disposed. /// Gets whether the allocator has been disposed.
/// </summary> /// </summary>
@@ -108,12 +104,18 @@ public unsafe struct FreeList : IDisposable
/// </summary> /// </summary>
public readonly nuint ChunkSize => _chunkSize; public readonly nuint ChunkSize => _chunkSize;
/// <summary>
/// Gets the maximum number of dedicated thread caches.
/// </summary>
public readonly int MaxConcurrencyLevel => _maxConcurrencyLevel;
/// <summary> /// <summary>
/// Initializes a new variable-size FreeList allocator with the specified parameters. /// Initializes a new variable-size FreeList allocator with the specified parameters.
/// </summary> /// </summary>
/// <param name="alignment">Alignment requirement for blocks (must be power of 2).</param> /// <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> /// <param name="chunkSize">Size of memory chunks to allocate (default: 64KB).</param>
public FreeList(nuint alignment, nuint chunkSize = _DEFAULT_CHUNK_SIZE) /// <param name="maxConcurrencyLevel">Maximum number of dedicated thread caches.</param>
public FreeList(nuint alignment, nuint chunkSize = _DEFAULT_CHUNK_SIZE, int maxConcurrencyLevel = _DEFAULT_MAX_CONCURRENCY_LEVEL)
{ {
if (alignment == 0 || (alignment & (alignment - 1)) != 0) if (alignment == 0 || (alignment & (alignment - 1)) != 0)
{ {
@@ -125,21 +127,72 @@ public unsafe struct FreeList : IDisposable
throw new ArgumentException("Chunk size must be at least 1KB", nameof(chunkSize)); throw new ArgumentException("Chunk size must be at least 1KB", nameof(chunkSize));
} }
if (maxConcurrencyLevel < 1)
{
throw new ArgumentOutOfRangeException(nameof(maxConcurrencyLevel), "Max concurrency level must be greater than zero.");
}
_alignment = alignment; _alignment = alignment;
_chunkSize = chunkSize; _chunkSize = chunkSize;
_chunks = null; _maxConcurrencyLevel = maxConcurrencyLevel;
_totalAllocatedBytes = 0;
_totalFreeBytes = 0;
_disposed = 0;
_chunkCreationLock = 0;
_chunkArena = new DynamicArena(1024); try
InitializeBuckets(); {
_instanceId = Malloc((nuint)sizeof(nint));
_chunks = null;
_cacheCount = 0;
_disposed = 0;
_chunkCreationLock = 0;
_cacheRegistrationLock = 0;
_overflowLock = 0;
_chunkArena = new DynamicArena(1024);
_caches = (ThreadCache**)Malloc((nuint)sizeof(ThreadCache*) * (nuint)(maxConcurrencyLevel + 1));
for (var i = 0; i <= maxConcurrencyLevel; i++)
{
_caches[i] = null;
}
var overflowCache = CreateCacheForThread(0);
if (overflowCache == null)
{
throw new OutOfMemoryException("Failed to initialize free list overflow cache.");
}
_caches[_OVERFLOW_CACHE_INDEX] = overflowCache;
}
catch
{
if (_instanceId != null)
{
Free(_instanceId);
_instanceId = null;
}
if (_caches != null)
{
Free(_caches);
_caches = null;
}
_chunkArena.Dispose();
throw;
}
} }
private readonly void InitializeBuckets() [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static SizeBucket* GetBuckets(ThreadCache* cache)
{ {
var buckets = GetBuckets(); return (SizeBucket*)cache->buckets;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InitializeBuckets(ThreadCache* cache)
{
var buckets = GetBuckets(cache);
var size = _MIN_BLOCK_SIZE; var size = _MIN_BLOCK_SIZE;
for (var i = 0; i < _MAX_BUCKETS; i++) for (var i = 0; i < _MAX_BUCKETS; i++)
@@ -147,42 +200,296 @@ public unsafe struct FreeList : IDisposable
buckets[i].blockSize = size; buckets[i].blockSize = size;
buckets[i].freeHead = 0; buckets[i].freeHead = 0;
buckets[i].freeCount = 0; buckets[i].freeCount = 0;
size *= 2; // Exponential size increase buckets[i].creationLock = 0;
size *= 2;
} }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] cache->remoteFreeHead = 0;
private readonly SizeBucket* GetBuckets()
{
fixed (byte* ptr = _buckets)
{
return (SizeBucket*)ptr;
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly int FindBucket(nuint size) private readonly int FindBucket(nuint size)
{ {
var buckets = GetBuckets(); var blockSize = _MIN_BLOCK_SIZE;
for (var i = 0; i < _MAX_BUCKETS; i++) for (var i = 0; i < _MAX_BUCKETS; i++)
{ {
if (size <= buckets[i].blockSize) if (size <= blockSize)
{ {
return i; return i;
} }
blockSize <<= 1;
} }
return -1; // Size too large for buckets return -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ThreadCache* CreateCacheForThread(int threadId)
{
var cache = (ThreadCache*)_chunkArena.Allocate(SizeOf<ThreadCache>(), AlignOf<ThreadCache>(), AllocationOption.Clear);
if (cache == null)
{
return null;
}
InitializeBuckets(cache);
cache->threadId = threadId;
cache->active = 1;
return cache;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrainRemoteFrees(ThreadCache* cache)
{
var head = (FreeNode*)Interlocked.Exchange(ref cache->remoteFreeHead, 0);
while (head != null)
{
var next = head->next;
PushToBucket(cache, head->bucketIndex, head, head->ownerChunk, head->blockSize);
head = next;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly ThreadCache* GetOverflowCache()
{
return _caches[_OVERFLOW_CACHE_INDEX];
}
private ThreadCache* RegisterThreadCache()
{
while (Interlocked.CompareExchange(ref _cacheRegistrationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
if (t_ownerId == _instanceId && t_cacheIndex > 0 && t_cacheIndex <= _cacheCount)
{
return _caches[t_cacheIndex];
}
if (_cacheCount >= _maxConcurrencyLevel)
{
t_ownerId = _instanceId;
t_cacheIndex = _OVERFLOW_CACHE_INDEX;
return GetOverflowCache();
}
var threadId = Environment.CurrentManagedThreadId;
var cache = CreateCacheForThread(threadId);
if (cache == null)
{
t_ownerId = _instanceId;
t_cacheIndex = _OVERFLOW_CACHE_INDEX;
return GetOverflowCache();
}
_cacheCount++;
_caches[_cacheCount] = cache;
t_ownerId = _instanceId;
t_cacheIndex = _cacheCount;
return cache;
}
finally
{
Interlocked.Exchange(ref _cacheRegistrationLock, 0);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ThreadCache* GetCurrentCache()
{
if (t_ownerId == _instanceId)
{
var index = t_cacheIndex;
if ((uint)index <= (uint)_cacheCount)
{
var cache = _caches[index];
if (cache != null)
{
return cache;
}
}
}
return RegisterThreadCache();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void* TryPopFromBucket(ThreadCache* cache, int cacheIndex, int bucketIndex)
{
var buckets = GetBuckets(cache);
var bucket = &buckets[bucketIndex];
var head = (FreeNode*)bucket->freeHead;
if (head == null)
{
return null;
}
bucket->freeHead = (nint)head->next;
bucket->freeCount--;
AssignBlockHeader((BlockHeader*)head, head->ownerChunk, head->blockSize, cacheIndex);
return head;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PushToBucket(ThreadCache* cache, int bucketIndex, void* ptr, MemoryChunk* ownerChunk, nuint blockSize)
{
var buckets = GetBuckets(cache);
var bucket = &buckets[bucketIndex];
var node = (FreeNode*)ptr;
node->ownerChunk = ownerChunk;
node->blockSize = blockSize;
node->bucketIndex = bucketIndex;
node->next = (FreeNode*)bucket->freeHead;
bucket->freeHead = (nint)node;
bucket->freeCount++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AssignBlockHeader(BlockHeader* header, MemoryChunk* ownerChunk, nuint blockSize, int ownerCacheIndex)
{
header->ownerChunk = ownerChunk;
header->blockSize = blockSize;
header->magicNumber = _MAGIC_NUMBER;
header->ownerCacheIndex = ownerCacheIndex;
}
private bool TryCreateBlocksForBucket(ThreadCache* cache, int cacheIndex, int bucketIndex)
{
var buckets = GetBuckets(cache);
var bucket = &buckets[bucketIndex];
while (Interlocked.CompareExchange(ref bucket->creationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
DrainRemoteFrees(cache);
if (bucket->freeHead != 0)
{
return true;
}
var blockSize = bucket->blockSize;
var blocksToCreate = Math.Min(_chunkSize / blockSize, 256);
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;
}
while (Interlocked.CompareExchange(ref _chunkCreationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
chunk->memory = memory;
chunk->size = totalSize;
chunk->used = totalSize;
chunk->next = _chunks;
_chunks = chunk;
}
finally
{
Interlocked.Exchange(ref _chunkCreationLock, 0);
}
for (nuint i = 0; i < blocksToCreate; i++)
{
var blockStartPtr = memory + (i * blockSize);
PushToBucket(cache, bucketIndex, blockStartPtr, chunk, blockSize);
}
return true;
}
finally
{
Interlocked.Exchange(ref bucket->creationLock, 0);
}
}
private void* AllocateFromChunk(int cacheIndex, nuint size, nuint alignment)
{
while (Interlocked.CompareExchange(ref _chunkCreationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
var chunk = _chunks;
while (chunk != null)
{
var alignedOffset = (chunk->used + alignment - 1) & ~(alignment - 1);
var totalNeeded = alignedOffset - chunk->used + size;
var available = chunk->size - chunk->used;
if (totalNeeded <= available)
{
var blockStartPtr = chunk->memory + alignedOffset;
chunk->used = alignedOffset + size;
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, size, cacheIndex);
return blockStartPtr;
}
chunk = chunk->next;
}
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;
AssignBlockHeader((BlockHeader*)newMemory, newChunk, size, cacheIndex);
return newMemory;
}
finally
{
Interlocked.Exchange(ref _chunkCreationLock, 0);
}
} }
/// <summary> /// <summary>
/// Allocates a memory block of the specified size. Thread-safe using lock-free algorithms. /// Allocates a memory block of the specified size.
/// </summary> /// </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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None) public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{ {
@@ -206,57 +513,69 @@ public unsafe struct FreeList : IDisposable
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment)); throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
} }
// Align size to alignment boundary
var alignedSize = (size + alignment - 1) & ~(alignment - 1); var alignedSize = (size + alignment - 1) & ~(alignment - 1);
alignedSize = Math.Max(alignedSize, _MIN_BLOCK_SIZE); alignedSize = Math.Max(alignedSize, _MIN_BLOCK_SIZE);
var totalSize = alignedSize + (nuint)sizeof(BlockHeader); var totalSize = alignedSize + (nuint)sizeof(BlockHeader);
var bucketIndex = FindBucket(totalSize); var bucketIndex = FindBucket(totalSize);
void* ptr = null; var cache = GetCurrentCache();
var cacheIndex = t_cacheIndex;
var requiresOverflowLock = cacheIndex == _OVERFLOW_CACHE_INDEX;
if (bucketIndex >= 0) if (requiresOverflowLock)
{ {
// Try to allocate from bucket while (Interlocked.CompareExchange(ref _overflowLock, 1, 0) != 0)
ptr = TryPopFromBucket(bucketIndex); {
Thread.SpinWait(1);
}
}
try
{
DrainRemoteFrees(cache);
void* ptr = null;
if (bucketIndex >= 0)
{
ptr = TryPopFromBucket(cache, cacheIndex, bucketIndex);
if (ptr == null && TryCreateBlocksForBucket(cache, cacheIndex, bucketIndex))
{
ptr = TryPopFromBucket(cache, cacheIndex, bucketIndex);
}
}
if (ptr == null) if (ptr == null)
{ {
// Create new blocks for this bucket ptr = AllocateFromChunk(cacheIndex, totalSize, alignment);
if (TryCreateBlocksForBucket(bucketIndex)) }
{ if (ptr == null)
ptr = TryPopFromBucket(bucketIndex); {
} return null;
} }
}
if (ptr == null)
{
// Fallback to direct allocation from chunk
ptr = AllocateFromChunk(totalSize, alignment);
}
if (ptr != null)
{
var header = (BlockHeader*)ptr; var header = (BlockHeader*)ptr;
Interlocked.Add(ref _totalAllocatedBytes, (long)header->blockSize); header->ownerCacheIndex = cacheIndex;
var pUserData = (byte*)ptr + sizeof(BlockHeader); var userPtr = (byte*)ptr + sizeof(BlockHeader);
if (allocationOption.HasFlag(AllocationOption.Clear)) if (allocationOption.HasFlag(AllocationOption.Clear))
{ {
MemClear(pUserData, alignedSize); MemClear(userPtr, alignedSize);
} }
return pUserData; return userPtr;
}
finally
{
if (requiresOverflowLock)
{
Interlocked.Exchange(ref _overflowLock, 0);
}
} }
return null;
} }
/// <summary> /// <summary>
/// Frees a previously allocated memory block. Thread-safe using lock-free algorithms. /// Frees a previously allocated memory block.
/// </summary> /// </summary>
/// <param name="block">MemoryBlock to free.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Free(void* ptr) public void Free(void* ptr)
{ {
@@ -267,226 +586,121 @@ public unsafe struct FreeList : IDisposable
var blockStartPtr = (byte*)ptr - sizeof(BlockHeader); var blockStartPtr = (byte*)ptr - sizeof(BlockHeader);
var header = (BlockHeader*)blockStartPtr; var header = (BlockHeader*)blockStartPtr;
if (header->magicNumber != _MAGIC_NUMBER) if (header->magicNumber != _MAGIC_NUMBER)
{ {
return; return;
} }
var chuck = header->ownerChunk; var chunk = header->ownerChunk;
if (chuck == null) if (chunk == null)
{ {
return; return;
} }
var bucketIndex = FindBucket(header->blockSize); var blockSize = header->blockSize;
if (bucketIndex >= 0) var ownerCacheIndex = header->ownerCacheIndex;
var bucketIndex = FindBucket(blockSize);
if (bucketIndex < 0)
{ {
PushToBucket(bucketIndex, blockStartPtr, header->blockSize); header->ownerChunk = null;
header->blockSize = 0;
header->magicNumber = 0;
header->ownerCacheIndex = 0;
return;
} }
Interlocked.Add(ref _totalAllocatedBytes, -(long)header->blockSize); var sameThread = t_ownerId == _instanceId && t_cacheIndex == ownerCacheIndex;
header->ownerChunk = null; var targetCache = ownerCacheIndex >= 0 && ownerCacheIndex <= _cacheCount ? _caches[ownerCacheIndex] : null;
header->blockSize = 0; if (targetCache == null)
header->magicNumber = 0;
}
// FIX: This may introduce ABA problem. Consider adding a version counter to the free list nodes if this becomes an issue.
[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; targetCache = GetOverflowCache();
if (head == 0) ownerCacheIndex = _OVERFLOW_CACHE_INDEX;
sameThread = t_ownerId == _instanceId && t_cacheIndex == ownerCacheIndex;
}
if (sameThread)
{
if (ownerCacheIndex == _OVERFLOW_CACHE_INDEX)
{ {
return null; while (Interlocked.CompareExchange(ref _overflowLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
PushToBucket(targetCache, bucketIndex, blockStartPtr, chunk, blockSize);
}
finally
{
Interlocked.Exchange(ref _overflowLock, 0);
}
}
else
{
PushToBucket(targetCache, bucketIndex, blockStartPtr, chunk, blockSize);
} }
headPtr = (FreeNode*)head; return;
newHead = (nint)headPtr->next; }
} while (Interlocked.CompareExchange(ref bucket->freeHead, newHead, head) != head); var remoteNode = (FreeNode*)blockStartPtr;
remoteNode->ownerChunk = chunk;
Interlocked.Decrement(ref bucket->freeCount); remoteNode->blockSize = blockSize;
return (void*)head; remoteNode->bucketIndex = bucketIndex;
}
[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; nint head;
do do
{ {
head = bucket->freeHead; head = targetCache->remoteFreeHead;
node->next = (FreeNode*)head; remoteNode->next = (FreeNode*)head;
} while (Interlocked.CompareExchange(ref targetCache->remoteFreeHead, (nint)remoteNode, head) != 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() public void Dispose()
{ {
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{ {
// Free all memory chunks return;
var chunk = _chunks;
while (chunk != null)
{
var next = chunk->next;
AlignedFree(chunk->memory);
chunk = next;
}
_chunkArena.Dispose();
_chunks = null;
_totalAllocatedBytes = 0;
_totalFreeBytes = 0;
} }
if (_caches != null)
{
for (var i = 0; i <= _cacheCount; i++)
{
var cache = _caches[i];
if (cache != null)
{
DrainRemoteFrees(cache);
cache->active = 0;
}
}
}
var chunk = _chunks;
while (chunk != null)
{
var next = chunk->next;
AlignedFree(chunk->memory);
chunk = next;
}
_chunkArena.Dispose();
if (_caches != null)
{
Free(_caches);
_caches = null;
}
if (_instanceId != null)
{
Free(_instanceId);
_instanceId = null;
}
_chunks = null;
_cacheCount = 0;
} }
} }

View File

@@ -87,12 +87,19 @@ public unsafe partial struct Stack : IDisposable
private void Init(nuint size) private void Init(nuint size)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(size);
if (_buffer != null) if (_buffer != null)
{ {
Free(_buffer); Free(_buffer);
} }
_buffer = (byte*)Malloc(size); _buffer = (byte*)Malloc(size);
if (_buffer == null)
{
throw new OutOfMemoryException("Failed to allocate memory for the stack.");
}
_size = size; _size = size;
_offset = 0; _offset = 0;
_activeScopeCount = 0; _activeScopeCount = 0;
@@ -103,15 +110,15 @@ public unsafe partial struct Stack : IDisposable
s_locker.Enter(ref token); s_locker.Enter(ref token);
if (s_pStackBuffers == null) if (s_pStackBuffers == null)
{ {
s_pStackBuffers = (void**)Malloc((nuint)sizeof(void*) * 4u); s_pStackBuffers = (void**)Malloc((nuint)(sizeof(void*) * Environment.ProcessorCount));
s_stackCapacity = 4; s_stackCapacity = Environment.ProcessorCount;
} }
if (s_stackCount >= s_stackCapacity) if (s_stackCount >= s_stackCapacity)
{ {
var pOld = s_pStackBuffers; var pOld = s_pStackBuffers;
var newCapacity = s_stackCapacity * 2; var newCapacity = s_stackCapacity * 2;
var pNew = (void**)Realloc(pOld, (nuint)sizeof(void*) * (nuint)newCapacity); var pNew = (void**)Realloc(pOld, (nuint)(sizeof(void*) * newCapacity));
s_pStackBuffers = pNew; s_pStackBuffers = pNew;
s_stackCapacity = newCapacity; s_stackCapacity = newCapacity;

View File

@@ -131,15 +131,9 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
public HashMapHelper(int capacity, int sizeOfTValue, int alignOfTValue, uint minGrowth, AllocationHandle handle, AllocationOption allocationOption) public HashMapHelper(int capacity, int sizeOfTValue, int alignOfTValue, uint minGrowth, AllocationHandle handle, AllocationOption allocationOption)
{ {
if (capacity <= 0) ArgumentOutOfRangeException.ThrowIfNegative(capacity);
{ ArgumentOutOfRangeException.ThrowIfNegative(sizeOfTValue);
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); ArgumentOutOfRangeException.ThrowIfNegative(alignOfTValue);
}
if (sizeOfTValue < 0 || alignOfTValue < 0)
{
throw new ArgumentOutOfRangeException(nameof(sizeOfTValue), "Size or alignment of TValue can not be less than zero.");
}
_capacity = CalcCapacityCeilPow2(capacity); _capacity = CalcCapacityCeilPow2(capacity);
_bucketCapacity = _capacity * 2; _bucketCapacity = _capacity * 2;

View File

@@ -3,6 +3,7 @@ using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections; using System.Collections;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections; namespace Misaki.HighPerformance.LowLevel.Collections;
@@ -154,10 +155,7 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
public UnsafeArray(int count, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None) public UnsafeArray(int count, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{ {
if (count < 0) ArgumentOutOfRangeException.ThrowIfNegative(count);
{
throw new ArgumentOutOfRangeException(nameof(count), "Count can not be less than zero.");
}
if (handle.Alloc == null) if (handle.Alloc == null)
{ {

View File

@@ -7,7 +7,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Misaki</Authors> <Authors>Misaki</Authors>
<AssemblyVersion>1.4.4</AssemblyVersion> <AssemblyVersion>1.4.5</AssemblyVersion>
<Version>$(AssemblyVersion)</Version> <Version>$(AssemblyVersion)</Version>
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl> <PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
<RepositoryUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</RepositoryUrl> <RepositoryUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</RepositoryUrl>

View File

@@ -21,18 +21,11 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* Malloc(nuint size) public static void* Malloc(nuint size)
{ {
try
{
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
return NativeMemory.Alloc(size); return NativeMemory.Alloc(size);
#else #else
return Marshal.AllocHGlobal((IntPtr)size).ToPointer(); return Marshal.AllocHGlobal((IntPtr)size).ToPointer();
#endif #endif
}
catch (Exception)
{
return null;
}
} }
/// <summary> /// <summary>
@@ -43,20 +36,13 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* Calloc(nuint size) public static void* Calloc(nuint size)
{ {
try
{
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
return NativeMemory.AllocZeroed(size); return NativeMemory.AllocZeroed(size);
#else #else
var ptr = Marshal.AllocHGlobal((IntPtr)size).ToPointer(); var ptr = Marshal.AllocHGlobal((IntPtr)size).ToPointer();
Unsafe.InitBlock(ptr, 0, (uint)size); Unsafe.InitBlock(ptr, 0, (uint)size);
return ptr; return ptr;
#endif #endif
}
catch (Exception)
{
return null;
}
} }
/// <summary> /// <summary>
@@ -68,18 +54,11 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* AlignedAlloc(nuint size, nuint alignment) public static void* AlignedAlloc(nuint size, nuint alignment)
{ {
try
{
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
return NativeMemory.AlignedAlloc(size, alignment); return NativeMemory.AlignedAlloc(size, alignment);
#else #else
return Marshal.AllocHGlobal((IntPtr)(size + alignment - 1)).ToPointer(); return Marshal.AllocHGlobal((IntPtr)(size + alignment - 1)).ToPointer();
#endif #endif
}
catch (Exception)
{
return null;
}
} }
/// <summary> /// <summary>
@@ -91,18 +70,11 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* Realloc(void* ptr, nuint size) public static void* Realloc(void* ptr, nuint size)
{ {
try
{
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
return NativeMemory.Realloc(ptr, size); return NativeMemory.Realloc(ptr, size);
#else #else
return Marshal.ReAllocHGlobal((IntPtr)ptr, (IntPtr)size).ToPointer(); return Marshal.ReAllocHGlobal((IntPtr)ptr, (IntPtr)size).ToPointer();
#endif #endif
}
catch (Exception)
{
return null;
}
} }
/// <summary> /// <summary>
@@ -116,30 +88,23 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* AlignedRealloc(void* ptr, nuint size, nuint alignment) public static void* AlignedRealloc(void* ptr, nuint size, nuint alignment)
{ {
try
{
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
return NativeMemory.AlignedRealloc(ptr, size, alignment); return NativeMemory.AlignedRealloc(ptr, size, alignment);
#else #else
var newPtr = Marshal.AllocHGlobal((IntPtr)(size + alignment - 1)).ToPointer(); var newPtr = Marshal.AllocHGlobal((IntPtr)(size + alignment - 1)).ToPointer();
if (newPtr == null) if (newPtr == null)
{
return null;
}
if (ptr != null)
{
Unsafe.CopyBlock(newPtr, ptr, (uint)size);
Marshal.FreeHGlobal((IntPtr)ptr);
}
return newPtr;
#endif
}
catch (Exception)
{ {
return null; return null;
} }
if (ptr != null)
{
Unsafe.CopyBlock(newPtr, ptr, (uint)size);
Marshal.FreeHGlobal((IntPtr)ptr);
}
return newPtr;
#endif
} }
/// <summary> /// <summary>

View File

@@ -7,12 +7,6 @@ namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass] [TestClass]
public class TestAllocationManager public class TestAllocationManager
{ {
[TestInitialize]
public void Initialize()
{
AllocationManager.EnableDebugLayer();
}
[TestMethod] [TestMethod]
public void ShouldNotLeakTest() public void ShouldNotLeakTest()
{ {

View File

@@ -0,0 +1,151 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Diagnostics;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
public unsafe class TestFreeList
{
[TestMethod]
public void SingleThreadedAllocFreeTest()
{
using var freeList = new FreeList(8, 1024);
// Allocate various sizes
void* p1 = freeList.Allocate(16, 8);
void* p2 = freeList.Allocate(32, 8);
void* p3 = freeList.Allocate(64, 8);
Assert.IsTrue(p1 != null);
Assert.IsTrue(p2 != null);
Assert.IsTrue(p3 != null);
// Free them
freeList.Free(p1);
freeList.Free(p2);
freeList.Free(p3);
// Allocate again - should reuse from buckets (or at least succeed)
void* p4 = freeList.Allocate(16, 8);
void* p5 = freeList.Allocate(32, 8);
Assert.IsTrue(p4 != null);
Assert.IsTrue(p5 != null);
freeList.Free(p4);
freeList.Free(p5);
}
[TestMethod]
public void MultiThreadedAllocSameThreadFreeTest()
{
const int threadCount = 8;
const int iterations = 1000;
using var freeList = new FreeList(8, 64 * 1024, threadCount);
var threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
for (int j = 0; j < iterations; j++)
{
void* ptr = freeList.Allocate(16, 8);
Assert.IsTrue(ptr != null);
freeList.Free(ptr);
}
});
}
foreach (var t in threads) t.Start();
foreach (var t in threads) t.Join();
}
[TestMethod]
public void MultiThreadedCrossThreadFreeTest()
{
const int producerCount = 4;
const int consumerCount = 4;
const int iterations = 5000;
using var freeList = new FreeList(8, 64 * 1024, producerCount + consumerCount);
var queue = new System.Collections.Concurrent.ConcurrentQueue<IntPtr>();
var producers = new Thread[producerCount];
var consumers = new Thread[consumerCount];
bool producing = true;
for (int i = 0; i < producerCount; i++)
{
producers[i] = new Thread(() =>
{
for (int j = 0; j < iterations; j++)
{
void* ptr = freeList.Allocate(32, 8);
Assert.IsTrue(ptr != null);
queue.Enqueue((IntPtr)ptr);
}
});
}
for (int i = 0; i < consumerCount; i++)
{
consumers[i] = new Thread(() =>
{
while (Volatile.Read(ref producing) || !queue.IsEmpty)
{
if (queue.TryDequeue(out var ptr))
{
freeList.Free((void*)ptr);
}
else
{
Thread.Yield();
}
}
});
}
foreach (var t in producers) t.Start();
foreach (var t in consumers) t.Start();
foreach (var t in producers) t.Join();
Volatile.Write(ref producing, false);
foreach (var t in consumers) t.Join();
}
[TestMethod]
public void OverflowCacheTest()
{
// Set maxConcurrencyLevel to 1, but use more threads
const int threadCount = 5;
using var freeList = new FreeList(8, 1024, 1);
var threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
void* ptr = freeList.Allocate(16, 8);
Assert.IsTrue(ptr != null);
freeList.Free(ptr);
});
}
foreach (var t in threads) t.Start();
foreach (var t in threads) t.Join();
}
[TestMethod]
public void LargeAllocationTest()
{
using var freeList = new FreeList(8, 1024);
// Allocate larger than default chunk size
nuint largeSize = 2048;
void* ptr = freeList.Allocate(largeSize, 8);
Assert.IsTrue(ptr != null);
freeList.Free(ptr);
}
}

View File

@@ -54,7 +54,7 @@ public static class CollectionUtility
/// <param name="index">The zero-based index of the element to retrieve.</param> /// <param name="index">The zero-based index of the element to retrieve.</param>
/// <returns>A reference to the element at the specified index in the span.</returns> /// <returns>A reference to the element at the specified index in the span.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref readonly T GetElementUnsafe<T>(this Span<T> span, int index) public static ref T GetElementUnsafe<T>(this Span<T> span, int index)
{ {
return ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index); return ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index);
} }