feat(memory): refactor allocation and add new queue

Refactored memory management by removing safety checks and introducing `MemoryHandle` for centralized tracking. Simplified allocation logic across allocators and enhanced `Dispose` methods for better resource cleanup.

Added `UnsafeChunkedQueue<T>`, a lock-free, dynamically resizing queue with chunk-based memory management, supporting parallel producers and consumers.

Updated unit tests to validate new queue functionality and ensure compatibility with refactored memory logic. Incremented assembly version to 1.6.12.

BREAKING CHANGE: Removed `#if MHP_ENABLE_SAFETY_CHECKS` blocks, altering memory validation behavior.
This commit is contained in:
2026-04-10 14:44:48 +09:00
parent dea8de60d0
commit a0deadc363
25 changed files with 647 additions and 456 deletions

View File

@@ -54,10 +54,6 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
return default;
}
public void Dispose()
{
}
}
// This buffer has 4 parts: TValue, TKey, Next, Buckets.
@@ -99,14 +95,7 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
#if MHP_ENABLE_SAFETY_CHECKS
if (_buffer != null)
{
if (_allocationHandle.IsValid != null)
{
return _allocationHandle.IsValid(_allocationHandle.State, _memoryHandle);
}
else
{
return true;
}
return _memoryHandle.IsValid;
}
return false;
@@ -156,7 +145,13 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
var totalSize = CalculateDataSize(_capacity, _bucketCapacity, sizeOfTValue,
out var keyOffset, out var nextOffset, out var bucketOffset);
allocationOption &= ~AllocationOption.Clear;
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle = MemoryHandle.Create(_buffer, (nuint)totalSize);
#endif
Clear();
}
@@ -256,22 +251,12 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
throw new InvalidOperationException("Target allocation handle does not support allocation.");
}
#if MHP_ENABLE_SAFETY_CHECKS
MemoryHandle memHandle;
#endif
var buf = (byte*)_allocationHandle.Alloc(_allocationHandle.State, (uint)totalSize, (nuint)_alignment, allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, &memHandle
#endif
);
var buf = (byte*)_allocationHandle.Alloc(_allocationHandle.State, (uint)totalSize, (nuint)_alignment, allocationOption);
_buffer = buf;
_keys = (TKey*)(_buffer + keyOffset);
_next = (int*)(_buffer + nextOffset);
_buckets = (int*)(_buffer + bucketOffset);
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle = memHandle;
#endif
}
private void ResizeExact(int newCapacity, int newBucketCapacity)
@@ -284,9 +269,6 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
var oldNext = _next;
var oldBuckets = _buckets;
var oldBucketCapacity = _bucketCapacity;
#if MHP_ENABLE_SAFETY_CHECKS
var oldMemoryHandle = _memoryHandle;
#endif
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, AllocationOption.None);
_capacity = newCapacity;
@@ -305,12 +287,12 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
if (_allocationHandle.Free != null)
{
_allocationHandle.Free(_allocationHandle.State, oldBuffer
#if MHP_ENABLE_SAFETY_CHECKS
, oldMemoryHandle
#endif
);
_allocationHandle.Free(_allocationHandle.State, oldBuffer);
}
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle.Update(_buffer, (nuint)totalSize);
#endif
}
public void Resize(int newCapacity)
@@ -719,42 +701,19 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
{
if (!IsCreated)
{
#if DEBUG
if (_buffer == null)
{
return;
}
var message = "The HashMapHelper is not created or already disposed.";
#if MHP_ENABLE_STACKTRACE
var stackTrace = new StackTrace(1, true);
var sb = new System.Text.StringBuilder();
foreach (var frame in stackTrace.GetFrames())
{
var fileName = frame?.GetFileName();
if (frame != null)
{
var methodInfo = DiagnosticMethodInfo.Create(frame);
sb.AppendLine($"File: {fileName}, Type: {methodInfo?.DeclaringTypeName}, Method: {methodInfo?.Name}, Line: {frame.GetFileLineNumber()}");
}
}
message += Environment.NewLine + sb.ToString();
#endif
Debug.WriteLine(message);
#endif
UnsafeCollectionUtility.ReportDoubleFree<HashMapHelper<TKey>>(_buffer);
return;
}
if (_allocationHandle.Free != null)
{
_allocationHandle.Free(_allocationHandle.State, _buffer
#if MHP_ENABLE_SAFETY_CHECKS
, _memoryHandle
#endif
);
_allocationHandle.Free(_allocationHandle.State, _buffer);
}
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle.Dispose();
#endif
_buffer = null;
_keys = null;
_next = null;

View File

@@ -108,14 +108,7 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
#if MHP_ENABLE_SAFETY_CHECKS
if (_buffer != null)
{
if (_allocationHandle.IsValid != null)
{
return _allocationHandle.IsValid(_allocationHandle.State, _memoryHandle);
}
else
{
return true;
}
return _memoryHandle.IsValid;
}
return false;
@@ -149,18 +142,9 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
throw new InvalidOperationException("Target allocation handle does not support allocation.");
}
_buffer = (T*)handle.Alloc(handle.State, (nuint)(count * sizeof(T)), AlignOf<T>(), allocationOption);
#if MHP_ENABLE_SAFETY_CHECKS
MemoryHandle memHandle;
#endif
var buff = handle.Alloc(handle.State, (nuint)(count * sizeof(T)), AlignOf<T>(), allocationOption
#if MHP_ENABLE_SAFETY_CHECKS
, &memHandle
#endif
);
_buffer = (T*)buff;
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle = memHandle;
_memoryHandle = MemoryHandle.Create(_buffer, (nuint)(count * sizeof(T)));
#endif
_allocationHandle = handle;
_count = count;
@@ -247,19 +231,12 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
return;
}
#if MHP_ENABLE_SAFETY_CHECKS
MemoryHandle memHandle = _memoryHandle;
#endif
var elemSize = SizeOf<T>();
_buffer = (T*)_allocationHandle.Realloc(_allocationHandle.State, _buffer, (nuint)Count * elemSize, (nuint)newSize * elemSize, AlignOf<T>(), option
#if MHP_ENABLE_SAFETY_CHECKS
, &memHandle
#endif
);
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle = memHandle;
#endif
_buffer = (T*)_allocationHandle.Realloc(_allocationHandle.State, _buffer, (nuint)Count * elemSize, (nuint)newSize * elemSize, AlignOf<T>(), option);
_count = newSize;
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle.Update(_buffer, (nuint)newSize * elemSize);
#endif
}
/// <inheritdoc/>
@@ -407,42 +384,19 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
{
if (!IsCreated)
{
#if DEBUG
if (_buffer == null)
{
return;
}
var message = "The UnsafeArray is not created or already disposed.";
#if MHP_ENABLE_STACKTRACE
var stackTrace = new StackTrace(1, true);
var sb = new System.Text.StringBuilder();
foreach (var frame in stackTrace.GetFrames())
{
var fileName = frame?.GetFileName();
if (frame != null)
{
var methodInfo = DiagnosticMethodInfo.Create(frame);
sb.AppendLine($"File: {fileName}, Type: {methodInfo?.DeclaringTypeName}, Method: {methodInfo?.Name}, Line: {frame.GetFileLineNumber()}");
}
}
message += Environment.NewLine + sb.ToString();
#endif
Debug.WriteLine(message);
#endif
UnsafeCollectionUtility.ReportDoubleFree<UnsafeArray<T>>(_buffer);
return;
}
if (_allocationHandle.Free != null)
{
_allocationHandle.Free(_allocationHandle.State, _buffer
#if MHP_ENABLE_SAFETY_CHECKS
, _memoryHandle
#endif
);
_allocationHandle.Free(_allocationHandle.State, _buffer);
}
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle.Dispose();
#endif
_buffer = null;
_count = 0;
}

