Refactor unsafe collections and improve memory handling
Some checks failed
Publish NuGet Packages / publish (pull_request) Has been cancelled

Refactored enumerators across multiple unsafe collections to use
`ref` returns for `Current`, improving performance and reducing
memory usage. Enhanced memory management with `AllocationOption`
support and optimized resizing logic for collections like
`UnsafeBitSet`, `UnsafeSlotMap`, and `UnsafeSparseSet`.

Updated `publish-nuget.yaml` to support manual workflow dispatch
and trigger on `push`/`pull_request` events. Incremented project
version to `1.1.2` and ensured NuGet package generation on build.
This commit is contained in:
2025-11-11 21:20:33 +09:00
parent bc8b2c0aaa
commit bf4dd5670e
12 changed files with 128 additions and 240 deletions

View File

@@ -40,6 +40,7 @@ public unsafe interface IUnsafeCollection<T> : IUnsafeCollection, IEnumerable<T>
/// </summary>
/// <remarks>This is to adjust the element count of the collection, not the size of the underlying buffer in memory.</remarks>
/// <param name="newSize">Specifies the new size to which the collection should be adjusted.</param>
/// <param name="option">Specifies allocation options that may affect how memory is managed during the resize operation.</param>
void Resize(int newSize, AllocationOption option);
}

View File

@@ -19,25 +19,21 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
private readonly UnsafeArray<T>* _collection;
private int _index;
public readonly ref T Current => ref _collection->_buffer[_index];
readonly T IEnumerator<T>.Current => Current;
readonly object IEnumerator.Current => Current;
public Enumerator(UnsafeArray<T>* collection)
{
_collection = collection;
_index = -1;
Current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->Count)
{
Current = UnsafeUtility.ReadArrayElement<T>(_collection->_buffer, _index);
return true;
}
Current = default;
return false;
return _index < _collection->_count;
}
public void Reset()
@@ -45,19 +41,6 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
_index = -1;
}
// Let NativeArray indexer check for out of range.
public T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get; private set;
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Current;
}
public void Dispose()
{
}

View File

