Added UnsafeMultiHashMap
This commit is contained in:
@@ -3,7 +3,6 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
|||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection.Metadata;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.Jobs;
|
namespace Misaki.HighPerformance.Jobs;
|
||||||
@@ -730,19 +729,25 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We can steal a up stream job to execute while waiting.
|
// TODO: Maybe we can steal a up stream or current job to execute while waiting?
|
||||||
// For example, if we wait on job A which depends on job B, and both are not scheduled yet, we can steal and execute job B to speed up the completion of A.
|
// For example, if we wait on job A which depends on job B, and both are not scheduled yet, we can steal and execute job B to speed up the completion of A.
|
||||||
// And then maybe we can even execute A after B if we can guarantee the order and avoid deadlock. This is a common optimization in job systems called "helping" or "work stealing with dependencies".
|
|
||||||
|
|
||||||
var spin = new SpinWait();
|
var spin = new SpinWait();
|
||||||
while (_jobInfoPool.TryGetElement(handle.ID, handle.Generation, out var jobInfo))
|
while (true)
|
||||||
{
|
{
|
||||||
|
ref readonly var jobInfo = ref _jobInfoPool.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
|
||||||
|
if (!exist)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Mask out RC
|
// Mask out RC
|
||||||
if ((jobInfo.state & (JobState)_STATE_MASK) == JobState.Completed)
|
if ((jobInfo.state & (JobState)_STATE_MASK) == JobState.Completed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// var sleepThreshold = jobInfo.jobRanges.totalIteration * jobInfo.jobRanges.batchSize * 100;
|
||||||
spin.SpinOnce(_SLEEP_THRESHOLD);
|
spin.SpinOnce(_SLEEP_THRESHOLD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,7 +767,7 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable
|
|||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
for (int i = completedCount; i < orderedHandles.Length; i++)
|
for (var i = completedCount; i < orderedHandles.Length; i++)
|
||||||
{
|
{
|
||||||
var handle = orderedHandles[i];
|
var handle = orderedHandles[i];
|
||||||
if (!_jobInfoPool.Contains(handle.ID, handle.Generation))
|
if (!_jobInfoPool.Contains(handle.ID, handle.Generation))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
@@ -214,6 +215,40 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private int AllocateEntry(in TKey key)
|
||||||
|
{
|
||||||
|
int idx;
|
||||||
|
|
||||||
|
if (_allocatedIndex >= _capacity && _firstFreeIndex < 0)
|
||||||
|
{
|
||||||
|
var newCap = CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth));
|
||||||
|
Resize(newCap);
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = _firstFreeIndex;
|
||||||
|
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
_firstFreeIndex = _next[idx];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
idx = _allocatedIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckIndexOutOfBounds(idx);
|
||||||
|
|
||||||
|
UnsafeUtility.WriteArrayElement(_keys, idx, key);
|
||||||
|
var bucket = GetBucket(key);
|
||||||
|
|
||||||
|
_next[idx] = _buckets[bucket];
|
||||||
|
_buckets[bucket] = idx;
|
||||||
|
_count++;
|
||||||
|
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private void AllocateBuffer(int totalSize, int keyOffset, int nextOffset, int bucketOffset, AllocationOption allocationOption)
|
private void AllocateBuffer(int totalSize, int keyOffset, int nextOffset, int bucketOffset, AllocationOption allocationOption)
|
||||||
{
|
{
|
||||||
@@ -254,7 +289,7 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
{
|
{
|
||||||
for (var idx = oldBuckets[i]; idx != -1; idx = oldNext[idx])
|
for (var idx = oldBuckets[i]; idx != -1; idx = oldNext[idx])
|
||||||
{
|
{
|
||||||
var newIdx = TryAdd(oldKeys[idx]);
|
var newIdx = Add(oldKeys[idx]);
|
||||||
MemCpy(_buffer + _sizeOfTValue * newIdx, oldBuffer + _sizeOfTValue * idx, (nuint)_sizeOfTValue);
|
MemCpy(_buffer + _sizeOfTValue * newIdx, oldBuffer + _sizeOfTValue * idx, (nuint)_sizeOfTValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,45 +358,19 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
{
|
{
|
||||||
ThrowIfNotCreated();
|
ThrowIfNotCreated();
|
||||||
|
|
||||||
var k = key;
|
|
||||||
if (Find(in key) != -1)
|
if (Find(in key) != -1)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate an entry from the free list
|
return AllocateEntry(key);
|
||||||
int idx;
|
|
||||||
int* next;
|
|
||||||
|
|
||||||
if (_allocatedIndex >= _capacity && _firstFreeIndex < 0)
|
|
||||||
{
|
|
||||||
var newCap = CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth));
|
|
||||||
Resize(newCap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
idx = _firstFreeIndex;
|
public int Add(in TKey key)
|
||||||
|
|
||||||
if (idx >= 0)
|
|
||||||
{
|
{
|
||||||
_firstFreeIndex = _next[idx];
|
ThrowIfNotCreated();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
idx = _allocatedIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckIndexOutOfBounds(idx);
|
return AllocateEntry(key);
|
||||||
|
|
||||||
UnsafeUtility.WriteArrayElement(_keys, idx, key);
|
|
||||||
var bucket = GetBucket(key);
|
|
||||||
|
|
||||||
// Add the index to the hash-map
|
|
||||||
next = _next;
|
|
||||||
next[idx] = _buckets[bucket];
|
|
||||||
_buckets[bucket] = idx;
|
|
||||||
_count++;
|
|
||||||
|
|
||||||
return idx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int TryRemove(in TKey key)
|
public int TryRemove(in TKey key)
|
||||||
@@ -416,6 +425,50 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
return 0 != removed ? removed : -1;
|
return 0 != removed ? removed : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int RemoveAll(in TKey key)
|
||||||
|
{
|
||||||
|
ThrowIfNotCreated();
|
||||||
|
|
||||||
|
if (_capacity == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed = 0;
|
||||||
|
var bucket = GetBucket(key);
|
||||||
|
var prevEntry = -1;
|
||||||
|
var entryIdx = _buckets[bucket];
|
||||||
|
|
||||||
|
while (entryIdx >= 0 && entryIdx < _capacity)
|
||||||
|
{
|
||||||
|
if (UnsafeUtility.ReadArrayElement<TKey>(_keys, entryIdx).Equals(key))
|
||||||
|
{
|
||||||
|
removed++;
|
||||||
|
|
||||||
|
var nextIdx = _next[entryIdx];
|
||||||
|
if (prevEntry < 0)
|
||||||
|
{
|
||||||
|
_buckets[bucket] = nextIdx;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_next[prevEntry] = nextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
_next[entryIdx] = _firstFreeIndex;
|
||||||
|
_firstFreeIndex = entryIdx;
|
||||||
|
entryIdx = nextIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevEntry = entryIdx;
|
||||||
|
entryIdx = _next[entryIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
_count -= removed;
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryGetValue<TValue>(in TKey key, out TValue item)
|
public bool TryGetValue<TValue>(in TKey key, out TValue item)
|
||||||
where TValue : unmanaged
|
where TValue : unmanaged
|
||||||
{
|
{
|
||||||
@@ -433,6 +486,43 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int FindNext(int entryIdx, in TKey key)
|
||||||
|
{
|
||||||
|
ThrowIfNotCreated();
|
||||||
|
|
||||||
|
if ((uint)entryIdx >= (uint)_capacity)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextIndex = _next[entryIdx];
|
||||||
|
while ((uint)nextIndex < (uint)_capacity)
|
||||||
|
{
|
||||||
|
if (UnsafeUtility.ReadArrayElement<TKey>(_keys, nextIndex).Equals(key))
|
||||||
|
{
|
||||||
|
return nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIndex = _next[nextIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CountValuesForKey(in TKey key)
|
||||||
|
{
|
||||||
|
ThrowIfNotCreated();
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
for (var idx = Find(key); idx != -1; idx = FindNext(idx, key))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnscopedRef]
|
||||||
public ref TValue GetValueRef<TValue>(in TKey key, out bool exists)
|
public ref TValue GetValueRef<TValue>(in TKey key, out bool exists)
|
||||||
where TValue : unmanaged
|
where TValue : unmanaged
|
||||||
{
|
{
|
||||||
@@ -449,6 +539,7 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
return ref Unsafe.NullRef<TValue>();
|
return ref Unsafe.NullRef<TValue>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnscopedRef]
|
||||||
public ref TValue GetValueRefOrAddDefault<TValue>(in TKey key, out bool exists)
|
public ref TValue GetValueRefOrAddDefault<TValue>(in TKey key, out bool exists)
|
||||||
where TValue : unmanaged
|
where TValue : unmanaged
|
||||||
{
|
{
|
||||||
@@ -489,35 +580,7 @@ public unsafe struct HashMapHelper<TKey> : IDisposable
|
|||||||
return ref UnsafeUtility.ReadArrayElementRef<TValue>(_buffer, idx);
|
return ref UnsafeUtility.ReadArrayElementRef<TValue>(_buffer, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
int* next;
|
idx = AllocateEntry(key);
|
||||||
|
|
||||||
if (_allocatedIndex >= _capacity && _firstFreeIndex < 0)
|
|
||||||
{
|
|
||||||
var newCap = CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth));
|
|
||||||
Resize(newCap);
|
|
||||||
}
|
|
||||||
|
|
||||||
idx = _firstFreeIndex;
|
|
||||||
|
|
||||||
if (idx >= 0)
|
|
||||||
{
|
|
||||||
_firstFreeIndex = _next[idx];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
idx = _allocatedIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckIndexOutOfBounds(idx);
|
|
||||||
|
|
||||||
UnsafeUtility.WriteArrayElement(_keys, idx, key);
|
|
||||||
bucket = GetBucket(hash);
|
|
||||||
|
|
||||||
// Add the index to the hash-map
|
|
||||||
next = _next;
|
|
||||||
next[idx] = _buckets[bucket];
|
|
||||||
_buckets[bucket] = idx;
|
|
||||||
_count++;
|
|
||||||
|
|
||||||
UnsafeUtility.WriteArrayElement(_buffer, idx, default(TValue));
|
UnsafeUtility.WriteArrayElement(_buffer, idx, default(TValue));
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor, use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> or <see cref="UnsafeArray(int, ref AllocationHandle, AllocationOption)"/> instead.
|
/// Invalid constructor, use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> or <see cref="UnsafeArray(int, AllocationHandle, AllocationOption)"/> instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeArray()
|
public UnsafeArray()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -2,6 +2,7 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
|||||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
@@ -91,7 +92,7 @@ public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeHashCollection<KeyValu
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor, use <see cref="UnsafeHashMap(int, Allocator, AllocationOption)"/> or <see cref="UnsafeHashMap(int, ref AllocationHandle, AllocationOption)"/> instead.
|
/// Invalid constructor, use <see cref="UnsafeHashMap(int, Allocator, AllocationOption)"/> or <see cref="UnsafeHashMap(int, AllocationHandle, AllocationOption)"/> instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeHashMap()
|
public UnsafeHashMap()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -180,11 +181,13 @@ public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeHashCollection<KeyValu
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnscopedRef]
|
||||||
public ref TValue GetValueRef(in TKey key, out bool exists)
|
public ref TValue GetValueRef(in TKey key, out bool exists)
|
||||||
{
|
{
|
||||||
return ref _helper.GetValueRef<TValue>(key, out exists);
|
return ref _helper.GetValueRef<TValue>(key, out exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnscopedRef]
|
||||||
public ref TValue GetValueRefOrAddDefault(in TKey key, out bool exists)
|
public ref TValue GetValueRefOrAddDefault(in TKey key, out bool exists)
|
||||||
{
|
{
|
||||||
return ref _helper.GetValueRefOrAddDefault<TValue>(key, out exists);
|
return ref _helper.GetValueRefOrAddDefault<TValue>(key, out exists);
|
||||||
@@ -64,7 +64,7 @@ public unsafe struct UnsafeHashSet<T> : IUnsafeHashCollection<T>, IEnumerable<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor. Use <see cref="UnsafeHashSet(int, Allocator, AllocationOption)"/> or <see cref="UnsafeHashSet(int, ref AllocationHandle, AllocationOption)"/> instead."/>
|
/// Invalid constructor. Use <see cref="UnsafeHashSet(int, Allocator, AllocationOption)"/> or <see cref="UnsafeHashSet(int, AllocationHandle, AllocationOption)"/> instead."/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeHashSet()
|
public UnsafeHashSet()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -176,7 +176,7 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor, use <see cref="UnsafeList(int, Allocator, AllocationOption)"/> or <see cref="UnsafeList(int, ref AllocationHandle, AllocationOption)"/> instead.
|
/// Invalid constructor, use <see cref="UnsafeList(int, Allocator, AllocationOption)"/> or <see cref="UnsafeList(int, AllocationHandle, AllocationOption)"/> instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeList()
|
public UnsafeList()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
public unsafe struct UnsafeMultiHashMap<TKey, TValue> : IUnsafeHashCollection<KeyValuePair<TKey, TValue>>
|
||||||
|
where TKey : unmanaged, IEquatable<TKey>
|
||||||
|
where TValue : unmanaged
|
||||||
|
{
|
||||||
|
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
|
||||||
|
{
|
||||||
|
internal HashMapHelper<TKey>.Enumerator _enumerator;
|
||||||
|
|
||||||
|
public readonly KeyValuePair<TKey, TValue> Current => _enumerator.GetCurrent<TValue>();
|
||||||
|
readonly object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public Enumerator(HashMapHelper<TKey>* data)
|
||||||
|
{
|
||||||
|
_enumerator = new HashMapHelper<TKey>.Enumerator(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
return _enumerator.MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_enumerator.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Iterator
|
||||||
|
{
|
||||||
|
internal TKey _key;
|
||||||
|
internal int _entryIndex;
|
||||||
|
|
||||||
|
internal Iterator(in TKey key, int entryIndex)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
_entryIndex = entryIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ValueEnumerable
|
||||||
|
{
|
||||||
|
private readonly HashMapHelper<TKey>* _data;
|
||||||
|
private readonly TKey _key;
|
||||||
|
|
||||||
|
internal ValueEnumerable(HashMapHelper<TKey>* data, in TKey key)
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly ValueEnumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return new(_data, _key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ValueEnumerator : IEnumerator<TValue>
|
||||||
|
{
|
||||||
|
private readonly HashMapHelper<TKey>* _data;
|
||||||
|
private readonly TKey _key;
|
||||||
|
private int _entryIndex;
|
||||||
|
private bool _started;
|
||||||
|
|
||||||
|
public readonly TValue Current => UnsafeUtility.ReadArrayElement<TValue>(_data->Buffer, _entryIndex);
|
||||||
|
readonly object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
internal ValueEnumerator(HashMapHelper<TKey>* data, in TKey key)
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
_key = key;
|
||||||
|
_entryIndex = -1;
|
||||||
|
_started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (!_started)
|
||||||
|
{
|
||||||
|
_entryIndex = _data->Find(_key);
|
||||||
|
_started = true;
|
||||||
|
return _entryIndex != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_entryIndex == -1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_entryIndex = _data->FindNext(_entryIndex, _key);
|
||||||
|
return _entryIndex != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_entryIndex = -1;
|
||||||
|
_started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashMapHelper<TKey> _helper;
|
||||||
|
|
||||||
|
public readonly int Count => _helper.Count;
|
||||||
|
public readonly int Capacity => _helper.Capacity;
|
||||||
|
public readonly bool IsCreated => _helper.IsCreated;
|
||||||
|
|
||||||
|
public Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return new((HashMapHelper<TKey>*)UnsafeUtility.AddressOf(ref this));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeMultiHashMap()
|
||||||
|
: this(0, Allocator.Invalid)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeMultiHashMap(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||||
|
{
|
||||||
|
_helper = new HashMapHelper<TKey>(capacity, sizeof(TValue), (int)AlignOf<TValue>(), HashMapHelper<TKey>.MINIMAL_CAPACITY, handle, allocationOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeMultiHashMap(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||||
|
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(in TKey key, TValue item)
|
||||||
|
{
|
||||||
|
var idx = _helper.Add(key);
|
||||||
|
UnsafeUtility.WriteArrayElement(_helper.Buffer, idx, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(in TKey key)
|
||||||
|
{
|
||||||
|
return _helper.RemoveAll(key) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetFirstValue(in TKey key, out TValue item, out Iterator iterator)
|
||||||
|
{
|
||||||
|
var entryIndex = _helper.Find(key);
|
||||||
|
if (entryIndex == -1)
|
||||||
|
{
|
||||||
|
item = default;
|
||||||
|
iterator = new(default, -1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = UnsafeUtility.ReadArrayElement<TValue>(_helper.Buffer, entryIndex);
|
||||||
|
iterator = new(key, entryIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetNextValue(out TValue item, ref Iterator iterator)
|
||||||
|
{
|
||||||
|
if (iterator._entryIndex == -1)
|
||||||
|
{
|
||||||
|
item = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryIndex = _helper.FindNext(iterator._entryIndex, iterator._key);
|
||||||
|
if (entryIndex == -1)
|
||||||
|
{
|
||||||
|
item = default;
|
||||||
|
iterator._entryIndex = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator._entryIndex = entryIndex;
|
||||||
|
item = UnsafeUtility.ReadArrayElement<TValue>(_helper.Buffer, entryIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValue(in TKey key, out TValue item)
|
||||||
|
{
|
||||||
|
return _helper.TryGetValue(key, out item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TValue GetValueOrDefault(in TKey key, TValue defaultValue = default)
|
||||||
|
{
|
||||||
|
if (_helper.TryGetValue<TValue>(key, out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueEnumerable GetValuesForKey(in TKey key)
|
||||||
|
{
|
||||||
|
return new((HashMapHelper<TKey>*)UnsafeUtility.AddressOf(ref this), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CountValuesForKey(in TKey key)
|
||||||
|
{
|
||||||
|
return _helper.CountValuesForKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsKey(in TKey key)
|
||||||
|
{
|
||||||
|
return _helper.Find(key) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TrimExcess()
|
||||||
|
{
|
||||||
|
_helper.TrimExcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||||
|
{
|
||||||
|
_helper.Resize(newSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_helper.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeArray<TKey> GetKeyArray(Allocator allocator)
|
||||||
|
{
|
||||||
|
return _helper.GetKeyArray(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeArray<TValue> GetValueArray(Allocator allocator)
|
||||||
|
{
|
||||||
|
return _helper.GetValueArray<TValue>(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeArray<KeyValuePair<TKey, TValue>> GetKeyValueArrays(Allocator allocator)
|
||||||
|
{
|
||||||
|
return _helper.GetKeyValueArrays<TValue>(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly void* GetUnsafePtr()
|
||||||
|
{
|
||||||
|
return _helper.Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_helper.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor. Use <see cref="UnsafeQueue(int, Allocator, AllocationOption)"/> or <see cref="UnsafeQueue(int, ref AllocationHandle, AllocationOption)"/> instead."/>
|
/// Invalid constructor. Use <see cref="UnsafeQueue(int, Allocator, AllocationOption)"/> or <see cref="UnsafeQueue(int, AllocationHandle, AllocationOption)"/> instead."/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeQueue()
|
public UnsafeQueue()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -102,7 +102,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor. Use <see cref="UnsafeSlotMap(int, Allocator, AllocationOption)"/> or <see cref="UnsafeSlotMap(int, ref AllocationHandle, AllocationOption)"/> instead."/>
|
/// Invalid constructor. Use <see cref="UnsafeSlotMap(int, Allocator, AllocationOption)"/> or <see cref="UnsafeSlotMap(int, AllocationHandle, AllocationOption)"/> instead."/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeSlotMap()
|
public UnsafeSlotMap()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -97,7 +97,7 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid constructor, use <see cref="UnsafeStack(int, Allocator, AllocationOption)"/> or <see cref="UnsafeStack(int, ref AllocationHandle, AllocationOption)"/> instead.
|
/// Invalid constructor, use <see cref="UnsafeStack(int, Allocator, AllocationOption)"/> or <see cref="UnsafeStack(int, AllocationHandle, AllocationOption)"/> instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UnsafeStack()
|
public UnsafeStack()
|
||||||
: this(0, Allocator.Invalid)
|
: this(0, Allocator.Invalid)
|
||||||
@@ -7,12 +7,13 @@
|
|||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<Authors>Misaki</Authors>
|
<Authors>Misaki</Authors>
|
||||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
<AssemblyVersion>1.4.1</AssemblyVersion>
|
||||||
<Version>$(AssemblyVersion)</Version>
|
<Version>$(AssemblyVersion)</Version>
|
||||||
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
|
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
|
||||||
<RepositoryUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</RepositoryUrl>
|
<RepositoryUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</RepositoryUrl>
|
||||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
<ContentTargetFolders>contentFiles</ContentTargetFolders>
|
<ContentTargetFolders>contentFiles</ContentTargetFolders>
|
||||||
|
<PackageType>Dependency</PackageType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@@ -29,20 +30,11 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="contentFiles\cs\any\**\*.cs">
|
<None Update="Collections\FixedString.tt">
|
||||||
<Pack>true</Pack>
|
|
||||||
<PackagePath>contentFiles\cs\any\Misaki.HighPerformance.LowLevel\</PackagePath>
|
|
||||||
<PackageCopyToOutput>false</PackageCopyToOutput>
|
|
||||||
<BuildAction>Compile</BuildAction>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="contentFiles\cs\any\Collections\FixedString.tt">
|
|
||||||
<Generator>TextTemplatingFileGenerator</Generator>
|
<Generator>TextTemplatingFileGenerator</Generator>
|
||||||
<LastGenOutput>FixedString.gen.cs</LastGenOutput>
|
<LastGenOutput>FixedString.gen.cs</LastGenOutput>
|
||||||
</None>
|
</None>
|
||||||
<None Update="contentFiles\cs\any\Collections\FixedText.tt">
|
<None Update="Collections\FixedText.tt">
|
||||||
<Generator>TextTemplatingFileGenerator</Generator>
|
<Generator>TextTemplatingFileGenerator</Generator>
|
||||||
<LastGenOutput>FixedText.gen.cs</LastGenOutput>
|
<LastGenOutput>FixedText.gen.cs</LastGenOutput>
|
||||||
</None>
|
</None>
|
||||||
@@ -53,12 +45,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="contentFiles\cs\any\Collections\FixedString.gen.cs">
|
<Compile Update="Collections\FixedString.gen.cs">
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
<DependentUpon>FixedString.tt</DependentUpon>
|
<DependentUpon>FixedString.tt</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="contentFiles\cs\any\Collections\FixedText.gen.cs">
|
<Compile Update="Collections\FixedText.gen.cs">
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
<DependentUpon>FixedText.tt</DependentUpon>
|
<DependentUpon>FixedText.tt</DependentUpon>
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class TestUnsafeMultiHashMap
|
||||||
|
{
|
||||||
|
private UnsafeMultiHashMap<int, int> _multiHashMap;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_multiHashMap = new UnsafeMultiHashMap<int, int>(4, Allocator.Persistent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
_multiHashMap.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Add_AllowsDuplicateKeys()
|
||||||
|
{
|
||||||
|
_multiHashMap.Add(1, 10);
|
||||||
|
_multiHashMap.Add(1, 20);
|
||||||
|
_multiHashMap.Add(2, 30);
|
||||||
|
_multiHashMap.Add(1, 40);
|
||||||
|
|
||||||
|
Assert.AreEqual(4, _multiHashMap.Count);
|
||||||
|
Assert.IsTrue(_multiHashMap.ContainsKey(1));
|
||||||
|
Assert.AreEqual(3, _multiHashMap.CountValuesForKey(1));
|
||||||
|
|
||||||
|
var values = new int[3];
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
Assert.IsTrue(_multiHashMap.TryGetFirstValue(1, out values[count++], out var iterator));
|
||||||
|
|
||||||
|
while (_multiHashMap.TryGetNextValue(out var value, ref iterator))
|
||||||
|
{
|
||||||
|
values[count++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(3, count);
|
||||||
|
Array.Sort(values);
|
||||||
|
CollectionAssert.AreEqual(new[] { 10, 20, 40 }, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetValuesForKey_EnumeratesAllMatchingValues()
|
||||||
|
{
|
||||||
|
_multiHashMap.Add(7, 1);
|
||||||
|
_multiHashMap.Add(7, 2);
|
||||||
|
_multiHashMap.Add(3, 99);
|
||||||
|
_multiHashMap.Add(7, 3);
|
||||||
|
|
||||||
|
var values = new int[3];
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
foreach (var value in _multiHashMap.GetValuesForKey(7))
|
||||||
|
{
|
||||||
|
values[index++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(3, index);
|
||||||
|
Array.Sort(values);
|
||||||
|
CollectionAssert.AreEqual(new[] { 1, 2, 3 }, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Remove_RemovesAllValuesForKey()
|
||||||
|
{
|
||||||
|
_multiHashMap.Add(5, 10);
|
||||||
|
_multiHashMap.Add(5, 20);
|
||||||
|
_multiHashMap.Add(6, 30);
|
||||||
|
|
||||||
|
Assert.IsTrue(_multiHashMap.Remove(5));
|
||||||
|
|
||||||
|
Assert.AreEqual(1, _multiHashMap.Count);
|
||||||
|
Assert.IsFalse(_multiHashMap.ContainsKey(5));
|
||||||
|
Assert.AreEqual(0, _multiHashMap.CountValuesForKey(5));
|
||||||
|
Assert.IsFalse(_multiHashMap.TryGetFirstValue(5, out _, out _));
|
||||||
|
Assert.IsTrue(_multiHashMap.TryGetValue(6, out var remainingValue));
|
||||||
|
Assert.AreEqual(30, remainingValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Clear_RemovesAllEntries()
|
||||||
|
{
|
||||||
|
_multiHashMap.Add(1, 10);
|
||||||
|
_multiHashMap.Add(1, 20);
|
||||||
|
_multiHashMap.Add(2, 30);
|
||||||
|
|
||||||
|
_multiHashMap.Clear();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, _multiHashMap.Count);
|
||||||
|
Assert.IsFalse(_multiHashMap.ContainsKey(1));
|
||||||
|
Assert.IsFalse(_multiHashMap.ContainsKey(2));
|
||||||
|
Assert.IsFalse(_multiHashMap.TryGetFirstValue(1, out _, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Resize_PreservesDuplicateValues()
|
||||||
|
{
|
||||||
|
const int keyCount = 4;
|
||||||
|
const int valuesPerKey = 8;
|
||||||
|
|
||||||
|
for (var key = 0; key < keyCount; key++)
|
||||||
|
{
|
||||||
|
for (var value = 0; value < valuesPerKey; value++)
|
||||||
|
{
|
||||||
|
_multiHashMap.Add(key, key * 100 + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(keyCount * valuesPerKey, _multiHashMap.Count);
|
||||||
|
|
||||||
|
for (var key = 0; key < keyCount; key++)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(valuesPerKey, _multiHashMap.CountValuesForKey(key));
|
||||||
|
|
||||||
|
var values = new int[valuesPerKey];
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
Assert.IsTrue(_multiHashMap.TryGetFirstValue(key, out values[index++], out var iterator));
|
||||||
|
while (_multiHashMap.TryGetNextValue(out var value, ref iterator))
|
||||||
|
{
|
||||||
|
values[index++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(valuesPerKey, index);
|
||||||
|
Array.Sort(values);
|
||||||
|
|
||||||
|
var expected = new int[valuesPerKey];
|
||||||
|
for (var value = 0; value < valuesPerKey; value++)
|
||||||
|
{
|
||||||
|
expected[value] = key * 100 + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(expected, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Enumerate_ReturnsEveryPair()
|
||||||
|
{
|
||||||
|
_multiHashMap.Add(1, 10);
|
||||||
|
_multiHashMap.Add(1, 20);
|
||||||
|
_multiHashMap.Add(2, 30);
|
||||||
|
|
||||||
|
var pairs = new List<KeyValuePair<int, int>>();
|
||||||
|
foreach (var pair in _multiHashMap)
|
||||||
|
{
|
||||||
|
pairs.Add(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(3, pairs.Count);
|
||||||
|
CollectionAssert.AreEquivalent(
|
||||||
|
new List<KeyValuePair<int, int>>
|
||||||
|
{
|
||||||
|
new(1, 10),
|
||||||
|
new(1, 20),
|
||||||
|
new(2, 30),
|
||||||
|
},
|
||||||
|
pairs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -263,73 +263,6 @@ public unsafe class TestJobSystem
|
|||||||
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(completedHandle));
|
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(completedHandle));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void RaceConditionTest()
|
|
||||||
{
|
|
||||||
const int jobCount = 20000;
|
|
||||||
|
|
||||||
var pExecutedCount = (int*)NativeMemory.Alloc(sizeof(int));
|
|
||||||
*pExecutedCount = 0;
|
|
||||||
|
|
||||||
var startSignal = false;
|
|
||||||
|
|
||||||
// 1. Create a "Gatekeeper" vectorJob that spins/blocks a worker thread until signaled.
|
|
||||||
// This allows us to control exactly when the dependency completes.
|
|
||||||
var rootJob = new WaitJob { pSignal = &startSignal };
|
|
||||||
var rootHandle = s_jobScheduler.Schedule(ref rootJob);
|
|
||||||
|
|
||||||
// 2. Start a background task to flood the scheduler with dependencies on the Gatekeeper.
|
|
||||||
using var barrier = new Barrier(2);
|
|
||||||
var scheduleTask = Task.Run(() =>
|
|
||||||
{
|
|
||||||
var depJob = new IncrementJob { pCounter = pExecutedCount };
|
|
||||||
barrier.SignalAndWait(TestContext.CancellationTokenSource.Token); // Synchronize start with main thread
|
|
||||||
|
|
||||||
for (var i = 0; i < jobCount; i++)
|
|
||||||
{
|
|
||||||
// CONTENTION POINT:
|
|
||||||
// Trying to add a dependency to 'rootHandle'.
|
|
||||||
// Eventually, this will happen exactly while 'rootHandle' is transitioning to Completed.
|
|
||||||
s_jobScheduler.Schedule(ref depJob, rootHandle);
|
|
||||||
}
|
|
||||||
}, TestContext.CancellationTokenSource.Token);
|
|
||||||
|
|
||||||
barrier.SignalAndWait(TestContext.CancellationTokenSource.Token); // Wait for scheduler task to be ready
|
|
||||||
|
|
||||||
// Allow the scheduling loop to get a head start and queue some readers
|
|
||||||
Thread.Sleep(5);
|
|
||||||
|
|
||||||
// 3. Open the gate.
|
|
||||||
// This triggers the Gatekeeper to complete. It will change its State and iterate its dependency list.
|
|
||||||
// This happens CONCURRENTLY with the loop above adding more items to that same list.
|
|
||||||
startSignal = true;
|
|
||||||
|
|
||||||
scheduleTask.Wait(TestContext.CancellationTokenSource.Token);
|
|
||||||
|
|
||||||
// 4. Validate results
|
|
||||||
// If the lock-free logic works, every single dependent vectorJob must eventually execute.
|
|
||||||
// If there is a race (e.g., missed notification), pExecutedCount will stick below jobCount.
|
|
||||||
var spin = new SpinWait();
|
|
||||||
var timeout = DateTime.Now.AddSeconds(10);
|
|
||||||
|
|
||||||
while (Volatile.Read(ref *pExecutedCount) < jobCount)
|
|
||||||
{
|
|
||||||
if (DateTime.Now > timeout)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
spin.SpinOnce();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the root vectorJob is officially cleaned up
|
|
||||||
s_jobScheduler.Wait(rootHandle);
|
|
||||||
|
|
||||||
Assert.AreEqual(jobCount, *pExecutedCount, "Race condition detected: Some dependent jobs failed to execute (Wait timeout).");
|
|
||||||
|
|
||||||
NativeMemory.Free(pExecutedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void SPMDCorrectness()
|
public void SPMDCorrectness()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.Utilities;
|
namespace Misaki.HighPerformance.Utilities;
|
||||||
@@ -13,6 +14,7 @@ public static class CollectionUtility
|
|||||||
/// <typeparam name="T">The type of elements in the list.</typeparam>
|
/// <typeparam name="T">The type of elements in the list.</typeparam>
|
||||||
/// <param name="list">The list whose elements the span will cover. Can be null.</param>
|
/// <param name="list">The list whose elements the span will cover. Can be null.</param>
|
||||||
/// <returns>A span over the elements of the list, or an empty span if the list is null or empty.</returns>
|
/// <returns>A span over the elements of the list, or an empty span if the list is null or empty.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Span<T> AsSpan<T>(this List<T>? list)
|
public static Span<T> AsSpan<T>(this List<T>? list)
|
||||||
{
|
{
|
||||||
return CollectionsMarshal.AsSpan(list);
|
return CollectionsMarshal.AsSpan(list);
|
||||||
@@ -43,4 +45,30 @@ public static class CollectionUtility
|
|||||||
list.RemoveAt(lastIndex);
|
list.RemoveAt(lastIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a reference to the element at the specified index within the given span without performing bounds checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of elements contained in the span.</typeparam>
|
||||||
|
/// <param name="span">The span from which to retrieve the element.</param>
|
||||||
|
/// <param name="index">The zero-based index of the element to retrieve.</param>
|
||||||
|
/// <returns>A reference to the element at the specified index in the span.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ref readonly T GetElementUnsafe<T>(this Span<T> span, int index)
|
||||||
|
{
|
||||||
|
return ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only reference to the element at the specified index within the given span without performing bounds checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of elements contained in the span.</typeparam>
|
||||||
|
/// <param name="span">The read-only span from which to retrieve the element.</param>
|
||||||
|
/// <param name="index">The zero-based index of the element to retrieve.</param>
|
||||||
|
/// <returns>A read-only reference to the element at the specified index in the span.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static ref readonly T GetElementUnsafe<T>(this ReadOnlySpan<T> span, int index)
|
||||||
|
{
|
||||||
|
return ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user