Refactor job system and update project configuration

Added:
- Added `JobExecutor.cs` for job execution management.
- Added `JobInfo.cs` to hold job execution information.
- Added `TestJobSystem.cs` for unit tests of the job system.
- Added `TestJobs.cs` for additional job implementation tests.
- Added `WorkerThread.cs` to manage worker threads for jobs.

Changed:
- Changed `AssemblyInfo.cs.cs` to include a global using directive for `unsafe JobExecuteFunc`.
- Changed `IJob.cs` to include an overload of the `Execute` method with a `threadIndex` parameter.
- Changed `JobHandle.cs` to include an `IsValid` property and updated internal structure.
- Changed `JobScheduler.cs` to improve job scheduling and management.
- Changed `JobsUtility.cs` to enhance job management functions.
- Changed `MemoryBlock.cs` to reference the heap from which memory was allocated.
- Changed `ParallelNoiseBenchmark.cs` to include benchmarks for the job system.
- Changed `Program.cs` to execute benchmarks instead of previous test code.

Removed:
- Removed `.gitignore` entries for default ignored files.
- Removed `JobBase.cs` to shift from structs to classes for jobs.
- Removed `JobExtensions.cs` indicating a change in job scheduling.
- Removed `JobStruct.cs` indicating a change in job structure.
- Removed `encodings.xml`, `indexLayout.xml`, and `vcs.xml` files to simplify project configuration.
- Removed fields from `JobData.cs` to simplify the job data structure.
- Removed `TestJobSystem.csproj` entries related to old project structure.
This commit is contained in:
2025-09-08 23:17:22 +09:00
parent a2a760594e
commit 07c99b8a5a
31 changed files with 1392 additions and 1204 deletions

View File

@@ -1,6 +1,6 @@
global using static Misaki.HighPerformance.LowLevel.Helpers.MemoryUtilities;
global using unsafe AllocFunc = delegate* unmanaged<void*, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, void*>;
global using unsafe FreeFunc = delegate* unmanaged<void*, void*, void>;
global using unsafe ReallocFunc = delegate* unmanaged<void*, void*, nuint, nuint, void*>;
global using unsafe AllocFunc = delegate*<void*, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, void*>;
global using unsafe FreeFunc = delegate*<void*, void*, void>;
global using unsafe ReallocFunc = delegate*<void*, void*, nuint, nuint, void*>;

View File

