Upgrade to .NET 10 and refactor core components

Upgraded target framework to .NET 10 across all projects to leverage new features and improve performance.

Refactored `JobScheduler` to fix method naming inconsistencies and ensure proper resource disposal. Enhanced `AllocationManager` with safer memory operations and better performance handling. Simplified `ReadOnlyUnsafeCollection` enumerator logic for efficiency.

Overhauled `UnsafeBitSet` with new properties, improved bitwise operations, and optimized memory management. Updated `UnsafeSlotMap` and `ConcurrentSlotMap` for better validation and naming consistency.

Revised `MemoryLeakException` to use `ReadOnlySpan` for improved performance. Simplified `MathematicsBenchmark` logic and integrated `BenchmarkDotNet` for testing.

Added AOT compatibility settings for `Debug` and `Release` configurations. Introduced unit tests for `UnsafeBitSet` to validate functionality. Cleaned up unused code, improved readability, and ensured consistent naming conventions.

Updated project references and metadata for consistency. Enabled inline methods for `NET10_0_OR_GREATER` in `VectorGenerator`.
This commit is contained in:
2025-11-14 11:14:09 +09:00
parent bf4dd5670e
commit 24a7d49ae2
18 changed files with 223 additions and 260 deletions

View File

@@ -8,7 +8,7 @@ namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// Holds information about a memory allocation.
/// </summary>
public readonly unsafe struct AllocationInfo
public readonly struct AllocationInfo
{
/// <summary>
/// Get the size of the allocation in bytes.
@@ -69,7 +69,7 @@ public static unsafe class AllocationManager
{
var selfPtr = (ArenaAllocator*)instance;
var newPtr = selfPtr->_arena.Allocate(newSize, alignment, allocationOption);
MemCpy(newPtr, ptr, newSize);
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
if (allocationOption.HasFlag(AllocationOption.Clear))
{
@@ -149,7 +149,7 @@ public static unsafe class AllocationManager
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{
var newPtr = s_stack.Allocate(newSize, alignment, AllocationOption.None);
MemCpy(newPtr, ptr, newSize);
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
if (allocationOption.HasFlag(AllocationOption.Clear))
{
@@ -174,7 +174,7 @@ public static unsafe class AllocationManager
}
}
private const uint _DEFAULT_MEMORY_POOL_SIZE = 512 * 1024;
private const uint _DEFAULT_MEMORY_POOL_SIZE = 512 * 1024; // 512 KB
private static readonly ArenaAllocator* s_pArenaAllocator;
private static readonly HeapAllocator* s_pHeapAllocator;
@@ -438,12 +438,10 @@ public static unsafe class AllocationManager
}
var newPtr = AlignedRealloc(ptr, newSize, alignment);
if (allocationOption.HasFlag(AllocationOption.Clear))
if (allocationOption.HasFlag(AllocationOption.Clear)
&& newSize > oldSize)
{
if (newSize > oldSize)
{
MemClear((byte*)newPtr + oldSize, newSize - oldSize);
}
MemClear((byte*)newPtr + oldSize, newSize - oldSize);
}
return newPtr;
@@ -535,7 +533,7 @@ public static unsafe class AllocationManager
if (unfreeBytes > 0u)
{
throw new MemoryLeakException(snapshot);
throw new MemoryLeakException(CollectionsMarshal.AsSpan(snapshot));
}
}
else if (s_activeHeapAllocations != 0)

View File

@@ -1,4 +1,4 @@
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections;
using System.Runtime.CompilerServices;
@@ -20,36 +20,21 @@ public readonly unsafe struct ReadOnlyUnsafeCollection<T> : IEnumerable<T>
{
private readonly ReadOnlyUnsafeCollection<T> _collection;
private int _index;
private T _value;
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value;
}
public readonly T Current => _collection[_index];
readonly object IEnumerator.Current => Current;
public Enumerator(ref readonly ReadOnlyUnsafeCollection<T> array)
{
_collection = array;
_index = -1;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection.Count)
{
_value = UnsafeUtility.ReadArrayElement<T>(_collection._buffer, _index);
return true;
}
_value = default;
return false;
return _index < _collection.Count;
}
public void Reset()
@@ -79,7 +64,8 @@ public readonly unsafe struct ReadOnlyUnsafeCollection<T> : IEnumerable<T>
get => UnsafeUtility.ReadArrayElement<T>(_buffer, index);
}
public IEnumerator<T> GetEnumerator() => new Enumerator(in this);
public Enumerator GetEnumerator() => new Enumerator(in this);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public ReadOnlyUnsafeCollection(T* buffer, int count)

View File

