Compare commits

2 Commits

Author SHA1 Message Date
53bba54d43 fixed generic types cannot have explicit layout issue 2026-05-29 08:43:33 +09:00
7c9612ceb0 Improve spin-wait, SPMCQueue, and add UnsafeString/Text
- JobScheduler: dynamic spin-wait thresholds for lower latency; removed SLEEP_THRESHOLD constant.
- SPMCQueue: switched to explicit struct layout, removed manual padding, fixed power-of-two capacity bug.
- WorkerThread: enhanced work-stealing with additional cascade loop.
- AllocationHandle: added IsValid property.
- VirtualMemoryBlock: fixed Dispose to pass correct size to Munmap.
- Added UnsafeString and UnsafeText for zero-allocation string/text handling.
- Updated project metadata, versions, and repository URLs.
- Minor inlining/optimization tweaks in GGXMipGenerationBenchmark.
2026-05-29 00:48:36 +09:00
9 changed files with 143 additions and 19 deletions

View File

@@ -121,9 +121,6 @@ internal sealed class WaitAnyItem : IThreadPoolWorkItem
/// </summary> /// </summary>
public sealed unsafe partial class JobScheduler : IDisposable public sealed unsafe partial class JobScheduler : IDisposable
{ {
// Don't sleep indefinitely because that causes our 1ms job to become 15ms.
private const int SLEEP_THRESHOLD = -1;
private readonly ConcurrentSlotMap<JobInfo> _jobInfoPool; private readonly ConcurrentSlotMap<JobInfo> _jobInfoPool;
private readonly ConcurrentQueue<JobHandle>[] _jobQueues; private readonly ConcurrentQueue<JobHandle>[] _jobQueues;
private readonly WorkerThread[] _workerThreads; private readonly WorkerThread[] _workerThreads;
@@ -834,7 +831,7 @@ public sealed unsafe partial class JobScheduler : IDisposable
if (!madeProgress) if (!madeProgress)
{ {
spin.SpinOnce(SLEEP_THRESHOLD); spin.SpinOnce(-1); // Never sleep and yield to achieve lowest latency for single job completion.
} }
} }
} }
@@ -854,6 +851,7 @@ public sealed unsafe partial class JobScheduler : IDisposable
} }
var spin = new SpinWait(); var spin = new SpinWait();
var sleepThreshold = handles.Length * 20;
var completedCount = 0; var completedCount = 0;
while (true) while (true)
@@ -877,7 +875,7 @@ public sealed unsafe partial class JobScheduler : IDisposable
return; return;
} }
spin.SpinOnce(SLEEP_THRESHOLD); spin.SpinOnce(sleepThreshold);
} }
} }
@@ -889,6 +887,7 @@ public sealed unsafe partial class JobScheduler : IDisposable
public JobHandle WaitAny(params ReadOnlySpan<JobHandle> handles) public JobHandle WaitAny(params ReadOnlySpan<JobHandle> handles)
{ {
var spin = new SpinWait(); var spin = new SpinWait();
var sleepThreshold = handles.Length * 10;
while (true) while (true)
{ {
@@ -900,7 +899,7 @@ public sealed unsafe partial class JobScheduler : IDisposable
} }
} }
spin.SpinOnce(SLEEP_THRESHOLD); spin.SpinOnce(sleepThreshold);
} }
} }

View File

@@ -6,13 +6,20 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<AssemblyVersion>3.1.7</AssemblyVersion> <AssemblyVersion>3.1.8</AssemblyVersion>
<Version>$(AssemblyVersion)</Version> <Version>$(AssemblyVersion)</Version>
<Authors>Misaki</Authors> <Authors>Misaki</Authors>
<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://github.com/misakieku/Misaki.HighPerformance.git</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>job-system;multithreading;concurrency;task-scheduler;work-stealing;zero-allocation;0gc;dag;dependency-graph;game-engine;ecs;high-performance;spmc</PackageTags>
<Description>
A high-performance, zero-allocation (0 GC), and zero-closure job system designed for custom game engines and data-oriented design (DOD).
Features a lock-free Work-Stealing scheduler (SPMC) with DAG-based multi-dependency resolution.
Includes a blazingly fast O(1) branchless priority queue (High/Normal/Low) using Cascade LUTs.
Uniquely supports both unmanaged and managed jobs seamlessly via internal pooling, offering maximum flexibility without compromising C# GC performance.
</Description>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -7,16 +7,16 @@ namespace Misaki.HighPerformance.Jobs;
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public class SPMCQueue<T> public class SPMCQueue<T>
{ {
private unsafe struct padding private struct __padding
{ {
private fixed byte _padding[64]; private unsafe fixed byte _padding[64];
} }
private readonly T[] _queue; private readonly T[] _queue;
private readonly int _mask; private readonly int _mask;
private int _head; private int _head;
private padding _padding; // Prevent false sharing between head and tail private __padding _padding;
private int _tail; private int _tail;
public bool IsEmpty => Volatile.Read(ref _tail) - Volatile.Read(ref _head) <= 0; public bool IsEmpty => Volatile.Read(ref _tail) - Volatile.Read(ref _head) <= 0;
@@ -30,8 +30,9 @@ public class SPMCQueue<T>
/// <param name="capacity">The capacity of the queue.</param> /// <param name="capacity">The capacity of the queue.</param>
public SPMCQueue(int capacity) public SPMCQueue(int capacity)
{ {
_queue = new T[(int)BitOperations.RoundUpToPowerOf2((uint)capacity)]; var powerOfTwoCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)capacity);
_mask = capacity - 1; _queue = new T[powerOfTwoCapacity];
_mask = powerOfTwoCapacity - 1;
} }
/// <summary> /// <summary>

View File

