Add UnsafeChunkedList<T> and tests; refactor alloc/util
Introduced high-performance UnsafeChunkedList<T> with parallel-safe add/read, custom enumerator, and chunk management. Added extensive unit tests for all behaviors and concurrency. Refactored AllocationManager zero-replacement logic, improved MemoryUtility alignment methods, and clarified MemoryBlock/UnsafeArray docs. Simplified Program.cs allocation test and updated build constants. Minor cleanups in GGXMipGenerationBenchmark.
This commit is contained in:
@@ -250,6 +250,28 @@ public static unsafe class AllocationManager
|
|||||||
|
|
||||||
private static nuint s_threadLocalStackSize;
|
private static nuint s_threadLocalStackSize;
|
||||||
|
|
||||||
|
private static void ReplaceIfZero(ref AllocationManagerDesc desc, AllocationManagerDesc defaultDesc)
|
||||||
|
{
|
||||||
|
desc.ArenaCapacity = desc.ArenaCapacity != 0
|
||||||
|
? desc.ArenaCapacity
|
||||||
|
: defaultDesc.ArenaCapacity;
|
||||||
|
desc.StackCapacity = desc.StackCapacity != 0
|
||||||
|
? desc.StackCapacity
|
||||||
|
: defaultDesc.StackCapacity;
|
||||||
|
desc.FreeListChunkSize = desc.FreeListChunkSize != 0
|
||||||
|
? desc.FreeListChunkSize
|
||||||
|
: defaultDesc.FreeListChunkSize;
|
||||||
|
desc.FreeListDefaultAlignment = desc.FreeListDefaultAlignment != 0
|
||||||
|
? desc.FreeListDefaultAlignment
|
||||||
|
: defaultDesc.FreeListDefaultAlignment;
|
||||||
|
desc.TLSFAlignment = desc.TLSFAlignment != 0
|
||||||
|
? desc.TLSFAlignment
|
||||||
|
: defaultDesc.TLSFAlignment;
|
||||||
|
desc.TLSFInitialChunkSize = desc.TLSFInitialChunkSize != 0
|
||||||
|
? desc.TLSFInitialChunkSize
|
||||||
|
: defaultDesc.TLSFInitialChunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
public static void Initialize(AllocationManagerDesc desc = default)
|
public static void Initialize(AllocationManagerDesc desc = default)
|
||||||
{
|
{
|
||||||
if (s_initialized)
|
if (s_initialized)
|
||||||
@@ -262,10 +284,7 @@ public static unsafe class AllocationManager
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
var defaultDesc = AllocationManagerDesc.Default;
|
var defaultDesc = AllocationManagerDesc.Default;
|
||||||
|
ReplaceIfZero(ref desc, defaultDesc);
|
||||||
var spanDesc = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref desc, 1));
|
|
||||||
var spanDefault = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref defaultDesc, 1));
|
|
||||||
MemoryUtility.ReplaceIfZeros(spanDesc, spanDefault);
|
|
||||||
|
|
||||||
s_arenaAllocator = new MemoryPool<VirtualArena, VirtualArena.CreationOptions>(new VirtualArena.CreationOptions
|
s_arenaAllocator = new MemoryPool<VirtualArena, VirtualArena.CreationOptions>(new VirtualArena.CreationOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public unsafe struct MemoryBlock : IDisposable
|
|||||||
{
|
{
|
||||||
private void* _buffer;
|
private void* _buffer;
|
||||||
private nuint _size;
|
private nuint _size;
|
||||||
private nuint _alignment;
|
private readonly nuint _alignment;
|
||||||
|
|
||||||
#if MHP_ENABLE_SAFETY_CHECKS
|
#if MHP_ENABLE_SAFETY_CHECKS
|
||||||
private readonly MemoryHandle _memoryHandle;
|
private readonly MemoryHandle _memoryHandle;
|
||||||
@@ -69,13 +69,13 @@ public unsafe struct MemoryBlock : IDisposable
|
|||||||
/// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. This does not copy the data.
|
/// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. This does not copy the data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="buffer">A pointer to the memory location that holds the elements of the array.</param>
|
/// <param name="buffer">A pointer to the memory location that holds the elements of the array.</param>
|
||||||
/// <param name="count">The total size of the data.</param>
|
/// <param name="size">The total size of the data.</param>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
|
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
|
||||||
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
|
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
|
||||||
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
|
/// Use <see cref="MemoryBlock(nuint, nuint, AllocationHandle, AllocationOption)"/> constructor and <see cref="MemoryUtility.MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public MemoryBlock(void* buffer, uint size)
|
public MemoryBlock(void* buffer, nuint size)
|
||||||
{
|
{
|
||||||
_buffer = buffer;
|
_buffer = buffer;
|
||||||
_size = size;
|
_size = size;
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
|
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
|
||||||
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
|
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
|
||||||
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
|
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemoryUtility.MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public UnsafeArray(T* buffer, int count)
|
public UnsafeArray(T* buffer, int count)
|
||||||
{
|
{
|
||||||
|
|||||||
753
Misaki.HighPerformance.LowLevel/Collections/UnsafeChunkedList.cs
Normal file
753
Misaki.HighPerformance.LowLevel/Collections/UnsafeChunkedList.cs
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class UnsafeChunkedListDebugView<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private readonly UnsafeChunkedList<T> _list;
|
||||||
|
|
||||||
|
public UnsafeChunkedListDebugView(UnsafeChunkedList<T> list)
|
||||||
|
{
|
||||||
|
_list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public T[] Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var array = new T[_list.Count];
|
||||||
|
_list.CopyTo(array);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A collection that stores elements in fixed-size chunks, enabling stable element addresses
|
||||||
|
/// and eliminating large reallocation during growth. Adding elements never moves existing ones.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Represents a type that can be stored in the collection, constrained to unmanaged types for performance and safety.</typeparam>
|
||||||
|
[DebuggerTypeProxy(typeof(UnsafeChunkedListDebugView<>))]
|
||||||
|
public unsafe struct UnsafeChunkedList<T> : IUnsafeCollection<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
public const int DEFAULT_CHUNK_SIZE_IN_BYTES = 16384;
|
||||||
|
|
||||||
|
public ref struct Enumerator
|
||||||
|
{
|
||||||
|
private ref UnsafeChunkedList<T> _collection;
|
||||||
|
private int _index;
|
||||||
|
|
||||||
|
public readonly ref T Current
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(_index, _collection._chunkCapacity);
|
||||||
|
return ref ((T*)_collection._chunks[chunkIdx])[offset];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enumerator(ref UnsafeChunkedList<T> collection)
|
||||||
|
{
|
||||||
|
_collection = ref collection;
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
_index++;
|
||||||
|
return _index < _collection._count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A parallel reader for an UnsafeChunkedList.
|
||||||
|
/// </summary>
|
||||||
|
public readonly unsafe struct ParallelReader
|
||||||
|
{
|
||||||
|
public readonly UnsafeChunkedList<T>* listData;
|
||||||
|
public readonly int Count => listData->_count;
|
||||||
|
public readonly int ChunkCapacity => listData->_chunkCapacity;
|
||||||
|
|
||||||
|
public ref readonly T this[int index]
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(index, listData->_chunkCapacity);
|
||||||
|
return ref ((T*)listData->_chunks[chunkIdx])[offset];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ref readonly T this[uint index]
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get => ref this[(int)index];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ParallelReader(UnsafeChunkedList<T>* list)
|
||||||
|
{
|
||||||
|
listData = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
ref var list = ref Unsafe.AsRef<UnsafeChunkedList<T>>(listData);
|
||||||
|
return new Enumerator(ref list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A parallel writer for an UnsafeChunkedList.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Adding elements is thread-safe and auto-allocates chunks as needed, since new chunks never move existing data.
|
||||||
|
/// The chunk pointer array must be pre-sized via <see cref="EnsureCapacity"/> before dispatching parallel writes.
|
||||||
|
/// </remarks>
|
||||||
|
public readonly struct ParallelWriter
|
||||||
|
{
|
||||||
|
public readonly UnsafeChunkedList<T>* listData;
|
||||||
|
|
||||||
|
internal ParallelWriter(UnsafeChunkedList<T>* list)
|
||||||
|
{
|
||||||
|
listData = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safely adds a value, auto-allocating new chunks as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void Add(scoped in T value)
|
||||||
|
{
|
||||||
|
var idx = Interlocked.Increment(ref listData->_count) - 1;
|
||||||
|
var (chunkIdx, offset) = SplitIndex(idx, listData->_chunkCapacity);
|
||||||
|
listData->EnsureChunkParallel(chunkIdx);
|
||||||
|
((T*)listData->_chunks[chunkIdx])[offset] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safely adds a range of elements, auto-allocating new chunks as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRange(ReadOnlySpan<T> collection)
|
||||||
|
{
|
||||||
|
var count = collection.Length;
|
||||||
|
var index = Interlocked.Add(ref listData->_count, count) - count;
|
||||||
|
|
||||||
|
fixed (T* pCollection = collection)
|
||||||
|
{
|
||||||
|
int remaining = count;
|
||||||
|
T* srcPtr = pCollection;
|
||||||
|
int currentIndex = index;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(currentIndex, listData->_chunkCapacity);
|
||||||
|
var copyCount = Math.Min(remaining, listData->_chunkCapacity - offset);
|
||||||
|
listData->EnsureChunkParallel(chunkIdx);
|
||||||
|
var dstPtr = (T*)listData->_chunks[chunkIdx] + offset;
|
||||||
|
MemoryUtility.MemCpy(dstPtr, srcPtr, (nuint)(copyCount * sizeof(T)));
|
||||||
|
srcPtr += copyCount;
|
||||||
|
currentIndex += copyCount;
|
||||||
|
remaining -= copyCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnsafeArray<nint> _chunks;
|
||||||
|
private int _chunkCount;
|
||||||
|
private int _count;
|
||||||
|
private readonly int _chunkCapacity;
|
||||||
|
private readonly AllocationHandle _allocationHandle;
|
||||||
|
|
||||||
|
public readonly int Count => _count;
|
||||||
|
public readonly int ChunkCapacity => _chunkCapacity;
|
||||||
|
public readonly int ChunkCount => _chunkCount;
|
||||||
|
public readonly int Capacity => _chunkCount * _chunkCapacity;
|
||||||
|
public readonly bool IsCreated => _chunks.IsCreated;
|
||||||
|
|
||||||
|
public readonly ref T this[int index]
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(index, _chunkCapacity);
|
||||||
|
return ref ((T*)_chunks[chunkIdx])[offset];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly ref T this[uint index]
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex((int)index, _chunkCapacity);
|
||||||
|
return ref ((T*)_chunks[chunkIdx])[offset];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid constructor, use <see cref="UnsafeChunkedList(int, AllocationHandle, AllocationOption)"/> instead.
|
||||||
|
/// </summary>
|
||||||
|
public UnsafeChunkedList()
|
||||||
|
: this(DEFAULT_CHUNK_SIZE_IN_BYTES / sizeof(T), AllocationHandle.Persistent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance with a specified chunk capacity and allocator.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chunkCapacity">The maximum number of elements per chunk.</param>
|
||||||
|
/// <param name="handle">A reference to an AllocationHandle that manages memory allocation.</param>
|
||||||
|
/// <param name="allocationOption">Specifies how the memory should be allocated.</param>
|
||||||
|
public UnsafeChunkedList(int chunkCapacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||||
|
{
|
||||||
|
chunkCapacity = Math.Max(1, chunkCapacity);
|
||||||
|
_chunks = new UnsafeArray<nint>(4, handle, allocationOption);
|
||||||
|
_chunkCount = 0;
|
||||||
|
_count = 0;
|
||||||
|
_chunkCapacity = chunkCapacity;
|
||||||
|
_allocationHandle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance with a specified chunk capacity and an allocation type.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use AllocationHandle instead.")]
|
||||||
|
public UnsafeChunkedList(int chunkCapacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||||
|
: this(chunkCapacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("MHP_ENABLE_SAFETY_CHECKS")]
|
||||||
|
private readonly void CheckIndexBounds(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _count)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("MHP_ENABLE_SAFETY_CHECKS")]
|
||||||
|
private readonly void CheckIndexCount(int index, int count)
|
||||||
|
{
|
||||||
|
if (count < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException($"Value for count {count} must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException($"Value for index {index} must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > Count)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException($"Value for index {index} is out of bounds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index + count > Count)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException($"Value for count {count} is out of bounds.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("MHP_ENABLE_SAFETY_CHECKS")]
|
||||||
|
private readonly void ThrowIfNotCreated()
|
||||||
|
{
|
||||||
|
if (!IsCreated)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The UnsafeChunkedList is not created.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static (int chunkIndex, int offset) SplitIndex(int index, int chunkCapacity)
|
||||||
|
{
|
||||||
|
return (index / chunkCapacity, index % chunkCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GrowChunkArray(int minCapacity)
|
||||||
|
{
|
||||||
|
var newCapacity = Math.Max(minCapacity, Math.Max(_chunks.Count * 2, 4));
|
||||||
|
_chunks.Resize(newCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AllocateChunk(int chunkIndex)
|
||||||
|
{
|
||||||
|
if (chunkIndex >= _chunks.Count)
|
||||||
|
{
|
||||||
|
GrowChunkArray(chunkIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeInBytes = (nuint)(_chunkCapacity * sizeof(T));
|
||||||
|
_chunks[chunkIndex] = (nint)_allocationHandle.Alloc(sizeInBytes, MemoryUtility.AlignOf<T>());
|
||||||
|
_chunkCount = Math.Max(_chunkCount, chunkIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureChunkIndex(int elementIndex)
|
||||||
|
{
|
||||||
|
if (elementIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (chunkIdx, _) = SplitIndex(elementIndex, _chunkCapacity);
|
||||||
|
while (_chunkCount <= chunkIdx)
|
||||||
|
{
|
||||||
|
AllocateChunk(_chunkCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureChunkParallel(int chunkIndex)
|
||||||
|
{
|
||||||
|
if (chunkIndex < Volatile.Read(ref _chunkCount))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunksPtr = (nint*)_chunks.GetUnsafePtr();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var currentCount = Volatile.Read(ref _chunkCount);
|
||||||
|
if (chunkIndex < currentCount)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toAlloc = currentCount;
|
||||||
|
|
||||||
|
if (toAlloc >= _chunks.Count)
|
||||||
|
{
|
||||||
|
Thread.SpinWait(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeInBytes = (nuint)(_chunkCapacity * sizeof(T));
|
||||||
|
var data = (nint)_allocationHandle.Alloc(sizeInBytes, MemoryUtility.AlignOf<T>());
|
||||||
|
|
||||||
|
var old = Interlocked.CompareExchange(ref chunksPtr[toAlloc], data, 0);
|
||||||
|
|
||||||
|
if (old == 0)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _chunkCount);
|
||||||
|
if (chunkIndex >= currentCount + 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_allocationHandle.Free((void*)data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeChunk(int chunkIndex)
|
||||||
|
{
|
||||||
|
var ptr = (void*)_chunks[chunkIndex];
|
||||||
|
if (ptr != null)
|
||||||
|
{
|
||||||
|
_allocationHandle.Free(ptr);
|
||||||
|
_chunks[chunkIndex] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeTrailingEmptyChunks()
|
||||||
|
{
|
||||||
|
var neededChunks = _count > 0 ? (_count + _chunkCapacity - 1) / _chunkCapacity : 0;
|
||||||
|
while (_chunkCount > neededChunks)
|
||||||
|
{
|
||||||
|
_chunkCount--;
|
||||||
|
FreeChunk(_chunkCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
[UnscopedRef]
|
||||||
|
public Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return new Enumerator(ref this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a parallel reader for the current list, enabling thread-safe read operations.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ParallelReader AsParallelReader()
|
||||||
|
{
|
||||||
|
return new((UnsafeChunkedList<T>*)Unsafe.AsPointer(ref this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a parallel writer for the current list, enabling thread-safe additions.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ParallelWriter AsParallelWriter()
|
||||||
|
{
|
||||||
|
return new((UnsafeChunkedList<T>*)Unsafe.AsPointer(ref this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new element to the end of the list, allocating new chunks as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void Add(scoped in T value)
|
||||||
|
{
|
||||||
|
EnsureChunkIndex(_count);
|
||||||
|
var (chunkIdx, offset) = SplitIndex(_count, _chunkCapacity);
|
||||||
|
((T*)_chunks[chunkIdx])[offset] = value;
|
||||||
|
_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified value to the collection. For chunked lists, this is equivalent to <see cref="Add"/>,
|
||||||
|
/// since allocating new chunks never moves existing elements.
|
||||||
|
/// </summary>
|
||||||
|
public void AddNoResize(scoped in T value)
|
||||||
|
{
|
||||||
|
EnsureChunkIndex(_count);
|
||||||
|
var (chunkIdx, offset) = SplitIndex(_count, _chunkCapacity);
|
||||||
|
((T*)_chunks[chunkIdx])[offset] = value;
|
||||||
|
_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a range of elements to the collection, allocating new chunks as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRange(ReadOnlySpan<T> values)
|
||||||
|
{
|
||||||
|
if (values.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureChunkIndex(_count + values.Length - 1);
|
||||||
|
CopyFromSpan(values, _count);
|
||||||
|
_count += values.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a range of elements from a pointer to the collection, allocating new chunks as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRange(T* ptr, int count)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureChunkIndex(_count + count - 1);
|
||||||
|
CopyFromPtr(ptr, _count, count);
|
||||||
|
_count += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a range of elements. For chunked lists, this is equivalent to <see cref="AddRange(ReadOnlySpan{T})"/>,
|
||||||
|
/// since allocating new chunks never moves existing elements.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRangeNoResize(ReadOnlySpan<T> collection)
|
||||||
|
{
|
||||||
|
if (collection.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureChunkIndex(_count + collection.Length - 1);
|
||||||
|
CopyFromSpan(collection, _count);
|
||||||
|
_count += collection.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a range of elements from a pointer. For chunked lists, this is equivalent to <see cref="AddRange(T*, int)"/>,
|
||||||
|
/// since allocating new chunks never moves existing elements.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRangeNoResize(T* ptr, int count)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureChunkIndex(_count + count - 1);
|
||||||
|
CopyFromPtr(ptr, _count, count);
|
||||||
|
_count += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyFromSpan(ReadOnlySpan<T> source, int startIndex)
|
||||||
|
{
|
||||||
|
fixed (T* pSrc = source)
|
||||||
|
{
|
||||||
|
CopyFromPtr(pSrc, startIndex, source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyFromPtr(T* srcPtr, int startIndex, int count)
|
||||||
|
{
|
||||||
|
var remaining = count;
|
||||||
|
var src = srcPtr;
|
||||||
|
var currentIndex = startIndex;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(currentIndex, _chunkCapacity);
|
||||||
|
var dstPtr = (T*)_chunks[chunkIdx] + offset;
|
||||||
|
var copyCount = Math.Min(remaining, _chunkCapacity - offset);
|
||||||
|
MemoryUtility.MemCpy(dstPtr, src, (nuint)(copyCount * sizeof(T)));
|
||||||
|
src += copyCount;
|
||||||
|
currentIndex += copyCount;
|
||||||
|
remaining -= copyCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a range of elements from the list starting at the specified index.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveRange(int start, int length)
|
||||||
|
{
|
||||||
|
CheckIndexCount(start, length);
|
||||||
|
|
||||||
|
if (length <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var copyFrom = Math.Min(start + length, _count);
|
||||||
|
var numToMove = _count - copyFrom;
|
||||||
|
|
||||||
|
for (var i = 0; i < numToMove; i++)
|
||||||
|
{
|
||||||
|
var (srcChunk, srcOffset) = SplitIndex(copyFrom + i, _chunkCapacity);
|
||||||
|
var (dstChunk, dstOffset) = SplitIndex(start + i, _chunkCapacity);
|
||||||
|
((T*)_chunks[dstChunk])[dstOffset] = ((T*)_chunks[srcChunk])[srcOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
_count -= length;
|
||||||
|
FreeTrailingEmptyChunks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the element at the specified index.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveAt(int index)
|
||||||
|
{
|
||||||
|
RemoveRange(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a range of elements by swapping them with elements from the end of the list.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveRangeSwapBack(int start, int length)
|
||||||
|
{
|
||||||
|
CheckIndexCount(start, length);
|
||||||
|
|
||||||
|
if (length <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var numToCopy = Math.Min(length, _count - (start + length));
|
||||||
|
var copyFrom = _count - numToCopy;
|
||||||
|
|
||||||
|
for (var i = 0; i < numToCopy; i++)
|
||||||
|
{
|
||||||
|
var (dstChunk, dstOffset) = SplitIndex(start + i, _chunkCapacity);
|
||||||
|
var (srcChunk, srcOffset) = SplitIndex(copyFrom + i, _chunkCapacity);
|
||||||
|
((T*)_chunks[dstChunk])[dstOffset] = ((T*)_chunks[srcChunk])[srcOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
_count -= length;
|
||||||
|
FreeTrailingEmptyChunks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the element at the specified index by swapping it with the last element.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveAtSwapBack(int index)
|
||||||
|
{
|
||||||
|
RemoveRangeSwapBack(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||||
|
{
|
||||||
|
if (newSize < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(newSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSize > _count)
|
||||||
|
{
|
||||||
|
EnsureChunkIndex(newSize - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_count = newSize;
|
||||||
|
FreeTrailingEmptyChunks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-allocates chunks to accommodate at least the specified number of elements.
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureCapacity(int capacity)
|
||||||
|
{
|
||||||
|
if (capacity > 0)
|
||||||
|
{
|
||||||
|
EnsureChunkIndex(capacity - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_count = 0;
|
||||||
|
FreeTrailingEmptyChunks();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly void* GetUnsafePtr()
|
||||||
|
{
|
||||||
|
ThrowIfNotCreated();
|
||||||
|
|
||||||
|
if (_chunkCount == 1)
|
||||||
|
{
|
||||||
|
return (void*)_chunks[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Cannot get a single contiguous pointer for a multi-chunk UnsafeChunkedList. Use CopyTo instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies all elements into a destination span.
|
||||||
|
/// </summary>
|
||||||
|
public readonly void CopyTo(Span<T> destination)
|
||||||
|
{
|
||||||
|
var size = Math.Min(destination.Length, Count);
|
||||||
|
var remaining = size;
|
||||||
|
var elementIndex = 0;
|
||||||
|
|
||||||
|
fixed (T* pDest = destination)
|
||||||
|
{
|
||||||
|
var dst = pDest;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(elementIndex, _chunkCapacity);
|
||||||
|
var srcPtr = (T*)_chunks[chunkIdx] + offset;
|
||||||
|
var copyCount = Math.Min(remaining, _chunkCapacity - offset);
|
||||||
|
MemoryUtility.MemCpy(dst, srcPtr, (nuint)(copyCount * sizeof(T)));
|
||||||
|
elementIndex += copyCount;
|
||||||
|
dst += copyCount;
|
||||||
|
remaining -= copyCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies a range of elements from the list to a destination span.
|
||||||
|
/// </summary>
|
||||||
|
public readonly void CopyTo(Span<T> destination, int sourceIndex, int destinationIndex, int length)
|
||||||
|
{
|
||||||
|
if (sourceIndex + length > _count || destinationIndex + length > destination.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length), "Source collection or destination span is too small for the specified range.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (T* pDest = destination)
|
||||||
|
{
|
||||||
|
var dst = pDest + destinationIndex;
|
||||||
|
var remaining = length;
|
||||||
|
var elementIndex = sourceIndex;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(elementIndex, _chunkCapacity);
|
||||||
|
var srcPtr = (T*)_chunks[chunkIdx] + offset;
|
||||||
|
var copyCount = Math.Min(remaining, _chunkCapacity - offset);
|
||||||
|
MemoryUtility.MemCpy(dst, srcPtr, (nuint)(copyCount * sizeof(T)));
|
||||||
|
elementIndex += copyCount;
|
||||||
|
dst += copyCount;
|
||||||
|
remaining -= copyCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies elements from a source span into the list, growing as needed.
|
||||||
|
/// </summary>
|
||||||
|
public void CopyFrom(ReadOnlySpan<T> source)
|
||||||
|
{
|
||||||
|
if (_count < source.Length)
|
||||||
|
{
|
||||||
|
Resize(source.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyFromSpan(source, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies a range of elements from a source span to the list.
|
||||||
|
/// </summary>
|
||||||
|
public void CopyFrom(ReadOnlySpan<T> source, int sourceIndex, int destinationIndex, int length)
|
||||||
|
{
|
||||||
|
if (sourceIndex + length > source.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length), "Source span or destination collection is too small for the specified range.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationIndex + length > _count)
|
||||||
|
{
|
||||||
|
Resize(destinationIndex + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (T* pSrc = source)
|
||||||
|
{
|
||||||
|
CopyFromPtr(pSrc + sourceIndex, destinationIndex, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="List{T}"/> containing the elements.
|
||||||
|
/// </summary>
|
||||||
|
public readonly List<T> ToList()
|
||||||
|
{
|
||||||
|
var list = new List<T>(_count);
|
||||||
|
var remaining = _count;
|
||||||
|
var elementIndex = 0;
|
||||||
|
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var (chunkIdx, offset) = SplitIndex(elementIndex, _chunkCapacity);
|
||||||
|
var chunkSize = Math.Min(remaining, _chunkCapacity - offset);
|
||||||
|
var srcPtr = (T*)_chunks[chunkIdx] + offset;
|
||||||
|
var span = new ReadOnlySpan<T>(srcPtr, chunkSize);
|
||||||
|
list.AddRange(span);
|
||||||
|
elementIndex += chunkSize;
|
||||||
|
remaining -= chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _chunkCount; i++)
|
||||||
|
{
|
||||||
|
FreeChunk(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
_chunks.Dispose();
|
||||||
|
_chunkCount = 0;
|
||||||
|
_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -545,6 +545,7 @@ public static unsafe partial class MemoryUtility
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Represents an unmanaged type for which the alignment size is being calculated.</typeparam>
|
/// <typeparam name="T">Represents an unmanaged type for which the alignment size is being calculated.</typeparam>
|
||||||
/// <returns>Returns the difference in size between a helper structure and the specified type.</returns>
|
/// <returns>Returns the difference in size between a helper structure and the specified type.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static nuint AlignOf<T>()
|
public static nuint AlignOf<T>()
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
@@ -556,19 +557,22 @@ public static unsafe partial class MemoryUtility
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Represents a value type that is used to determine the alignment size.</typeparam>
|
/// <typeparam name="T">Represents a value type that is used to determine the alignment size.</typeparam>
|
||||||
/// <returns>Returns the size difference in bytes as an integer.</returns>
|
/// <returns>Returns the size difference in bytes as an integer.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static int MarshalAlignOf<T>()
|
public static int MarshalAlignOf<T>()
|
||||||
where T : struct
|
where T : struct
|
||||||
{
|
{
|
||||||
return Marshal.SizeOf<AlignOfHelper<T>>() - Marshal.SizeOf<T>();
|
return Marshal.SizeOf<AlignOfHelper<T>>() - Marshal.SizeOf<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aligns a given value up to the nearest multiple of the specified alignment.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to align.</param>
|
||||||
|
/// <param name="alignment">The alignment boundary.</param>
|
||||||
|
/// <returns>The aligned value.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static nuint AlignUp(nuint value, nuint alignment)
|
public static nuint AlignUp(nuint value, nuint alignment)
|
||||||
{
|
{
|
||||||
if (alignment == 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Alignment must be greater than zero.", nameof(alignment));
|
|
||||||
}
|
|
||||||
|
|
||||||
var mask = alignment - 1;
|
var mask = alignment - 1;
|
||||||
return (value + mask) & ~mask;
|
return (value + mask) & ~mask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using BenchmarkDotNet.Attributes;
|
|||||||
using BenchmarkDotNet.Engines;
|
using BenchmarkDotNet.Engines;
|
||||||
using Misaki.HighPerformance.Image;
|
using Misaki.HighPerformance.Image;
|
||||||
using Misaki.HighPerformance.Jobs;
|
using Misaki.HighPerformance.Jobs;
|
||||||
using Misaki.HighPerformance.Mathematics;
|
|
||||||
using Misaki.HighPerformance.Mathematics.SPMD;
|
using Misaki.HighPerformance.Mathematics.SPMD;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||||
<DefineConstants>$(DefineConstants);PLATFORM_WINDOWS</DefineConstants>
|
<DefineConstants>$(DefineConstants);PLATFORM_WINDOWS;</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -34,23 +34,9 @@ using System.Buffers;
|
|||||||
|
|
||||||
AllocationManager.Initialize();
|
AllocationManager.Initialize();
|
||||||
|
|
||||||
Console.WriteLine(0);
|
var arr = new UnsafeArray<int>(10, AllocationHandle.Persistent);
|
||||||
for (var i = 0; i < 64; i++)
|
Console.WriteLine(arr[0]);
|
||||||
{
|
Console.WriteLine(arr[10]);
|
||||||
var size = Random.Shared.Next(2048, 8192);
|
|
||||||
var arr = new UnsafeArray<Guid>(size, AllocationHandle.FreeList); // AllocationHandle.FreeList
|
|
||||||
arr.Dispose();
|
arr.Dispose();
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
|
|
||||||
Console.WriteLine(1);
|
|
||||||
for (var i = 0; i < 64; i++)
|
|
||||||
{
|
|
||||||
var size = Random.Shared.Next(2048, 8192);
|
|
||||||
var arr = new UnsafeArray<Guid>(size, AllocationHandle.FreeList); // AllocationHandle.FreeList
|
|
||||||
arr.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Read();
|
|
||||||
AllocationManager.Dispose();
|
AllocationManager.Dispose();
|
||||||
@@ -0,0 +1,676 @@
|
|||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public unsafe class TestUnsafeChunkedList
|
||||||
|
{
|
||||||
|
private UnsafeChunkedList<int> _list;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_list = new UnsafeChunkedList<int>(3, AllocationHandle.Persistent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
if (_list.IsCreated)
|
||||||
|
{
|
||||||
|
_list.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAdd()
|
||||||
|
{
|
||||||
|
_list.Add(1);
|
||||||
|
_list.Add(2);
|
||||||
|
_list.Add(3);
|
||||||
|
Assert.AreEqual(3, _list.Count);
|
||||||
|
Assert.AreEqual(1, _list[0]);
|
||||||
|
Assert.AreEqual(2, _list[1]);
|
||||||
|
Assert.AreEqual(3, _list[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddMultiChunk()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(10, _list.Count);
|
||||||
|
Assert.IsTrue(_list.Capacity >= 10);
|
||||||
|
Assert.IsTrue(_list.ChunkCount >= 4);
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, _list[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveAt()
|
||||||
|
{
|
||||||
|
_list.Add(0);
|
||||||
|
_list.Add(1);
|
||||||
|
_list.Add(2);
|
||||||
|
_list.RemoveAt(1);
|
||||||
|
Assert.AreEqual(2, _list.Count);
|
||||||
|
Assert.AreEqual(0, _list[0]);
|
||||||
|
Assert.AreEqual(2, _list[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveAtSwapBack()
|
||||||
|
{
|
||||||
|
_list.Add(10);
|
||||||
|
_list.Add(11);
|
||||||
|
_list.Add(12);
|
||||||
|
_list.Add(13);
|
||||||
|
|
||||||
|
_list.RemoveAtSwapBack(1);
|
||||||
|
|
||||||
|
Assert.AreEqual(3, _list.Count, "Count should be 3");
|
||||||
|
Assert.AreEqual(10, _list[0], "Index 0 should be 10");
|
||||||
|
Assert.AreEqual(13, _list[1], "Index 1 should be 13");
|
||||||
|
Assert.AreEqual(12, _list[2], "Index 2 should be 12");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveAtAcrossChunks()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
_list.RemoveAt(3);
|
||||||
|
|
||||||
|
Assert.AreEqual(6, _list.Count);
|
||||||
|
Assert.AreEqual(0, _list[0]);
|
||||||
|
Assert.AreEqual(1, _list[1]);
|
||||||
|
Assert.AreEqual(2, _list[2]);
|
||||||
|
Assert.AreEqual(4, _list[3]);
|
||||||
|
Assert.AreEqual(5, _list[4]);
|
||||||
|
Assert.AreEqual(6, _list[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveAtSwapBackAcrossChunks()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
_list.RemoveAtSwapBack(1);
|
||||||
|
|
||||||
|
Assert.AreEqual(6, _list.Count, "Count should be 6");
|
||||||
|
Assert.AreEqual(0, _list[0], "Index 0 should be 0");
|
||||||
|
Assert.AreEqual(6, _list[1], "Index 1 should be 6 (swapped from last)");
|
||||||
|
Assert.AreEqual(2, _list[2], "Index 2 should be 2");
|
||||||
|
Assert.AreEqual(3, _list[3], "Index 3 should be 3");
|
||||||
|
Assert.AreEqual(4, _list[4], "Index 4 should be 4");
|
||||||
|
Assert.AreEqual(5, _list[5], "Index 5 should be 5");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestClear()
|
||||||
|
{
|
||||||
|
_list.Add(1);
|
||||||
|
_list.Add(2);
|
||||||
|
_list.Clear();
|
||||||
|
Assert.AreEqual(0, _list.Count);
|
||||||
|
Assert.AreEqual(0, _list.ChunkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddRange()
|
||||||
|
{
|
||||||
|
int[] values = { 10, 20, 30 };
|
||||||
|
_list.AddRange(values);
|
||||||
|
Assert.AreEqual(3, _list.Count);
|
||||||
|
Assert.AreEqual(10, _list[0]);
|
||||||
|
Assert.AreEqual(20, _list[1]);
|
||||||
|
Assert.AreEqual(30, _list[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddRangeMultiChunk()
|
||||||
|
{
|
||||||
|
int[] values = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
|
||||||
|
_list.AddRange(values);
|
||||||
|
Assert.AreEqual(10, _list.Count);
|
||||||
|
Assert.IsTrue(_list.ChunkCount >= 4);
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, _list[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEnumerator()
|
||||||
|
{
|
||||||
|
_list.Add(1);
|
||||||
|
_list.Add(2);
|
||||||
|
_list.Add(3);
|
||||||
|
var sum = 0;
|
||||||
|
foreach (var item in _list)
|
||||||
|
{
|
||||||
|
sum += item;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(6, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestEnumeratorMultiChunk()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = 0;
|
||||||
|
foreach (var item in _list)
|
||||||
|
{
|
||||||
|
sum += item;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(45, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCopyTo()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dest = new int[7];
|
||||||
|
_list.CopyTo(dest);
|
||||||
|
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, dest[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCopyToPartial()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dest = new int[5];
|
||||||
|
_list.CopyTo(dest);
|
||||||
|
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, dest[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddNoResize()
|
||||||
|
{
|
||||||
|
_list.EnsureCapacity(5);
|
||||||
|
_list.AddNoResize(1);
|
||||||
|
_list.AddNoResize(2);
|
||||||
|
_list.AddNoResize(3);
|
||||||
|
_list.AddNoResize(4);
|
||||||
|
_list.AddNoResize(5);
|
||||||
|
|
||||||
|
Assert.AreEqual(5, _list.Count);
|
||||||
|
Assert.AreEqual(1, _list[0]);
|
||||||
|
Assert.AreEqual(5, _list[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestChunksFreedOnShrink()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(_list.ChunkCount >= 4);
|
||||||
|
|
||||||
|
_list.RemoveRange(3, 7);
|
||||||
|
|
||||||
|
Assert.AreEqual(3, _list.Count);
|
||||||
|
Assert.IsTrue(_list.ChunkCount <= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestChunksFreedOnClear()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(_list.ChunkCount >= 4);
|
||||||
|
_list.Clear();
|
||||||
|
Assert.AreEqual(0, _list.ChunkCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestParallelReader()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = _list.AsParallelReader();
|
||||||
|
Assert.AreEqual(7, reader.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, reader[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestParallelWriterSingleThreaded()
|
||||||
|
{
|
||||||
|
_list.EnsureCapacity(5);
|
||||||
|
var writer = _list.AsParallelWriter();
|
||||||
|
writer.Add(10);
|
||||||
|
writer.Add(20);
|
||||||
|
writer.Add(30);
|
||||||
|
|
||||||
|
Assert.AreEqual(3, _list.Count);
|
||||||
|
Assert.AreEqual(10, _list[0]);
|
||||||
|
Assert.AreEqual(20, _list[1]);
|
||||||
|
Assert.AreEqual(30, _list[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestParallelWriterAddRange()
|
||||||
|
{
|
||||||
|
_list.EnsureCapacity(6);
|
||||||
|
int[] values = { 1, 2, 3, 4, 5, 6 };
|
||||||
|
var writer = _list.AsParallelWriter();
|
||||||
|
writer.AddRange(values);
|
||||||
|
|
||||||
|
Assert.AreEqual(6, _list.Count);
|
||||||
|
for (var i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i + 1, _list[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestParallelWriterAutoAllocatesChunks()
|
||||||
|
{
|
||||||
|
_list.EnsureCapacity(10);
|
||||||
|
var writer = _list.AsParallelWriter();
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
writer.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(10, _list.Count);
|
||||||
|
Assert.IsTrue(_list.ChunkCount >= 4);
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, _list[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestToList()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var managedList = _list.ToList();
|
||||||
|
Assert.AreEqual(7, managedList.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(i, managedList[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCopyToRange()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dest = new int[8];
|
||||||
|
_list.CopyTo(dest, 2, 1, 5);
|
||||||
|
|
||||||
|
Assert.AreEqual(2, dest[1]);
|
||||||
|
Assert.AreEqual(3, dest[2]);
|
||||||
|
Assert.AreEqual(4, dest[3]);
|
||||||
|
Assert.AreEqual(5, dest[4]);
|
||||||
|
Assert.AreEqual(6, dest[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCopyFrom()
|
||||||
|
{
|
||||||
|
int[] source = { 100, 200, 300, 400, 500 };
|
||||||
|
_list.CopyFrom(source);
|
||||||
|
|
||||||
|
Assert.AreEqual(5, _list.Count);
|
||||||
|
Assert.AreEqual(100, _list[0]);
|
||||||
|
Assert.AreEqual(500, _list[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCopyFromRange()
|
||||||
|
{
|
||||||
|
_list.Resize(5);
|
||||||
|
int[] source = { 0, 0, 10, 20, 30 };
|
||||||
|
_list.CopyFrom(source, 2, 2, 3);
|
||||||
|
|
||||||
|
Assert.AreEqual(10, _list[2]);
|
||||||
|
Assert.AreEqual(20, _list[3]);
|
||||||
|
Assert.AreEqual(30, _list[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetUnsafePtrSingleChunk()
|
||||||
|
{
|
||||||
|
_list.Add(42);
|
||||||
|
_list.Add(99);
|
||||||
|
|
||||||
|
var ptr = (int*)_list.GetUnsafePtr();
|
||||||
|
Assert.AreEqual(42, ptr[0]);
|
||||||
|
Assert.AreEqual(99, ptr[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetUnsafePtrMultiChunkThrows()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.ThrowsExactly<InvalidOperationException>(() => _list.GetUnsafePtr());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestResize()
|
||||||
|
{
|
||||||
|
_list.Resize(5);
|
||||||
|
Assert.AreEqual(5, _list.Count);
|
||||||
|
|
||||||
|
_list[0] = 10;
|
||||||
|
_list[4] = 50;
|
||||||
|
|
||||||
|
Assert.AreEqual(10, _list[0]);
|
||||||
|
Assert.AreEqual(50, _list[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveRangeSwapBack()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
_list.RemoveRangeSwapBack(2, 3);
|
||||||
|
|
||||||
|
Assert.AreEqual(7, _list.Count);
|
||||||
|
Assert.AreEqual(0, _list[0]);
|
||||||
|
Assert.AreEqual(1, _list[1]);
|
||||||
|
Assert.AreEqual(7, _list[2]);
|
||||||
|
Assert.AreEqual(8, _list[3]);
|
||||||
|
Assert.AreEqual(9, _list[4]);
|
||||||
|
Assert.AreEqual(5, _list[5]);
|
||||||
|
Assert.AreEqual(6, _list[6]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestChunkCapacityProperty()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(3, _list.ChunkCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddAfterClear()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
_list.Clear();
|
||||||
|
|
||||||
|
_list.Add(100);
|
||||||
|
_list.Add(200);
|
||||||
|
|
||||||
|
Assert.AreEqual(2, _list.Count);
|
||||||
|
Assert.AreEqual(100, _list[0]);
|
||||||
|
Assert.AreEqual(200, _list[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public unsafe void TestConcurrentAddsNoCorruption()
|
||||||
|
{
|
||||||
|
const int threadCount = 4;
|
||||||
|
const int perThread = 250;
|
||||||
|
const int totalCount = threadCount * perThread;
|
||||||
|
|
||||||
|
_list.EnsureCapacity(totalCount);
|
||||||
|
var listPtr = (UnsafeChunkedList<int>*)Unsafe.AsPointer(ref _list);
|
||||||
|
var tasks = new Task[threadCount];
|
||||||
|
|
||||||
|
for (var t = 0; t < threadCount; t++)
|
||||||
|
{
|
||||||
|
var threadId = t;
|
||||||
|
tasks[t] = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var writer = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelWriter();
|
||||||
|
for (var i = 0; i < perThread; i++)
|
||||||
|
{
|
||||||
|
writer.Add(threadId * perThread + i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(tasks);
|
||||||
|
|
||||||
|
Assert.AreEqual(totalCount, _list.Count);
|
||||||
|
|
||||||
|
var found = new bool[totalCount];
|
||||||
|
for (var i = 0; i < totalCount; i++)
|
||||||
|
{
|
||||||
|
var value = _list[i];
|
||||||
|
Assert.IsTrue(value >= 0 && value < totalCount, $"Value {value} out of range at index {i}");
|
||||||
|
Assert.IsFalse(found[value], $"Duplicate value {value} at index {i}");
|
||||||
|
found[value] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < totalCount; i++)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(found[i], $"Value {i} was never written");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public unsafe void TestConcurrentAddRangeNoCorruption()
|
||||||
|
{
|
||||||
|
const int threadCount = 4;
|
||||||
|
const int perThread = 250;
|
||||||
|
const int totalCount = threadCount * perThread;
|
||||||
|
|
||||||
|
_list.EnsureCapacity(totalCount);
|
||||||
|
var listPtr = (UnsafeChunkedList<int>*)Unsafe.AsPointer(ref _list);
|
||||||
|
var tasks = new Task[threadCount];
|
||||||
|
|
||||||
|
for (var t = 0; t < threadCount; t++)
|
||||||
|
{
|
||||||
|
var threadId = t;
|
||||||
|
tasks[t] = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var values = new int[perThread];
|
||||||
|
for (var i = 0; i < perThread; i++)
|
||||||
|
{
|
||||||
|
values[i] = threadId * perThread + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
var writer = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelWriter();
|
||||||
|
writer.AddRange(values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(tasks);
|
||||||
|
|
||||||
|
Assert.AreEqual(totalCount, _list.Count);
|
||||||
|
|
||||||
|
var found = new bool[totalCount];
|
||||||
|
for (var i = 0; i < totalCount; i++)
|
||||||
|
{
|
||||||
|
var value = _list[i];
|
||||||
|
Assert.IsTrue(value >= 0 && value < totalCount);
|
||||||
|
Assert.IsFalse(found[value]);
|
||||||
|
found[value] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < totalCount; i++)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(found[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public unsafe void TestReaderDropsStaleCountAfterWrite()
|
||||||
|
{
|
||||||
|
_list.Add(1);
|
||||||
|
_list.Add(2);
|
||||||
|
_list.Add(3);
|
||||||
|
|
||||||
|
var listPtr = (UnsafeChunkedList<int>*)Unsafe.AsPointer(ref _list);
|
||||||
|
|
||||||
|
var writer = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelWriter();
|
||||||
|
writer.Add(4);
|
||||||
|
writer.Add(5);
|
||||||
|
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
|
||||||
|
var reader = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelReader();
|
||||||
|
Assert.AreEqual(5, reader.Count);
|
||||||
|
Assert.AreEqual(1, reader[0]);
|
||||||
|
Assert.AreEqual(2, reader[1]);
|
||||||
|
Assert.AreEqual(3, reader[2]);
|
||||||
|
Assert.AreEqual(4, reader[3]);
|
||||||
|
Assert.AreEqual(5, reader[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public unsafe void TestParallelWriterAutoAllocatesChunksConcurrently()
|
||||||
|
{
|
||||||
|
_list.EnsureCapacity(20);
|
||||||
|
|
||||||
|
var listPtr = (UnsafeChunkedList<int>*)Unsafe.AsPointer(ref _list);
|
||||||
|
var tasks = new Task[2];
|
||||||
|
|
||||||
|
tasks[0] = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var writer = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelWriter();
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
writer.Add(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks[1] = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var writer = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelWriter();
|
||||||
|
for (var i = 10; i < 20; i++)
|
||||||
|
{
|
||||||
|
writer.Add(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Task.WaitAll(tasks);
|
||||||
|
|
||||||
|
Assert.AreEqual(20, _list.Count);
|
||||||
|
Assert.IsTrue(_list.ChunkCount >= 7);
|
||||||
|
|
||||||
|
var found = new bool[20];
|
||||||
|
for (var i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
found[_list[i]] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(found[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public unsafe void TestIndexerDoesNotCrashDuringConcurrentWrite()
|
||||||
|
{
|
||||||
|
const int prePopulate = 3;
|
||||||
|
for (var i = 0; i < prePopulate; i++)
|
||||||
|
{
|
||||||
|
_list.Add(i * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
_list.EnsureCapacity(prePopulate + 100);
|
||||||
|
|
||||||
|
var listPtr = (UnsafeChunkedList<int>*)Unsafe.AsPointer(ref _list);
|
||||||
|
var readerDone = false;
|
||||||
|
Exception? readException = null;
|
||||||
|
|
||||||
|
var readTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reader = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelReader();
|
||||||
|
for (var iteration = 0; iteration < 1000; iteration++)
|
||||||
|
{
|
||||||
|
var count = reader.Count;
|
||||||
|
for (var i = 0; i < Math.Min(count, prePopulate); i++)
|
||||||
|
{
|
||||||
|
_ = reader[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
readException = ex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var writeTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var writer = Unsafe.AsRef<UnsafeChunkedList<int>>(listPtr).AsParallelWriter();
|
||||||
|
for (var i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
writer.Add(i + 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Task.WaitAll(readTask, writeTask);
|
||||||
|
|
||||||
|
Assert.IsNull(readException, $"Reader threw: {readException?.Message}");
|
||||||
|
Assert.AreEqual(prePopulate + 100, _list.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user