Refactor memory management and improve allocation APIs

Updated `ReallocFunc` to support `oldSize`, `newSize`, and `AllocationOption`, enabling more granular control over memory reallocation. Simplified `AllocationInfo` by removing the `FreeHandler` property. Enhanced `Reallocate` and `Allocate` methods in `AllocationManager` to handle memory clearing and tracking more effectively.

Refactored `UnsafeSparseSet` to use `UnsafeArray<T>` directly, added a `generations` array, and replaced the `freeList` with `UnsafeStack<int>` for better performance and simplicity. Updated `Resize`, `Add`, and `Remove` methods to improve memory handling and code clarity.

Introduced `AllocationOption` support in `Resize` methods across `IUnsafeCollection` implementations for flexible resizing behavior. Added `GetUnsafePtr` extension methods in `UnsafeUtilities` for direct access to span data.

Added new tests for `UnsafeSparseSet` to validate resizing, clearing, enumeration, and memory compaction. These changes improve memory management, enhance performance, and ensure correctness.
This commit is contained in:
2025-10-08 15:40:49 +09:00
parent a92ab93731
commit 081103372f
14 changed files with 236 additions and 123 deletions

View File

@@ -2,5 +2,5 @@
global using unsafe AllocFunc = delegate*<void*, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, void*>; global using unsafe AllocFunc = delegate*<void*, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, void*>;
global using unsafe ReallocFunc = delegate*<void*, void*, nuint, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, void*>;
global using unsafe FreeFunc = delegate*<void*, void*, void>; global using unsafe FreeFunc = delegate*<void*, void*, void>;
global using unsafe ReallocFunc = delegate*<void*, void*, nuint, nuint, void*>;

View File

