diff --git a/Misaki.HighPerformance.Jobs/JobScheduler.cs b/Misaki.HighPerformance.Jobs/JobScheduler.cs index d165272..094cc0f 100644 --- a/Misaki.HighPerformance.Jobs/JobScheduler.cs +++ b/Misaki.HighPerformance.Jobs/JobScheduler.cs @@ -6,6 +6,15 @@ using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.Jobs; +/// +/// Provides a mechanism for scheduling and executing jobs across multiple worker threads. +/// +/// The class is designed to manage the execution of jobs, including support +/// for dependencies, parallel execution, and thread-specific job assignment. It allows developers to schedule jobs that +/// implement the or interfaces, and it ensures efficient utilization +/// of worker threads through job batching and work-stealing mechanisms. This class is thread-safe and can be used in +/// multi-threaded environments. However, it must be disposed when no longer needed to release resources and terminate +/// worker threads. public unsafe sealed class JobScheduler : IDisposable { private FreeList _jobDataAllocator; @@ -23,6 +32,10 @@ public unsafe sealed class JobScheduler : IDisposable internal bool IsCancellationRequested => _cts.IsCancellationRequested; + /// + /// Initializes a new instance of the class with the specified number of worker threads. + /// + /// The number of worker threads to create. If less than 1, at least one thread will be created. public JobScheduler(int threadCount) { _jobDataAllocator = new(8); @@ -333,7 +346,7 @@ public unsafe sealed class JobScheduler : IDisposable /// /// A collection of instances representing the dependencies to combine. /// A that represents the combined dependencies. The returned handle can be used to ensure - /// that all specified dependencies are completed before proceeding. + /// that all specified dependencies are completed before proceeding. public JobHandle CombineDependencies(params ReadOnlySpan dependencies) { var jobInfo = new JobInfo @@ -350,6 +363,28 @@ public unsafe sealed class JobScheduler : IDisposable return CreateJobHandle(ref jobInfo, dependencies); } + /// + /// Retrieves the current status of a job identified by the specified handle. + /// + /// The handle representing the job whose status is to be retrieved. The handle must be valid. + /// The current status of the job as a value. + /// Returns if the handle is invalid or the job does not exist. + public JobStatus GetJobStatus(JobHandle handle) + { + if (!handle.IsValid) + { + return JobStatus.Invalid; + } + + ref var jobInfo = ref _jobInfoPool.GetElementReferenceAt(handle._id, handle._generation, out var exist); + if (!exist) + { + return JobStatus.Invalid; + } + + return (JobStatus)Volatile.Read(ref Unsafe.As(ref jobInfo.status)); + } + /// /// Blocks the calling thread until the specified job is completed. /// diff --git a/Misaki.HighPerformance.Jobs/WorkerThread.cs b/Misaki.HighPerformance.Jobs/WorkerThread.cs index 9a3adcf..e030c56 100644 --- a/Misaki.HighPerformance.Jobs/WorkerThread.cs +++ b/Misaki.HighPerformance.Jobs/WorkerThread.cs @@ -29,23 +29,67 @@ internal class WorkerThread : IDisposable public void Start() => _thread.Start(); + private JobHandle FindJob() + { + var handle = JobHandle.Invalid; + if (_localQueue.TryDequeue(out handle) + || _scheduler.TryStealJob(-1, out handle)) + { + return handle; + } + + while (true) + { + var randomIndex = _random.Next(0, _scheduler.WorkerCount); + if (_scheduler.TryStealJob(randomIndex, out handle)) + { + return handle; + } + } + } + private unsafe void WorkLoop() { while (!_scheduler.IsCancellationRequested) { - var handle = JobHandle.Invalid; - - // Always try the local thread and main thread queue first. - if (!_localQueue.TryDequeue(out handle) - && !_scheduler.TryStealJob(-1, out handle)) + var spinner = new SpinWait(); + for (var i = 0; i < 25; i++) { - var randomIndex = _random.Next(0, _scheduler.WorkerCount); - if (_scheduler.TryStealJob(randomIndex, out var tempHandle)) + spinner.SpinOnce(-1); + + if (_scheduler.HasWork()) { - handle = tempHandle; + // Instead of goto, we still need to go through the WaitForWork to claim a release. + // This causes lock and lots of branches inside the SemaphoreSlim, which lost 0.03ms. + // goto DoWork; + break; } } + try + { + _scheduler.WaitForWork(); + } + catch (OperationCanceledException) + { + continue; + } + + //var handle = JobHandle.Invalid; + + //// Always try the local thread and main thread queue first. + //if (!_localQueue.TryDequeue(out handle) + // && !_scheduler.TryStealJob(-1, out handle)) + //{ + // var randomIndex = _random.Next(0, _scheduler.WorkerCount); + // if (_scheduler.TryStealJob(randomIndex, out var tempHandle)) + // { + // handle = tempHandle; + // } + //} + + //DoWork: + var handle = FindJob(); ref var jobInfo = ref _scheduler.GetJobInfoReference(handle, out var exist); if (exist) @@ -59,31 +103,6 @@ internal class WorkerThread : IDisposable _scheduler.MarkJobComplete(handle); } } - else - { - var spinner = new SpinWait(); - for (var i = 0; i < 25; i++) - { - spinner.SpinOnce(-1); - - if (_scheduler.HasWork()) - { - goto FoundWork; - } - } - - try - { - _scheduler.WaitForWork(); - } - catch (OperationCanceledException) - { - continue; - } - } - - FoundWork: - ; } } diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs index 71f684b..33d9e89 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs @@ -51,7 +51,6 @@ public readonly unsafe struct AllocationInfo /// public static unsafe class AllocationManager { - private unsafe struct ArenaAllocator : IAllocator, IDisposable { private DynamicArena _arena; diff --git a/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs b/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs index b0fafde..f0d6139 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs @@ -49,15 +49,6 @@ public unsafe struct Arena : IDisposable throw new ObjectDisposedException(nameof(DynamicArena)); } - //var offset = _offset + alignment - 1 & ~(alignment - 1); - //if (offset + size > _size) - //{ - // return null; - //} - - //_offset = offset + size; - //var ptr = _buffer + offset; - nuint currentOffset, newOffset, alignedOffset; do diff --git a/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs b/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs index 6b81320..fbe1f58 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs @@ -90,22 +90,6 @@ public unsafe struct DynamicArena : IDisposable // Release the spinlock Interlocked.Exchange(ref _nodeCreationLock, 0); } - - //var newNode = (ArenaNode*)Malloc(SizeOf()); - //try - //{ - // newNode->arena = new Arena(size); - // newNode->next = null; - - // _current->next = newNode; - // _current = newNode; - // return true; - //} - //catch - //{ - // Free(newNode); - // return false; - //} } /// @@ -162,9 +146,6 @@ public unsafe struct DynamicArena : IDisposable _current = _root; } - /// - /// Disposes all arenas and frees associated memory. - /// public void Dispose() { if (_root == null) diff --git a/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs b/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs index bea4983..f815129 100644 --- a/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs +++ b/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs @@ -6,25 +6,6 @@ namespace Misaki.HighPerformance.LowLevel.Buffer; /// /// A lock-free, thread-safe variable-size allocator that manages memory blocks of different sizes. /// Optimized for high-performance scenarios with frequent allocations and deallocations. -/// -/// Example usage: -/// -/// // Create a free list with multiple size buckets -/// var freeList = new FreeList(); -/// -/// // Allocate a 70-byte block -/// var block = freeList.Allocate(70); -/// if (block.IsValid) -/// { -/// // Use the memory block... -/// -/// // Free the block when done -/// freeList.Free(block); -/// } -/// -/// // Dispose when finished -/// freeList.Dispose(); -/// /// [StructLayout(LayoutKind.Explicit, Size = 256)] // Cache line aligned to prevent false sharing public unsafe struct FreeList : IDisposable @@ -476,10 +457,6 @@ public unsafe struct FreeList : IDisposable } } - /// - /// Disposes the free list and frees all allocated memory. - /// Note: This method is NOT thread-safe by design as requested. - /// public void Dispose() { if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) diff --git a/Misaki.HighPerformance.LowLevel/Collections/CollectionHandle.cs b/Misaki.HighPerformance.LowLevel/Collections/CollectionHandle.cs new file mode 100644 index 0000000..87033c4 --- /dev/null +++ b/Misaki.HighPerformance.LowLevel/Collections/CollectionHandle.cs @@ -0,0 +1,47 @@ +namespace Misaki.HighPerformance.LowLevel.Collections; + +public readonly struct CollectionHandle +{ + public readonly int id; + public readonly int generation; + + public static CollectionHandle Invalid => new(-1, -1); + + public bool IsValid => this != Invalid; + + internal CollectionHandle(int id, int generation) + { + this.id = id; + this.generation = generation; + } + + public bool Equals(CollectionHandle other) + { + return id == other.id && generation == other.generation; + } + + public override bool Equals(object? obj) + { + return obj is CollectionHandle handle && Equals(handle); + } + + public override int GetHashCode() + { + return HashCode.Combine(id, generation); + } + + public override string ToString() + { + return IsValid ? $"CollectionHandle({id}, {generation})" : "CollectionHandle(Invalid)"; + } + + public static bool operator ==(CollectionHandle left, CollectionHandle right) + { + return left.Equals(right); + } + + public static bool operator !=(CollectionHandle left, CollectionHandle right) + { + return !(left == right); + } +} \ No newline at end of file diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs index f3fd2a2..e7760e4 100644 --- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs +++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs @@ -66,122 +66,6 @@ public unsafe struct UnsafeSparseSet : IUnsafeCollection } } - public struct ParallelWriter - { - private UnsafeSparseSet* _sparseSet; - - internal ParallelWriter(UnsafeSparseSet* sparseSet) - { - _sparseSet = sparseSet; - } - - /// - /// Adds a value to the sparse set without resizing the internal arrays. - /// - /// The value to add to the sparse set. - /// Returns the sparse index assigned to the value. -1 if the sparse index is out of bounds. - public int AddNoResize(T value) - { - int sparseIndex; - - if (_sparseSet->_freeCount > 0) - { - var index = Interlocked.Decrement(ref _sparseSet->_freeCount); - sparseIndex = _sparseSet->_freeList[index]; - } - else - { - sparseIndex = Interlocked.Increment(ref _sparseSet->_nextId) - 1; - } - - if (sparseIndex >= _sparseSet->_sparse.Count) - { - return -1; - } - - var count = Interlocked.Increment(ref _sparseSet->_count) - 1; - - _sparseSet->_dense[count] = value; - _sparseSet->_sparse[sparseIndex] = count; - _sparseSet->_reverse[count] = sparseIndex; - - - return sparseIndex; - } - - /// - /// Attempts to add a value at the specified sparse index without resizing the underlying collection. - /// - /// The index in the sparse array where the value should be added. Must be within the valid range of the sparse - /// array. - /// The value to add to the collection. - /// if the value was successfully added at the specified index; otherwise, . - public bool AddAtNoResize(int sparseIndex, T value) - { - if (sparseIndex < 0 || sparseIndex >= _sparseSet->_sparse.Count) - { - return false; - } - - if (_sparseSet->Contains(sparseIndex)) - { - return false; - } - - if (_sparseSet->_count >= _sparseSet->_dense.Count) - { - return false; - } - - var count = Interlocked.Increment(ref _sparseSet->_count) - 1; - - _sparseSet->_dense[count] = value; - _sparseSet->_sparse[sparseIndex] = count; - _sparseSet->_reverse[count] = sparseIndex; - - return true; - } - - /// - /// Removes a value at the specified sparse index without resizing the internal arrays. - /// - /// The sparse index of the value to remove. - /// Returns if the value was successfully removed; otherwise, . - public bool RemoveNoResize(int sparseIndex) - { - if (!_sparseSet->Contains(sparseIndex)) - { - return false; - } - - var denseIndex = _sparseSet->_sparse[sparseIndex]; - var lastIndex = _sparseSet->_count - 1; - - if (denseIndex != lastIndex) - { - var lastValue = _sparseSet->_dense[lastIndex]; - var lastSparseIndex = _sparseSet->_reverse[lastIndex]; - _sparseSet->_dense[denseIndex] = lastValue; - _sparseSet->_reverse[denseIndex] = lastSparseIndex; - _sparseSet->_sparse[lastSparseIndex] = denseIndex; - } - - _sparseSet->_sparse[sparseIndex] = -1; - if (_sparseSet->_freeCount >= _sparseSet->_freeList.Count) - { - return false; - } - - _sparseSet->_freeList[_sparseSet->_freeCount] = sparseIndex; - - Interlocked.Increment(ref _sparseSet->_freeCount); - Interlocked.Decrement(ref _sparseSet->_count); - - return true; - } - } - private UnsafeArray _dense; private UnsafeArray _sparse; private UnsafeArray _reverse; // Maps dense index to sparse index @@ -194,12 +78,6 @@ public unsafe struct UnsafeSparseSet : IUnsafeCollection public readonly int Capacity => _dense.Count; public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated && _freeList.IsCreated; - public readonly ref T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref _dense[index]; - } - public IEnumerator GetEnumerator() => new Enumerator((UnsafeSparseSet*)UnsafeUtilities.AddressOf(ref this)); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeSparseSet.cs b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeSparseSet.cs new file mode 100644 index 0000000..6448f96 --- /dev/null +++ b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeSparseSet.cs @@ -0,0 +1,46 @@ +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; + +namespace Misaki.HighPerformance.Test.UnitTest.Collections; + +[TestClass] +public class TestUnsafeSparseSet +{ + private UnsafeSparseSet _sparseSet; + + [TestInitialize] + public void Initialize() + { + _sparseSet = new UnsafeSparseSet(16, Allocator.Persistent); + } + + [TestMethod] + public void Add() + { + var id = _sparseSet.Add(10); + Assert.IsTrue(_sparseSet.Contains(id)); + } + + [TestMethod] + public void Remove() + { + var id = _sparseSet.Add(20); + Assert.IsTrue(_sparseSet.Contains(id)); + + _sparseSet.Remove(id); + Assert.IsFalse(_sparseSet.Contains(id)); + } + + [TestMethod] + public void IndexReuse() + { + var id = _sparseSet.Add(20); + Assert.IsTrue(_sparseSet.Contains(id)); + + _sparseSet.Remove(id); + Assert.IsFalse(_sparseSet.Contains(id)); + + var newId = _sparseSet.Add(30); + Assert.AreEqual(id, newId); + } +} \ No newline at end of file