@@ -61,11 +61,10 @@ public static unsafe class AllocationManager
public void Init(uint initialSize)
{
_arena = new DynamicArena(initialSize);
_handle = new AllocationHandle(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
_arena = new(initialSize);
_handle = new(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
}
[UnmanagedCallersOnly]
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{
var selfPtr = (ArenaAllocator*)instance;
@@ -74,7 +73,6 @@ public static unsafe class AllocationManager
return ptr;
}
[UnmanagedCallersOnly]
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment)
{
var selfPtr = (ArenaAllocator*)instance;
@@ -84,7 +82,6 @@ public static unsafe class AllocationManager
return newPtr;
}
[UnmanagedCallersOnly]
private static void FreeBlock(void* instance, void* ptr)
{
// The arena allocator does not free individual blocks, as it manages memory in chunks.
@@ -109,10 +106,9 @@ public static unsafe class AllocationManager
public void Init()
{
_handle = new AllocationHandle(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
_handle = new(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
}
[UnmanagedCallersOnly]
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{
var ptr = AlignedAlloc(size, alignment);
@@ -130,7 +126,6 @@ public static unsafe class AllocationManager
return ptr;
}
[UnmanagedCallersOnly]
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment)
{
var newPtr = AlignedRealloc(ptr, size, alignment);
@@ -138,7 +133,6 @@ public static unsafe class AllocationManager
return newPtr;
}
[UnmanagedCallersOnly]
private static void FreeBlock(void* instance, void* ptr)
{
AlignedFree(ptr);
@@ -154,16 +148,6 @@ public static unsafe class AllocationManager
private static bool s_debugLayer;
private static ConcurrentDictionary<nint, AllocationInfo>? s_allocated;
/// <summary>
/// Gets a reference to the allocation handle for temporary allocations.
/// </summary>
public static ref AllocationHandle TempHandle => ref s_arenaAllocator->Handle;
/// <summary>
/// Gets a reference to the persistent allocation handle.
/// </summary>
public static ref AllocationHandle PersistentHandle => ref s_persistentAllocator->Handle;
static AllocationManager()
{
s_arenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator));
@@ -176,6 +160,7 @@ public static unsafe class AllocationManager
/// <summary>
/// Enables the debug layer, allowing additional diagnostic information to be collected.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnableDebugLayer()
{
s_debugLayer = true;
@@ -188,14 +173,15 @@ public static unsafe class AllocationManager
/// <param name="allocator">The allocator type for which to retrieve the allocation handle.</param>
/// <returns>A reference to the allocation handle associated with the specified allocator type.</returns>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref AllocationHandle GetAllocationHandle(Allocator allocator)
{
switch (allocator)
{
case Allocator.Temp:
return ref TempHandle;
return ref s_arenaAllocator->Handle;
case Allocator.Persistent:
return ref PersistentHandle;
return ref s_persistentAllocator->Handle;
default:
throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator));
}
@@ -258,6 +244,7 @@ public static unsafe class AllocationManager
/// Removes the specified memory allocation from the tracking system.
/// </summary>
/// <param name="ptr">A pointer to the memory allocation to untrack.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UntrackAllocation(void* ptr)
{
if (s_allocated == null)
@@ -268,6 +255,15 @@ public static unsafe class AllocationManager
s_allocated.Remove((nint)ptr, out _);
}
/// <summary>
/// Resets the temporary memory allocator, clearing all allocated memory.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ResetTempAllocator()
{
s_arenaAllocator->Reset();
}
/// <summary>
/// Disposes of the AllocationManager, freeing all allocated memory and resources.
/// </summary>

View File

@@ -1,5 +1,4 @@
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
@@ -30,9 +29,6 @@ namespace Misaki.HighPerformance.LowLevel.Buffer;
[StructLayout(LayoutKind.Explicit, Size = 256)] // Cache line aligned to prevent false sharing
public unsafe struct FreeList : IDisposable
{
/// <summary>
/// Node structure for the lock-free free list with size information.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct FreeNode
{
@@ -40,9 +36,6 @@ public unsafe struct FreeList : IDisposable
public nuint size;
}
/// <summary>
/// Memory chunk that contains variable-size blocks.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct MemoryChunk
{
@@ -52,20 +45,35 @@ public unsafe struct FreeList : IDisposable
public nuint used; // Amount of memory used in this chunk
}
/// <summary>
/// Size bucket for different allocation sizes.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
[StructLayout(LayoutKind.Explicit, Size = 32)]
private struct SizeBucket
{
public nint freeHead; // Free list head for this size
public nuint blockSize; // Fixed size for this bucket
[FieldOffset(0)]
public long freeCount; // Number of free blocks
[FieldOffset(8)]
public nint freeHead; // Free list head for this size
[FieldOffset(16)]
public nuint blockSize; // Fixed size for this bucket
[FieldOffset(24)]
public int creationLock;
}
[StructLayout(LayoutKind.Explicit, Size = 24)]
private struct BlockHeader
{
// Ensure the size is fixed across x86 and x64
[FieldOffset(0)]
public MemoryChunk* ownerChunk;
[FieldOffset(8)]
public nuint blockSize;
[FieldOffset(16)]
public ulong magicNumber;
}
private const int _MAX_BUCKETS = 16; // Number of size buckets
private const nuint _MIN_BLOCK_SIZE = 16; // Minimum block size
private const nuint _DEFAULT_CHUNK_SIZE = 64 * 1024; // 64KB chunks
private const ulong _MAGIC_NUMBER = 0xDEADBEEFDEADBEEF; // For validating blocks
[FieldOffset(0)]
private fixed byte _buckets[_MAX_BUCKETS * 32]; // SizeBucket array (32 bytes per bucket)
@@ -77,10 +85,10 @@ public unsafe struct FreeList : IDisposable
private MemoryChunk* _chunks; // 8
[FieldOffset(648)]
private nuint _chunkSize; // 8
private readonly nuint _chunkSize; // 8
[FieldOffset(656)]
private nuint _alignment; // 8
private readonly nuint _alignment; // 8
[FieldOffset(664)]
private long _totalAllocatedBytes; // 8
@@ -124,13 +132,17 @@ public unsafe struct FreeList : IDisposable
/// </summary>
/// <param name="alignment">Alignment requirement for blocks (must be power of 2).</param>
/// <param name="chunkSize">Size of memory chunks to allocate (default: 64KB).</param>
public FreeList(nuint alignment = 8, nuint chunkSize = _DEFAULT_CHUNK_SIZE)
public FreeList(nuint alignment, nuint chunkSize = _DEFAULT_CHUNK_SIZE)
{
if (alignment == 0 || (alignment & (alignment - 1)) != 0)
{
throw new ArgumentException("Alignment must be a power of 2", nameof(alignment));
}
if (chunkSize < 1024)
{
throw new ArgumentException("Chunk size must be at least 1KB", nameof(chunkSize));
}
_alignment = alignment;
_chunkSize = chunkSize;
@@ -144,9 +156,6 @@ public unsafe struct FreeList : IDisposable
InitializeBuckets();
}
/// <summary>
/// Initializes the size buckets with exponential sizes.
/// </summary>
private readonly void InitializeBuckets()
{
var buckets = GetBuckets();
@@ -161,9 +170,6 @@ public unsafe struct FreeList : IDisposable
}
}
/// <summary>
/// Gets a pointer to the size buckets array.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly SizeBucket* GetBuckets()
{
@@ -173,11 +179,6 @@ public unsafe struct FreeList : IDisposable
}
}
/// <summary>
/// Finds the appropriate bucket for the given size.
/// </summary>
/// <param name="size">Size to find bucket for.</param>
/// <returns>Bucket index, or -1 if too large for buckets.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly int FindBucket(nuint size)
{
@@ -186,7 +187,9 @@ public unsafe struct FreeList : IDisposable
for (var i = 0; i < _MAX_BUCKETS; i++)
{
if (size <= buckets[i].blockSize)
{
return i;
}
}
return -1; // Size too large for buckets
@@ -200,19 +203,25 @@ public unsafe struct FreeList : IDisposable
/// <param name="allocationOption">Options for allocation (e.g., clear memory).</param>
/// <returns>MemoryBlock containing allocated memory, or Invalid if allocation fails.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MemoryBlock Allocate(nuint size, nuint alignment = 0, AllocationOption allocationOption = AllocationOption.None)
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{
if (_disposed != 0 || size == 0)
return MemoryBlock.Invalid;
{
return null;
}
if (alignment == 0)
{
alignment = _alignment;
}
// Align size to alignment boundary
var alignedSize = (size + alignment - 1) & ~(alignment - 1);
alignedSize = Math.Max(alignedSize, _MIN_BLOCK_SIZE);
var bucketIndex = FindBucket(alignedSize);
var totalSize = alignedSize + (nuint)sizeof(BlockHeader);
var bucketIndex = FindBucket(totalSize);
void* ptr = null;
if (bucketIndex >= 0)
@@ -233,22 +242,24 @@ public unsafe struct FreeList : IDisposable
if (ptr == null)
{
// Fallback to direct allocation from chunk
ptr = AllocateFromChunk(alignedSize, alignment);
ptr = AllocateFromChunk(totalSize, alignment);
}
if (ptr != null)
{
Interlocked.Add(ref _totalAllocatedBytes, (long)alignedSize);
var header = (BlockHeader*)ptr;
Interlocked.Add(ref _totalAllocatedBytes, (long)header->blockSize);
var pUserData = (byte*)ptr + sizeof(BlockHeader);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, alignedSize);
MemClear(pUserData, alignedSize);
}
return new MemoryBlock(ptr, alignedSize, alignment);
return pUserData;
}
return MemoryBlock.Invalid;
return null;
}
/// <summary>
@@ -256,26 +267,39 @@ public unsafe struct FreeList : IDisposable
/// </summary>
/// <param name="block">MemoryBlock to free.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Free(MemoryBlock block)
public void Free(void* ptr)
{
if (!block.IsValid || _disposed != 0)
return;
if (!IsValidBlock(block.Ptr))
return; // Invalid pointer, ignore
var bucketIndex = FindBucket(block.Size);
if (bucketIndex >= 0)
if (_disposed != 0 || ptr == null)
{
PushToBucket(bucketIndex, block.Ptr, block.Size);
return;
}
Interlocked.Add(ref _totalAllocatedBytes, -(long)block.Size);
var blockStartPtr = (byte*)ptr - sizeof(BlockHeader);
var header = (BlockHeader*)blockStartPtr;
if (header->magicNumber != _MAGIC_NUMBER)
{
return;
}
var chuck = header->ownerChunk;
if (chuck == null)
{
return;
}
var bucketIndex = FindBucket(header->blockSize);
if (bucketIndex >= 0)
{
PushToBucket(bucketIndex, blockStartPtr, header->blockSize);
}
Interlocked.Add(ref _totalAllocatedBytes, -(long)header->blockSize);
header->ownerChunk = null;
header->blockSize = 0;
header->magicNumber = 0;
}
/// <summary>
/// Tries to pop a free block from the specified bucket.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void* TryPopFromBucket(int bucketIndex)
{
@@ -289,7 +313,9 @@ public unsafe struct FreeList : IDisposable
{
head = bucket->freeHead;
if (head == 0)
{
return null;
}
headPtr = (FreeNode*)head;
newHead = (nint)headPtr->next;
@@ -300,9 +326,6 @@ public unsafe struct FreeList : IDisposable
return (void*)head;
}
/// <summary>
/// Pushes a block to the specified bucket's free list.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void PushToBucket(int bucketIndex, void* ptr, nuint size)
{
@@ -323,29 +346,45 @@ public unsafe struct FreeList : IDisposable
Interlocked.Increment(ref bucket->freeCount);
}
/// <summary>
/// Creates new blocks for the specified bucket.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AssignBlockHeader(BlockHeader* header, MemoryChunk* ownerChunk, nuint blockSize)
{
header->ownerChunk = ownerChunk;
header->blockSize = blockSize;
header->magicNumber = _MAGIC_NUMBER;
}
private bool TryCreateBlocksForBucket(int bucketIndex)
{
while (Interlocked.CompareExchange(ref _chunkCreationLock, 1, 0) != 0)
var buckets = GetBuckets();
var bucket = &buckets[bucketIndex];
while (Interlocked.CompareExchange(ref bucket->creationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
var buckets = GetBuckets();
var blockSize = buckets[bucketIndex].blockSize;
if (bucket->freeHead != 0)
{
return true; // Another thread did the work for us!
}
var blockSize = bucket->blockSize;
var blocksToCreate = Math.Min(_chunkSize / blockSize, 256); // Limit number of blocks
if (blocksToCreate == 0)
{
return false;
}
var totalSize = blocksToCreate * blockSize;
var memory = (byte*)AlignedAlloc(totalSize, _alignment);
if (memory == null)
{
return false;
}
var chunk = (MemoryChunk*)_chunkArena.Allocate(SizeOf<MemoryChunk>(), AlignOf<MemoryChunk>(), AllocationOption.None);
if (chunk == null)
@@ -363,21 +402,20 @@ public unsafe struct FreeList : IDisposable
// Add all blocks to the bucket's free list
for (nuint i = 0; i < blocksToCreate; i++)
{
var blockPtr = memory + (i * blockSize);
PushToBucket(bucketIndex, blockPtr, blockSize);
var blockStartPtr = memory + (i * blockSize);
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, blockSize);
PushToBucket(bucketIndex, blockStartPtr, blockSize);
}
return true;
}
finally
{
Interlocked.Exchange(ref _chunkCreationLock, 0);
Interlocked.Exchange(ref bucket->creationLock, 0);
}
}
/// <summary>
/// Allocates memory directly from a chunk (for large allocations).
/// </summary>
private void* AllocateFromChunk(nuint size, nuint alignment)
{
while (Interlocked.CompareExchange(ref _chunkCreationLock, 1, 0) != 0)
@@ -397,9 +435,11 @@ public unsafe struct FreeList : IDisposable
if (totalNeeded <= available)
{
var ptr = chunk->memory + alignedOffset;
chunk->used += totalNeeded;
return ptr;
var blockStartPtr = chunk->memory + alignedOffset;
// Write the header and return the pointer WITH the header
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, size);
return blockStartPtr;
}
chunk = chunk->next;
@@ -409,7 +449,9 @@ public unsafe struct FreeList : IDisposable
var newChunkSize = Math.Max(_chunkSize, size + alignment);
var newMemory = (byte*)AlignedAlloc(newChunkSize, alignment);
if (newMemory == null)
{
return null;
}
var newChunk = (MemoryChunk*)_chunkArena.Allocate(SizeOf<MemoryChunk>(), AlignOf<MemoryChunk>(), AllocationOption.None);
if (newChunk == null)
@@ -424,6 +466,8 @@ public unsafe struct FreeList : IDisposable
newChunk->next = _chunks;
_chunks = newChunk;
// Write the header and return the pointer WITH the header
AssignBlockHeader((BlockHeader*)newMemory, newChunk, size);
return newMemory;
}
finally
@@ -432,27 +476,6 @@ public unsafe struct FreeList : IDisposable
}
}
/// <summary>
/// Validates that a pointer belongs to one of our memory chunks.
/// </summary>
private readonly bool IsValidBlock(void* ptr)
{
var chunk = _chunks;
while (chunk != null)
{
var chunkStart = (nuint)chunk->memory;
var chunkEnd = chunkStart + chunk->size;
var ptrValue = (nuint)ptr;
if (ptrValue >= chunkStart && ptrValue < chunkEnd)
return true;
chunk = chunk->next;
}
return false;
}
/// <summary>
/// Disposes the free list and frees all allocated memory.
/// Note: This method is NOT thread-safe by design as requested.
@@ -467,10 +490,11 @@ public unsafe struct FreeList : IDisposable
{
var next = chunk->next;
AlignedFree(chunk->memory);
MemoryUtilities.Free(chunk);
chunk = next;
}
_chunkArena.Dispose();
_chunks = null;
_totalAllocatedBytes = 0;
_totalFreeBytes = 0;