@@ -14,47 +14,38 @@ public unsafe struct UnsafeBitSet : IDisposable
private const int _MASK = (1 << 5) - 1; // 0x1F, the mask to get the bit index inside a uint
private static readonly int s_padding = Vector<uint>.Count; // The padding used for vectorization, the amount of uints required for being vectorized basically
/// <summary>
/// Determines the required length of an <see cref="UnsafeBitSet"/> to hold the passed id or bit.
/// </summary>
/// <param name="id">The id or bit.</param>
/// <returns>A size of required <see cref="uint"/>s for the bitset.</returns>
public static int RequiredLength(int id)
{
return (id >> 5) + int.Sign(id & _BIT_SIZE);
}
/// <summary>
/// Rounds the given length to the next padding size.
/// </summary>
/// <param name="length">The length to round.</param>
/// <returns>The rounded length.</returns>
public static int RoundToPadding(int length)
{
return (length + s_padding - 1) / s_padding * s_padding;
}
/// <summary>
/// The bits from the bitset.
/// </summary>
private UnsafeArray<uint> _bits;
private int _highestBit;
private int _max;
/// <summary>
/// The highest uint index in use inside the <see cref="_bits"/>-array.
/// </summary>
public readonly int HighestIndex => _max;
/// <summary>
/// The highest bit set.
/// </summary>
private int _highestBit;
public readonly int HighestBit => _highestBit;
/// <summary>
/// The maximum <see cref="_bits"/>-index current in use.
/// Returns the count of the bitset, how many uints it consists of.
/// </summary>
private int _max;
public readonly int Count => _bits.Count;
/// <summary>
/// Gets the total number of bits represented by the current instance.
/// </summary>
public readonly int BitCount => _bits.Count << _INDEX_SIZE;
public readonly bool IsCreated => _bits.IsCreated;
/// <summary>
/// Initializes a new instance of the <see cref="UnsafeBitSet" /> class.
/// </summary>
public UnsafeBitSet()
{
_bits = new UnsafeArray<uint>(s_padding, Allocator.Persistent, AllocationOption.None);
_bits = new UnsafeArray<uint>(0, Allocator.Invalid, AllocationOption.None);
}
/// <summary>
@@ -94,33 +85,19 @@ public unsafe struct UnsafeBitSet : IDisposable
}
}
/// <summary>
/// The highest uint index in use inside the <see cref="_bits"/>-array.
/// </summary>
public int HighestIndex
private static int RoundToPadding(int length)
{
get => _max;
return (length + s_padding - 1) / s_padding * s_padding;
}
/// <summary>
/// The highest bit set.
/// Determines the required length of an <see cref="UnsafeBitSet"/> to hold the passed id or bit.
/// </summary>
public int HighestBit
/// <param name="id">The id or bit.</param>
/// <returns>A size of required <see cref="uint"/>s for the bitset.</returns>
public static int RequiredLength(int id)
{
get => _highestBit;
}
/// <summary>
/// Returns the length of the bitset, how many ints it consists of.
/// </summary>
public int Length
{
get => _bits.Count;
}
public bool IsCreated
{
get => _bits.IsCreated;
return (id >> _INDEX_SIZE) + int.Sign(id & _BIT_SIZE);
}
/// <summary>
@@ -128,7 +105,7 @@ public unsafe struct UnsafeBitSet : IDisposable
/// </summary>
/// <param name="index">The index.</param>
/// <returns>True if it is, otherwise false</returns>
public bool IsSet(int index)
public readonly bool IsSet(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Count)
@@ -149,9 +126,10 @@ public unsafe struct UnsafeBitSet : IDisposable
var b = index >> _INDEX_SIZE;
if (b >= _bits.Count)
{
_bits.Resize(RoundToPadding(b));
_bits.Resize(index);
}
// Track highest set bit
_highestBit = Math.Max(_highestBit, index);
_max = _highestBit / (_BIT_SIZE + 1) + 1;
@@ -228,9 +206,12 @@ public unsafe struct UnsafeBitSet : IDisposable
public void Resize(int minimalLength, AllocationOption option = AllocationOption.None)
{
var oldSize = _bits.Count;
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
var length = RoundToPadding(uints);
_bits.Resize(length, option);
_bits.AsSpan()[oldSize..].Clear();
}
/// <summary>
@@ -238,10 +219,9 @@ public unsafe struct UnsafeBitSet : IDisposable
/// </summary>
/// <param name="other">The other <see cref="UnsafeBitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
[SkipLocalsInit]
public bool All(UnsafeBitSet other)
public readonly bool All(UnsafeBitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
@@ -300,9 +280,9 @@ public unsafe struct UnsafeBitSet : IDisposable
/// </summary>
/// <param name="other">The other <see cref="UnsafeBitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
public bool Any(UnsafeBitSet other)
public readonly bool Any(UnsafeBitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
@@ -361,9 +341,9 @@ public unsafe struct UnsafeBitSet : IDisposable
/// </summary>
/// <param name="other">The other <see cref="UnsafeBitSet"/>.</param>
/// <returns>True if none match, false if not.</returns>
public bool None(UnsafeBitSet other)
public readonly bool None(UnsafeBitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
@@ -403,9 +383,9 @@ public unsafe struct UnsafeBitSet : IDisposable
/// </summary>
/// <param name="other">The other <see cref="UnsafeBitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
public bool Exclusive(UnsafeBitSet other)
public readonly bool Exclusive(UnsafeBitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
var min = Math.Min(Math.Min(Count, other.Count), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
@@ -460,152 +440,85 @@ public unsafe struct UnsafeBitSet : IDisposable
return true;
}
public unsafe void AndOperation(UnsafeBitSet other)
public void And(UnsafeBitSet other)
{
var min = Math.Min(Length, other.Length);
var temp = stackalloc uint[min];
if (!Vector.IsHardwareAccelerated || min < s_padding)
if (Count != other.Count)
{
for (var i = 0; i < min; i++)
throw new ArgumentException("Bitsets must be of the same length for AND operation.");
}
if (!Vector.IsHardwareAccelerated || Count < s_padding)
{
for (var i = 0; i < Count; i++)
{
temp[i] = _bits[i] & other._bits[i];
_bits[i] &= other._bits[i];
}
}
else
{
for (var i = 0; i < min; i += s_padding)
for (var i = 0; i < Count; i += s_padding)
{
var vectorLeft = new Vector<uint>(_bits.AsSpan()[i..]);
var vectorRight = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vectorLeft, vectorRight);
resultVector.CopyTo(new Span<uint>(temp + i, s_padding));
resultVector.CopyTo(_bits.AsSpan(i, s_padding));
}
}
_bits.CopyFrom(new Span<uint>(temp, min));
}
public unsafe void OrOperation(UnsafeBitSet other)
public void Or(UnsafeBitSet other)
{
var min = Math.Min(Length, other.Length);
var temp = stackalloc uint[min];
if (!Vector.IsHardwareAccelerated || min < s_padding)
if (Count != other.Count)
{
for (var i = 0; i < min; i++)
throw new ArgumentException("Bitsets must be of the same length for AND operation.");
}
if (!Vector.IsHardwareAccelerated || Count < s_padding)
{
for (var i = 0; i < Count; i++)
{
temp[i] = _bits[i] | other._bits[i];
_bits[i] |= other._bits[i];
}
}
else
{
for (var i = 0; i < min; i += s_padding)
for (var i = 0; i < Count; i += s_padding)
{
var vectorLeft = new Vector<uint>(_bits.AsSpan()[i..]);
var vectorRight = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseOr(vectorLeft, vectorRight);
resultVector.CopyTo(new Span<uint>(temp + i, s_padding));
resultVector.CopyTo(_bits.AsSpan(i, s_padding));
}
}
_bits.CopyFrom(new Span<uint>(temp, min));
}
public unsafe void XorOperation(UnsafeBitSet other)
public void Xor(UnsafeBitSet other)
{
var min = Math.Min(Length, other.Length);
var temp = stackalloc uint[min];
if (!Vector.IsHardwareAccelerated || min < s_padding)
if (Count != other.Count)
{
for (var i = 0; i < min; i++)
throw new ArgumentException("Bitsets must be of the same length for AND operation.");
}
if (!Vector.IsHardwareAccelerated || Count < s_padding)
{
for (var i = 0; i < Count; i++)
{
temp[i] = _bits[i] ^ other._bits[i];
_bits[i] ^= other._bits[i];
}
}
else
{
for (var i = 0; i < min; i += s_padding)
for (var i = 0; i < Count; i += s_padding)
{
var vectorLeft = new Vector<uint>(_bits.AsSpan()[i..]);
var vectorRight = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.Xor(vectorLeft, vectorRight);
resultVector.CopyTo(new Span<uint>(temp + i, s_padding));
}
}
_bits.CopyFrom(new Span<uint>(temp, min));
}
public static UnsafeBitSet operator &(UnsafeBitSet left, UnsafeBitSet right)
{
var min = Math.Min(left.Length, right.Length);
var result = new UnsafeBitSet(min, Allocator.Persistent);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
for (var i = 0; i < min; i++)
{
result._bits[i] = left._bits[i] & right._bits[i];
resultVector.CopyTo(_bits.AsSpan(i, s_padding));
}
}
else
{
for (var i = 0; i < min; i += s_padding)
{
var vectorLeft = new Vector<uint>(left._bits.AsSpan()[i..]);
var vectorRight = new Vector<uint>(right._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vectorLeft, vectorRight);
resultVector.CopyTo(result._bits.AsSpan(i, s_padding));
}
}
return result;
}
public static UnsafeBitSet operator |(UnsafeBitSet left, UnsafeBitSet right)
{
var min = Math.Min(left.Length, right.Length);
var result = new UnsafeBitSet(min, Allocator.Persistent);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
for (var i = 0; i < min; i++)
{
result._bits[i] = left._bits[i] | right._bits[i];
}
}
else
{
for (var i = 0; i < min; i += s_padding)
{
var vectorLeft = new Vector<uint>(left._bits.AsSpan()[i..]);
var vectorRight = new Vector<uint>(right._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseOr(vectorLeft, vectorRight);
resultVector.CopyTo(result._bits.AsSpan(i, s_padding));
}
}
return result;
}
public static UnsafeBitSet operator ~(UnsafeBitSet bitSet)
{
if (!Vector.IsHardwareAccelerated || bitSet.Length < s_padding)
{
for (var i = 0; i < bitSet.Length; i++)
{
bitSet._bits[i] = ~bitSet._bits[i];
}
}
else
{
for (var i = 0; i < bitSet.Length; i += s_padding)
{
var vector = new Vector<uint>(bitSet._bits.AsSpan()[i..]);
var resultVector = ~vector;
resultVector.CopyTo(bitSet._bits.AsSpan(i, s_padding));
}
}
return bitSet;
}
/// <summary>
@@ -624,10 +537,10 @@ public unsafe struct UnsafeBitSet : IDisposable
/// <param name="span">The <see cref="Span{T}"/> to copy into.</param>
/// <param name="zero">If true, it will zero the unused space from the <see cref="span"/>.</param>
/// <returns>The <see cref="Span{T}"/>.</returns>
public Span<uint> AsSpan(Span<uint> span, bool zero = true)
public readonly Span<uint> AsSpan(Span<uint> span, bool zero = true)
{
// Copy everything thats possible from one to another
var length = Math.Min(Length, span.Length);
var length = Math.Min(Count, span.Length);
for (var index = 0; index < length; index++)
{
span[index] = _bits[index];
@@ -639,10 +552,10 @@ public unsafe struct UnsafeBitSet : IDisposable
span[index] = 0;
}
return span[..Length];
return span[..Count];
}
public override string ToString()
public readonly override string ToString()
{
// Convert uint to binary form for pretty printing
var binaryBuilder = new StringBuilder();
@@ -652,7 +565,7 @@ public unsafe struct UnsafeBitSet : IDisposable
}
binaryBuilder.Length--;
return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Length)}: {Length}";
return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Count)}: {Count}";
}
public void Dispose()