View File

@@ -0,0 +1,361 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
/// <summary>
/// A dynamically resizing, parallel, lock-free queue using unmanaged chunks.
/// Uses a very brief spin lock only during chunk allocation, alongside a lock-free segment cache.
/// </summary>
public unsafe struct UnsafeChunkedQueue<T> : IDisposable
where T : unmanaged
{
[StructLayout(LayoutKind.Sequential)]
private struct ChunkSlot
{
// 0 = Empty, 1 = Ready (Writer has finished writing)
public int state;
public T value;
}
[StructLayout(LayoutKind.Sequential)]
private struct ChunkHeader
{
public ChunkHeader* next;
public ChunkHeader* nextFree;
public int capacity;
// Cache line padding to avoid false sharing between atomic counters
private readonly long _pad1, _pad2, _pad3;
public int head;
private readonly long _pad4, _pad5, _pad6;
public int tail;
private readonly long _pad7, _pad8, _pad9;
public int consumedSlots;
}
public readonly unsafe struct ParallelProducer
{
private readonly UnsafeChunkedQueue<T>* _queue;
internal ParallelProducer(UnsafeChunkedQueue<T>* queue)
{
_queue = queue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Enqueue(T item) => _queue->Enqueue(item);
}
public readonly unsafe struct ParallelConsumer
{
private readonly UnsafeChunkedQueue<T>* _queue;
internal ParallelConsumer(UnsafeChunkedQueue<T>* queue)
{
_queue = queue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryDequeue(out T item) => _queue->TryDequeue(out item);
}
// Pointer representations (nint utilized for straightforward Interlocked compatibility)
private nint _head;
private nint _tail;
private nint _freeList;
private int _expandLock;
#if MHP_ENABLE_SAFETY_CHECKS
private readonly MemoryHandle _memoryHandle;
#endif
private readonly AllocationHandle _allocHandle;
private readonly AllocationOption _allocOption;
private readonly int _chunkCapacity;
public readonly bool IsCreated => _head != 0;
public UnsafeChunkedQueue(int capacityPerChunk, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
_chunkCapacity = Math.Max(32, capacityPerChunk);
_allocHandle = handle;
_allocOption = allocationOption;
_freeList = 0;
_expandLock = 0;
// Preallocate the first chunk
var initialChunk = AllocateNewChunk();
_head = (nint)initialChunk;
_tail = (nint)initialChunk;
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle = MemoryHandle.Create(initialChunk, (nuint)(_chunkCapacity * sizeof(ChunkSlot)));
#endif
}
public UnsafeChunkedQueue(int capacityPerChunk, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(capacityPerChunk, AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
/// <summary>
/// Try to enqueue an item. Expands automatically if the current chunk is full.
/// </summary>
public void Enqueue(T item)
{
SpinWait spin = new SpinWait();
while (true)
{
ChunkHeader* tail = (ChunkHeader*)_tail;
// Reserve our slot
int tailIdx = Interlocked.Increment(ref tail->tail) - 1;
if (tailIdx < tail->capacity)
{
// Slot secured. Let's write.
var slot = (ChunkSlot*)(tail + 1) + tailIdx;
slot->value = item;
Volatile.Write(ref slot->state, 1); // Mark as readable
return;
}
// Chunk is full. Expand the queue.
if (Interlocked.CompareExchange(ref _expandLock, 1, 0) == 0)
{
// Verify no other thread already expanded
if (tail == (ChunkHeader*)_tail)
{
var newChunk = GetChunkFromPoolOrAllocate();
// Pre-write our object onto the new chunk's first spot safely
newChunk->tail = 1;
var slot = (ChunkSlot*)(newChunk + 1) + 0;
slot->value = item;
Volatile.Write(ref slot->state, 1);
// Attach new chunk
Volatile.Write(ref *(nint*)&tail->next, (nint)newChunk);
Volatile.Write(ref _tail, (nint)newChunk);
Volatile.Write(ref _expandLock, 0);
return;
}
Volatile.Write(ref _expandLock, 0); // Release if another thread expanded
}
// Another thread is allocating the chunk. Spin and retry.
spin.SpinOnce();
}
}
/// <summary>
/// Attempts to dequeue an item.
/// </summary>
public bool TryDequeue(out T item)
{
SpinWait spin = new SpinWait();
while (true)
{
ChunkHeader* head = (ChunkHeader*)Volatile.Read(ref _head);
if (head == null)
{
item = default;
return false;
}
int currentHead = Volatile.Read(ref head->head);
int currentTail = Volatile.Read(ref head->tail);
if (currentHead >= head->capacity)
{
// Current chunk exhausted. Advance _head to Next chunk.
ChunkHeader* next = (ChunkHeader*)Volatile.Read(ref *(nint*)&head->next);
if (next != null)
{
if (Interlocked.CompareExchange(ref _head, (nint)next, (nint)head) == (nint)head)
{
// Successfully unlinked this chunk from _head.
// If all slots have already been read, recycle it safely now!
if (Volatile.Read(ref head->consumedSlots) >= head->capacity)
{
RecycleChunk(head);
}
}
continue;
}
else
{
// We reached the end of the chunks, but a writer might be locking to expand right now.
if (Volatile.Read(ref _expandLock) == 1)
{
spin.SpinOnce();
continue;
}
item = default;
return false;
}
}
// Prevent infinite loop: if head has caught up to tail, the queue chunk is empty.
if (currentHead >= currentTail)
{
item = default;
return false;
}
// Try to acquire the slot at currentHead lock-free
if (Interlocked.CompareExchange(ref head->head, currentHead + 1, currentHead) == currentHead)
{
var slot = (ChunkSlot*)(head + 1) + currentHead;
// Wait until the Enqueuing thread has finished writing (usually 0 spins)
SpinWait innerWait = new SpinWait();
while (Volatile.Read(ref slot->state) == 0)
{
innerWait.SpinOnce();
}
item = slot->value;
// Track how many values have been permanently read
int consumed = Interlocked.Increment(ref head->consumedSlots);
// We recycle only if all readers are done AND this chunk is already detached from _head
// (prevents ABA object reuse crashes where _head still points to a recycled memory block).
if (consumed >= head->capacity && Volatile.Read(ref _head) != (nint)head)
{
RecycleChunk(head);
}
return true;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ChunkHeader* GetChunkFromPoolOrAllocate()
{
// Pop lock-free from the free list
while (true)
{
ChunkHeader* free = (ChunkHeader*)Volatile.Read(ref _freeList);
if (free == null)
{
break;
}
var nextFree = free->nextFree;
if (Interlocked.CompareExchange(ref _freeList, (nint)nextFree, (nint)free) == (nint)free)
{
// Reset chunk
free->next = null;
free->nextFree = null;
free->head = 0;
free->tail = 0;
free->consumedSlots = 0;
var slots = (ChunkSlot*)(free + 1);
MemClear(slots, (uint)(_chunkCapacity * sizeof(ChunkSlot)));
return free;
}
}
return AllocateNewChunk();
}
private readonly ChunkHeader* AllocateNewChunk()
{
nuint byteSize = (nuint)sizeof(ChunkHeader) + (nuint)(_chunkCapacity * sizeof(ChunkSlot));
ChunkHeader* block = (ChunkHeader*)_allocHandle.Alloc(_allocHandle.State, byteSize, AlignOf<int>(), _allocOption);
block->next = null;
block->nextFree = null;
block->capacity = _chunkCapacity;
block->head = 0;
block->tail = 0;
block->consumedSlots = 0;
var slots = (ChunkSlot*)(block + 1);
MemClear(slots, (uint)(_chunkCapacity * sizeof(ChunkSlot)));
return block;
}
private void RecycleChunk(ChunkHeader* chunk)
{
// Push lock-free to the free list
while (true)
{
ChunkHeader* free = (ChunkHeader*)Volatile.Read(ref _freeList);
chunk->nextFree = free;
if (Interlocked.CompareExchange(ref _freeList, (nint)chunk, (nint)free) == (nint)free)
{
break;
}
}
}
/// <summary>
/// Returns a parallel producer for this queue. The returned struct contains a raw pointer
/// to the queue and can be used from multiple threads as long as the queue struct itself
/// remains alive and its address stable.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ParallelProducer AsParallelProducer()
{
return new ParallelProducer((UnsafeChunkedQueue<T>*)Unsafe.AsPointer(ref this));
}
/// <summary>
/// Returns a parallel consumer for this queue. The returned struct contains a raw pointer
/// to the queue and can be used from multiple threads as long as the queue struct itself
/// remains alive and its address stable.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ParallelConsumer AsParallelConsumer()
{
return new ParallelConsumer((UnsafeChunkedQueue<T>*)Unsafe.AsPointer(ref this));
}
public void Dispose()
{
if (!IsCreated)
{
return;
}
// Dispose Active Chunks
ChunkHeader* curr = (ChunkHeader*)_head;
while (curr != null)
{
var next = curr->next;
_allocHandle.Free(_allocHandle.State, curr);
curr = next;
}
// Dispose FreeList cache Chunks
ChunkHeader* free = (ChunkHeader*)_freeList;
while (free != null)
{
var next = free->nextFree;
_allocHandle.Free(_allocHandle.State, free);
free = next;
}
#if MHP_ENABLE_SAFETY_CHECKS
_memoryHandle.Dispose();
#endif
_head = 0;
_tail = 0;
_freeList = 0;
}
}

View File

@@ -1,7 +1,6 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;