@@ -1,4 +1,5 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -6,7 +7,7 @@ using System.Text;
namespace Misaki.HighPerformance.LowLevel.Collections;
public struct UnsafeBitSet : IDisposable
public unsafe struct UnsafeBitSet : IDisposable
{
private const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31
private const int _INDEX_SIZE = 5; // log_2(BitSize + 1)
@@ -53,23 +54,31 @@ public struct UnsafeBitSet : IDisposable
/// </summary>
public UnsafeBitSet()
{
_bits = new UnsafeArray<uint>(s_padding, Allocator.Persistent, AllocationOption.Clear);
}
/// <summary>
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
/// </summary>
public UnsafeBitSet(int minimalLength, Allocator allocator, AllocationOption option = AllocationOption.Clear)
{
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
var length = RoundToPadding(uints);
_bits = new UnsafeArray<uint>(length, allocator, option);
_bits = new UnsafeArray<uint>(s_padding, Allocator.Persistent, AllocationOption.None);
}
/// <summary>
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
/// </summary>
public UnsafeBitSet(Span<uint> bits, Allocator allocator, AllocationOption option = AllocationOption.Clear)
public UnsafeBitSet(int minimalLength, ref AllocationHandle handle, AllocationOption option = AllocationOption.None)
{
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
var length = RoundToPadding(uints);
_bits = new UnsafeArray<uint>(length, ref handle, option);
}
/// <summary>
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
/// </summary>
public UnsafeBitSet(int minimalLength, Allocator allocator, AllocationOption option = AllocationOption.None)
: this(minimalLength, ref AllocationManager.GetAllocationHandle(allocator), option)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
/// </summary>
public UnsafeBitSet(Span<uint> bits, Allocator allocator, AllocationOption option = AllocationOption.None)
{
_bits = new UnsafeArray<uint>(bits.Length, allocator, option);
_bits.CopyFrom(bits);
@@ -169,11 +178,7 @@ public struct UnsafeBitSet : IDisposable
/// </summary>
public void SetAll()
{
var count = _bits.Count;
for (var i = 0; i < count; i++)
{
_bits[i] = 0xffffffff;
}
_bits.AsSpan().Fill(0xffffffff);
_highestBit = _bits.Count * (_BIT_SIZE + 1) - 1;
_max = _highestBit / (_BIT_SIZE + 1) + 1;
@@ -192,7 +197,7 @@ public struct UnsafeBitSet : IDisposable
/// <summary>
/// Finds the next set bit at or after `startIndex`, or -1 if none.
/// </summary>
public int NextSetBit(int startIndex)
public readonly int NextSetBit(int startIndex)
{
var wordIndex = startIndex >> _BIT_SIZE;
if (wordIndex >= _bits.Count)
@@ -221,6 +226,13 @@ public struct UnsafeBitSet : IDisposable
}
}
public void Resize(int minimalLength, AllocationOption option = AllocationOption.None)
{
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
var length = RoundToPadding(uints);
_bits.Resize(length, option);
}
/// <summary>
/// Checks if all bits from this instance match those of the other instance.
/// </summary>

View File

@@ -14,41 +14,19 @@ public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeCollection<KeyValuePai
{
internal HashMapHelper<TKey>.Enumerator _enumerator;
public KeyValuePair<TKey, TValue> Current => _enumerator.GetCurrent<TValue>();
object IEnumerator.Current => Current;
public Enumerator(HashMapHelper<TKey>* data)
{
_enumerator = new HashMapHelper<TKey>.Enumerator(data);
}
/// <summary>
/// The current key-value pair.
/// </summary>
/// <value>The current key-value pair.</value>
public KeyValuePair<TKey, TValue> Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _enumerator.GetCurrent<TValue>();
}
/// <summary>
/// Gets the element at the current position of the enumerator in the container.
/// </summary>
object IEnumerator.Current => Current;
/// <summary>
/// Advances the enumerator to the next key-value pair.
/// </summary>
/// <returns>True if <see cref="Current"/> is valid to read after the call.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => _enumerator.MoveNext();
/// <summary>
/// Resets the enumerator to its initial state.
/// </summary>
public void Reset() => _enumerator.Reset();
/// <summary>
/// Does nothing.
/// </summary>
public void Dispose()
{
}

View File

@@ -19,19 +19,14 @@ public unsafe struct UnsafeHashSet<T> : IUnsafeCollection<T>, IEnumerable<T>
{
internal HashMapHelper<T>.Enumerator _enumerator;
public readonly T Current => _enumerator.buffer->_keys[_enumerator.index];
readonly object IEnumerator.Current => Current;
public Enumerator(HashMapHelper<T>* hashMap)
{
_enumerator = new HashMapHelper<T>.Enumerator(hashMap);
}
public T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _enumerator.buffer->_keys[_enumerator.index];
}
object IEnumerator.Current => Current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => _enumerator.MoveNext();

View File

@@ -18,27 +18,22 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
{
private readonly UnsafeList<T>* _collection;
private int _index;
private T _value;
public readonly ref T Current => ref _collection->_array[_index];
readonly T IEnumerator<T>.Current => Current;
readonly object IEnumerator.Current => Current;
public Enumerator(UnsafeList<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = UnsafeUtility.ReadArrayElement<T>(_collection->_array.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
return _index < _collection->_count;
}
public void Reset()
@@ -46,25 +41,6 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_index = -1;
}
// Let NativeArray indexer check for out of range.
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _value;
}
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return Current;
}
}
public readonly void Dispose()
{
}

View File