@@ -84,6 +84,11 @@ internal class WorkerThread : IDisposable
{ {
return true; return true;
} }
}
for (var offset = 0; offset < helperThreadCount; offset++)
{
var p = cascade[index + offset];
for (var i = 1; i < _scheduler.WorkerCount; i++) for (var i = 1; i < _scheduler.WorkerCount; i++)
{ {

View File

@@ -143,6 +143,8 @@ public readonly unsafe struct AllocationHandle
private readonly ReallocFunc _realloc; private readonly ReallocFunc _realloc;
private readonly FreeFunc _free; private readonly FreeFunc _free;
public bool IsValid => _alloc != null && _realloc != null && _free != null;
public AllocationHandle(void* state, AllocFunc alloc, ReallocFunc realloc, FreeFunc free) public AllocationHandle(void* state, AllocFunc alloc, ReallocFunc realloc, FreeFunc free)
{ {
_state = state; _state = state;

View File

@@ -23,10 +23,12 @@ public unsafe struct VirtualMemoryBlock : IDisposable
} }
var addr = _baseAddress; var addr = _baseAddress;
var size = _size;
_baseAddress = null; _baseAddress = null;
_size = 0; _size = 0;
_committed = 0; _committed = 0;
MemoryUtility.Munmap(addr, _size); MemoryUtility.Munmap(addr, size);
} }
} }

View File

@@ -0,0 +1,101 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeString : IDisposable
{
private UnsafeArray<char> _chars;
public readonly int Length => _chars.Length;
public readonly char this[int index] => _chars[index];
public UnsafeString(ReadOnlySpan<char> span, AllocationHandle handle)
{
_chars = new UnsafeArray<char>(span.Length, handle);
span.CopyTo(_chars.AsSpan());
}
public UnsafeString(UnsafeString other)
{
_chars = new UnsafeArray<char>(other.Length, other._chars.AllocationHandle);
other.AsSpan().CopyTo(_chars.AsSpan());
}
public readonly UnsafeString Copy(AllocationHandle handle = default)
{
handle = handle.IsValid ? handle : _chars.AllocationHandle;
var clone = new UnsafeString(AsSpan(), handle);
return clone;
}
public readonly ReadOnlySpan<char> AsSpan()
{
return _chars.AsSpan();
}
public readonly void* GetUnsafePtr()
{
return _chars.GetUnsafePtr();
}
public readonly override string ToString()
{
return new string(_chars.AsSpan());
}
public void Dispose()
{
_chars.Dispose();
}
}
public unsafe struct UnsafeText : IDisposable
{
private UnsafeArray<byte> _chars;
public readonly int Length => _chars.Length;
public readonly byte this[int index] => _chars[index];
public UnsafeText(ReadOnlySpan<byte> span, AllocationHandle handle)
{
_chars = new UnsafeArray<byte>(span.Length, handle);
span.CopyTo(_chars.AsSpan());
}
public UnsafeText(UnsafeText other)
{
_chars = new UnsafeArray<byte>(other.Length, other._chars.AllocationHandle);
other.AsSpan().CopyTo(_chars.AsSpan());
}
public readonly UnsafeText Copy(AllocationHandle handle = default)
{
handle = handle.IsValid ? handle : _chars.AllocationHandle;
var clone = new UnsafeText(AsSpan(), handle);
return clone;
}
public readonly ReadOnlySpan<byte> AsSpan()
{
return _chars.AsSpan();
}
public readonly void* GetUnsafePtr()
{
return _chars.GetUnsafePtr();
}
public readonly override string ToString()
{
return Encoding.UTF8.GetString(_chars.AsSpan());
}
public void Dispose()
{
_chars.Dispose();
}
}

View File

@@ -7,12 +7,19 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Misaki</Authors> <Authors>Misaki</Authors>
<AssemblyVersion>1.7.3</AssemblyVersion> <AssemblyVersion>1.7.4</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://github.com/misakieku/Misaki.HighPerformance.git</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>native;unsafe;memory;allocator;mmap;arena;tlsf;freelist;high-performance;dod;ecs;native-collections;native-array;game-engine</PackageTags>
<Description>
A true high-performance, low-level native memory and collection library for C#.
Features a pluggable memory allocation architecture (AllocationHandle) with built-in allocators: Arena, FreeList, TLSF, and Malloc (mimalloc support).
Includes fully unmanaged native collections (UnsafeArray, UnsafeList, UnsafeHashMap, etc.) and cross-platform mmap/munmap wrappers.
Designed for Data-Oriented Design (DOD), custom game engines, and zero-allocation systems.
</Description>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>contentFiles</ContentTargetFolders> <ContentTargetFolders>contentFiles</ContentTargetFolders>
</PropertyGroup> </PropertyGroup>

View File

@@ -248,7 +248,7 @@ internal unsafe struct GGXMipGenerationJobSPMD<TFloat, TInt> : IJobParallel
return MathV.GatherVector3<TFloat, float>(img, idx.GetUnsafePtr(), 4); return MathV.GatherVector3<TFloat, float>(img, idx.GetUnsafePtr(), 4);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveOptimization)]
private readonly void ProcessPixel(int local_i, MipLevel* pLevel) private readonly void ProcessPixel(int local_i, MipLevel* pLevel)
{ {
var w = (int)pLevel->width; var w = (int)pLevel->width;
@@ -350,7 +350,7 @@ internal unsafe struct GGXMipGenerationJobSPMD<TFloat, TInt> : IJobParallel
pData[out_idx + 2] = prefilteredColor.z; pData[out_idx + 2] = prefilteredColor.z;
} }
[MethodImpl(MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public void Execute(int startIndex, int endIndex, ref readonly JobExecutionContext ctx) public void Execute(int startIndex, int endIndex, ref readonly JobExecutionContext ctx)
{ {
var m = 0; var m = 0;