From d5616daa05a941c262a4fdc9f6cc6790b124b4ec Mon Sep 17 00:00:00 2001 From: Misaki Date: Fri, 17 Apr 2026 19:49:42 +0900 Subject: [PATCH] Refactor JobExecutionContext, add scheduler state support - Refactored JobExecutionContext to use init-only properties and added State and SelfHandle for richer context. - Updated JobInfo field layout for clarity and memory alignment. - JobScheduler now accepts and exposes an optional state object, passed to each job context. - Improved memory management by using _freeList for allocations and disposal. - WorkerThread and benchmarks updated to use new JobExecutionContext pattern. - Bumped version to 1.6.1 and performed minor code cleanup. --- .../JobExecutionContext.cs | 14 +- Misaki.HighPerformance.Jobs/JobInfo.cs | 6 +- Misaki.HighPerformance.Jobs/JobScheduler.cs | 16 +- .../Misaki.HighPerformance.Jobs.csproj | 2 +- Misaki.HighPerformance.Jobs/WorkerThread.cs | 9 +- .../Benchmark/SPMDBenchmark.cs | 3 +- Misaki.HighPerformance.Test/Program.cs | 143 ++++++++++++++++-- .../UnitTest/Jobs/TestJobSystem.cs | 3 +- 8 files changed, 164 insertions(+), 32 deletions(-) diff --git a/Misaki.HighPerformance.Jobs/JobExecutionContext.cs b/Misaki.HighPerformance.Jobs/JobExecutionContext.cs index 1f3943b..9cbe01c 100644 --- a/Misaki.HighPerformance.Jobs/JobExecutionContext.cs +++ b/Misaki.HighPerformance.Jobs/JobExecutionContext.cs @@ -4,17 +4,21 @@ public readonly ref struct JobExecutionContext { public int ThreadIndex { - get; + get; init; } public IJobScheduler JobScheduler { - get; + get; init; } - public JobExecutionContext(int threadIndex, IJobScheduler jobScheduler) + public object? State { - ThreadIndex = threadIndex; - JobScheduler = jobScheduler; + get; init; + } + + public JobHandle SelfHandle + { + get; init; } } \ No newline at end of file diff --git a/Misaki.HighPerformance.Jobs/JobInfo.cs b/Misaki.HighPerformance.Jobs/JobInfo.cs index d236cdb..c371690 100644 --- a/Misaki.HighPerformance.Jobs/JobInfo.cs +++ b/Misaki.HighPerformance.Jobs/JobInfo.cs @@ -84,6 +84,9 @@ internal unsafe struct JobInfo public const int MAX_DEPENDENTS = 8; + public void* pJobData; + public JobExecutionFunc pExecutionFunc; + // The list of jobs that are waiting for THIS job to complete. public fixed int dependentsID[MAX_DEPENDENTS]; // The actual list of IDs public fixed int dependentsGeneration[MAX_DEPENDENTS]; // The actual list of generations @@ -93,9 +96,6 @@ internal unsafe struct JobInfo public JobRanges jobRanges; - public void* pJobData; - public JobExecutionFunc pExecutionFunc; - public int state; public int remainingBatches; diff --git a/Misaki.HighPerformance.Jobs/JobScheduler.cs b/Misaki.HighPerformance.Jobs/JobScheduler.cs index fc7493c..389a382 100644 --- a/Misaki.HighPerformance.Jobs/JobScheduler.cs +++ b/Misaki.HighPerformance.Jobs/JobScheduler.cs @@ -5,7 +5,6 @@ using Misaki.HighPerformance.LowLevel.Utilities; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Misaki.HighPerformance.Jobs; @@ -26,9 +25,12 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable private readonly SemaphoreSlim _workSignal; private readonly CancellationTokenSource _cts; + private readonly object? _state; + private bool _disposed = false; internal bool IsCancellationRequested => _cts.IsCancellationRequested; + internal object? State => _state; public int WorkerCount => _workerThreads.Length; @@ -37,7 +39,8 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable /// /// The number of worker threads to create. If less than 1, at least one thread will be created. /// The priority of the worker threads. - public JobScheduler(int threadCount, ThreadPriority priority = ThreadPriority.Normal) + /// The state object for the job scheduler. + public JobScheduler(int threadCount, ThreadPriority priority = ThreadPriority.Normal, object? state = null) { var workerCount = Math.Max(1, threadCount); @@ -49,6 +52,8 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable _workSignal = new SemaphoreSlim(0); _cts = new CancellationTokenSource(); + _state = state; + _workerThreads = new WorkerThread[workerCount]; for (var i = 0; i < workerCount; i++) @@ -308,6 +313,8 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable } } + info.additionalDependents.Dispose(); + _freeList.Free(info.pJobData); _jobInfoPool.Remove(handle.ID, handle.Generation); } @@ -320,7 +327,7 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable { return JobHandle.Invalid; } - + *(T*)pJobData = job; var jobInfo = new JobInfo @@ -573,10 +580,11 @@ public sealed unsafe partial class JobScheduler : IJobScheduler, IDisposable { if (info.pJobData != null) { - NativeMemory.Free(info.pJobData); + _freeList.Free(info.pJobData); } } + _freeList.Dispose(); _workSignal.Dispose(); _cts.Dispose(); diff --git a/Misaki.HighPerformance.Jobs/Misaki.HighPerformance.Jobs.csproj b/Misaki.HighPerformance.Jobs/Misaki.HighPerformance.Jobs.csproj index c5b19bc..84987f7 100644 --- a/Misaki.HighPerformance.Jobs/Misaki.HighPerformance.Jobs.csproj +++ b/Misaki.HighPerformance.Jobs/Misaki.HighPerformance.Jobs.csproj @@ -6,7 +6,7 @@ enable True True - 1.6.0 + 1.6.1 $(AssemblyVersion) Misaki https://git.personalnas.com/Misaki/Misaki.HighPerformance.git diff --git a/Misaki.HighPerformance.Jobs/WorkerThread.cs b/Misaki.HighPerformance.Jobs/WorkerThread.cs index ef98ab1..4721d03 100644 --- a/Misaki.HighPerformance.Jobs/WorkerThread.cs +++ b/Misaki.HighPerformance.Jobs/WorkerThread.cs @@ -106,7 +106,14 @@ internal class WorkerThread : IDisposable if (jobInfo.pExecutionFunc != null) { - var ctx = new JobExecutionContext(_index, _scheduler); + var ctx = new JobExecutionContext + { + ThreadIndex = _index, + JobScheduler = _scheduler, + State = _scheduler.State, + SelfHandle = handle, + }; + if (!jobInfo.pExecutionFunc(jobInfo.pJobData, ref jobInfo.jobRanges, ref jobInfo.remainingBatches, in ctx)) { // If the job returns false, it means it we are not the last worker to process this job, so we should not mark it as complete yet. diff --git a/Misaki.HighPerformance.Test/Benchmark/SPMDBenchmark.cs b/Misaki.HighPerformance.Test/Benchmark/SPMDBenchmark.cs index 7e603c4..72a245a 100644 --- a/Misaki.HighPerformance.Test/Benchmark/SPMDBenchmark.cs +++ b/Misaki.HighPerformance.Test/Benchmark/SPMDBenchmark.cs @@ -94,7 +94,6 @@ public unsafe class SPMDBenchmark height = _SIZE, }; - var ctx = new JobExecutionContext(-1, _scheduler); - job.Run(_SIZE * _SIZE, in ctx); + job.Run(_SIZE * _SIZE, default); } } diff --git a/Misaki.HighPerformance.Test/Program.cs b/Misaki.HighPerformance.Test/Program.cs index b276556..a6aa1b7 100644 --- a/Misaki.HighPerformance.Test/Program.cs +++ b/Misaki.HighPerformance.Test/Program.cs @@ -1,21 +1,136 @@ using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; //BenchmarkRunner.Run(); -//AllocationManager.Initialize(AllocationManagerInitOpts.Default); -//var set = new UnsafeBitSet(100, AllocationHandle.Persistent, AllocationOption.Clear); -//set.SetBit(0); -//Console.WriteLine(set.NextSetBit(0)); +AllocationManager.Initialize(AllocationManagerInitOpts.Default); +var set = new UnsafeBitSet(100, AllocationHandle.Persistent, AllocationOption.Clear); +set.SetBit(0); +Console.WriteLine(set.NextSetBit(0)); -//set.Dispose(); -//AllocationManager.Dispose(); +set.Dispose(); +AllocationManager.Dispose(); -var span = Span.Empty; -unsafe -{ - fixed (int* ptr = span) - { - Console.WriteLine((IntPtr)ptr); - } -} +//unsafe +//{ +// var testC = new TestC(); +// testC.BindToVtables(); +// testC.MyValue = 42; + +// // 1. Casting to IB (Direct cast, offset 0) +// var pB = (IB.Interface*)&testC; +// pB->MethodA(); +// pB->MethodB(); + +// // 2. Casting to IC (Pointer adjustment needed!) +// // We must offset the pointer by the size of one void* so it points to lpVtbl_IC +// var pC = (IC.Interface*)((byte*)&testC + sizeof(void*)); +// pC->MethodC(); +//} + +//public interface IA +//{ +// [StructLayout(LayoutKind.Sequential)] +// unsafe struct Interface +// { +// public void** lpVtbl; +// public void MethodA() => ((delegate*)lpVtbl[0])((Interface*)Unsafe.AsPointer(ref this)); +// } +//} + +//public interface IB : IA +//{ +// [StructLayout(LayoutKind.Sequential)] +// new unsafe struct Interface +// { +// public void** lpVtbl; +// // IA Methods +// public void MethodA() => ((delegate*)lpVtbl[0])((Interface*)Unsafe.AsPointer(ref this)); +// // IB Methods +// public void MethodB() => ((delegate*)lpVtbl[1])((Interface*)Unsafe.AsPointer(ref this)); +// } +//} + +//public interface IC +//{ +// [StructLayout(LayoutKind.Sequential)] +// unsafe struct Interface +// { +// public void** lpVtbl; +// public void MethodC() => ((delegate*)lpVtbl[0])((Interface*)Unsafe.AsPointer(ref this)); +// } +//} + +//[StructLayout(LayoutKind.Sequential)] +//public unsafe partial struct TestC +//{ +// // Offset 0: Primary VTable (Covers IB and IA) +// public void** lpVtbl_IB; + +// // Offset 8 (on 64-bit): Secondary VTable (Covers IC) +// public void** lpVtbl_IC; + +// // Offset 16: Fields +// public int MyValue; +//} + +//public unsafe partial struct TestC +//{ +// public struct Vtbl_IB +// { +// public delegate* MethodA; +// public delegate* MethodB; +// } + +// public struct Vtbl_IC +// { +// public delegate* MethodC; +// } + +// [FixedAddressValueType] +// public static readonly Vtbl_IB Table_IB = new Vtbl_IB +// { +// MethodA = &Native_MethodA, +// MethodB = &Native_MethodB +// }; + +// [FixedAddressValueType] +// public static readonly Vtbl_IC Table_IC = new Vtbl_IC +// { +// MethodC = &Native_MethodC +// }; + +// public void BindToVtables() +// { +// fixed (Vtbl_IB* pIB = &Table_IB) +// lpVtbl_IB = (void**)pIB; +// fixed (Vtbl_IC* pIC = &Table_IC) +// lpVtbl_IC = (void**)pIC; +// } + +// // --- IB & IA Implementations --- +// public static void Native_MethodA(IB.Interface* self) +// { +// // For IB/IA, no adjustment is needed because lpVtbl_IB is at offset 0 +// var pThis = (TestC*)self; +// Console.WriteLine($"MethodA called. MyValue: {pThis->MyValue}"); +// } + +// public static void Native_MethodB(IB.Interface* self) +// { +// var pThis = (TestC*)self; +// Console.WriteLine($"MethodB called. MyValue: {pThis->MyValue}"); +// } + +// // --- IC Implementations --- +// public static void Native_MethodC(IC.Interface* self) +// { +// // WARNING: 'self' points to the lpVtbl_IC field, NOT the start of TestC! +// // We must shift the pointer backward by the size of lpVtbl_IB (sizeof(void*)) +// // to reconstruct the original TestC pointer, otherwise we read garbage memory. +// var pThis = (TestC*)((byte*)self - sizeof(void*)); +// Console.WriteLine($"MethodC called. MyValue: {pThis->MyValue}"); +// } +//} \ No newline at end of file diff --git a/Misaki.HighPerformance.Test/UnitTest/Jobs/TestJobSystem.cs b/Misaki.HighPerformance.Test/UnitTest/Jobs/TestJobSystem.cs index d4a645e..4552bd9 100644 --- a/Misaki.HighPerformance.Test/UnitTest/Jobs/TestJobSystem.cs +++ b/Misaki.HighPerformance.Test/UnitTest/Jobs/TestJobSystem.cs @@ -275,8 +275,7 @@ public unsafe class TestJobSystem height = size, }; - var ctx = new JobExecutionContext(-1, s_jobScheduler); - vectorJob.Run(size * size, in ctx); + vectorJob.Run(size * size, default); var spmdBuf = stackalloc float[size * size]; var ss = new Span(spmdBuf, size * size);