Added WaitAll and WaitAny to JobScheduler;

Added generation support to UnsafeSparseSet;
This commit is contained in:
2025-09-11 18:45:40 +09:00
parent 1546c2cabe
commit 94f10de90e
14 changed files with 336 additions and 161 deletions

View File

@@ -9,7 +9,7 @@
<Version>1.0.0</Version> <Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>True</IsPackable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'"> <PropertyGroup Condition="'$(Configuration)'=='Release'">

View File

@@ -6,7 +6,7 @@
public enum JobState public enum JobState
{ {
/// <summary> /// <summary>
/// The job is in an invalid state, indicating an error or uninitialized state. Or already finished the execution and cleaned up by the system. /// The job is in an invalid state, indicating an error or uninitialized state.
/// </summary> /// </summary>
Invalid = -1, Invalid = -1,
/// <summary> /// <summary>

View File

@@ -17,6 +17,8 @@ namespace Misaki.HighPerformance.Jobs;
/// worker threads.</remarks> /// worker threads.</remarks>
public unsafe sealed class JobScheduler : IDisposable public unsafe sealed class JobScheduler : IDisposable
{ {
private const int _SLEEP_THRESHOLD = 100;
private FreeList _jobDataAllocator; private FreeList _jobDataAllocator;
private readonly ConcurrentSlotMap<JobInfo> _jobInfoPool; private readonly ConcurrentSlotMap<JobInfo> _jobInfoPool;
private readonly ConcurrentQueue<JobHandle> _jobQueue; private readonly ConcurrentQueue<JobHandle> _jobQueue;
@@ -268,7 +270,7 @@ public unsafe sealed class JobScheduler : IDisposable
} }
/// <summary> /// <summary>
/// Schedules a single job for execution on a specified thread, with an optional dependency on another job. /// Schedules a single job for execution on a specified thread without dependency.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJob"/> and be unmanaged.</typeparam> /// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJob"/> and be unmanaged.</typeparam>
/// <param name="job">The job instance to be executed. The job data will be copied internally.</param> /// <param name="job">The job instance to be executed. The job data will be copied internally.</param>
@@ -279,6 +281,30 @@ public unsafe sealed class JobScheduler : IDisposable
where T : unmanaged, IJob where T : unmanaged, IJob
=> Schedule(ref job, threadIndex, JobHandle.Invalid); => Schedule(ref job, threadIndex, JobHandle.Invalid);
/// <summary>
/// Schedules a single job for execution on any thread, with an optional dependency on another job.
/// </summary>
/// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJob"/> and be unmanaged.</typeparam>
/// <param name="job">The job instance to be executed. The job data will be copied internally.</param>
/// <param name="threadIndex">The index of the thread that will execute the job. This is used to assign thread-specific data. Use -1 to allow any thread to execute the job.</param>
/// <returns>A <see cref="JobHandle"/> that can be used to track the completion of the scheduled job.
/// Returns <see cref="JobHandle.Invalid"/> if the job data allocation fails.</returns>
public JobHandle Schedule<T>(ref T job, JobHandle dependency)
where T : unmanaged, IJob
=> Schedule(ref job, -1, dependency);
/// <summary>
/// Schedules a single job for execution on any thread without dependency.
/// </summary>
/// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJob"/> and be unmanaged.</typeparam>
/// <param name="job">The job instance to be executed. The job data will be copied internally.</param>
/// <param name="threadIndex">The index of the thread that will execute the job. This is used to assign thread-specific data. Use -1 to allow any thread to execute the job.</param>
/// <returns>A <see cref="JobHandle"/> that can be used to track the completion of the scheduled job.
/// Returns <see cref="JobHandle.Invalid"/> if the job data allocation fails.</returns>
public JobHandle Schedule<T>(ref T job)
where T : unmanaged, IJob
=> Schedule(ref job, -1, JobHandle.Invalid);
/// <summary> /// <summary>
/// Schedules a parallel job for execution, dividing the workload into batches and distributing it across threads. /// Schedules a parallel job for execution, dividing the workload into batches and distributing it across threads.
/// </summary> /// </summary>
@@ -328,7 +354,7 @@ public unsafe sealed class JobScheduler : IDisposable
} }
/// <summary> /// <summary>
/// Schedules a parallel job for execution, dividing the workload into batches and distributing it across threads. /// Schedules a parallel job for execution, dividing the workload into batches and distributing it across threads on a specified thread without dependency.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJobParallelFor"/> and be unmanaged.</typeparam> /// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJobParallelFor"/> and be unmanaged.</typeparam>
/// <param name="job">The job instance to be executed. The job data will be copied internally.</param> /// <param name="job">The job instance to be executed. The job data will be copied internally.</param>
@@ -341,6 +367,34 @@ public unsafe sealed class JobScheduler : IDisposable
where T : unmanaged, IJobParallelFor where T : unmanaged, IJobParallelFor
=> ScheduleParallel(ref job, totalIteration, batchSize, threadIndex, JobHandle.Invalid); => ScheduleParallel(ref job, totalIteration, batchSize, threadIndex, JobHandle.Invalid);
/// <summary>
/// Schedules a parallel job for execution, dividing the workload into batches and distributing it across threads on any thread, with an optional dependency on another job..
/// </summary>
/// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJobParallelFor"/> and be unmanaged.</typeparam>
/// <param name="job">The job instance to be executed. The job data will be copied internally.</param>
/// <param name="totalIteration">The total number of iterations to be processed by the job.</param>
/// <param name="batchSize">The number of iterations to include in each batch.</param>
/// <param name="threadIndex">The index of the thread that will execute the job. This is used to assign thread-specific data. Use -1 to allow any thread to execute the job.</param>
/// <returns>A <see cref="JobHandle"/> that can be used to track the completion of the scheduled job.
/// Returns <see cref="JobHandle.Invalid"/> if the job data allocation fails.</returns>
public JobHandle ScheduleParallel<T>(ref T job, int totalIteration, int batchSize, JobHandle dependency)
where T : unmanaged, IJobParallelFor
=> ScheduleParallel(ref job, totalIteration, batchSize, -1, dependency);
/// <summary>
/// Schedules a parallel job for execution, dividing the workload into batches and distributing it across threads on any thread without dependency.
/// </summary>
/// <typeparam name="T">The type of the job to execute. Must implement <see cref="IJobParallelFor"/> and be unmanaged.</typeparam>
/// <param name="job">The job instance to be executed. The job data will be copied internally.</param>
/// <param name="totalIteration">The total number of iterations to be processed by the job.</param>
/// <param name="batchSize">The number of iterations to include in each batch.</param>
/// <param name="threadIndex">The index of the thread that will execute the job. This is used to assign thread-specific data. Use -1 to allow any thread to execute the job.</param>
/// <returns>A <see cref="JobHandle"/> that can be used to track the completion of the scheduled job.
/// Returns <see cref="JobHandle.Invalid"/> if the job data allocation fails.</returns>
public JobHandle ScheduleParallel<T>(ref T job, int totalIteration, int batchSize)
where T : unmanaged, IJobParallelFor
=> ScheduleParallel(ref job, totalIteration, batchSize, -1, JobHandle.Invalid);
/// <summary> /// <summary>
/// Combines multiple job dependencies into a single <see cref="JobHandle"/>. /// Combines multiple job dependencies into a single <see cref="JobHandle"/>.
/// </summary> /// </summary>
@@ -379,7 +433,7 @@ public unsafe sealed class JobScheduler : IDisposable
ref var jobInfo = ref _jobInfoPool.GetElementReferenceAt(handle._id, handle._generation, out var exist); ref var jobInfo = ref _jobInfoPool.GetElementReferenceAt(handle._id, handle._generation, out var exist);
if (!exist) if (!exist)
{ {
return JobState.Invalid; return JobState.Completed; // We assume completed if not found. Invalid state is reserved for error.
} }
return (JobState)Volatile.Read(ref Unsafe.As<JobState, int>(ref jobInfo.state)); return (JobState)Volatile.Read(ref Unsafe.As<JobState, int>(ref jobInfo.state));
@@ -404,7 +458,69 @@ public unsafe sealed class JobScheduler : IDisposable
return; return;
} }
spin.SpinOnce(-1); spin.SpinOnce(_SLEEP_THRESHOLD);
}
}
/// <summary>
/// Blocks the calling thread until all specified job handles have completed.
/// </summary>
/// <remarks>This method waits for all jobs referenced by the provided handles to complete before
/// returning. The calling thread will be blocked until every job has finished. If any handle is invalid or does not
/// correspond to an active job, it is considered completed. This method is not thread-safe and should not be called
/// concurrently from multiple threads.</remarks>
/// <param name="handles">A collection of job handles to wait for. Each handle represents an asynchronous job whose completion is awaited.
/// The collection must not be empty.</param>
public void WaitAll(params ReadOnlySpan<JobHandle> handles)
{
var sleepThreshold = _SLEEP_THRESHOLD * handles.Length;
var spin = new SpinWait();
while (true)
{
var completedCount = 0;
foreach (var handle in handles)
{
if (!_jobInfoPool.Contain(handle._id, handle._generation))
{
completedCount++;
}
}
if (completedCount == handles.Length)
{
return;
}
spin.SpinOnce(sleepThreshold);
}
}
/// <summary>
/// Waits until any of the specified job handles has completed and returns the first completed handle.
/// </summary>
/// <remarks>This method blocks the calling thread until at least one of the specified jobs has finished.
/// The returned handle corresponds to the job that completed first among those provided. The order of handles in
/// the span may affect which handle is returned if multiple jobs complete simultaneously.</remarks>
/// <param name="handles">A read-only span containing the job handles to monitor for completion. Each handle represents a job whose
/// completion status will be checked.</param>
/// <returns>The first job handle from the provided collection that has completed.</returns>
public JobHandle WaitAny(params ReadOnlySpan<JobHandle> handles)
{
var sleepThreshold = _SLEEP_THRESHOLD * handles.Length;
var spin = new SpinWait();
while (true)
{
foreach (var handle in handles)
{
if (!_jobInfoPool.Contain(handle._id, handle._generation))
{
return handle;
}
}
spin.SpinOnce(sleepThreshold);
} }
} }