@@ -17,46 +17,29 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
public struct Enumerator : IEnumerator<T>
{
private readonly UnsafeQueue<T>* _collection;
private int _index;
private T _value;
private int _currentIndex;
// We assume _currentIndex will always be in range when accessed.
public readonly ref T Current => ref _collection->_array[(_collection->_offset + _currentIndex) % _collection->Capacity];
readonly T IEnumerator<T>.Current => Current;
readonly object IEnumerator.Current => Current;
public Enumerator(UnsafeQueue<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
_currentIndex = -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = UnsafeUtility.ReadArrayElement<T>(_collection->_array.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
_currentIndex++;
return _currentIndex < _collection->_count;
}
public void Reset()
{
_index = -1;
}
// Let NativeArray indexer check for out of range.
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value;
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Current;
_currentIndex = -1;
}
public readonly void Dispose()
@@ -128,7 +111,7 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
{
if (_count >= Capacity)
{
Resize(Capacity + (int)(Capacity * 0.5f));
Resize((int)(Capacity * 1.5f));
}
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), (_offset + _count) % Capacity, value);

View File

@@ -20,44 +20,36 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
private readonly UnsafeSlotMap<T>* _collection;
private int _currentIndex;
public readonly ref T Current => ref _collection->_data[_currentIndex];
readonly T IEnumerator<T>.Current => Current;
readonly object? IEnumerator.Current => Current;
public Enumerator(UnsafeSlotMap<T>* collection)
{
_collection = collection;
_currentIndex = -1;
}
public readonly T Current => _collection->_data[_currentIndex].value;
readonly object? IEnumerator.Current => Current;
public bool MoveNext()
{
while (++_currentIndex < _collection->_capacity)
{
if (_collection->_data[_currentIndex].isValid)
{
return true;
}
}
return false;
_currentIndex = _collection->_validBits.NextSetBit(_currentIndex + 1);
return _currentIndex != -1;
}
public void Reset() => _currentIndex = -1;
public void Reset()
{
_currentIndex = -1;
}
public void Dispose()
{
}
}
private struct SlotData
{
public T value;
public int generation;
public bool isValid;
}
private UnsafeArray<SlotData> _data;
private UnsafeArray<T> _data;
private UnsafeArray<int> _generations;
private UnsafeQueue<int> _freeSlots;
private UnsafeBitSet _validBits;
private int _count;
private int _capacity;
@@ -94,8 +86,12 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
}
_data = new UnsafeArray<SlotData>(capacity, ref handle, allocationOption);
_data = new UnsafeArray<T>(capacity, ref handle, allocationOption);
_generations = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_freeSlots = new UnsafeQueue<int>(capacity, ref handle, allocationOption);
_validBits = new UnsafeBitSet(GetBitSetCapacity(capacity), ref handle, allocationOption);
_validBits.ClearAll();
_count = 0;
_capacity = capacity;
@@ -113,6 +109,12 @@ 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>
@@ -123,27 +125,26 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
{
if (_count >= _capacity)
{
Resize(_capacity * 2);
Resize((int)(_capacity * 1.5f));
}
int slotIndex;
int index;
if (_freeSlots.Count == 0)
{
slotIndex = _count;
index = _count;
}
else
{
slotIndex = _freeSlots.Dequeue();
index = _freeSlots.Dequeue();
}
ref var slot = ref _data[slotIndex];
slot.value = item;
slot.isValid = true;
generation = slot.generation;
_data[index] = item;
_validBits.SetBit(index);
_count++;
return slotIndex;
generation = _generations[index];
return index;
}
/// <summary>
@@ -160,16 +161,16 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
return false;
}
ref var slot = ref _data[slotIndex];
if (slot.generation != generation)
ref var gen = ref _generations[slotIndex];
if (gen != generation)
{
return false;
}
slot.generation++;
slot.isValid = false;
gen++;
_validBits.ClearBit(slotIndex);
_freeSlots.Enqueue(slotIndex);
_count--;
return true;
@@ -188,9 +189,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
return false;
}
ref var slot = ref _data[slotIndex];
if (slot.isValid && slot.generation == generation)
if (_validBits.IsSet(slotIndex) && _generations[slotIndex] == generation)
{
return true;
}
@@ -215,14 +214,13 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
return false;
}
ref var slot = ref _data[slotIndex];
if (slot.generation != generation)
if (_generations[slotIndex] != generation)
{
value = default;
return false;
}
value = slot.value;
value = _data[slotIndex];
return true;
}
@@ -241,13 +239,12 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
}
ref var slot = ref _data[slotIndex];
if (!slot.isValid || slot.generation != generation)
if (!_validBits.IsSet(slotIndex)|| _generations[slotIndex] != generation)
{
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
}
return slot.value;
return _data[slotIndex];
}
/// <summary>
@@ -268,28 +265,31 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
ref var slot = ref _data[slotIndex];
if (!slot.isValid || slot.generation != generation)
if (!_validBits.IsSet(slotIndex) || _generations[slotIndex] != generation)
{
exist = false;
return ref Unsafe.NullRef<T>();
}
exist = true;
return ref slot.value;
return ref _data[slotIndex];
}
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
{
_data.Resize(newSize, option);
_generations.Resize(newSize, option);
_freeSlots.Resize(newSize, option);
_validBits.Resize(GetBitSetCapacity(newSize), option);
_capacity = newSize;
}
public void Clear()
{
_data.Clear();
_generations.Clear();
_freeSlots.Clear();
_validBits.ClearAll();
_count = 0;
}

