Reserve index 0 in SlotMap, improve unsafe collections
- Reserve index 0 as always invalid in SlotMap, ConcurrentSlotMap, UnsafeSlotMap, and UnsafeSparseSet; update all index checks and slot operations accordingly - Refactor SlotMap to use parallel arrays and BitArray for occupancy - Double capacity on resize for all major unsafe collections - Add debugger display support for unsafe collections - Improve NuGet publishing workflow to skip existing versions - Increment package versions (LowLevel: 1.3.1, main: 1.0.2) - Add comprehensive unit tests for SlotMap and ConcurrentSlotMap - Update main program and documentation for new slot map behavior
This commit is contained in:
@@ -22,29 +22,69 @@ jobs:
|
|||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
|
||||||
- name: Build all projects
|
# Note: We skipped the global "Build all projects" step.
|
||||||
run: dotnet build -c Release
|
# dotnet pack will build automatically, and we only want to build/pack
|
||||||
|
# the specific projects that strictly need updates.
|
||||||
|
|
||||||
- name: Pack all projects
|
- name: Check and Pack Projects
|
||||||
|
env:
|
||||||
|
NUGET_API_KEY: ${{ secrets.NUGET_TOKEN }}
|
||||||
|
SOURCE_URL: "https://git.personalnas.com/api/packages/Misaki/nuget"
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p ./artifacts
|
||||||
|
|
||||||
for projfile in $(find . -name "*.csproj"); do
|
for projfile in $(find . -name "*.csproj"); do
|
||||||
if grep -q "<GeneratePackageOnBuild>True</GeneratePackageOnBuild>" "$projfile"; then
|
# 1. Check if the project is configured to generate a package
|
||||||
echo "Packing $projfile..."
|
GENERATE_PKG=$(dotnet build "$projfile" --getProperty:GeneratePackageOnBuild)
|
||||||
|
if [[ "$GENERATE_PKG" != "true" && "$GENERATE_PKG" != "True" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Extract Package ID and Version efficiently
|
||||||
|
PACKAGE_ID=$(dotnet build "$projfile" --getProperty:PackageId)
|
||||||
|
VERSION=$(dotnet build "$projfile" --getProperty:Version)
|
||||||
|
|
||||||
|
# Fallback: If PackageId is not explicitly set in csproj, use the filename
|
||||||
|
if [ -z "$PACKAGE_ID" ]; then
|
||||||
|
PACKAGE_ID=$(basename "$projfile" .csproj)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking $PACKAGE_ID version $VERSION..."
|
||||||
|
|
||||||
|
# 3. Check if version exists in Gitea Registry
|
||||||
|
# Convert ID to lowercase for URL consistency (NuGet V3 standard)
|
||||||
|
LOWER_ID=$(echo "$PACKAGE_ID" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
# Gitea supports the NuGet V3 Registration endpoint
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
-H "Authorization: token $NUGET_API_KEY" \
|
||||||
|
"${SOURCE_URL}/registration/${LOWER_ID}/${VERSION}.json")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||||
|
echo "✅ Version $VERSION of $PACKAGE_ID already exists. Skipping..."
|
||||||
|
else
|
||||||
|
echo "🆕 Version $VERSION of $PACKAGE_ID is new. Building and Packing..."
|
||||||
|
|
||||||
|
# Build and Pack only this project
|
||||||
if ! dotnet pack "$projfile" -c Release -o ./artifacts; then
|
if ! dotnet pack "$projfile" -c Release -o ./artifacts; then
|
||||||
echo "Failed to pack $projfile"
|
echo "❌ Failed to pack $projfile"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Publish all packages
|
- name: Publish new packages
|
||||||
env:
|
env:
|
||||||
NUGET_API_KEY: ${{ secrets.NUGET_TOKEN }}
|
NUGET_API_KEY: ${{ secrets.NUGET_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
for pkg in ./artifacts/*.nupkg; do
|
# If no packages were created (all skipped), exit gracefully
|
||||||
# Skip if no files exist (in case globs fail)
|
if [ ! -d "./artifacts" ] || [ -z "$(ls -A ./artifacts/*.nupkg 2>/dev/null)" ]; then
|
||||||
[ -e "$pkg" ] || continue
|
echo "No new packages to publish."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Skip symbol packages if they exist
|
for pkg in ./artifacts/*.nupkg; do
|
||||||
|
# Skip symbol packages
|
||||||
if [[ "$pkg" == *".symbols.nupkg" ]]; then
|
if [[ "$pkg" == *".symbols.nupkg" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ namespace Misaki.HighPerformance.Jobs;
|
|||||||
public unsafe partial class JobScheduler
|
public unsafe partial class JobScheduler
|
||||||
{
|
{
|
||||||
public static readonly TempJobAllocator* pTempAllocator;
|
public static readonly TempJobAllocator* pTempAllocator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the allocation handle for the temporary job allocator.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// You must dispose the allocation before the fourth time you call <see cref="TempJobAllocator.AdvanceFrame"/> after obtaining this handle.
|
||||||
|
/// </remarks>
|
||||||
public static AllocationHandle TempAllocatorHandle => pTempAllocator->Handle;
|
public static AllocationHandle TempAllocatorHandle => pTempAllocator->Handle;
|
||||||
|
|
||||||
static JobScheduler()
|
static JobScheduler()
|
||||||
|
|||||||
@@ -7,10 +7,38 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class UnsafeArrayDebugView<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private readonly UnsafeArray<T> _array;
|
||||||
|
|
||||||
|
public UnsafeArrayDebugView(UnsafeArray<T> array)
|
||||||
|
{
|
||||||
|
_array = array;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public T[] Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var count = _array.Count;
|
||||||
|
var result = new T[count];
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
result[i] = _array[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A structure for managing an array of unmanaged types with unsafe memory operations.
|
/// A structure for managing an array of unmanaged types with unsafe memory operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Represents a type that can be stored in an unmanaged memory context.</typeparam>
|
/// <typeparam name="T">Represents a type that can be stored in an unmanaged memory context.</typeparam>
|
||||||
|
[DebuggerTypeProxy(typeof(UnsafeArrayDebugView<>))]
|
||||||
public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
|
public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -7,6 +8,31 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class UnsafeBitSetDebugView
|
||||||
|
{
|
||||||
|
private readonly UnsafeBitSet _bitSet;
|
||||||
|
public UnsafeBitSetDebugView(UnsafeBitSet bitSet)
|
||||||
|
{
|
||||||
|
_bitSet = bitSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public bool[] Bits
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var bits = new bool[_bitSet.Count];
|
||||||
|
for (var i = 0; i < bits.Length; i++)
|
||||||
|
{
|
||||||
|
bits[i] = _bitSet.IsSet(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerTypeProxy(typeof(UnsafeBitSetDebugView))]
|
||||||
public unsafe struct UnsafeBitSet : IDisposable, IEquatable<UnsafeBitSet>
|
public unsafe struct UnsafeBitSet : IDisposable, IEquatable<UnsafeBitSet>
|
||||||
{
|
{
|
||||||
public ref struct Iterator
|
public ref struct Iterator
|
||||||
@@ -65,6 +91,9 @@ public unsafe struct UnsafeBitSet : IDisposable, IEquatable<UnsafeBitSet>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
|
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="minimalLength">The minimal length in bits.</param>
|
||||||
|
/// <param name="handle">The allocation handle.</param>
|
||||||
|
/// <param name="option">The allocation option.</param>
|
||||||
public UnsafeBitSet(int minimalLength, AllocationHandle handle, AllocationOption option = AllocationOption.None)
|
public UnsafeBitSet(int minimalLength, AllocationHandle handle, AllocationOption option = AllocationOption.None)
|
||||||
{
|
{
|
||||||
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
|
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
|
||||||
|
|||||||
@@ -2,14 +2,40 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
|||||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class UnsafeListDebugView<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private readonly UnsafeList<T> _list;
|
||||||
|
public UnsafeListDebugView(UnsafeList<T> list)
|
||||||
|
{
|
||||||
|
_list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public T[] Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var array = new T[_list.Count];
|
||||||
|
for (int i = 0; i < _list.Count; i++)
|
||||||
|
{
|
||||||
|
array[i] = _list[i];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A collection that allows for unsafe operations on a list of unmanaged types.
|
/// A collection that allows for unsafe operations on a list of unmanaged types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Represents a type that can be stored in the collection, constrained to unmanaged types for performance and safety.</typeparam>
|
/// <typeparam name="T">Represents a type that can be stored in the collection, constrained to unmanaged types for performance and safety.</typeparam>
|
||||||
|
[DebuggerTypeProxy(typeof(UnsafeListDebugView<>))]
|
||||||
public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
|
public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
@@ -210,7 +236,7 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
|
|||||||
{
|
{
|
||||||
if (_count >= Capacity)
|
if (_count >= Capacity)
|
||||||
{
|
{
|
||||||
Resize(Capacity + (int)(Capacity * 0.5f));
|
Resize(Math.Max(1, Capacity * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
|
|||||||
{
|
{
|
||||||
if (_count >= Capacity)
|
if (_count >= Capacity)
|
||||||
{
|
{
|
||||||
Resize((int)(Capacity * 1.5f));
|
Resize(Math.Max(1, Capacity * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), (_offset + _count) % Capacity, value);
|
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), (_offset + _count) % Capacity, value);
|
||||||
|
|||||||
@@ -2,15 +2,43 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
|||||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class UnsafeSlotMapDebugView<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private readonly UnsafeSlotMap<T> _slotMap;
|
||||||
|
public UnsafeSlotMapDebugView(UnsafeSlotMap<T> slotMap)
|
||||||
|
{
|
||||||
|
_slotMap = slotMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public T[] Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var items = new List<T>(_slotMap.Count);
|
||||||
|
var enumerator = _slotMap.GetEnumerator();
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
items.Add(enumerator.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides an unsafe, high-performance slot map for storing and managing unmanaged values, supporting fast insertion,
|
/// Provides an unsafe, high-performance slot map for storing and managing unmanaged values, supporting fast insertion,
|
||||||
/// removal, and lookup by slot index and generation.
|
/// removal, and lookup by slot index and generation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of value to store in the slot map. Must be unmanaged.</typeparam>
|
/// <typeparam name="T">The type of value to store in the slot map. Must be unmanaged.</typeparam>
|
||||||
|
[DebuggerTypeProxy(typeof(UnsafeSlotMapDebugView<>))]
|
||||||
public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
@@ -53,7 +81,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
private int _count;
|
private int _count;
|
||||||
private int _capacity;
|
private int _capacity;
|
||||||
|
|
||||||
public readonly int Count => _count;
|
public readonly int Count => _count - 1;
|
||||||
public readonly int Capacity => _capacity;
|
public readonly int Capacity => _capacity;
|
||||||
|
|
||||||
public readonly bool IsCreated => _data.IsCreated && _generations.IsCreated && _freeSlots.IsCreated && _validBits.IsCreated;
|
public readonly bool IsCreated => _data.IsCreated && _generations.IsCreated && _freeSlots.IsCreated && _validBits.IsCreated;
|
||||||
@@ -88,7 +116,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
_data = new UnsafeArray<T>(capacity, handle, allocationOption);
|
_data = new UnsafeArray<T>(capacity, handle, allocationOption);
|
||||||
_generations = new UnsafeArray<int>(capacity, handle, allocationOption);
|
_generations = new UnsafeArray<int>(capacity, handle, allocationOption);
|
||||||
_freeSlots = new UnsafeQueue<int>(capacity, handle, allocationOption);
|
_freeSlots = new UnsafeQueue<int>(capacity, handle, allocationOption);
|
||||||
_validBits = new UnsafeBitSet(GetBitSetCapacity(capacity), handle, allocationOption);
|
_validBits = new UnsafeBitSet(capacity, handle, allocationOption);
|
||||||
|
|
||||||
if (!allocationOption.HasFlag(AllocationOption.Clear))
|
if (!allocationOption.HasFlag(AllocationOption.Clear))
|
||||||
{
|
{
|
||||||
@@ -98,6 +126,9 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
|
|
||||||
_count = 0;
|
_count = 0;
|
||||||
_capacity = capacity;
|
_capacity = capacity;
|
||||||
|
|
||||||
|
// Add a default item for invalid slot
|
||||||
|
Add(default, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -112,12 +143,6 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetBitSetCapacity(int capacity)
|
|
||||||
{
|
|
||||||
// Each uint32 can hold 32 bits.
|
|
||||||
return (capacity + 31) / 32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the specified item to the collection and returns the index of the slot where it was stored.
|
/// Adds the specified item to the collection and returns the index of the slot where it was stored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -128,7 +153,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
{
|
{
|
||||||
if (_count >= _capacity)
|
if (_count >= _capacity)
|
||||||
{
|
{
|
||||||
Resize((int)(_capacity * 1.5f));
|
Resize(Math.Max(1, _capacity * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
int index;
|
int index;
|
||||||
@@ -185,9 +210,10 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
/// <param name="slotIndex">The zero-based index of the slot to check. Must be greater than or equal to 0 and less than the current capacity.</param>
|
/// <param name="slotIndex">The zero-based index of the slot to check. Must be greater than or equal to 0 and less than the current capacity.</param>
|
||||||
/// <param name="generation">The generation value to compare against the slot's generation.</param>
|
/// <param name="generation">The generation value to compare against the slot's generation.</param>
|
||||||
/// <returns>true if the slot at the specified index is valid and its generation matches the specified value; otherwise, false.</returns>
|
/// <returns>true if the slot at the specified index is valid and its generation matches the specified value; otherwise, false.</returns>
|
||||||
public bool Contains(int slotIndex, int generation)
|
public readonly bool Contains(int slotIndex, int generation)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
// 0 is reserved for invalid slot
|
||||||
|
if (slotIndex <= 0 || slotIndex >= _capacity)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -211,13 +237,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
/// <returns>true if the element at the specified slot index and generation is found; otherwise, false.</returns>
|
/// <returns>true if the element at the specified slot index and generation is found; otherwise, false.</returns>
|
||||||
public readonly bool TryGetElementAt(int slotIndex, int generation, out T value)
|
public readonly bool TryGetElementAt(int slotIndex, int generation, out T value)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_generations[slotIndex] != generation)
|
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
return false;
|
return false;
|
||||||
@@ -237,14 +257,9 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
/// <exception cref="InvalidOperationException">Thrown when the specified slot is not occupied or the generation does not match.</exception>
|
/// <exception cref="InvalidOperationException">Thrown when the specified slot is not occupied or the generation does not match.</exception>
|
||||||
public readonly T GetElementAt(int slotIndex, int generation)
|
public readonly T GetElementAt(int slotIndex, int generation)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
|
throw new InvalidOperationException("The specified slot is not occupied or the generation does not match.");
|
||||||
}
|
|
||||||
|
|
||||||
if (!_validBits.IsSet(slotIndex) || _generations[slotIndex] != generation)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _data[slotIndex];
|
return _data[slotIndex];
|
||||||
@@ -260,15 +275,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
/// <returns>A reference to the element of type <typeparamref name="T"/> at the specified slot and generation if it exists; otherwise, a null reference.</returns>
|
/// <returns>A reference to the element of type <typeparamref name="T"/> at the specified slot and generation if it exists; otherwise, a null reference.</returns>
|
||||||
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
|
||||||
exist = false;
|
|
||||||
return ref Unsafe.NullRef<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
|
||||||
|
|
||||||
if (!_validBits.IsSet(slotIndex) || _generations[slotIndex] != generation)
|
|
||||||
{
|
{
|
||||||
exist = false;
|
exist = false;
|
||||||
return ref Unsafe.NullRef<T>();
|
return ref Unsafe.NullRef<T>();
|
||||||
@@ -283,7 +290,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
|||||||
_data.Resize(newSize, option);
|
_data.Resize(newSize, option);
|
||||||
_generations.Resize(newSize, option);
|
_generations.Resize(newSize, option);
|
||||||
_freeSlots.Resize(newSize, option);
|
_freeSlots.Resize(newSize, option);
|
||||||
_validBits.Resize(GetBitSetCapacity(newSize), option);
|
_validBits.Resize(newSize, option);
|
||||||
|
|
||||||
_capacity = newSize;
|
_capacity = newSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,37 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
|||||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class ConcurrentSparseSetDebugView<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private readonly UnsafeSparseSet<T> _set;
|
||||||
|
public ConcurrentSparseSetDebugView(UnsafeSparseSet<T> set)
|
||||||
|
{
|
||||||
|
_set = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public T[] Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var items = new T[_set.Count];
|
||||||
|
var index = 0;
|
||||||
|
foreach (var item in _set)
|
||||||
|
{
|
||||||
|
items[index++] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A sparse set data structure that provides O(1) insertion, deletion, and lookup operations.
|
/// A sparse set data structure that provides O(1) insertion, deletion, and lookup operations.
|
||||||
/// The sparse set uses three arrays: a dense array for storing values, a sparse array for mapping indices,
|
/// The sparse set uses three arrays: a dense array for storing values, a sparse array for mapping indices,
|
||||||
@@ -13,6 +40,7 @@ namespace Misaki.HighPerformance.LowLevel.Collections;
|
|||||||
/// Sparse indices work like entity IDs and are automatically generated.
|
/// Sparse indices work like entity IDs and are automatically generated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Represents a type that can be stored in the sparse set, constrained to unmanaged types for performance and safety.</typeparam>
|
/// <typeparam name="T">Represents a type that can be stored in the sparse set, constrained to unmanaged types for performance and safety.</typeparam>
|
||||||
|
[DebuggerTypeProxy(typeof(ConcurrentSparseSetDebugView<>))]
|
||||||
public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
@@ -57,7 +85,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
|||||||
private int _nextId; // Next available sparse index
|
private int _nextId; // Next available sparse index
|
||||||
private int _capacity;
|
private int _capacity;
|
||||||
|
|
||||||
public readonly int Count => _count;
|
public readonly int Count => _count - 1;
|
||||||
public readonly int Capacity => _capacity;
|
public readonly int Capacity => _capacity;
|
||||||
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
|
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
|
||||||
|
|
||||||
@@ -103,7 +131,10 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
|||||||
_nextId = 0;
|
_nextId = 0;
|
||||||
_capacity = capacity;
|
_capacity = capacity;
|
||||||
|
|
||||||
Clear();
|
_sparse.AsSpan().Fill(-1);
|
||||||
|
_generations.Clear();
|
||||||
|
|
||||||
|
Add(default, out _); // Make index 0 invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -147,7 +178,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
|||||||
// Resize dense arrays if necessary
|
// Resize dense arrays if necessary
|
||||||
if (_count >= _capacity)
|
if (_count >= _capacity)
|
||||||
{
|
{
|
||||||
Resize((int)(_capacity * 1.5f));
|
Resize(Math.Max(1, _capacity * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the value to the dense array and update mappings
|
// Add the value to the dense array and update mappings
|
||||||
@@ -209,7 +240,8 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly bool Contains(int sparseIndex, int generation)
|
public readonly bool Contains(int sparseIndex, int generation)
|
||||||
{
|
{
|
||||||
if (sparseIndex < 0 || sparseIndex >= _sparse.Count)
|
// 0 is reserved as invalid index
|
||||||
|
if (sparseIndex <= 0 || sparseIndex >= _sparse.Count)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,43 @@ using Misaki.HighPerformance.LowLevel.Buffer;
|
|||||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
|
internal class UnsafeStackDebugView<T>
|
||||||
|
where T : unmanaged
|
||||||
|
{
|
||||||
|
private readonly UnsafeStack<T> _stack;
|
||||||
|
public UnsafeStackDebugView(UnsafeStack<T> stack)
|
||||||
|
{
|
||||||
|
_stack = stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public unsafe T[] Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var items = new T[_stack.Count];
|
||||||
|
var pItems = (T*)_stack.GetUnsafePtr();
|
||||||
|
for (int i = 0; i < _stack.Count; i++)
|
||||||
|
{
|
||||||
|
items[i] = pItems[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a high-performance, unsafe stack data structure for unmanaged types, supporting manual memory management
|
/// Provides a high-performance, unsafe stack data structure for unmanaged types, supporting manual memory management
|
||||||
/// and allocation control.
|
/// and allocation control.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of elements stored in the stack. Must be an unmanaged type.</typeparam>
|
/// <typeparam name="T">The type of elements stored in the stack. Must be an unmanaged type.</typeparam>
|
||||||
|
[DebuggerTypeProxy(typeof(UnsafeStackDebugView<>))]
|
||||||
public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
|
public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
|
||||||
where T : unmanaged
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
@@ -96,7 +124,7 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
|
|||||||
{
|
{
|
||||||
if (_count >= Capacity)
|
if (_count >= Capacity)
|
||||||
{
|
{
|
||||||
Resize((int)(Capacity * 1.5f));
|
Resize(Math.Max(1, Capacity * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
<Authors>Misaki</Authors>
|
<Authors>Misaki</Authors>
|
||||||
<AssemblyVersion>1.3.0</AssemblyVersion>
|
<AssemblyVersion>1.3.1</AssemblyVersion>
|
||||||
<Version>$(AssemblyVersion)</Version>
|
<Version>$(AssemblyVersion)</Version>
|
||||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
|
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
|
||||||
|
|||||||
@@ -22,17 +22,10 @@
|
|||||||
|
|
||||||
//BenchmarkDotNet.Running.BenchmarkRunner.Run<Misaki.HighPerformance.Test.Benchmark.CollectionBenchmark>();
|
//BenchmarkDotNet.Running.BenchmarkRunner.Run<Misaki.HighPerformance.Test.Benchmark.CollectionBenchmark>();
|
||||||
|
|
||||||
|
using Misaki.HighPerformance.Collections;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
|
||||||
using var scope = AllocationManager.CreateStackScope();
|
var csm = new ConcurrentSlotMap<int>();
|
||||||
var array = new UnsafeArray<int>(10, scope.AllocationHandle);
|
Console.WriteLine(csm.Contains(0, 0));
|
||||||
for (var i = 0; i < array.Count; i++)
|
Console.WriteLine(csm.Count == 0);
|
||||||
{
|
|
||||||
array[i] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in array.AsSpan())
|
|
||||||
{
|
|
||||||
Console.WriteLine(item);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using Misaki.HighPerformance.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class TestSlotMap
|
||||||
|
{
|
||||||
|
private SlotMap<int> _slotMap = null!;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_slotMap = new SlotMap<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestDefaultIndex()
|
||||||
|
{
|
||||||
|
Assert.IsFalse(_slotMap.Contains(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddAndContains()
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(42, out var generation);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
Assert.AreEqual(42, _slotMap.GetElementAt(slotIndex, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemove()
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(100, out var generation);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
var removed = _slotMap.Remove(slotIndex, generation);
|
||||||
|
Assert.IsTrue(removed);
|
||||||
|
Assert.IsFalse(_slotMap.Contains(slotIndex, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveInvalid()
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(200, out var generation);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
var removed = _slotMap.Remove(slotIndex, generation + 1); // Wrong generation
|
||||||
|
Assert.IsFalse(removed);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestIndexReuse()
|
||||||
|
{
|
||||||
|
var slotIndex1 = _slotMap.Add(300, out var generation1);
|
||||||
|
_slotMap.Remove(slotIndex1, generation1);
|
||||||
|
var slotIndex2 = _slotMap.Add(400, out var generation2);
|
||||||
|
Assert.AreEqual(slotIndex1, slotIndex2);
|
||||||
|
Assert.AreNotEqual(generation1, generation2);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
Misaki.HighPerformance.Test/UnitTest/Collections/TestSlotMap.cs
Normal file
127
Misaki.HighPerformance.Test/UnitTest/Collections/TestSlotMap.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using Misaki.HighPerformance.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class TestConcurrentSlotMap
|
||||||
|
{
|
||||||
|
private ConcurrentSlotMap<int> _slotMap = null!;
|
||||||
|
|
||||||
|
public TestContext TestContext
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_slotMap = new ConcurrentSlotMap<int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestDefaultIndex()
|
||||||
|
{
|
||||||
|
Assert.IsFalse(_slotMap.Contains(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddAndContains()
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(42, out var generation);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
Assert.AreEqual(42, _slotMap.GetElementAt(slotIndex, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemove()
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(100, out var generation);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
var removed = _slotMap.Remove(slotIndex, generation);
|
||||||
|
Assert.IsTrue(removed);
|
||||||
|
Assert.IsFalse(_slotMap.Contains(slotIndex, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveInvalid()
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(200, out var generation);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
var removed = _slotMap.Remove(slotIndex, generation + 1); // Wrong generation
|
||||||
|
Assert.IsFalse(removed);
|
||||||
|
Assert.IsTrue(_slotMap.Contains(slotIndex, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestIndexReuse()
|
||||||
|
{
|
||||||
|
var slotIndex1 = _slotMap.Add(300, out var generation1);
|
||||||
|
_slotMap.Remove(slotIndex1, generation1);
|
||||||
|
var slotIndex2 = _slotMap.Add(400, out var generation2);
|
||||||
|
Assert.AreEqual(slotIndex1, slotIndex2);
|
||||||
|
Assert.AreNotEqual(generation1, generation2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestConcurrentAdditions()
|
||||||
|
{
|
||||||
|
const int threadCount = 8;
|
||||||
|
const int itemsPerThread = 1000;
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
for (int t = 0; t < threadCount; t++)
|
||||||
|
{
|
||||||
|
tasks.Add(Task.Run(() =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < itemsPerThread; i++)
|
||||||
|
{
|
||||||
|
_slotMap.Add(i, out _);
|
||||||
|
}
|
||||||
|
}, TestContext.CancellationTokenSource.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(tasks, TestContext.CancellationTokenSource.Token);
|
||||||
|
Assert.AreEqual(threadCount * itemsPerThread, _slotMap.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestConcurrentRandomAddRemove()
|
||||||
|
{
|
||||||
|
const int threadCount = 8;
|
||||||
|
const int operationsPerThread = 1000;
|
||||||
|
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
var rand = new Random();
|
||||||
|
var addedItems = new ConcurrentBag<(int slotIndex, int generation)>();
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
for (int t = 0; t < threadCount; t++)
|
||||||
|
{
|
||||||
|
tasks.Add(Task.Run(() =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < operationsPerThread; i++)
|
||||||
|
{
|
||||||
|
if (rand.NextDouble() < 0.5)
|
||||||
|
{
|
||||||
|
var slotIndex = _slotMap.Add(i, out var generation);
|
||||||
|
addedItems.Add((slotIndex, generation));
|
||||||
|
|
||||||
|
Interlocked.Increment(ref count);
|
||||||
|
}
|
||||||
|
else if (addedItems.TryTake(out var item))
|
||||||
|
{
|
||||||
|
_slotMap.Remove(item.slotIndex, item.generation);
|
||||||
|
Interlocked.Decrement(ref count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TestContext.CancellationTokenSource.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(tasks, TestContext.CancellationTokenSource.Token);
|
||||||
|
|
||||||
|
Assert.AreEqual(count, _slotMap.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
|
|||||||
// For lock-free resizing
|
// For lock-free resizing
|
||||||
private int _isResizing;
|
private int _isResizing;
|
||||||
|
|
||||||
public int Count => Volatile.Read(ref _count);
|
public int Count => Volatile.Read(ref _count) - 1;
|
||||||
public int Capacity => Volatile.Read(ref _capacity);
|
public int Capacity => Volatile.Read(ref _capacity);
|
||||||
|
|
||||||
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
|
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
|
||||||
@@ -75,6 +75,8 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
|
|||||||
|
|
||||||
_data = new SlotEntry[initialCapacity];
|
_data = new SlotEntry[initialCapacity];
|
||||||
_freeSlots = new();
|
_freeSlots = new();
|
||||||
|
|
||||||
|
Add(default!, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
@@ -209,7 +211,7 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
|
|||||||
|
|
||||||
public bool Contains(int slotIndex, int generation)
|
public bool Contains(int slotIndex, int generation)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
if (slotIndex <= 0 || slotIndex >= Volatile.Read(ref _capacity))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -232,84 +234,47 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
|
|||||||
|
|
||||||
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;
|
if (!Contains(slotIndex, generation))
|
||||||
|
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
|
||||||
{
|
{
|
||||||
|
value = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
value = _data[slotIndex].value!;
|
||||||
|
return true;
|
||||||
// Read generation first, then validity, then value for consistency
|
|
||||||
var currentGeneration = Volatile.Read(ref slot.generation);
|
|
||||||
var isValid = Volatile.Read(ref slot.isValid) == 1;
|
|
||||||
|
|
||||||
if (isValid && currentGeneration == generation)
|
|
||||||
{
|
|
||||||
// Double-check that the slot is still valid with same generation
|
|
||||||
// to avoid race condition where slot gets removed between reads
|
|
||||||
if (Volatile.Read(ref slot.isValid) == 1 && Volatile.Read(ref slot.generation) == generation)
|
|
||||||
{
|
|
||||||
value = slot.value!;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetElementAt(int slotIndex, int generation)
|
public T GetElementAt(int slotIndex, int generation)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
if (!TryGetElement(slotIndex, generation, out var value))
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
|
||||||
|
|
||||||
if (Volatile.Read(ref slot.isValid) == 0 || Volatile.Read(ref slot.generation) != generation)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Slot {slotIndex} is not occupied or generation mismatch.");
|
throw new InvalidOperationException($"Slot {slotIndex} is not occupied or generation mismatch.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return slot.value!;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
|
||||||
exist = false;
|
|
||||||
return ref Unsafe.NullRef<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
|
||||||
|
|
||||||
if (Volatile.Read(ref slot.isValid) == 0 || Volatile.Read(ref slot.generation) != generation)
|
|
||||||
{
|
{
|
||||||
exist = false;
|
exist = false;
|
||||||
return ref Unsafe.NullRef<T>();
|
return ref Unsafe.NullRef<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
exist = true;
|
exist = true;
|
||||||
return ref slot.value!;
|
return ref _data[slotIndex].value!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateElement(int slotIndex, int generation, T newValue)
|
public bool UpdateElement(int slotIndex, int generation, T newValue)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
_data[slotIndex].value = newValue;
|
||||||
if (Volatile.Read(ref slot.isValid) == 0 || Volatile.Read(ref slot.generation) != generation)
|
return true;
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Slot {slotIndex} is not occupied or generation mismatch.");
|
|
||||||
}
|
|
||||||
|
|
||||||
slot.value = newValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
@@ -328,9 +293,6 @@ public class ConcurrentSlotMap<T> : IEnumerable<T>
|
|||||||
slot.value = default;
|
slot.value = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear free slots queue
|
_freeSlots.Clear();
|
||||||
while (_freeSlots.TryDequeue(out _))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ public class SlotMap<T> : IEnumerable<T>
|
|||||||
_currentIndex = -1;
|
_currentIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly T Current => _slotMap._data[_currentIndex].value;
|
public readonly T Current => _slotMap._data[_currentIndex];
|
||||||
readonly object? IEnumerator.Current => Current;
|
readonly object? IEnumerator.Current => Current;
|
||||||
|
|
||||||
public bool MoveNext()
|
public bool MoveNext()
|
||||||
{
|
{
|
||||||
while (++_currentIndex < _slotMap._capacity)
|
while (++_currentIndex < _slotMap._capacity)
|
||||||
{
|
{
|
||||||
if (_slotMap._data[_currentIndex].isValid)
|
if (_slotMap._isOccupiedBits[_currentIndex])
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -40,14 +40,9 @@ public class SlotMap<T> : IEnumerable<T>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SlotData
|
private T[] _data;
|
||||||
{
|
private int[] _generations;
|
||||||
public T value;
|
private readonly BitArray _isOccupiedBits;
|
||||||
public int generation;
|
|
||||||
public bool isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SlotData[] _data;
|
|
||||||
private readonly Queue<int> _freeSlots;
|
private readonly Queue<int> _freeSlots;
|
||||||
|
|
||||||
private int _count;
|
private int _count;
|
||||||
@@ -64,8 +59,12 @@ public class SlotMap<T> : IEnumerable<T>
|
|||||||
{
|
{
|
||||||
_capacity = initialCapacity;
|
_capacity = initialCapacity;
|
||||||
|
|
||||||
_data = new SlotData[initialCapacity];
|
_data = new T[initialCapacity];
|
||||||
|
_generations = new int[initialCapacity];
|
||||||
|
_isOccupiedBits = new BitArray(initialCapacity);
|
||||||
_freeSlots = new(initialCapacity);
|
_freeSlots = new(initialCapacity);
|
||||||
|
|
||||||
|
Add(default!, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Resize()
|
private void Resize()
|
||||||
@@ -73,6 +72,9 @@ public class SlotMap<T> : IEnumerable<T>
|
|||||||
var newCapacity = _capacity * 2;
|
var newCapacity = _capacity * 2;
|
||||||
|
|
||||||
Array.Resize(ref _data, newCapacity);
|
Array.Resize(ref _data, newCapacity);
|
||||||
|
Array.Resize(ref _generations, newCapacity);
|
||||||
|
|
||||||
|
_isOccupiedBits.Length = newCapacity;
|
||||||
_freeSlots.EnsureCapacity(newCapacity);
|
_freeSlots.EnsureCapacity(newCapacity);
|
||||||
|
|
||||||
_capacity = newCapacity;
|
_capacity = newCapacity;
|
||||||
@@ -95,48 +97,23 @@ public class SlotMap<T> : IEnumerable<T>
|
|||||||
slotIndex = _freeSlots.Dequeue();
|
slotIndex = _freeSlots.Dequeue();
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
_data[slotIndex] = item;
|
||||||
slot.value = item;
|
_isOccupiedBits[slotIndex] = true;
|
||||||
slot.isValid = true;
|
|
||||||
generation = slot.generation;
|
|
||||||
|
|
||||||
_count++;
|
_count++;
|
||||||
|
|
||||||
|
generation = _generations[slotIndex];
|
||||||
return slotIndex;
|
return slotIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Remove(int slotIndex, int generation)
|
public bool Contains(int slotIndex, int generation)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
if (slotIndex <= 0 || slotIndex >= Volatile.Read(ref _capacity))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
if (_isOccupiedBits[slotIndex] && _generations[slotIndex] == generation)
|
||||||
if (slot.generation != generation)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
slot.generation++;
|
|
||||||
slot.isValid = false;
|
|
||||||
|
|
||||||
_freeSlots.Enqueue(slotIndex);
|
|
||||||
_count--;
|
|
||||||
|
|
||||||
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 true;
|
||||||
}
|
}
|
||||||
@@ -144,75 +121,65 @@ public class SlotMap<T> : IEnumerable<T>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Remove(int slotIndex, int generation)
|
||||||
|
{
|
||||||
|
if (!Contains(slotIndex, generation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_generations[slotIndex]++;
|
||||||
|
_isOccupiedBits[slotIndex] = false;
|
||||||
|
|
||||||
|
_freeSlots.Enqueue(slotIndex);
|
||||||
|
_count--;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryGetElement(int slotIndex, int generation, [MaybeNullWhen(false)] out T value)
|
public bool TryGetElement(int slotIndex, int generation, [MaybeNullWhen(false)] out T value)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
value = _data[slotIndex];
|
||||||
if (slot.generation != generation)
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = slot.value;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetElementAt(int slotIndex, int generation)
|
public T GetElementAt(int slotIndex, int generation)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
|
throw new InvalidOperationException($"Slot {slotIndex} is not occupied or generation mismatch.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
return _data[slotIndex];
|
||||||
if (!slot.isValid || slot.generation != generation)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return slot.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
|
||||||
exist = false;
|
|
||||||
return ref Unsafe.NullRef<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
|
||||||
|
|
||||||
if (!slot.isValid || slot.generation != generation)
|
|
||||||
{
|
{
|
||||||
exist = false;
|
exist = false;
|
||||||
return ref Unsafe.NullRef<T>();
|
return ref Unsafe.NullRef<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
exist = true;
|
exist = true;
|
||||||
return ref slot.value!;
|
return ref _data[slotIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateElement(int slotIndex, int generation, T newValue)
|
public bool UpdateElement(int slotIndex, int generation, T newValue)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
|
if (!Contains(slotIndex, generation))
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref var slot = ref _data[slotIndex];
|
_data[slotIndex] = newValue;
|
||||||
if (!slot.isValid || slot.generation != generation)
|
return true;
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Slot {slotIndex} is not occupied or generation mismatch.");
|
|
||||||
}
|
|
||||||
|
|
||||||
slot.value = newValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
<Authors>Misaki</Authors>
|
<Authors>Misaki</Authors>
|
||||||
<AssemblyVersion>1.0.1</AssemblyVersion>
|
<AssemblyVersion>1.0.2</AssemblyVersion>
|
||||||
<Version>$(AssemblyVersion)</Version>
|
<Version>$(AssemblyVersion)</Version>
|
||||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
|
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>
|
||||||
|
|||||||
Reference in New Issue
Block a user