View File

@@ -16,6 +16,14 @@ public unsafe readonly struct MemoryBlock
get;
}
/// <summary>
/// The heap from which the memory was allocated.
/// </summary>
public void* Heap
{
get;
}
/// <summary>
/// Size of the allocated memory in bytes.
/// </summary>
@@ -43,9 +51,10 @@ public unsafe readonly struct MemoryBlock
/// <param name="ptr">Pointer to the allocated memory.</param>
/// <param name="size">Size of the allocated memory.</param>
/// <param name="alignment">Alignment of the allocated memory.</param>
public MemoryBlock(void* ptr, nuint size, nuint alignment)
public MemoryBlock(void* ptr, void* heap, nuint size, nuint alignment)
{
Ptr = ptr;
Heap = heap;
Size = size;
Alignment = alignment;
}
@@ -53,7 +62,7 @@ public unsafe readonly struct MemoryBlock
/// <summary>
/// Creates an invalid MemoryBlock.
/// </summary>
public static MemoryBlock Invalid => new(null, 0, 0);
public static MemoryBlock Invalid => new(null, null, 0, 0);
public Span<T> AsSpan<T>()
where T : unmanaged

View File

@@ -1,152 +0,0 @@
using System.Collections;
namespace Misaki.HighPerformance.LowLevel.Collections;
public class SlotMap<T> : IEnumerable<T>
{
public struct Enumerator : IEnumerator<T>
{
private readonly SlotMap<T> _slotMap;
private int _currentIndex;
public Enumerator(SlotMap<T> slotMap)
{
_slotMap = slotMap;
_currentIndex = -1;
}
public readonly T Current => _slotMap._data[_currentIndex].value;
readonly object? IEnumerator.Current => Current;
public bool MoveNext()
{
while (++_currentIndex < _slotMap._capacity)
{
if (_slotMap._data[_currentIndex].isValid)
{
return true;
}
}
return false;
}
public void Reset() => _currentIndex = -1;
public void Dispose()
{
}
}
private struct SlotData
{
public T value;
public bool isValid;
}
private SlotData[] _data;
private readonly Queue<int> _freeSlots;
private int _count;
private int _capacity;
public int Count => _count;
public int Capacity => _capacity;
public ref T this[int slotIndex]
{
get
{
if (slotIndex < 0 || slotIndex >= _capacity)
{
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
}
ref var slot = ref _data[slotIndex];
if (!slot.isValid)
{
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
}
return ref slot.value;
}
}
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public SlotMap(int initialCapacity = 16)
{
_capacity = initialCapacity;
_data = new SlotData[initialCapacity];
_freeSlots = new Queue<int>(initialCapacity);
}
private void Resize()
{
var newCapacity = _capacity * 2;
Array.Resize(ref _data, newCapacity);
_freeSlots.EnsureCapacity(newCapacity);
_capacity = newCapacity;
}
public int Add(T item)
{
if (_count >= _capacity)
{
Resize();
}
int slotIndex;
if (_freeSlots.Count == 0)
{
slotIndex = _count;
}
else
{
slotIndex = _freeSlots.Dequeue();
}
ref var slot = ref _data[slotIndex];
slot.value = item;
slot.isValid = true;
_count++;
return slotIndex;
}
public bool Remove(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= _capacity)
{
return false;
}
ref var slot = ref _data[slotIndex];
if (!slot.isValid)
{
return false;
}
slot.isValid = false;
_freeSlots.Enqueue(slotIndex);
_count--;
return true;
}
public void Clear()
{
_count = 0;
_data.AsSpan().Clear();
_freeSlots.Clear();
}
}