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:
2025-12-12 16:10:49 +09:00
parent a0a4b347dd
commit fb31fd8ca8
16 changed files with 509 additions and 203 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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
{ {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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;
} }
@@ -231,85 +233,48 @@ 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)
{
if (!Contains(slotIndex, generation))
{ {
value = default; value = default;
if (slotIndex < 0 || slotIndex >= Volatile.Read(ref _capacity))
{
return false; return false;
} }
ref var slot = ref _data[slotIndex]; value = _data[slotIndex].value!;
// 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 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 _))
{
}
} }
} }

View File

@@ -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,31 +97,39 @@ 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 Contains(int slotIndex, int generation)
{
if (slotIndex <= 0 || slotIndex >= Volatile.Read(ref _capacity))
{
return false;
}
if (_isOccupiedBits[slotIndex] && _generations[slotIndex] == generation)
{
return true;
}
return false;
}
public bool Remove(int slotIndex, int generation) public bool Remove(int slotIndex, int generation)
{ {
if (slotIndex < 0 || slotIndex >= _capacity) if (!Contains(slotIndex, generation))
{ {
return false; return false;
} }
ref var slot = ref _data[slotIndex]; _generations[slotIndex]++;
if (slot.generation != generation) _isOccupiedBits[slotIndex] = false;
{
return false;
}
slot.generation++;
slot.isValid = false;
_freeSlots.Enqueue(slotIndex); _freeSlots.Enqueue(slotIndex);
_count--; _count--;
@@ -127,92 +137,49 @@ 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 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()

View File

@@ -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>