Added UnsafeMultiHashMap

This commit is contained in:
2026-03-08 15:38:00 +09:00
parent 37d548085e
commit 080ad16724
40 changed files with 619 additions and 156 deletions

View File

@@ -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))

View File

@@ -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) public int Add(in TKey key)
{ {
var newCap = CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth)); ThrowIfNotCreated();
Resize(newCap);
}
idx = _firstFreeIndex; return AllocateEntry(key);
if (idx >= 0)
{
_firstFreeIndex = _next[idx];
}
else
{
idx = _allocatedIndex++;
}
CheckIndexOutOfBounds(idx);
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));

View File

@@ -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)

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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()
{ {

View File

@@ -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);
}
} }