@@ -28,14 +28,6 @@ public readonly unsafe struct AllocationInfo
get; init; get; init;
} }
/// <summary>
/// Get the function pointer used to free the allocated memory.
/// </summary>
public FreeFunc FreeHandler
{
get; init;
}
/// <summary> /// <summary>
/// Get the stack trace at the time of allocation for debugging purposes. /// Get the stack trace at the time of allocation for debugging purposes.
/// </summary> /// </summary>
@@ -72,11 +64,19 @@ public static unsafe class AllocationManager
return ptr; return ptr;
} }
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment) private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{ {
var selfPtr = (ArenaAllocator*)instance; var selfPtr = (ArenaAllocator*)instance;
var newPtr = selfPtr->_arena.Allocate(size, alignment, AllocationOption.None); var newPtr = selfPtr->_arena.Allocate(newSize, alignment, allocationOption);
MemCpy(newPtr, ptr, size); MemCpy(newPtr, ptr, newSize);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
if (newSize > oldSize)
{
MemClear((byte*)newPtr + oldSize, newSize - oldSize);
}
}
// We do not free the old pointer here, as it is managed by the arena. // We do not free the old pointer here, as it is managed by the arena.
return newPtr; return newPtr;
@@ -113,23 +113,39 @@ public static unsafe class AllocationManager
{ {
var ptr = AlignedAlloc(size, alignment); var ptr = AlignedAlloc(size, alignment);
if (!allocationOption.HasFlag(AllocationOption.UnTracked))
{
TrackAllocation(ptr, size, instance, &FreeBlock);
}
if (allocationOption.HasFlag(AllocationOption.Clear)) if (allocationOption.HasFlag(AllocationOption.Clear))
{ {
MemClear(ptr, size); MemClear(ptr, size);
} }
if (!allocationOption.HasFlag(AllocationOption.UnTracked))
{
TrackAllocation(ptr, size, instance);
}
return ptr; return ptr;
} }
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment) private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{ {
var newPtr = AlignedRealloc(ptr, size, alignment); var newPtr = AlignedRealloc(ptr, newSize, alignment);
UpdateAllocation(ptr, newPtr, size, instance, &FreeBlock);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
if (newSize > oldSize)
{
MemClear((byte*)newPtr + oldSize, newSize - oldSize);
}
}
if (!allocationOption.HasFlag(AllocationOption.UnTracked))
{
UpdateAllocation(ptr, newPtr, newSize, instance);
}
else
{
UntrackAllocation(ptr);
}
return newPtr; return newPtr;
} }
@@ -161,10 +177,19 @@ public static unsafe class AllocationManager
return ptr; return ptr;
} }
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment) private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{ {
var newPtr = s_stack.Allocate(size, alignment, AllocationOption.None); var newPtr = s_stack.Allocate(newSize, alignment, AllocationOption.None);
MemCpy(newPtr, ptr, size); MemCpy(newPtr, ptr, newSize);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
if (newSize > oldSize)
{
MemClear((byte*)newPtr + oldSize, newSize - oldSize);
}
}
// We do not free the old pointer here, as it is managed by the stack. // We do not free the old pointer here, as it is managed by the stack.
return newPtr; return newPtr;
} }
@@ -240,7 +265,7 @@ public static unsafe class AllocationManager
/// <param name="allocator">The allocator used for the allocation.</param> /// <param name="allocator">The allocator used for the allocation.</param>
/// <param name="freeFunc">The function pointer used to free the allocated memory.</param> /// <param name="freeFunc">The function pointer used to free the allocated memory.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void TrackAllocation(void* ptr, nuint allocationSize, void* allocator, FreeFunc freeFunc) public static void TrackAllocation(void* ptr, nuint allocationSize, void* allocator)
{ {
if (!s_debugLayer || s_allocated == null || ptr == null) if (!s_debugLayer || s_allocated == null || ptr == null)
{ {
@@ -251,7 +276,6 @@ public static unsafe class AllocationManager
{ {
Size = allocationSize, Size = allocationSize,
Allocator = allocator, Allocator = allocator,
FreeHandler = freeFunc,
StackTrace = new StackTrace(true) StackTrace = new StackTrace(true)
}; };
} }
@@ -265,7 +289,7 @@ public static unsafe class AllocationManager
/// <param name="allocator">A pointer to the allocator responsible for the new allocation.</param> /// <param name="allocator">A pointer to the allocator responsible for the new allocation.</param>
/// <param name="freeFunc">A delegate or function pointer used to free the memory associated with the allocation.</param> /// <param name="freeFunc">A delegate or function pointer used to free the memory associated with the allocation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UpdateAllocation(void* oldPtr, void* newPtr, nuint allocationSize, void* allocator, FreeFunc freeFunc) public static void UpdateAllocation(void* oldPtr, void* newPtr, nuint allocationSize, void* allocator)
{ {
if (!s_debugLayer || s_allocated == null || oldPtr == null || newPtr == null) if (!s_debugLayer || s_allocated == null || oldPtr == null || newPtr == null)
{ {
@@ -279,7 +303,6 @@ public static unsafe class AllocationManager
{ {
Size = allocationSize, Size = allocationSize,
Allocator = allocator, Allocator = allocator,
FreeHandler = freeFunc,
StackTrace = info.StackTrace StackTrace = info.StackTrace
}; };
} }
@@ -330,7 +353,6 @@ public static unsafe class AllocationManager
foreach (var pair in s_allocated) foreach (var pair in s_allocated)
{ {
unfreeBytes += pair.Value.Size; unfreeBytes += pair.Value.Size;
pair.Value.FreeHandler(pair.Value.Allocator, (void*)pair.Key);
} }
if (unfreeBytes > 0u) if (unfreeBytes > 0u)

View File

@@ -1,4 +1,6 @@
namespace Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Buffer;
namespace Misaki.HighPerformance.LowLevel.Collections.Contracts;
public unsafe interface IUnsafeCollection : IDisposable public unsafe interface IUnsafeCollection : IDisposable
{ {
@@ -38,7 +40,7 @@ public unsafe interface IUnsafeCollection<T> : IUnsafeCollection, IEnumerable<T>
/// </summary> /// </summary>
/// <remarks>This is to adjust the element count of the collection, not the size of the underlying buffer in memory.</remarks> /// <remarks>This is to adjust the element count of the collection, not the size of the underlying buffer in memory.</remarks>
/// <param name="newSize">Specifies the new size to which the collection should be adjusted.</param> /// <param name="newSize">Specifies the new size to which the collection should be adjusted.</param>
public void Resize(int newSize); public void Resize(int newSize, AllocationOption option);
} }
public unsafe interface IUnTypedCollection : IUnsafeCollection public unsafe interface IUnTypedCollection : IUnsafeCollection

View File

@@ -79,14 +79,14 @@ public unsafe struct UnTypedArray : IUnTypedCollection
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Resize(uint newSize) public void Resize(uint newSize, AllocationOption option = AllocationOption.None)
{ {
if (newSize == _size) if (newSize == _size)
{ {
return; return;
} }
_buffer = _handle->Realloc(_handle->Allocator, _buffer, newSize, _alignment); _buffer = _handle->Realloc(_handle->Allocator, _buffer, _size, newSize, _alignment, option);
_size = newSize; _size = newSize;
} }

View File

@@ -168,14 +168,15 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
if (newSize == _count) if (newSize == _count)
{ {
return; return;
} }
_buffer = (T*)_handle->Realloc(_handle->Allocator, _buffer, (nuint)newSize * SizeOf<T>(), AlignOf<T>()); var elemSize = SizeOf<T>();
_buffer = (T*)_handle->Realloc(_handle->Allocator, _buffer, (nuint)_count * elemSize, (nuint)newSize * elemSize, AlignOf<T>(), option);
_count = newSize; _count = newSize;
} }

View File

@@ -184,7 +184,7 @@ public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeCollection<KeyValuePai
/// </summary> /// </summary>
public void TrimExcess() => _hashMap.TrimExcess(); public void TrimExcess() => _hashMap.TrimExcess();
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
_hashMap.Resize(newSize); _hashMap.Resize(newSize);
} }

