Refactor memory management with MemoryHandle

Replaced `SafeHandle` with a new `MemoryHandle` system for improved memory tracking, safety, and leak detection. Updated allocators (`ArenaAllocator`, `HeapAllocator`, `StackAllocator`) and collections (`UnTypedArray`, `UnsafeArray<T>`, `UnsafeBitSet`) to use `MemoryHandle`.

Refactored `AllocationManager` to use `ConcurrentSlotMap` for live allocation tracking and added methods for managing `MemoryHandle` instances. Simplified alignment and padding logic across allocators and collections.

Enhanced performance with optimized memory operations (`MemClear`, `MemSet`, `MemCpy`) and vectorized operations in `MemoryUtility` and `UnsafeBitSet`. Fixed alignment issues in vectorized memory operations.

Updated tests to reflect the new memory management system and added new tests for `UnsafeBitSet` bitwise operations. Enabled `ENABLE_COLLECTION_CHECKS` for debug builds and improved error messages and documentation.

Removed unused `SafeHandle` code and adjusted project configuration to include necessary references.
This commit is contained in:
2025-11-25 00:56:21 +09:00
parent 517abd64d6
commit 3269244ab1
15 changed files with 478 additions and 307 deletions

View File

@@ -77,7 +77,8 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
private readonly int _sizeOfTValue;
private readonly int _log2MinGrowth;
private AllocationHandle* _handle;
private MemoryHandle _memoryHandle;
private AllocationHandle* _allocationHandle;
public const int MINIMAL_CAPACITY = 64;
@@ -89,43 +90,22 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
public readonly bool IsEmpty => !IsCreated || _count == 0;
public readonly bool IsCreated
public readonly bool IsCreated => _buffer != null && _allocationHandle != null && _memoryHandle.IsValid;
private static int CalculateDataSize(int capacity, int bucketCapacity, int sizeOfTValue, out int outKeyOffset, out int outNextOffset, out int outBucketOffset)
{
get
{
var handle = SafeHandle.GetSafeHandle(_buffer, (nuint)_alignment);
return handle != null && Volatile.Read(ref handle->valid) == 1;
}
}
var sizeOfTKey = sizeof(TKey);
var sizeOfInt = sizeof(int);
private static int CalculateDataSize(int capacity, int bucketCapacity, int sizeOfTValue, int alignment, out int outValueOffset, out int outKeyOffset, out int outNextOffset, out int outBucketOffset)
{
static int AlignUp(int size, int align)
{
return (size + (align - 1)) & ~(align - 1);
}
var valuesSize = sizeOfTValue * capacity;
var keysSize = sizeOfTKey * capacity;
var nextSize = sizeOfInt * capacity;
var bucketSize = sizeOfInt * bucketCapacity;
var totalSize = valuesSize + keysSize + nextSize + bucketSize;
var headerSize = SafeHandle.GetPaddedHeaderSize((nuint)alignment);
var valuesSize = (sizeOfTValue * capacity);
var valuesSizePadded = AlignUp(valuesSize, alignment);
var keysSize = (sizeof(TKey) * capacity);
var keysSizePadded = AlignUp(keysSize, alignment);
var nextSize = (sizeof(int) * capacity);
var nextSizePadded = AlignUp(nextSize, alignment);
// Buckets are the last item, doesn't need padding after it
var bucketSize = (sizeof(int) * bucketCapacity);
outValueOffset = (int)headerSize;
outKeyOffset = outValueOffset + valuesSizePadded;
outNextOffset = outKeyOffset + keysSizePadded;
outBucketOffset = outNextOffset + nextSizePadded;
// Total size is header + all buffers
var totalSize = (int)headerSize + outKeyOffset + keysSizePadded + nextSizePadded + bucketSize;
outKeyOffset = 0 + valuesSize;
outNextOffset = outKeyOffset + keysSize;
outBucketOffset = outNextOffset + nextSize;
return totalSize;
}
@@ -149,16 +129,16 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
var alignOfInt = (int)AlignOf<int>();
var maxDataAlign = Math.Max(Math.Max(alignOfTValue, alignOfKey), alignOfInt);
_alignment = (int)SafeHandle.GetAlignWithHeader((nuint)maxDataAlign);
_alignment = maxDataAlign;
_sizeOfTValue = sizeOfTValue;
_log2MinGrowth = BitOperations.Log2(minGrowth);
_handle = (AllocationHandle*)Unsafe.AsPointer(ref handle);
_allocationHandle = (AllocationHandle*)Unsafe.AsPointer(ref handle);
var totalSize = CalculateDataSize(_capacity, _bucketCapacity, sizeOfTValue, _alignment,
out var valueOffset, out var keyOffset, out var nextOffset, out var bucketOffset);
var totalSize = CalculateDataSize(_capacity, _bucketCapacity, sizeOfTValue,
out var keyOffset, out var nextOffset, out var bucketOffset);
AllocateBuffer(totalSize, valueOffset, keyOffset, nextOffset, bucketOffset, _alignment, allocationOption);
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, allocationOption);
Clear();
}
@@ -210,28 +190,31 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AllocateBuffer(int totalSize, int valueOffset, int keyOffset, int nextOffset, int bucketOffset, int alignment, AllocationOption allocationOption)
private void AllocateBuffer(int totalSize, int keyOffset, int nextOffset, int bucketOffset, AllocationOption allocationOption)
{
var buf = (byte*)_handle->Alloc(_handle->Allocator, (uint)totalSize, (nuint)alignment, allocationOption);
MemoryHandle memHandle;
var buf = (byte*)_allocationHandle->Alloc(_allocationHandle->Allocator, (uint)totalSize, (nuint)_alignment, allocationOption, &memHandle);
_buffer = buf + valueOffset;
_buffer = buf;
_keys = (TKey*)(_buffer + keyOffset);
_next = (int*)(_buffer + nextOffset);
_buckets = (int*)(_buffer + bucketOffset);
_memoryHandle = memHandle;
}
private void ResizeExact(int newCapacity, int newBucketCapacity)
{
var totalSize = CalculateDataSize(newCapacity, newBucketCapacity, _sizeOfTValue, _alignment,
out var valueOffset, out var keyOffset, out var nextOffset, out var bucketOffset);
var totalSize = CalculateDataSize(newCapacity, newBucketCapacity, _sizeOfTValue,
out var keyOffset, out var nextOffset, out var bucketOffset);
var oldBuffer = _buffer;
var oldKeys = _keys;
var oldNext = _next;
var oldBuckets = _buckets;
var oldBucketCapacity = _bucketCapacity;
var oldMemoryHandle = _memoryHandle;
AllocateBuffer(totalSize, valueOffset, keyOffset, nextOffset, bucketOffset, _alignment, AllocationOption.None);
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, AllocationOption.None);
_capacity = newCapacity;
_bucketCapacity = newBucketCapacity;
@@ -246,7 +229,7 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
}
}
_handle->Free(_handle->Allocator, oldBuffer);
_allocationHandle->Free(_allocationHandle->Allocator, oldBuffer, oldMemoryHandle);
}
public void Resize(int newCapacity)
@@ -540,9 +523,9 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
return;
}
if (_handle != null)
if (_allocationHandle != null)
{
_handle->Free(_handle->Allocator, _buffer);
_allocationHandle->Free(_allocationHandle->Allocator, _buffer, _memoryHandle);
}
_buffer = null;

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
@@ -119,7 +119,7 @@ public static unsafe partial class MemoryUtility
{
if ((((uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
{
// Not currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
// Invert currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
// with no upper bound e.g. String.strlen.
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
// This ensures we do not fault across memory pages while searching for an end of string.
@@ -141,7 +141,7 @@ public static unsafe partial class MemoryUtility
if ((((uint)searchSpace + offset) & (nuint)(Vector512<byte>.Count - 1)) != 0)
{
// Not currently aligned to Vector512 (is aligned to Vector256); this can cause a problem for searches
// Invert currently aligned to Vector512 (is aligned to Vector256); this can cause a problem for searches
// with no upper bound e.g. String.strlen.
// Start with a check on Vector256 to align to Vector512, before moving to processing Vector256.
// This ensures we do not fault across memory pages while searching for an end of string.
@@ -232,7 +232,7 @@ public static unsafe partial class MemoryUtility
{
if ((((uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
{
// Not currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
// Invert currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
// with no upper bound e.g. String.strlen.
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
// This ensures we do not fault across memory pages while searching for an end of string.

View File

@@ -92,11 +92,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MemClear(void* ptr, nuint size)
{
if (ptr == null || size == 0)
{
return;
}
NativeMemory.Clear(ptr, size);
}
@@ -109,11 +104,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MemSet(void* ptr, byte value, nuint size)
{
if (ptr == null || size == 0)
{
return;
}
NativeMemory.Fill(ptr, size, value);
}
@@ -126,11 +116,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MemCpy(void* source, void* destination, nuint size)
{
if (source == null || destination == null || size == 0)
{
return;
}
NativeMemory.Copy(source, destination, size);
}