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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user