View File

@@ -114,7 +114,7 @@ public unsafe struct UnsafeHashSet<T> : IUnsafeCollection<T>, IEnumerable<T>
return _hashMap.GetKeyArray(allocator); return _hashMap.GetKeyArray(allocator);
} }
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
_hashMap.Resize(newSize); _hashMap.Resize(newSize);
} }

View File

@@ -362,9 +362,9 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
RemoveRangeSwapBack(index, 1); RemoveRangeSwapBack(index, 1);
} }
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
_array.Resize(newSize); _array.Resize(newSize, option);
if (_count > newSize) if (_count > newSize)
{ {

View File

@@ -170,9 +170,9 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
return true; return true;
} }
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
_array.Resize(newSize); _array.Resize(newSize, option);
if (_count > newSize) if (_count > newSize)
{ {

View File

@@ -276,10 +276,10 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
return ref slot.value; return ref slot.value;
} }
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
_data.Resize(newSize); _data.Resize(newSize, option);
_freeSlots.Resize(newSize); _freeSlots.Resize(newSize, option);
_capacity = newSize; _capacity = newSize;
} }

View File

@@ -17,12 +17,6 @@ namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T> public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
where T : unmanaged where T : unmanaged
{ {
private struct SlotEntry
{
public T value;
public int generation;
}
public struct Enumerator : IEnumerator<T> public struct Enumerator : IEnumerator<T>
{ {
private readonly UnsafeSparseSet<T>* _collection; private readonly UnsafeSparseSet<T>* _collection;
@@ -40,14 +34,10 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
public bool MoveNext() public bool MoveNext()
{ {
_index++; _index++;
if (_index < _collection->_sparse.Count) if (_index < _collection->_count)
{ {
var index = _collection->_sparse[_index]; _value = _collection->_dense[_index];
if (index >= 0 && index < _collection->_count) return true;
{
_value = _collection->_dense[index].value;
return true;
}
} }
_value = default; _value = default;
@@ -76,17 +66,19 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
} }
private UnsafeArray<SlotEntry> _dense; private UnsafeArray<T> _dense;
private UnsafeArray<int> _generations;
private UnsafeArray<int> _sparse; private UnsafeArray<int> _sparse;
private UnsafeArray<int> _reverse; // Maps dense index to sparse index private UnsafeArray<int> _reverse; // Maps dense index to sparse index. Since this is a general purpose sparse set, we have to include reverse array. In real world ecs, this should be replaced with entity id array.
private UnsafeArray<int> _freeList; // Stack of available sparse indices private UnsafeStack<int> _freeSparse;
private int _count; private int _count;
private int _nextId; // Next available sparse index private int _nextId; // Next available sparse index
private int _freeCount; // Number of items in the free list private int _capacity;
public readonly int Count => _count; public readonly int Count => _count;
public readonly int Capacity => _dense.Count; public readonly int Capacity => _capacity;
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated && _freeList.IsCreated; public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeSparseSet<T>*)UnsafeUtilities.AddressOf(ref this)); public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeSparseSet<T>*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@@ -113,13 +105,15 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
} }
_dense = new UnsafeArray<SlotEntry>(capacity, ref handle, allocationOption); _dense = new UnsafeArray<T>(capacity, ref handle, allocationOption);
_generations = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_sparse = new UnsafeArray<int>(capacity, ref handle, allocationOption); _sparse = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_reverse = new UnsafeArray<int>(capacity, ref handle, allocationOption); _reverse = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_freeList = new UnsafeArray<int>(capacity, ref handle, allocationOption); _freeSparse = new UnsafeStack<int>(capacity, ref handle, allocationOption);
_count = 0; _count = 0;
_nextId = 0; _nextId = 0;
_freeCount = 0; _capacity = capacity;
Clear(); Clear();
} }
@@ -136,6 +130,12 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
{ {
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly ref T GetDenseReferenceUnchecked(int sparseIndex)
{
return ref _dense[_sparse[sparseIndex]];
}
/// <summary> /// <summary>
/// Adds a value to the sparse set and returns a unique sparse index for the value. /// Adds a value to the sparse set and returns a unique sparse index for the value.
/// </summary> /// </summary>
@@ -144,15 +144,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
/// <returns>A unique sparse index that can be used to reference this value.</returns> /// <returns>A unique sparse index that can be used to reference this value.</returns>
public int Add(T value, out int generation) public int Add(T value, out int generation)
{ {
int sparseIndex; if (!_freeSparse.TryPop(out var sparseIndex))
// Try to reuse a freed sparse index first
if (_freeCount > 0)
{
_freeCount--;
sparseIndex = _freeList[_freeCount];
}
else
{ {
// Use the next available ID // Use the next available ID
sparseIndex = _nextId++; sparseIndex = _nextId++;
@@ -167,21 +159,18 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
// Resize dense arrays if necessary // Resize dense arrays if necessary
if (_count >= _dense.Count) if (_count >= _dense.Count)
{ {
var newCapacity = _dense.Count + Math.Max(1, _dense.Count / 2); var newCapacity = _dense.Count + (int)Math.Max(1, _dense.Count * 0.5f);
_dense.Resize(newCapacity); Resize(newCapacity);
_reverse.Resize(newCapacity);
} }
// Add the value to the dense array and update mappings // Add the value to the dense array and update mappings
ref var entry = ref _dense[_count]; _dense[_count] = value;
entry.value = value;
_sparse[sparseIndex] = _count; _sparse[sparseIndex] = _count;
_reverse[_count] = sparseIndex; _reverse[_count] = sparseIndex;
_count++; _count++;
generation = entry.generation; generation = _generations[sparseIndex];
return sparseIndex; return sparseIndex;
} }
@@ -216,17 +205,9 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
// Mark the sparse index as unused and add to free list // Mark the sparse index as unused and add to free list
_sparse[sparseIndex] = -1; _sparse[sparseIndex] = -1;
_dense[lastIndex].generation++; // Increment generation to invalidate old references _generations[sparseIndex]++; // Increment generation to invalidate old references
// Add the freed sparse index to the free list for reuse
if (_freeCount >= _freeList.Count)
{
_freeList.Resize(_freeList.Count + Math.Max(1, _freeList.Count / 2));
}
_freeList[_freeCount] = sparseIndex;
_freeCount++;
_freeSparse.Push(sparseIndex);
_count--; _count--;
return true; return true;
@@ -247,7 +228,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
var denseIndex = _sparse[sparseIndex]; var denseIndex = _sparse[sparseIndex];
return denseIndex >= 0 && denseIndex < _count && _dense[denseIndex].generation == generation; return denseIndex >= 0 && denseIndex < _count && _generations[denseIndex] == generation;
} }
/// <summary> /// <summary>
@@ -261,10 +242,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
{ {
if (Contains(sparseIndex, generation)) if (Contains(sparseIndex, generation))
{ {
var denseIndex = _sparse[sparseIndex]; value = GetDenseReferenceUnchecked(sparseIndex);
ref var entry = ref _dense[denseIndex];
value = entry.value;
return true; return true;
} }
@@ -286,10 +264,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index and feneration not found in the set."); throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index and feneration not found in the set.");
} }
var denseIndex = _sparse[sparseIndex]; return GetDenseReferenceUnchecked(sparseIndex);
ref var entry = ref _dense[denseIndex];
return entry.value;
} }
/// <summary> /// <summary>
@@ -308,12 +283,8 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
return ref Unsafe.NullRef<T>(); return ref Unsafe.NullRef<T>();
} }
var denseIndex = _sparse[sparseIndex];
ref var entry = ref _dense[denseIndex];
exist = true; exist = true;
return ref GetDenseReferenceUnchecked(sparseIndex);
return ref entry.value;
} }
/// <summary> /// <summary>
@@ -330,10 +301,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
return false; return false;
} }
var denseIndex = _sparse[sparseIndex]; GetDenseReferenceUnchecked(sparseIndex) = value;
ref var entry = ref _dense[denseIndex];
entry.value = value;
return true; return true;
} }
@@ -354,32 +322,32 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
_sparse.AsSpan().Fill(-1); _sparse.AsSpan().Fill(-1);
_generations.AsSpan().Clear();
_count = 0; _count = 0;
_nextId = 0; _nextId = 0;
_freeCount = 0;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
if (newSize <= 0) if (newSize <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be greater than zero."); throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be greater than zero.");
} }
_dense.Resize(newSize); var oldSize = _capacity;
_reverse.Resize(newSize);
_freeList.Resize(newSize); _dense.Resize(newSize, option);
_generations.Resize(newSize, option | AllocationOption.Clear);
_reverse.Resize(newSize, option);
if (newSize > _sparse.Count) if (newSize > _sparse.Count)
{ {
ResizeSparse(newSize); ResizeSparse(newSize);
} }
if (_count > newSize) _capacity = newSize;
{
_count = newSize;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -403,11 +371,12 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
public void Dispose() public void Dispose()
{ {
_dense.Dispose(); _dense.Dispose();
_generations.Dispose();
_sparse.Dispose(); _sparse.Dispose();
_reverse.Dispose(); _reverse.Dispose();
_freeList.Dispose(); _freeSparse.Dispose();
_count = 0; _count = 0;
_nextId = 0; _nextId = 0;
_freeCount = 0;
} }
} }