View File

@@ -57,7 +57,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
public readonly int Count => _count;
public readonly int Capacity => _capacity;
public readonly bool IsCreated => _data.IsCreated && _freeSlots.IsCreated;
public readonly bool IsCreated => _data.IsCreated && _generations.IsCreated && _freeSlots.IsCreated && _validBits.IsCreated;
public Enumerator GetEnumerator() => new((UnsafeSlotMap<T>*)UnsafeUtility.AddressOf(ref this));
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
@@ -239,7 +239,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
}
if (!_validBits.IsSet(slotIndex)|| _generations[slotIndex] != generation)
if (!_validBits.IsSet(slotIndex) || _generations[slotIndex] != generation)
{
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
}
@@ -294,7 +294,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
_count = 0;
}
public readonly unsafe void* GetUnsafePtr()
public readonly void* GetUnsafePtr()
{
return _data.GetUnsafePtr();
}
@@ -303,6 +303,7 @@ public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
{
_data.Dispose();
_freeSlots.Dispose();
_validBits.Dispose();
_count = 0;
_capacity = 0;

View File

@@ -10,12 +10,21 @@ namespace Misaki.HighPerformance.LowLevel;
/// <param name="Infos">An array of AllocationInfo containing details about the memory leaks.</param>
public class MemoryLeakException : Exception
{
private readonly IEnumerable<AllocationInfo>? _infos;
private readonly string _message = string.Empty;
private readonly string _message;
public MemoryLeakException(IEnumerable<AllocationInfo> infos)
public override string Message => _message;
public MemoryLeakException(ReadOnlySpan<AllocationInfo> infos)
{
_infos = infos;
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"Found {infos.Length} memory lakes!");
foreach (var info in infos)
{
stringBuilder.AppendLine(GetMessage(info.StackTrace));
}
_message = stringBuilder.ToString();
}
public MemoryLeakException(string message)
@@ -44,25 +53,4 @@ public class MemoryLeakException : Exception
return stringBuilder.ToString();
}
public override string Message
{
get
{
if (_infos == null)
{
return _message;
}
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"Found {_infos.Count()} memory lakes!");
foreach (var info in _infos)
{
stringBuilder.AppendLine(GetMessage(info.StackTrace));
}
return stringBuilder.ToString();
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
@@ -21,10 +21,6 @@
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Misaki.HighPerformance\Misaki.HighPerformance.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Collections\FixedText.tt">
<Generator>TextTemplatingFileGenerator</Generator>