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

@@ -7,10 +7,38 @@ using System.Runtime.CompilerServices;
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>
/// A structure for managing an array of unmanaged types with unsafe memory operations.
/// </summary>
/// <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>
where T : unmanaged
{

View File

@@ -1,5 +1,6 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -7,6 +8,31 @@ using System.Text;
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 ref struct Iterator
@@ -65,6 +91,9 @@ public unsafe struct UnsafeBitSet : IDisposable, IEquatable<UnsafeBitSet>
/// <summary>
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
/// </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)
{
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.Utilities;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
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>
/// A collection that allows for unsafe operations on a list of unmanaged types.
/// </summary>
/// <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>
where T : unmanaged
{
@@ -210,7 +236,7 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
{
if (_count >= Capacity)
{
Resize(Capacity + (int)(Capacity * 0.5f));
Resize(Math.Max(1, Capacity * 2));
}
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);

View File

@@ -110,7 +110,7 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
{
if (_count >= Capacity)
{
Resize((int)(Capacity * 1.5f));
Resize(Math.Max(1, Capacity * 2));
}
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.Utilities;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
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>
/// Provides an unsafe, high-performance slot map for storing and managing unmanaged values, supporting fast insertion,
/// removal, and lookup by slot index and generation.
/// </summary>
/// <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>
where T : unmanaged
{
@@ -53,7 +81,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
private int _count;
private int _capacity;
public readonly int Count => _count;
public readonly int Count => _count - 1;
public readonly int Capacity => _capacity;
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);
_generations = new UnsafeArray<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))
{
@@ -98,6 +126,9 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
_count = 0;
_capacity = capacity;
// Add a default item for invalid slot
Add(default, out _);
}
/// <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>
/// Adds the specified item to the collection and returns the index of the slot where it was stored.
/// </summary>
@@ -128,7 +153,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
{
if (_count >= _capacity)
{
Resize((int)(_capacity * 1.5f));
Resize(Math.Max(1, _capacity * 2));
}
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="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>
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;
}
@@ -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>
public readonly bool TryGetElementAt(int slotIndex, int generation, out T value)
{
if (slotIndex < 0 || slotIndex >= _capacity)
{
value = default;
return false;
}
if (_generations[slotIndex] != generation)
if (!Contains(slotIndex, generation))
{
value = default;
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>
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.");
}
if (!_validBits.IsSet(slotIndex) || _generations[slotIndex] != generation)
{
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
throw new InvalidOperationException("The specified slot is not occupied or the generation does not match.");
}
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>
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
{
if (slotIndex < 0 || slotIndex >= _capacity)
{
exist = false;
return ref Unsafe.NullRef<T>();
}
ref var slot = ref _data[slotIndex];
if (!_validBits.IsSet(slotIndex) || _generations[slotIndex] != generation)
if (!Contains(slotIndex, generation))
{
exist = false;
return ref Unsafe.NullRef<T>();
@@ -283,7 +290,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
_data.Resize(newSize, option);
_generations.Resize(newSize, option);
_freeSlots.Resize(newSize, option);
_validBits.Resize(GetBitSetCapacity(newSize), option);
_validBits.Resize(newSize, option);
_capacity = newSize;
}

View File

@@ -2,10 +2,37 @@ using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
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>
/// 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,
@@ -13,6 +40,7 @@ namespace Misaki.HighPerformance.LowLevel.Collections;
/// Sparse indices work like entity IDs and are automatically generated.
/// </summary>
/// <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>
where T : unmanaged
{
@@ -57,7 +85,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
private int _nextId; // Next available sparse index
private int _capacity;
public readonly int Count => _count;
public readonly int Count => _count - 1;
public readonly int Capacity => _capacity;
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
@@ -103,7 +131,10 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
_nextId = 0;
_capacity = capacity;
Clear();
_sparse.AsSpan().Fill(-1);
_generations.Clear();
Add(default, out _); // Make index 0 invalid
}
/// <summary>
@@ -147,7 +178,7 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
// Resize dense arrays if necessary
if (_count >= _capacity)
{
Resize((int)(_capacity * 1.5f));
Resize(Math.Max(1, _capacity * 2));
}
// Add the value to the dense array and update mappings
@@ -209,7 +240,8 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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;
}

View File

@@ -2,15 +2,43 @@ using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
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>
/// Provides a high-performance, unsafe stack data structure for unmanaged types, supporting manual memory management
/// and allocation control.
/// </summary>
/// <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>
where T : unmanaged
{
@@ -96,7 +124,7 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
{
if (_count >= Capacity)
{
Resize((int)(Capacity * 1.5f));
Resize(Math.Max(1, Capacity * 2));
}
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);

View File

@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Authors>Misaki</Authors>
<AssemblyVersion>1.3.0</AssemblyVersion>
<AssemblyVersion>1.3.1</AssemblyVersion>
<Version>$(AssemblyVersion)</Version>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageProjectUrl>https://git.personalnas.com/Misaki/Misaki.HighPerformance.git</PackageProjectUrl>