View File

@@ -126,9 +126,9 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
return _array[_count - 1]; return _array[_count - 1];
} }
public void Resize(int newSize) public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{ {
_array.Resize(newSize); _array.Resize(newSize, option);
if (_count > newSize) if (_count > newSize)
{ {

View File

@@ -156,4 +156,34 @@ public static unsafe class UnsafeUtilities
{ {
return new UnsafeArray<TOut>((TOut*)array.GetUnsafePtr(), array.Count * sizeof(TIn) / sizeof(TOut)); return new UnsafeArray<TOut>((TOut*)array.GetUnsafePtr(), array.Count * sizeof(TIn) / sizeof(TOut));
} }
/// <summary>
/// Returns a pointer to the first element of the specified span. This method enables direct, unsafe access to the underlying data of the span.
/// </summary>
/// <typeparam name="T">The type of elements in the span. Must be an unmanaged type.</typeparam>
/// <param name="span">The span whose underlying data pointer is to be obtained.</param>
/// <returns>A pointer to the first element of the span. If the span is empty, the returned pointer is undefined and must not be dereferenced.</returns>
public static T* GetUnsafePtr<T>(this Span<T> span)
where T : unmanaged
{
fixed (T* ptr = span)
{
return ptr;
}
}
/// <summary>
/// Returns a pointer to the first element of the specified span. This method enables direct, unsafe access to the underlying data of the span.
/// </summary>
/// <typeparam name="T">The type of elements in the span. Must be an unmanaged type.</typeparam>
/// <param name="span">The span whose underlying data pointer is to be obtained.</param>
/// <returns>A pointer to the first element of the span. If the span is empty, the returned pointer is undefined and must not be dereferenced.</returns>
public static T* GetUnsafePtr<T>(this ReadOnlySpan<T> span)
where T : unmanaged
{
fixed (T* ptr = span)
{
return ptr;
}
}
} }

View File

@@ -50,4 +50,93 @@ public class TestUnsafeSparseSet
Assert.AreEqual(id, newId); Assert.AreEqual(id, newId);
Assert.AreNotEqual(gen, newGen); Assert.AreNotEqual(gen, newGen);
} }
[TestMethod]
public unsafe void Resize()
{
const int count = 20;
var indices = stackalloc int[count];
var generations = stackalloc int[count];
for (var i = 0; i < count; i++)
{
indices[i] = _sparseSet.Add(i, out generations[i]);
}
Assert.AreEqual(count, _sparseSet.Count);
for (var i = 0; i < count; i++)
{
Assert.IsTrue(_sparseSet.Contains(indices[i], generations[i]));
}
}
[TestMethod]
public void Clear()
{
var id1 = _sparseSet.Add(10, out var gen1);
var id2 = _sparseSet.Add(20, out var gen2);
Assert.AreEqual(2, _sparseSet.Count);
_sparseSet.Clear();
Assert.AreEqual(0, _sparseSet.Count);
Assert.IsFalse(_sparseSet.Contains(id1, gen1));
Assert.IsFalse(_sparseSet.Contains(id2, gen2));
}
[TestMethod]
public unsafe void Enumerate()
{
const int count = 3;
var values = stackalloc int[count] { 10, 20, 30 };
var ids = stackalloc int[count];
var gens = stackalloc int[count];
for (var i = 0; i < count; i++)
{
ids[i] = _sparseSet.Add(values[i], out gens[i]);
}
var index = 0;
foreach (var value in _sparseSet)
{
Assert.AreEqual(values[index], value);
index++;
}
Assert.AreEqual(count, index);
}
[TestMethod]
public unsafe void MemoryCompact()
{
const int count = 3;
var values = stackalloc int[count] { 10, 20, 30 };
var ids = stackalloc int[count];
var gens = stackalloc int[count];
for (var i = 0; i < count; i++)
{
ids[i] = _sparseSet.Add(values[i], out gens[i]);
}
_sparseSet.Remove(ids[1], gens[1]); // Remove the second element (20)
var ptr = (int*)_sparseSet.GetUnsafePtr();
Assert.AreEqual(2, _sparseSet.Count);
var index = 0;
foreach (var value in _sparseSet)
{
Assert.AreEqual(ptr[index], value);
index++;
}
Assert.AreEqual(2, index);
}
} }