View File

@@ -6,7 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<IsPackable>false</IsPackable> <IsPackable>True</IsPackable>
<Version>1.1.0</Version>
<AssemblyVersion>1.1.0</AssemblyVersion>
<FileVersion>1.1.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,47 +0,0 @@
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);
}
}

View File

@@ -76,10 +76,12 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
get get
{ {
#if DISABLE_COLLECTION_CHECKS
if (index < 0 || index >= _count) if (index < 0 || index >= _count)
{ {
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range."); throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
} }
#endif
return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index); return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index);
} }
@@ -90,10 +92,12 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
get get
{ {
#if DISABLE_COLLECTION_CHECKS
if (index >= _count) if (index >= _count)
{ {
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range."); throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
} }
#endif
return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index); return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index);
} }

View File

@@ -17,6 +17,12 @@ namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T> public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
where T : unmanaged where T : unmanaged
{ {
private struct SlotEntry
{
public T value;
public int generation;
}
public struct Enumerator : IEnumerator<T> public struct Enumerator : IEnumerator<T>
{ {
private UnsafeSparseSet<T>* _collection; private UnsafeSparseSet<T>* _collection;
@@ -34,10 +40,14 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
public bool MoveNext() public bool MoveNext()
{ {
_index++; _index++;
if (_index < _collection->_count) if (_index < _collection->_sparse.Count)
{ {
_value = UnsafeUtilities.ReadArrayElement<T>(_collection->_dense.GetUnsafePtr(), _index); var index = _collection->_sparse[_index];
return true; if (index >= 0 && index < _collection->_count)
{
_value = _collection->_dense[index].value;
return true;
}
} }
_value = default; _value = default;
@@ -66,7 +76,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
} }
private UnsafeArray<T> _dense; private UnsafeArray<SlotEntry> _dense;
private UnsafeArray<int> _sparse; private UnsafeArray<int> _sparse;
private UnsafeArray<int> _reverse; // Maps dense index to sparse index private UnsafeArray<int> _reverse; // Maps dense index to sparse index
private UnsafeArray<int> _freeList; // Stack of available sparse indices private UnsafeArray<int> _freeList; // Stack of available sparse indices
@@ -103,7 +113,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
} }
_dense = new UnsafeArray<T>(capacity, ref handle, allocationOption); _dense = new UnsafeArray<SlotEntry>(capacity, ref handle, allocationOption);
_sparse = new UnsafeArray<int>(capacity, ref handle, allocationOption); _sparse = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_reverse = new UnsafeArray<int>(capacity, ref handle, allocationOption); _reverse = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_freeList = new UnsafeArray<int>(capacity, ref handle, allocationOption); _freeList = new UnsafeArray<int>(capacity, ref handle, allocationOption);
@@ -130,8 +140,9 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
/// Adds a value to the sparse set and returns a unique sparse index for the value. /// Adds a value to the sparse set and returns a unique sparse index for the value.
/// </summary> /// </summary>
/// <param name="value">The value to add to the sparse set.</param> /// <param name="value">The value to add to the sparse set.</param>
/// <param name="generation">Outputs the generation number associated with the added value.</param>
/// <returns>A unique sparse index that can be used to reference this value.</returns> /// <returns>A unique sparse index that can be used to reference this value.</returns>
public int Add(T value) public int Add(T value, out int generation)
{ {
int sparseIndex; int sparseIndex;
@@ -162,68 +173,27 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
// Add the value to the dense array and update mappings // Add the value to the dense array and update mappings
_dense[_count] = value; ref var entry = ref _dense[_count];
entry.value = value;
_sparse[sparseIndex] = _count; _sparse[sparseIndex] = _count;
_reverse[_count] = sparseIndex; _reverse[_count] = sparseIndex;
_count++; _count++;
generation = entry.generation;
return sparseIndex; return sparseIndex;
} }
/// <summary>
/// Adds a value to the sparse set at the specified sparse index.
/// This method is provided for compatibility when you need to specify the exact sparse index.
/// </summary>
/// <param name="sparseIndex">The index in the sparse array where the value should be mapped.</param>
/// <param name="value">The value to add to the sparse set.</param>
/// <returns>True if the value was added, false if the sparse index is already occupied.</returns>
public bool AddAt(int sparseIndex, T value)
{
if (sparseIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index must be non-negative.");
}
if (sparseIndex >= _sparse.Count)
{
ResizeSparse(sparseIndex + 1);
}
if (Contains(sparseIndex))
{
return false;
}
if (_count >= _dense.Count)
{
var newCapacity = _dense.Count + Math.Max(1, _dense.Count / 2);
_dense.Resize(newCapacity);
_reverse.Resize(newCapacity);
}
// Add the value to the dense array and update mappings
_dense[_count] = value;
_sparse[sparseIndex] = _count;
_reverse[_count] = sparseIndex; // Store reverse mapping
_count++;
// Update _nextId if we're using a higher ID
if (sparseIndex >= _nextId)
{
_nextId = sparseIndex + 1;
}
return true;
}
/// <summary> /// <summary>
/// Removes the value at the specified sparse index. /// Removes the value at the specified sparse index.
/// </summary> /// </summary>
/// <param name="sparseIndex">The sparse index of the value to remove.</param> /// <param name="sparseIndex">The sparse index of the value to remove.</param>
/// <param name="generation">The generation number associated with the sparse index to validate.</param>
/// <returns>True if the value was removed, false if the sparse index was not found.</returns> /// <returns>True if the value was removed, false if the sparse index was not found.</returns>
public bool Remove(int sparseIndex) public bool Remove(int sparseIndex, int generation)
{ {
if (!Contains(sparseIndex)) if (!Contains(sparseIndex, generation))
{ {
return false; return false;
} }
@@ -246,12 +216,14 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
// Mark the sparse index as unused and add to free list // Mark the sparse index as unused and add to free list
_sparse[sparseIndex] = -1; _sparse[sparseIndex] = -1;
_dense[lastIndex].generation++; // Increment generation to invalidate old references
// Add the freed sparse index to the free list for reuse // Add the freed sparse index to the free list for reuse
if (_freeCount >= _freeList.Count) if (_freeCount >= _freeList.Count)
{ {
_freeList.Resize(_freeList.Count + Math.Max(1, _freeList.Count / 2)); _freeList.Resize(_freeList.Count + Math.Max(1, _freeList.Count / 2));
} }
_freeList[_freeCount] = sparseIndex; _freeList[_freeCount] = sparseIndex;
_freeCount++; _freeCount++;
@@ -264,9 +236,10 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
/// Checks if the sparse set contains a value at the specified sparse index. /// Checks if the sparse set contains a value at the specified sparse index.
/// </summary> /// </summary>
/// <param name="sparseIndex">The sparse index to check.</param> /// <param name="sparseIndex">The sparse index to check.</param>
/// <param name="generation">The generation number to validate against the stored generation.</param>
/// <returns>True if the sparse index is valid and contains a value, false otherwise.</returns> /// <returns>True if the sparse index is valid and contains a value, false otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Contains(int sparseIndex) public readonly bool Contains(int sparseIndex, int generation)
{ {
if (sparseIndex < 0 || sparseIndex >= _sparse.Count) if (sparseIndex < 0 || sparseIndex >= _sparse.Count)
{ {
@@ -274,21 +247,24 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
var denseIndex = _sparse[sparseIndex]; var denseIndex = _sparse[sparseIndex];
return denseIndex >= 0 && denseIndex < _count; return denseIndex >= 0 && denseIndex < _count && _dense[denseIndex].generation == generation;
} }
/// <summary> /// <summary>
/// Gets the value at the specified sparse index. /// Gets the value at the specified sparse index and generation.
/// </summary> /// </summary>
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param> /// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
/// <param name="generation">The generation number to validate against the stored generation.</param>
/// <param name="value">When this method returns, contains the value at the specified sparse index, if found.</param> /// <param name="value">When this method returns, contains the value at the specified sparse index, if found.</param>
/// <returns>True if the sparse index contains a value, false otherwise.</returns> /// <returns>True if the sparse index contains a value, false otherwise.</returns>
public readonly bool TryGetValue(int sparseIndex, out T value) public readonly bool TryGetValue(int sparseIndex, int generation, out T value)
{ {
if (Contains(sparseIndex)) if (Contains(sparseIndex, generation))
{ {
var denseIndex = _sparse[sparseIndex]; var denseIndex = _sparse[sparseIndex];
value = _dense[denseIndex]; ref var entry = ref _dense[denseIndex];
value = entry.value;
return true; return true;
} }
@@ -297,40 +273,71 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
} }
/// <summary> /// <summary>
/// Gets the value at the specified sparse index. /// Gets the value at the specified sparse index and generation.
/// </summary> /// </summary>
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param> /// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
/// <param name="generation">The generation number to validate against the stored generation.</param>
/// <returns>The value at the specified sparse index.</returns> /// <returns>The value at the specified sparse index.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the sparse index is not found.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when the sparse index is not found.</exception>
public readonly T GetValue(int sparseIndex) public readonly T GetValue(int sparseIndex, int generation)
{ {
if (!Contains(sparseIndex)) if (!Contains(sparseIndex, generation))
{ {
throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index not found in the set."); throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index and feneration not found in the set.");
} }
var denseIndex = _sparse[sparseIndex]; var denseIndex = _sparse[sparseIndex];
return _dense[denseIndex]; ref var entry = ref _dense[denseIndex];
return entry.value;
}
/// <summary>
/// Gets reference of the value at the specified sparse index and generation.
/// </summary>
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
/// <param name="generation">The generation number to validate against the stored generation.</param>
/// <param name="exist">Outputs whether the sparse index exists in the set.</param>
/// <returns>Reference of the value at the specified sparse index.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the sparse index is not found.</exception>
public readonly ref T GetValueReference(int sparseIndex, int generation, out bool exist)
{
if (!Contains(sparseIndex, generation))
{
exist = false;
return ref Unsafe.NullRef<T>();
}
var denseIndex = _sparse[sparseIndex];
ref var entry = ref _dense[denseIndex];
exist = true;
return ref entry.value;
} }
/// <summary> /// <summary>
/// Updates the value at the specified sparse index. /// Updates the value at the specified sparse index.
/// </summary> /// </summary>
/// <param name="sparseIndex">The sparse index of the value to update.</param> /// <param name="sparseIndex">The sparse index of the value to update.</param>
/// <param name="generation">The generation number to validate against the stored generation.</param>
/// <param name="value">The new value.</param> /// <param name="value">The new value.</param>
/// <returns>True if the value was updated, false if the sparse index was not found.</returns> /// <returns>True if the value was updated, false if the sparse index was not found.</returns>
public bool SetValue(int sparseIndex, T value) public bool SetValue(int sparseIndex, int generation, T value)
{ {
if (!Contains(sparseIndex)) if (!Contains(sparseIndex, generation))
{ {
return false; return false;
} }
var denseIndex = _sparse[sparseIndex]; var denseIndex = _sparse[sparseIndex];
_dense[denseIndex] = value; ref var entry = ref _dense[denseIndex];
entry.value = value;
return true; return true;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResizeSparse(int newSize) private void ResizeSparse(int newSize)
{ {
var oldSize = _sparse.Count; var oldSize = _sparse.Count;

View File

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<IsPackable>false</IsPackable> <IsPackable>True</IsPackable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -7,7 +7,7 @@
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Title>$(AssemblyName)</Title> <Title>$(AssemblyName)</Title>
<Authors>Misaki</Authors> <Authors>Misaki</Authors>
<IsPackable>false</IsPackable> <IsPackable>True</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -14,33 +14,40 @@ public class TestUnsafeSparseSet
_sparseSet = new UnsafeSparseSet<int>(16, Allocator.Persistent); _sparseSet = new UnsafeSparseSet<int>(16, Allocator.Persistent);
} }
[TestCleanup]
public void Cleanup()
{
_sparseSet.Dispose();
}
[TestMethod] [TestMethod]
public void Add() public void Add()
{ {
var id = _sparseSet.Add(10); var id = _sparseSet.Add(10, out var gen);
Assert.IsTrue(_sparseSet.Contains(id)); Assert.IsTrue(_sparseSet.Contains(id, gen));
} }
[TestMethod] [TestMethod]
public void Remove() public void Remove()
{ {
var id = _sparseSet.Add(20); var id = _sparseSet.Add(20, out var gen);
Assert.IsTrue(_sparseSet.Contains(id)); Assert.IsTrue(_sparseSet.Contains(id, gen));
_sparseSet.Remove(id); _sparseSet.Remove(id, gen);
Assert.IsFalse(_sparseSet.Contains(id)); Assert.IsFalse(_sparseSet.Contains(id, gen));
} }
[TestMethod] [TestMethod]
public void IndexReuse() public void IndexReuse()
{ {
var id = _sparseSet.Add(20); var id = _sparseSet.Add(20, out var gen);
Assert.IsTrue(_sparseSet.Contains(id)); Assert.IsTrue(_sparseSet.Contains(id, gen));
_sparseSet.Remove(id); _sparseSet.Remove(id, gen);
Assert.IsFalse(_sparseSet.Contains(id)); Assert.IsFalse(_sparseSet.Contains(id, gen));
var newId = _sparseSet.Add(30); var newId = _sparseSet.Add(30, out var newGen);
Assert.AreEqual(id, newId); Assert.AreEqual(id, newId);
Assert.AreNotEqual(gen, newGen);
} }
} }

View File

@@ -75,7 +75,7 @@ public unsafe class TestJobSystem
result = result result = result
}; };
var handle1 = _jobScheduler.Schedule(ref job1, -1); var handle1 = _jobScheduler.Schedule(ref job1);
_jobScheduler.WaitComplete(handle1); _jobScheduler.WaitComplete(handle1);
var job2 = new AddJob var job2 = new AddJob
@@ -84,7 +84,7 @@ public unsafe class TestJobSystem
result = result result = result
}; };
var handle2 = _jobScheduler.Schedule(ref job2, -1, handle1); var handle2 = _jobScheduler.Schedule(ref job2, handle1);
_jobScheduler.WaitComplete(handle2); _jobScheduler.WaitComplete(handle2);
Assert.AreEqual(8.0f, *result); Assert.AreEqual(8.0f, *result);
@@ -101,7 +101,7 @@ public unsafe class TestJobSystem
result = result result = result
}; };
var handle1 = _jobScheduler.Schedule(ref job1, -1); var handle1 = _jobScheduler.Schedule(ref job1);
var job2 = new AddJob var job2 = new AddJob
{ {
@@ -109,7 +109,7 @@ public unsafe class TestJobSystem
result = result result = result
}; };
var handle2 = _jobScheduler.Schedule(ref job2, -1, handle1); var handle2 = _jobScheduler.Schedule(ref job2, handle1);
var job3 = new AddJob var job3 = new AddJob
{ {
@@ -118,7 +118,7 @@ public unsafe class TestJobSystem
}; };
var combinedHandle = _jobScheduler.CombineDependencies(handle1, handle2); var combinedHandle = _jobScheduler.CombineDependencies(handle1, handle2);
var handle3 = _jobScheduler.Schedule(ref job3, -1, combinedHandle); var handle3 = _jobScheduler.Schedule(ref job3, combinedHandle);
_jobScheduler.WaitComplete(handle3); _jobScheduler.WaitComplete(handle3);
@@ -137,7 +137,7 @@ public unsafe class TestJobSystem
inout = result inout = result
}; };
var handle = _jobScheduler.ScheduleParallel(ref job, size, 64, -1, JobHandle.Invalid); var handle = _jobScheduler.ScheduleParallel(ref job, size, 64);
_jobScheduler.WaitComplete(handle); _jobScheduler.WaitComplete(handle);
Assert.AreEqual(1.0f, result[500]); Assert.AreEqual(1.0f, result[500]);
@@ -189,13 +189,66 @@ public unsafe class TestJobSystem
output = result output = result
}; };
var handle1 = _jobScheduler.ScheduleParallel(ref addJob, arraySize, 64, -1, JobHandle.Invalid); var handle1 = _jobScheduler.ScheduleParallel(ref addJob, arraySize, 64);
var handle2 = _jobScheduler.ScheduleParallel(ref multiplyJob, arraySize, 64, -1, handle1); var handle2 = _jobScheduler.ScheduleParallel(ref multiplyJob, arraySize, 64);
var handle3 = _jobScheduler.Schedule(ref sumJob, -1, handle2); var handle3 = _jobScheduler.Schedule(ref sumJob, handle2);
_jobScheduler.WaitComplete(handle3); _jobScheduler.WaitComplete(handle3);
var expected = ComputeExpectedSum(arraySize); var expected = ComputeExpectedSum(arraySize);
Assert.AreEqual(expected, *result, 0.01f); Assert.AreEqual(expected, *result, 0.01f);
} }
[TestMethod]
public void WaitAll()
{
var result1 = stackalloc float[1];
var result2 = stackalloc float[1];
var job1 = new AddJob
{
value = 1.0f,
result = result1
};
var job2 = new AddJob
{
value = 1.0f,
result = result2
};
var handle1 = _jobScheduler.Schedule(ref job1);
var handle2 = _jobScheduler.Schedule(ref job2);
_jobScheduler.WaitAll(handle1, handle2);
Assert.AreEqual(JobState.Completed, _jobScheduler.GetJobStatus(handle1));
Assert.AreEqual(JobState.Completed, _jobScheduler.GetJobStatus(handle2));
}
[TestMethod]
public void WaitAny()
{
var result1 = stackalloc float[1];
var result2 = stackalloc float[1];
var job1 = new AddJob
{
value = 1.0f,
result = result1
};
var job2 = new AddJob
{
value = 1.0f,
result = result2
};
var handle1 = _jobScheduler.Schedule(ref job1);
var handle2 = _jobScheduler.Schedule(ref job2);
var completedHandle = _jobScheduler.WaitAny(handle1, handle2);
Assert.AreEqual(JobState.Completed, _jobScheduler.GetJobStatus(completedHandle));
}
} }