View File

@@ -20,45 +20,27 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
public struct Enumerator : IEnumerator<T>
{
private readonly UnsafeSparseSet<T>* _collection;
private int _index;
private T _value;
private int _currentIndex;
public readonly ref T Current => ref _collection->_dense[_currentIndex];
readonly T IEnumerator<T>.Current => Current;
readonly object IEnumerator.Current => Current;
public Enumerator(UnsafeSparseSet<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
_currentIndex = -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = _collection->_dense[_index];
return true;
}
_value = default;
return false;
_currentIndex++;
return _currentIndex < _collection->_count;
}
public void Reset()
{
_index = -1;
}
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value;
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Current;
_currentIndex = -1;
}
public readonly unsafe void Dispose()
@@ -158,10 +140,9 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
}
// Resize dense arrays if necessary
if (_count >= _dense.Count)
if (_count >= _capacity)
{
var newCapacity = _dense.Count + (int)Math.Max(1, _dense.Count * 0.5f);
Resize(newCapacity);
Resize((int)(_capacity * 1.5f));
}
// Add the value to the dense array and update mappings
@@ -337,8 +318,6 @@ public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be greater than zero.");
}
var oldSize = _capacity;
_dense.Resize(newSize, option);
_generations.Resize(newSize, option | AllocationOption.Clear);
_reverse.Resize(newSize, option);

View File

@@ -19,27 +19,22 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
{
private readonly UnsafeStack<T>* _collection;
private int _index;
private T _value;
public readonly ref T Current => ref _collection->_array[_index];
readonly T IEnumerator<T>.Current => Current;
readonly object IEnumerator.Current => Current;
public Enumerator(UnsafeStack<T>* collection)
{
_collection = collection;
_index = collection->Count;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index--;
if (_index >= 0)
{
_value = UnsafeUtility.ReadArrayElement<T>(_collection->_array.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
return _index >= 0;
}
public void Reset()
@@ -47,18 +42,6 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
_index = _collection->Count;
}
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value;
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Current;
}
public readonly void Dispose()
{
}
@@ -68,6 +51,7 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
private int _count;
public readonly int Count => _count;
public readonly int Capacity => _array.Count;
public readonly bool IsCreated => _array.IsCreated;
public Enumerator GetEnumerator() => new((UnsafeStack<T>*)UnsafeUtility.AddressOf(ref this));
@@ -111,9 +95,9 @@ public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
/// <param name="value">The element to add to the stack.</param>
public void Push(T value)
{
if (_count >= _array.Count)
if (_count >= Capacity)
{
Resize(_array.Count + (int)(_array.Count * 0.5f));
Resize((int)(Capacity * 1.5f));
}
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);