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:
@@ -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(() =>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user