View File

@@ -7,6 +7,13 @@ namespace Misaki.HighPerformance.Collections;
public class ConcurrentSlotMap<T> : IEnumerable<T> public class ConcurrentSlotMap<T> : IEnumerable<T>
{ {
private struct SlotEntry
{
public T? value;
public int generation;
public int isValid;
}
public struct Enumerator : IEnumerator<T> public struct Enumerator : IEnumerator<T>
{ {
private readonly ConcurrentSlotMap<T> _slotMap; private readonly ConcurrentSlotMap<T> _slotMap;
@@ -42,21 +49,6 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
} }
} }
// Lock-free slot using separate fields for atomic operations
private struct SlotEntry
{
public T? value;
public int generation;
public int isValid;
public SlotEntry()
{
value = default;
generation = 0;
isValid = 0;
}
}
private volatile SlotEntry[] _data; private volatile SlotEntry[] _data;
private readonly ConcurrentQueue<int> _freeSlots; private readonly ConcurrentQueue<int> _freeSlots;
@@ -215,6 +207,29 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
return false; // Another thread already removed it return false; // Another thread already removed it
} }
public bool Contain(int slotIndex, int generation)
{
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
{
return false;
}
ref var slot = ref _data[slotIndex];
var currentGeneration = Volatile.Read(ref slot.generation);
var isValid = Volatile.Read(ref slot.isValid) == 1;
if (isValid && currentGeneration == generation)
{
if (Volatile.Read(ref slot.isValid) == 1 && Volatile.Read(ref slot.generation) == generation)
{
return true;
}
}
return false;
}
public bool TryGetElement(int slotIndex, int generation, [MaybeNullWhen(false)] out T value) public bool TryGetElement(int slotIndex, int generation, [MaybeNullWhen(false)] out T value)
{ {
value = default; value = default;

View File

@@ -126,6 +126,23 @@ public class SlotMap<T> : IEnumerable<T>
return true; return true;
} }
public bool Contain(int slotIndex, int generation)
{
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
{
return false;
}
ref var slot = ref _data[slotIndex];
if (slot.isValid && slot.generation == generation)
{
return true;
}
return false;
}
public ref T GetElementAt(int slotIndex, int generation) public ref T GetElementAt(int slotIndex, int generation)
{ {
if (slotIndex < 0 || slotIndex >= _capacity) if (slotIndex < 0 || slotIndex >= _capacity)

View File

@@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>True</IsPackable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">