Job system priorities, async waits, parallel map/queue

Major refactor:
- Add job priority tiers and async wait APIs to IJobScheduler
- Implement priority-based job queues and scheduling logic
- Introduce UnsafeParallelHashMap and refactor UnsafeParallelQueue
- Refactor UnsafeSlotMap to chunked storage for scalability
- Update SlotMap/ConcurrentSlotMap for consistency and perf
- Add new benchmarks and unit tests for parallel collections
- Misc: add MemoryUtility.AlignUp, version bumps, test improvements, bug fixes
This commit is contained in:
2026-04-18 11:26:08 +09:00
parent d5616daa05
commit 13802ca6c8
22 changed files with 1459 additions and 267 deletions

View File

@@ -15,7 +15,7 @@ public class TestUnsafeChunkedQueue
[TestMethod]
public void BasicEnqueueDequeueTest()
{
using var queue = new UnsafeChunkedQueue<int>(32, AllocationHandle.Persistent);
using var queue = new UnsafeParallelQueue<int>(32, AllocationHandle.Persistent);
Assert.IsTrue(queue.IsCreated);
@@ -35,7 +35,7 @@ public class TestUnsafeChunkedQueue
public void ChunkExpansionTest()
{
// Force chunk expansions by enqueuing more than the chunk capacity
using var queue = new UnsafeChunkedQueue<int>(16, AllocationHandle.Persistent);
using var queue = new UnsafeParallelQueue<int>(16, AllocationHandle.Persistent);
var totalItems = 100;
@@ -57,7 +57,7 @@ public class TestUnsafeChunkedQueue
public void ConcurrentEnqueueDequeueTest()
{
// Multi-threaded stress test to verify lock-free safety and chunk caching
using var queue = new UnsafeChunkedQueue<int>(64, AllocationHandle.Persistent);
using var queue = new UnsafeParallelQueue<int>(64, AllocationHandle.Persistent);
var totalElements = 100_000;
var enqueueTask = Task.Run(() =>

View File

@@ -0,0 +1,154 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
[TestClass]
public class TestUnsafeParallelHashMap
{
[TestMethod]
public void TestParallelWrite()
{
using var map = new UnsafeParallelHashMap<int, int>(10000, 1, AllocationHandle.Temp, AllocationOption.None);
var writer = map.AsParallelWriter();
Parallel.For(0, 10000, i =>
{
writer.TryAdd(i, i * 2);
});
Assert.AreEqual(10000, map.Count);
for (var i = 0; i < 10000; i++)
{
Assert.IsTrue(map.TryGetValue(i, out var val));
Assert.AreEqual(i * 2, val);
}
}
[TestMethod]
public void TestBasicOperations()
{
using var map = new UnsafeParallelHashMap<int, int>(16, 1, AllocationHandle.Temp, AllocationOption.None);
Assert.IsTrue(map.IsEmpty);
Assert.AreEqual(0, map.Count);
// Add
map.Add(1, 10);
map.Add(2, 20);
map.Add(3, 30);
Assert.IsFalse(map.IsEmpty);
Assert.AreEqual(3, map.Count);
// TryGetValue existing
Assert.IsTrue(map.TryGetValue(2, out var val));
Assert.AreEqual(20, val);
// TryGetValue non-existing
Assert.IsFalse(map.TryGetValue(4, out _));
// Remove existing
Assert.IsTrue(map.Remove(2));
Assert.AreEqual(2, map.Count);
Assert.IsFalse(map.TryGetValue(2, out _));
// Remove non-existing
Assert.IsFalse(map.Remove(4));
// Clear
map.Clear();
Assert.AreEqual(0, map.Count);
Assert.IsTrue(map.IsEmpty);
}
[TestMethod]
public void TestResize()
{
using var map = new UnsafeParallelHashMap<int, int>(16, 1, AllocationHandle.Temp, AllocationOption.None);
// Single thread adds causing resize
for (var i = 0; i < 1000; i++)
{
map.Add(i, i * 10);
}
Assert.AreEqual(1000, map.Count);
Assert.IsTrue(map.Capacity >= 1000);
for (var i = 0; i < 1000; i++)
{
Assert.IsTrue(map.TryGetValue(i, out var val));
Assert.AreEqual(i * 10, val);
}
}
private struct BadKey : IEquatable<BadKey>
{
public int Id;
public BadKey(int id) => Id = id;
public bool Equals(BadKey other) => Id == other.Id;
public override int GetHashCode() => 1; // Force collision
}
[TestMethod]
public void TestHashCollisions()
{
using var map = new UnsafeParallelHashMap<BadKey, int>(16, 1, AllocationHandle.Temp, AllocationOption.None);
for (var i = 0; i < 10; i++)
{
map.Add(new BadKey(i), i * 5);
}
Assert.AreEqual(10, map.Count);
// Verify we can retrieve them all out of the same bucket
for (var i = 0; i < 10; i++)
{
Assert.IsTrue(map.TryGetValue(new BadKey(i), out var val));
Assert.AreEqual(i * 5, val);
}
// Remove from the middle of the linked list
Assert.IsTrue(map.Remove(new BadKey(5)));
Assert.IsFalse(map.TryGetValue(new BadKey(5), out _));
Assert.AreEqual(9, map.Count);
// Make sure everything else is intact
Assert.IsTrue(map.TryGetValue(new BadKey(6), out var val6));
Assert.AreEqual(6 * 5, val6);
}
[TestMethod]
public void TestAddDuplicate()
{
using var map = new UnsafeParallelHashMap<int, int>(16, 1, AllocationHandle.Temp, AllocationOption.None);
Assert.AreEqual(0, map.Add(1, 100));
// Adding again should return -1
Assert.AreEqual(-1, map.Add(1, 200));
Assert.AreEqual(1, map.Count);
var writer = map.AsParallelWriter();
Assert.IsFalse(writer.TryAdd(1, 300));
}
[TestMethod]
public void TestParallelWriteExceedsCapacity()
{
using var map = new UnsafeParallelHashMap<int, int>(50, 1, AllocationHandle.Temp, AllocationOption.None);
var writer = map.AsParallelWriter();
// The exact exception will be wrapped in AggregateException by Parallel.For
Assert.ThrowsExactly<AggregateException>(() =>
{
Parallel.For(0, 100, i =>
{
writer.TryAdd(i, i);
});
});
}
}

View File

@@ -71,6 +71,23 @@ public class TestUnsafeSlotMap
}
}
[TestMethod]
public void ReferenceValidAfterResize()
{
var id = _slotMap.Add(10, out var gen);
ref var value = ref _slotMap.GetElementReferenceAt(id, gen, out _);
Assert.AreEqual(10, value);
// Force resize
for (var i = 0; i < 20; i++)
{
_slotMap.Add(i, out _);
}
Assert.AreEqual(10, value);
}
[TestMethod]
public void Clear()
{

View File

@@ -9,7 +9,7 @@ namespace Misaki.HighPerformance.Test.UnitTest.Jobs;
[TestClass]
[DoNotParallelize]
public unsafe class TestJobSystem
public class TestJobSystem
{
private static JobScheduler s_jobScheduler = null!;
@@ -32,7 +32,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void SingleJob()
public unsafe void SingleJob()
{
var result = stackalloc float[1];
var job = new TwoSumJob
@@ -49,7 +49,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void JobDependency()
public unsafe void JobDependency()
{
var result = stackalloc float[1];
var job1 = new TwoSumJob
@@ -74,7 +74,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void CompletedDependency()
public unsafe void CompletedDependency()
{
var result = stackalloc float[1];
var job1 = new TwoSumJob
@@ -100,7 +100,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void CombineDependencies()
public unsafe void CombineDependencies()
{
var result = stackalloc float[1];
var job1 = new TwoSumJob
@@ -135,7 +135,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void SingleParallelJob()
public unsafe void SingleParallelJob()
{
const int size = 1000;
var result = stackalloc float[size];
@@ -167,7 +167,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void ChainJob()
public unsafe void ChainJob()
{
const int arraySize = 10000;
@@ -209,7 +209,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void WaitAll()
public unsafe void WaitAll()
{
var result1 = stackalloc float[1];
var result2 = stackalloc float[1];
@@ -235,8 +235,42 @@ public unsafe class TestJobSystem
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(handle2));
}
[TestMethod]
public void WaitAny()
public async Task WaitAllAsync()
{
AddJob job1;
AddJob job2;
unsafe
{
var result1 = stackalloc float[1];
var result2 = stackalloc float[1];
job1 = new AddJob
{
value = 1.0f,
result = result1
};
job2 = new AddJob
{
value = 1.0f,
result = result2
};
}
var handle1 = s_jobScheduler.Schedule(ref job1);
var handle2 = s_jobScheduler.Schedule(ref job2);
await s_jobScheduler.WaitAllAsync(new Memory<JobHandle>(new[] { handle1, handle2 }));
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(handle1));
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(handle2));
}
[TestMethod]
public unsafe void WaitAny()
{
var result1 = stackalloc float[1];
var result2 = stackalloc float[1];
@@ -262,7 +296,7 @@ public unsafe class TestJobSystem
}
[TestMethod]
public void SPMDCorrectness()
public unsafe void SPMDCorrectness()
{
const int size = 8;