Add image processing and memory management features

Added new namespace `Misaki.HighPerformance.Image` for image processing, including classes for animated GIF handling and memory management.
Added `AnimatedFrameResult` class for individual frames in animated images.
Added `AnimatedGifEnumerator` class for enumerating frames in animated GIFs.
Added `ColorComponents` enum for different color formats.
Added `ImageInfo` struct for image dimensions and color components.
Added `CRuntime` class for low-level memory management functions.
Added `MemoryStats` class to track memory allocation statistics.
Added utility functions for creating multi-dimensional arrays.
Added new structures for fixed-size UTF-8 encoded strings.
Added benchmarking classes to test new memory management features.

Changed `StbImage.cs` to include new namespaces and functionality for image data manipulation.
Changed project files to target .NET 9.0 and enable new features.
Changed `Arena.cs` and `DynamicArena.cs` to use `nuint` for size parameters.
Changed `BitSet.cs` to enhance bit manipulation methods.
Changed `Program.cs` to run `FunctionPtrBenchmark` for performance testing.

Removed memory tracking code from `AllocationManager.cs`, including the `_allocated` dictionary and related logic.
Removed `Free` method from `IAllocator.cs` interface.
Removed `UNSAFE_COLLECTION_CHECK` preprocessor directive from the codebase.

Refactored various files to improve organization, moving from `Unsafe` to `LowLevel` namespace.
Refactored `MemoryUtilities` class to include new memory operation methods.
Refactored `UnsafeUtilities.cs` to support new collection structures.
This commit is contained in:
2025-07-12 19:48:42 +09:00
parent d306f183de
commit eeff3313b5
72 changed files with 14444 additions and 471 deletions

View File

@@ -0,0 +1,2 @@
global using static Misaki.HighPerformance.LowLevel.Helpers.MemoryUtilities;
global using SystemUnsfae = System.Runtime.CompilerServices.Unsafe;

View File

@@ -0,0 +1,263 @@
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Exceptions;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
using unsafe FreeFunc = delegate* unmanaged<void*, void*, void>;
public unsafe struct ArenaAllocator : IAllocator, IDisposable
{
private DynamicArena _arena;
private AllocationHandle _handle;
public readonly ref AllocationHandle Handle => ref Unsafe.AsRef(in _handle);
public ArenaAllocator(uint initialSize)
{
_arena = new DynamicArena(initialSize);
_handle = new AllocationHandle(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
}
[UnmanagedCallersOnly]
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{
var selfPtr = (ArenaAllocator*)instance;
var ptr = selfPtr->_arena.Allocate(size, alignment, allocationOption);
return ptr;
}
[UnmanagedCallersOnly]
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment)
{
var selfPtr = (ArenaAllocator*)instance;
var newPtr = selfPtr->_arena.Allocate(size, alignment, AllocationOption.None);
MemCpy(newPtr, ptr, size);
// NOTE: We do not free the old pointer here, as it is managed by the arena.
return newPtr;
}
[UnmanagedCallersOnly]
private static void FreeBlock(void* instance, void* ptr)
{
}
public void Reset()
{
_arena.Reset();
}
public void Dispose()
{
_arena.Dispose();
}
}
public unsafe struct DefaultAllocator : IAllocator
{
private AllocationHandle _handle;
public ref AllocationHandle Handle => ref Unsafe.AsRef(in _handle);
public DefaultAllocator()
{
_handle = new AllocationHandle(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
}
[UnmanagedCallersOnly]
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{
var ptr = AlignedAlloc(size, alignment);
AllocationManager.TrackAllocation(ptr, size, instance, &FreeBlock);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, size);
}
return ptr;
}
[UnmanagedCallersOnly]
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment)
{
var newPtr = AlignedRealloc(ptr, size, alignment);
AllocationManager.UpdateAllocation(ptr, newPtr, size, instance, &FreeBlock);
return newPtr;
}
[UnmanagedCallersOnly]
private static void FreeBlock(void* instance, void* ptr)
{
AlignedFree(ptr);
AllocationManager.RemoveAllocation(ptr);
}
}
public unsafe struct EmptyAllocator : IAllocator
{
private AllocationHandle _handle;
public ref AllocationHandle Handle => ref Unsafe.AsRef(in _handle);
public EmptyAllocator()
{
_handle = new AllocationHandle(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock);
}
[UnmanagedCallersOnly]
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{
return null;
}
[UnmanagedCallersOnly]
private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment)
{
return ptr;
}
[UnmanagedCallersOnly]
private static void FreeBlock(void* instance, void* ptr)
{
}
}
public static unsafe class AllocationManager
{
public readonly struct AllocationInfo
{
public nuint Size
{
get; init;
}
public void* Allocator
{
get; init;
}
public FreeFunc FreeHandler
{
get; init;
}
public StackTrace StackTrace
{
get; init;
}
}
private const uint _DEFAULT_ARENA_SIZE = 512 * 1024;
private static ArenaAllocator s_arenaAllocator = new(_DEFAULT_ARENA_SIZE);
private static DefaultAllocator s_persistentAllocator = new();
private static EmptyAllocator s_emptyAllocator = new();
private static bool s_debugLayer;
private static Dictionary<nint, AllocationInfo>? s_allocated;
public static ArenaAllocator TempAllocator => s_arenaAllocator;
public static DefaultAllocator PersistentAllocator => s_persistentAllocator;
public static EmptyAllocator EmptyAllocator => s_emptyAllocator;
public static void EnableDebugLayer()
{
s_debugLayer = true;
s_allocated ??= new Dictionary<nint, AllocationInfo>(64);
}
public static ref AllocationHandle GetAllocationHandle(Allocator allocator)
{
switch (allocator)
{
case Allocator.Temp:
return ref s_arenaAllocator.Handle;
case Allocator.Persistent:
return ref s_persistentAllocator.Handle;
case Allocator.External:
return ref s_emptyAllocator.Handle;
default:
throw new ArgumentException("Invalid allocator type.", nameof(allocator));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void TrackAllocation(void* ptr, nuint allocationSize, void* allocator, FreeFunc freeFunc)
{
if (!s_debugLayer || s_allocated == null || ptr == null)
{
return;
}
s_allocated[(nint)ptr] = new AllocationInfo
{
Size = allocationSize,
Allocator = allocator,
FreeHandler = freeFunc,
StackTrace = new StackTrace(true)
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UpdateAllocation(void* oldPtr, void* newPtr, nuint allocationSize, void* allocator, FreeFunc freeFunc)
{
if (!s_debugLayer || s_allocated == null || oldPtr == null || newPtr == null)
{
return;
}
if (s_allocated.Remove((nint)oldPtr, out var info))
{
s_allocated[(nint)newPtr] = new AllocationInfo
{
Size = allocationSize,
Allocator = allocator,
FreeHandler = freeFunc,
StackTrace = info.StackTrace
};
}
else
{
TrackAllocation(newPtr, allocationSize, allocator, freeFunc);
}
}
public static void RemoveAllocation(void* ptr)
{
if (s_allocated == null)
{
return;
}
s_allocated.Remove((nint)ptr);
}
/// <summary>
/// Disposes of the AllocationManager, freeing all allocated memory and resources.
/// </summary>
public static void Dispose()
{
s_arenaAllocator.Dispose();
if (s_allocated != null)
{
nuint unfreeBytes = 0u;
foreach (var pair in s_allocated)
{
unfreeBytes += pair.Value.Size;
pair.Value.FreeHandler(pair.Value.Allocator, (void*)pair.Key);
}
if (unfreeBytes > 0u)
{
throw new MemoryLeakException([.. s_allocated.Values]);
}
s_allocated.Clear();
}
}
}

View File

@@ -0,0 +1,92 @@
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// A memory management structure that allocates and resets memory blocks with specified alignment.
/// </summary>
public unsafe struct Arena : IDisposable
{
private byte* _buffer;
private nuint _size;
private nuint _offset;
private bool _disposed;
public Arena(nuint size)
{
Initialize(size);
}
public void Initialize(nuint size)
{
if (_buffer != null)
{
return;
}
_buffer = (byte*)Malloc(size);
_size = size;
_offset = 0;
}
/// <summary>
/// Allocates a block of memory of a specified size with a given alignment. Returns a pointer to the allocated
/// memory or null if allocation fails.
/// You don't need to free the memory manually, it will be freed when the arena is disposed.
/// </summary>
/// <param name="size">Specifies the amount of memory to allocate in bytes.</param>
/// <param name="alignment">Defines the alignment requirement for the allocated memory.</param>
/// <param name="allocationOption">The option when allocating memory.</param>
/// <returns>A pointer to the allocated memory block or null if the allocation cannot be fulfilled.</returns>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(DynamicArena));
}
var offset = _offset + alignment - 1 & ~(alignment - 1);
if (offset + size > _size)
{
return null;
}
_offset = offset + size;
var ptr = _buffer + offset;
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, size);
}
return ptr;
}
/// <summary>
/// Resets the arena, optionally clearing the allocated memory.
/// </summary>
/// <param name="clear">If true, the allocated memory will be cleared; otherwise, it will not be cleared.</param>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void Reset()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(DynamicArena));
}
_offset = 0;
}
public void Dispose()
{
Free(_buffer);
_buffer = null;
_size = 0;
_offset = 0;
_disposed = true;
}
}

View File

@@ -0,0 +1,143 @@
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// A dynamic memory management structure that automatically grows by creating linked arenas
/// when more space is needed.
/// </summary>
public unsafe struct DynamicArena : IDisposable
{
private struct ArenaNode
{
public Arena arena;
public ArenaNode* next;
}
private ArenaNode* _root;
private ArenaNode* _current;
private uint _initialSize;
/// <summary>
/// Initializes a new instance of DynamicArena with the specified initial size.
/// </summary>
/// <param name="initialSize">The initial size in bytes for the first arena block.</param>
public DynamicArena(uint initialSize)
{
_initialSize = initialSize;
_root = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
_root->arena = new Arena(initialSize);
_root->next = null;
_current = _root;
}
public void Initialize(uint initialSize)
{
if (_root != null)
{
return;
}
_initialSize = initialSize;
_root = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
_root->arena = new Arena(initialSize);
_root->next = null;
_current = _root;
}
private bool CreateNewNode(nuint size)
{
var newNode = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
try
{
newNode->arena = new Arena(size);
newNode->next = null;
_current->next = newNode;
_current = newNode;
return true;
}
catch
{
Free(newNode);
return false;
}
}
/// <summary>
/// Allocates a block of memory with specified size and alignment. Creates a new arena if current one is full.
/// </summary>
/// <param name="size">Size of the memory block to allocate in bytes.</param>
/// <param name="alignment">Alignment requirement for the memory block.</param>
/// <returns>Pointer to the allocated memory block.</returns>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption)
{
if (_root == null)
{
return null;
}
void* result = null;
var current = _current;
while (current != null)
{
result = current->arena.Allocate(size, alignment, allocationOption);
if (result != null)
{
return result;
}
if (current->next == null && !CreateNewNode(Math.Max(size, _initialSize)))
{
return null;
}
current = current->next;
}
_current = current;
return result;
}
/// <summary>
/// Resets all arenas in the chain, optionally clearing their memory.
/// </summary>
/// <param name="clear">If true, memory will be cleared during reset.</param>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void Reset()
{
var current = _root;
while (current != null)
{
current->arena.Reset();
current = current->next;
}
_current = _root;
}
/// <summary>
/// Disposes all arenas and frees associated memory.
/// </summary>
public void Dispose()
{
if (_root == null)
{
return;
}
var current = _root;
while (current != null)
{
var next = current->next;
current->arena.Dispose();
Free(current);
current = next;
}
_root = null;
_current = null;
}
}

View File

@@ -0,0 +1,926 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 32 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 32 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString32"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 32)]
public unsafe struct FixedStackString32
{
private ushort _length;
private fixed byte _buffer[30];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 30)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString32.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 30));
}
}
}
public FixedStackString32(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 30)
{
throw new ArgumentException("Input string is too long to fit in FixedString32.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 30);
_length = (ushort)actualByteCount;
}
}
public FixedStackString32(string input)
: this(input.AsSpan())
{
}
public FixedStackString32(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString32(ReadOnlySpan<byte> input)
{
if (input.Length > 30)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString32.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString32(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 64 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 64 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString64"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 64)]
public unsafe struct FixedStackString64
{
private ushort _length;
private fixed byte _buffer[62];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 62)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString64.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 62));
}
}
}
public FixedStackString64(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 62)
{
throw new ArgumentException("Input string is too long to fit in FixedString64.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 62);
_length = (ushort)actualByteCount;
}
}
public FixedStackString64(string input)
: this(input.AsSpan())
{
}
public FixedStackString64(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString64(ReadOnlySpan<byte> input)
{
if (input.Length > 62)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString64.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString64(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 128 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 128 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString128"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 128)]
public unsafe struct FixedStackString128
{
private ushort _length;
private fixed byte _buffer[126];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 126)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString128.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 126));
}
}
}
public FixedStackString128(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 126)
{
throw new ArgumentException("Input string is too long to fit in FixedString128.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 126);
_length = (ushort)actualByteCount;
}
}
public FixedStackString128(string input)
: this(input.AsSpan())
{
}
public FixedStackString128(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString128(ReadOnlySpan<byte> input)
{
if (input.Length > 126)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString128.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString128(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 256 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 256 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString256"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 256)]
public unsafe struct FixedStackString256
{
private ushort _length;
private fixed byte _buffer[254];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 254)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString256.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 254));
}
}
}
public FixedStackString256(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 254)
{
throw new ArgumentException("Input string is too long to fit in FixedString256.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 254);
_length = (ushort)actualByteCount;
}
}
public FixedStackString256(string input)
: this(input.AsSpan())
{
}
public FixedStackString256(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString256(ReadOnlySpan<byte> input)
{
if (input.Length > 254)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString256.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString256(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 512 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 512 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString512"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 512)]
public unsafe struct FixedStackString512
{
private ushort _length;
private fixed byte _buffer[510];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 510)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString512.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 510));
}
}
}
public FixedStackString512(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 510)
{
throw new ArgumentException("Input string is too long to fit in FixedString512.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 510);
_length = (ushort)actualByteCount;
}
}
public FixedStackString512(string input)
: this(input.AsSpan())
{
}
public FixedStackString512(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString512(ReadOnlySpan<byte> input)
{
if (input.Length > 510)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString512.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString512(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 1024 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 1024 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString1024"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 1024)]
public unsafe struct FixedStackString1024
{
private ushort _length;
private fixed byte _buffer[1022];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 1022)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString1024.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 1022));
}
}
}
public FixedStackString1024(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 1022)
{
throw new ArgumentException("Input string is too long to fit in FixedString1024.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 1022);
_length = (ushort)actualByteCount;
}
}
public FixedStackString1024(string input)
: this(input.AsSpan())
{
}
public FixedStackString1024(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString1024(ReadOnlySpan<byte> input)
{
if (input.Length > 1022)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString1024.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString1024(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 2048 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 2048 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString2048"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 2048)]
public unsafe struct FixedStackString2048
{
private ushort _length;
private fixed byte _buffer[2046];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 2046)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString2048.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 2046));
}
}
}
public FixedStackString2048(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 2046)
{
throw new ArgumentException("Input string is too long to fit in FixedString2048.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 2046);
_length = (ushort)actualByteCount;
}
}
public FixedStackString2048(string input)
: this(input.AsSpan())
{
}
public FixedStackString2048(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString2048(ReadOnlySpan<byte> input)
{
if (input.Length > 2046)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString2048.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString2048(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 4096 bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 4096 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString4096"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = 4096)]
public unsafe struct FixedStackString4096
{
private ushort _length;
private fixed byte _buffer[4094];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 4094)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString4096.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, 4094));
}
}
}
public FixedStackString4096(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 4094)
{
throw new ArgumentException("Input string is too long to fit in FixedString4096.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, 4094);
_length = (ushort)actualByteCount;
}
}
public FixedStackString4096(string input)
: this(input.AsSpan())
{
}
public FixedStackString4096(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString4096(ReadOnlySpan<byte> input)
{
if (input.Length > 4094)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString4096.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString4096(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}

View File

@@ -0,0 +1,129 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Buffer;
<# for (int i = 32; i <= 4096; i *= 2) { #>
/// <summary>
/// Represents a stack allocated fixed-size UTF-8 encoded string of length <#= i #> bytes.
/// </summary>
/// <remarks>
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
/// If you need a heap allocated fixed-size UTF-8 encoded string of length <#= i #> bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString<#= i #>"/>.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Size = <#= i #>)]
public unsafe struct FixedStackString<#= i #>
{
private ushort _length;
private fixed byte _buffer[<#= i - 2 #>];
public ushort Length => _length;
public string Value
{
get
{
fixed (byte* bufferPtr = _buffer)
{
return Encoding.UTF8.GetString(bufferPtr, _length);
}
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > <#= i - 2 #>)
{
throw new ArgumentException("Input string is too long to fit in FixedStackString<#= i #>.");
}
fixed (byte* bufferPtr = _buffer)
{
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, <#= i - 2 #>));
}
}
}
public FixedStackString<#= i #>(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > <#= i - 2 #>)
{
throw new ArgumentException("Input string is too long to fit in FixedString<#= i #>.");
}
fixed (char* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, <#= i - 2 #>);
_length = (ushort)actualByteCount;
}
}
public FixedStackString<#= i #>(string input)
: this(input.AsSpan())
{
}
public FixedStackString<#= i #>(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedStackString<#= i #>(ReadOnlySpan<byte> input)
{
if (input.Length > <#= i - 2 #>)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString<#= i #>.");
}
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
fixed (byte* bufferPtr = _buffer)
{
SystemUnsfae.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
}
}
public FixedStackString<#= i #>(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> AsSpan()
{
fixed (byte* ptr = _buffer)
{
return new(ptr, _length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePointer()
{
fixed (byte* ptr = _buffer)
{
return ptr;
}
}
public override string ToString()
{
return Value;
}
}
<# } #>

View File

@@ -0,0 +1,894 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 32 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 32)]
public unsafe struct FixedString32 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 30)
{
throw new ArgumentException("Input string is too long to fit in FixedString32.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 30));
}
}
public FixedString32(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 30)
{
throw new ArgumentException("Input string is too long to fit in FixedString32.");
}
_buffer = (byte*)NativeMemory.Alloc(30);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 30);
_length = (ushort)actualByteCount;
}
}
public FixedString32(string input)
: this(input.AsSpan())
{
}
public FixedString32(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString32(ReadOnlySpan<byte> input)
{
if (input.Length > 30)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString32.");
}
_buffer = (byte*)NativeMemory.Alloc(30);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString32(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 64 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 64)]
public unsafe struct FixedString64 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 62)
{
throw new ArgumentException("Input string is too long to fit in FixedString64.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 62));
}
}
public FixedString64(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 62)
{
throw new ArgumentException("Input string is too long to fit in FixedString64.");
}
_buffer = (byte*)NativeMemory.Alloc(62);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 62);
_length = (ushort)actualByteCount;
}
}
public FixedString64(string input)
: this(input.AsSpan())
{
}
public FixedString64(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString64(ReadOnlySpan<byte> input)
{
if (input.Length > 62)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString64.");
}
_buffer = (byte*)NativeMemory.Alloc(62);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString64(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 128 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 128)]
public unsafe struct FixedString128 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 126)
{
throw new ArgumentException("Input string is too long to fit in FixedString128.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 126));
}
}
public FixedString128(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 126)
{
throw new ArgumentException("Input string is too long to fit in FixedString128.");
}
_buffer = (byte*)NativeMemory.Alloc(126);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 126);
_length = (ushort)actualByteCount;
}
}
public FixedString128(string input)
: this(input.AsSpan())
{
}
public FixedString128(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString128(ReadOnlySpan<byte> input)
{
if (input.Length > 126)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString128.");
}
_buffer = (byte*)NativeMemory.Alloc(126);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString128(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 256 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 256)]
public unsafe struct FixedString256 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 254)
{
throw new ArgumentException("Input string is too long to fit in FixedString256.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 254));
}
}
public FixedString256(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 254)
{
throw new ArgumentException("Input string is too long to fit in FixedString256.");
}
_buffer = (byte*)NativeMemory.Alloc(254);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 254);
_length = (ushort)actualByteCount;
}
}
public FixedString256(string input)
: this(input.AsSpan())
{
}
public FixedString256(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString256(ReadOnlySpan<byte> input)
{
if (input.Length > 254)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString256.");
}
_buffer = (byte*)NativeMemory.Alloc(254);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString256(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 512 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 512)]
public unsafe struct FixedString512 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 510)
{
throw new ArgumentException("Input string is too long to fit in FixedString512.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 510));
}
}
public FixedString512(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 510)
{
throw new ArgumentException("Input string is too long to fit in FixedString512.");
}
_buffer = (byte*)NativeMemory.Alloc(510);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 510);
_length = (ushort)actualByteCount;
}
}
public FixedString512(string input)
: this(input.AsSpan())
{
}
public FixedString512(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString512(ReadOnlySpan<byte> input)
{
if (input.Length > 510)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString512.");
}
_buffer = (byte*)NativeMemory.Alloc(510);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString512(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 1024 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 1024)]
public unsafe struct FixedString1024 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 1022)
{
throw new ArgumentException("Input string is too long to fit in FixedString1024.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 1022));
}
}
public FixedString1024(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 1022)
{
throw new ArgumentException("Input string is too long to fit in FixedString1024.");
}
_buffer = (byte*)NativeMemory.Alloc(1022);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 1022);
_length = (ushort)actualByteCount;
}
}
public FixedString1024(string input)
: this(input.AsSpan())
{
}
public FixedString1024(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString1024(ReadOnlySpan<byte> input)
{
if (input.Length > 1022)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString1024.");
}
_buffer = (byte*)NativeMemory.Alloc(1022);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString1024(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 2048 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 2048)]
public unsafe struct FixedString2048 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 2046)
{
throw new ArgumentException("Input string is too long to fit in FixedString2048.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 2046));
}
}
public FixedString2048(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 2046)
{
throw new ArgumentException("Input string is too long to fit in FixedString2048.");
}
_buffer = (byte*)NativeMemory.Alloc(2046);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 2046);
_length = (ushort)actualByteCount;
}
}
public FixedString2048(string input)
: this(input.AsSpan())
{
}
public FixedString2048(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString2048(ReadOnlySpan<byte> input)
{
if (input.Length > 2046)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString2048.");
}
_buffer = (byte*)NativeMemory.Alloc(2046);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString2048(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length 4096 bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 4096)]
public unsafe struct FixedString4096 : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > 4094)
{
throw new ArgumentException("Input string is too long to fit in FixedString4096.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, 4094));
}
}
public FixedString4096(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > 4094)
{
throw new ArgumentException("Input string is too long to fit in FixedString4096.");
}
_buffer = (byte*)NativeMemory.Alloc(4094);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, 4094);
_length = (ushort)actualByteCount;
}
}
public FixedString4096(string input)
: this(input.AsSpan())
{
}
public FixedString4096(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString4096(ReadOnlySpan<byte> input)
{
if (input.Length > 4094)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString4096.");
}
_buffer = (byte*)NativeMemory.Alloc(4094);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString4096(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}

View File

@@ -0,0 +1,125 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Buffer;
<# for (int i = 32; i <= 4096; i *= 2) { #>
/// <summary>
/// Represents a heap allocated fixed-size UTF-8 encoded string of length <#= i #> bytes.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = <#= i #>)]
public unsafe struct FixedString<#= i #> : IDisposable
{
private ushort _length;
private byte* _buffer;
public ushort Length => _length;
public string Value
{
readonly get
{
return Encoding.UTF8.GetString(_buffer, _length);
}
set
{
if (string.IsNullOrEmpty(value))
{
_length = 0;
return;
}
var maxBytes = Encoding.UTF8.GetByteCount(value);
if (maxBytes > <#= i - 2 #>)
{
throw new ArgumentException("Input string is too long to fit in FixedString<#= i #>.");
}
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(_buffer, <#= i - 2 #>));
}
}
public FixedString<#= i #>(ReadOnlySpan<char> input)
{
var maxBytes = Encoding.UTF8.GetByteCount(input);
if (maxBytes > <#= i - 2 #>)
{
throw new ArgumentException("Input string is too long to fit in FixedString<#= i #>.");
}
_buffer = (byte*)NativeMemory.Alloc(<#= i - 2 #>);
fixed (char* inputPtr = input)
{
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, _buffer, <#= i - 2 #>);
_length = (ushort)actualByteCount;
}
}
public FixedString<#= i #>(string input)
: this(input.AsSpan())
{
}
public FixedString<#= i #>(char* input, ushort length)
: this(new Span<char>(input, length))
{
}
public FixedString<#= i #>(ReadOnlySpan<byte> input)
{
if (input.Length > <#= i - 2 #>)
{
throw new ArgumentException("Input byte array is too long to fit in FixedString<#= i #>.");
}
_buffer = (byte*)NativeMemory.Alloc(<#= i - 2 #>);
_length = (ushort)input.Length;
fixed (byte* inputPtr = input)
{
SystemUnsfae.CopyBlockUnaligned(_buffer, inputPtr, _length);
}
}
public FixedString<#= i #>(byte* input, ushort length)
: this(new ReadOnlySpan<byte>(input, length))
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return new(_buffer, _length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte* GetUnsafePointer()
{
return _buffer;
}
public override string ToString()
{
return Value;
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_length = 0;
_buffer = null;
}
}
}
<# } #>

View File

@@ -0,0 +1,19 @@
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.LowLevel.Buffer;
// TODO: Implement a pool for UnsafeArray<T>.
public unsafe static class UnsafeArrayPool
{
public static UnsafeArray<T> Rent<T>(int minimalSize)
where T : unmanaged
{
throw new NotImplementedException();
}
public static void Return<T>(UnsafeArray<T> array)
where T : unmanaged
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,38 @@
namespace Misaki.HighPerformance.LowLevel.Collections;
[Flags]
public enum AllocationOption : byte
{
None = 0,
/// <summary>
/// Allocator for initialized memory.
/// </summary>
Clear = 1 << 0,
/// <summary>
/// Allocator for untracked memory. It always allocates memory without using the allocation manager.
/// Always free it manually even if you use the <see cref="Allocator.Temp"/> allocator.
/// </summary>
/// <remarks>
/// Use this option carefully, as the allocation manager will not track the memory.
/// No warning will be given if the memory is not freed.
/// </remarks>
UnTracked = 1 << 1,
}
public enum Allocator : byte
{
// Make the first allocator as invalid because we don't want to user create a default collection without passing any parameters
Invalid,
/// <summary>
/// Allocator for temporary allocations. Allocations are cleared after use.
/// </summary>
Temp,
/// <summary>
/// Allocator for persistent allocations. Allocations are not cleared after use.
/// </summary>
Persistent,
/// <summary>
/// Allocator for external memory. Allocations are not cleared after use.
/// </summary>
External
}

View File

@@ -0,0 +1,711 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Collections;
public sealed class BitSet
{
private const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31
private const int _INDEX_SIZE = 5; // log_2(BitSize + 1)
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="BitSet"/> 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 uint[] _bits;
/// <summary>
/// The highest bit set.
/// </summary>
private int _highestBit;
/// <summary>
/// The maximum <see cref="_bits"/>-index current in use.
/// </summary>
private int _max;
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public BitSet()
{
_bits = new uint[s_padding];
}
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public BitSet(int minimalLength)
{
var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE);
var length = RoundToPadding(uints);
_bits = new uint[length];
}
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public BitSet(params Span<uint> bits)
{
_bits = bits.ToArray();
_highestBit = 0;
_max = _bits.Length * (_BIT_SIZE + 1) - 1; // Calculate the maximum index in use
for (var i = 0; i < _bits.Length; i++)
{
if (_bits[i] != 0)
{
_highestBit = Math.Max(_highestBit, i * (_BIT_SIZE + 1) + BitOperations.Log2(_bits[i]) + 1);
}
}
}
/// <summary>
/// The highest uint index in use inside the <see cref="_bits"/>-array.
/// </summary>
public int HighestIndex
{
get => _max;
}
/// <summary>
/// The highest bit set.
/// </summary>
public int HighestBit
{
get => _highestBit;
}
/// <summary>
/// Returns the length of the bitset, how many ints it consists of.
/// </summary>
public int Length
{
get => _bits.Length;
}
/// <summary>
/// Checks whether a bit is set at the index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>True if it is, otherwise false</returns>
public bool IsSet(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Length)
{
return false;
}
return (_bits[b] & 1 << (index & _BIT_SIZE)) != 0;
}
/// <summary>
/// Sets a bit at the given index.
/// Resizes its internal array if necessary.
/// </summary>
/// <param name="index">The index.</param>
public void SetBit(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Length)
{
Array.Resize(ref _bits, RoundToPadding(b));
}
// Track highest set bit
_highestBit = Math.Max(_highestBit, index);
_max = _highestBit / (_BIT_SIZE + 1) + 1;
_bits[b] |= 1u << (index & _BIT_SIZE);
}
/// <summary>
/// Clears the bit at the given index.
/// </summary>
/// <param name="index">The index.</param>
public void ClearBit(int index)
{
var b = index >> _INDEX_SIZE;
if (b >= _bits.Length)
{
return;
}
_bits[b] &= ~(1u << (index & _BIT_SIZE));
}
/// <summary>
/// Sets all bits.
/// </summary>
public void SetAll()
{
var count = _bits.Length;
for (var i = 0; i < count; i++)
{
_bits[i] = 0xffffffff;
}
_highestBit = _bits.Length * (_BIT_SIZE + 1) - 1;
_max = _highestBit / (_BIT_SIZE + 1) + 1;
}
/// <summary>
/// Clears all set bits.
/// </summary>
public void ClearAll()
{
Array.Clear(_bits);
_highestBit = 0;
_max = 0;
}
/// <summary>
/// Finds the next set bit at or after `startIndex`, or -1 if none.
/// </summary>
public int NextSetBit(int startIndex)
{
var wordIndex = startIndex >> _BIT_SIZE;
if (wordIndex >= _bits.Length)
{
return -1;
}
// Mask off bits below startIndex in the first word:
var word = _bits[wordIndex] & ~0u << (startIndex & _MASK);
while (true)
{
if (word != 0)
{
// get the least-significant set bit
var bit = BitOperations.TrailingZeroCount(word);
return (wordIndex << _BIT_SIZE) + bit;
}
wordIndex++;
if (wordIndex >= _bits.Length)
{
return -1;
}
word = _bits[wordIndex];
}
}
/// <summary>
/// Checks if all bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
[SkipLocalsInit]
public bool All(BitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise and
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit & otherBits[i]) != bit)
{
return false;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i++)
{
if (bits[i] != 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise and
for (var i = 0; i < min; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vector, otherVector);
if (!Vector.EqualsAll(resultVector, vector))
{
return false;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
{
return false;
}
}
}
return true;
}
/// <summary>
/// Checks if any bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
public bool Any(BitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise and, return true since any is met
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit & otherBits[i]) > 0)
{
return true;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i++)
{
if (bits[i] > 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise and, return true since any is met
for (var i = 0; i < min; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vector, otherVector);
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
{
return true;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
{
return false;
}
}
}
return _highestBit <= 0;
}
/// <summary>
/// Checks if none bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if none match, false if not.</returns>
public bool None(BitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise and, return true since any is met
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit & otherBits[i]) != 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise and, return true since any is met
for (var i = 0; i < min; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.BitwiseAnd(vector, otherVector);
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
{
return false;
}
}
}
return true;
}
/// <summary>
/// Checks if exactly all bits from this instance match those of the other instance.
/// </summary>
/// <param name="other">The other <see cref="BitSet"/>.</param>
/// <returns>True if they match, false if not.</returns>
public bool Exclusive(BitSet other)
{
var min = Math.Min(Math.Min(Length, other.Length), _max);
if (!Vector.IsHardwareAccelerated || min < s_padding)
{
var bits = _bits.AsSpan();
var otherBits = other._bits.AsSpan();
// Bitwise xor, if both are not totally equal, return false
for (var i = 0; i < min; i++)
{
var bit = bits[i];
if ((bit ^ otherBits[i]) != 0)
{
return false;
}
}
// handle extra bits on our side that might just be all zero
for (var i = min; i < _max; i++)
{
if (bits[i] != 0)
{
return false;
}
}
}
else
{
// Vectorized bitwise xor, return true since any is met
for (var i = 0; i < min; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
var otherVector = new Vector<uint>(other._bits.AsSpan()[i..]);
var resultVector = Vector.Xor(vector, otherVector);
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
{
return false;
}
}
// Handle extra bits on our side that might just be all zero.
for (var i = min; i < _max; i += s_padding)
{
var vector = new Vector<uint>(_bits.AsSpan()[i..]);
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
{
return false;
}
}
}
return true;
}
public static BitSet operator &(BitSet left, BitSet right)
{
var min = Math.Min(left.Length, right.Length);
var result = new BitSet(min);
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.BitwiseAnd(vectorLeft, vectorRight);
resultVector.CopyTo(result._bits.AsSpan(i, s_padding));
}
}
return result;
}
public static BitSet operator |(BitSet left, BitSet right)
{
var min = Math.Min(left.Length, right.Length);
var result = new BitSet(min);
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 BitSet operator ~(BitSet 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>
/// Creates a <see cref="Span{T}"/> to access the <see cref="_bits"/>.
/// </summary>
/// <returns>The hash.</returns>
public Span<uint> AsSpan()
{
var max = _highestBit / (_BIT_SIZE + 1) + 1;
return _bits.AsSpan()[..max];
}
/// <summary>
/// Copies the bits into a <see cref="Span{T}"/> and returns a slice containing the copied <see cref="_bits"/>.
/// </summary>
/// <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)
{
// Copy everything thats possible from one to another
var length = Math.Min(Length, span.Length);
for (var index = 0; index < length; index++)
{
span[index] = _bits[index];
}
// Zero the rest space which was not overriden due to the copy.
for (var index = length; zero && index < span.Length; index++)
{
span[index] = 0;
}
return span[..Length];
}
/// <summary>
/// Prints the content of this instance.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
// Convert uint to binary form for pretty printing
var binaryBuilder = new StringBuilder();
foreach (var bit in _bits)
{
binaryBuilder.Append(Convert.ToString(bit, 2).PadLeft(32, '0')).Append(',');
}
binaryBuilder.Length--;
return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Length)}: {Length}";
}
}
/// <summary>
/// The <see cref="SpanBitSet"/> struct
/// represents a non resizable collection of bits.
/// Used to set, check and clear bits on a allocated <see cref="BitSet"/> or on the stack.
/// </summary>
public readonly ref struct SpanBitSet
{
private const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31
// NOTE: Is a byte not 8 bits?
private const int _BYTE_SIZE = 5; // log_2(BitSize + 1)
/// <summary>
/// The bits from the bitset.
/// </summary>
private readonly Span<uint> _bits;
/// <summary>
/// Initializes a new instance of the <see cref="BitSet" /> class.
/// </summary>
public SpanBitSet(Span<uint> bits)
{
_bits = bits;
}
/// <summary>
/// Checks whether a bit is set at the index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>True if it is, otherwise false</returns>
public bool IsSet(int index)
{
var b = index >> _BYTE_SIZE;
if (b >= _bits.Length)
{
return false;
}
return (_bits[b] & 1 << (index & _BIT_SIZE)) != 0;
}
/// <summary>
/// Sets a bit at the given index.
/// Resizes its internal array if necessary.
/// </summary>
/// <param name="index">The index.</param>
public void SetBit(int index)
{
var b = index >> _BYTE_SIZE;
if (b >= _bits.Length)
{
return;
}
_bits[b] |= 1u << (index & _BIT_SIZE);
}
/// <summary>
/// Clears the bit at the given index.
/// </summary>
/// <param name="index">The index.</param>
public void ClearBit(int index)
{
var b = index >> _BYTE_SIZE;
if (b >= _bits.Length)
{
return;
}
_bits[b] &= ~(1u << (index & _BIT_SIZE));
}
/// <summary>
///
/// </summary>
public void SetAll()
{
var count = _bits.Length;
for (var i = 0; i < count; i++)
{
_bits[i] = 0xffffffff;
}
}
/// <summary>
/// Clears all set bits.
/// </summary>
public void ClearAll()
{
_bits.Clear();
}
/// <summary>
/// Creates a <see cref="Span{T}"/> to access the <see cref="_bits"/>.
/// </summary>
/// <returns>The hash.</returns>
public Span<uint> AsSpan()
{
return _bits;
}
/// <summary>
/// Copies the bits into a <see cref="Span{T}"/> and returns a slice containing the copied <see cref="_bits"/>.
/// </summary>
/// <param name=""></param>
/// <returns>The hash.</returns>
public Span<uint> AsSpan(Span<uint> span, bool zero = true)
{
// Prevent exception because target array is to small for copy operation
var length = Math.Min(_bits.Length, span.Length);
for (var index = 0; index < length; index++)
{
span[index] = _bits[index];
}
// Zero the rest space which was not overriden due to the copy.
for (var index = length; zero && index < span.Length; index++)
{
span[index] = 0;
}
return span[.._bits.Length];
}
/// <summary>
/// Prints the content of this instance.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
// Convert uint to binary form for pretty printing
var binaryBuilder = new StringBuilder();
foreach (var bit in _bits)
{
binaryBuilder.Append(Convert.ToString(bit, 2).PadLeft(32, '0')).Append(',');
}
binaryBuilder.Length--;
return $"{nameof(_bits)}: {string.Join(",", binaryBuilder)}";
}
}

View File

@@ -0,0 +1,38 @@
namespace Misaki.HighPerformance.LowLevel.Collections.Contracts;
public unsafe interface IUnsafeCollection<T> : IEnumerable<T>, IDisposable
where T : unmanaged
{
/// <summary>
/// Gets the number of elements in a collection. The value is read-only.
/// </summary>
public int Count
{
get;
}
/// <summary>
/// Indicates whether the object has been created. Returns true if the object is created, otherwise false.
/// </summary>
public bool IsCreated
{
get;
}
/// <summary>
/// Removes all elements from the collection. The collection will be empty after this operation.
/// </summary>
public void Clear();
/// <summary>
/// Changes the size of a collection or array to the specified value.
/// </summary>
/// <param name="newSize">Specifies the new size to which the collection or array should be adjusted.</param>
public void Resize(int newSize);
/// <summary>
/// Returns a pointer to an unmanaged memory location. This pointer can be used for low-level memory operations.
/// </summary>
/// <returns>The method returns a void pointer to the unsafe memory location.</returns>
public void* GetUnsafePtr();
}

View File

@@ -0,0 +1,5 @@
namespace Misaki.HighPerformance.LowLevel.Collections.Contracts;
internal class IUnsafeSet<T>
where T : unmanaged, IEquatable<T>
{
}

View File

@@ -0,0 +1,186 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
/// <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>
public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
where T : unmanaged
{
public struct Enumerator : IEnumerator<T>
{
private UnsafeArray<T>* _collection;
private int _index;
private T _value;
public Enumerator(UnsafeArray<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = UnsafeUtilities.ReadArrayElement<T>(_collection->_buffer, _index);
return true;
}
_value = default;
return false;
}
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;
}
public void Dispose()
{
}
}
private T* _buffer;
private int _count;
private AllocationHandle* _handle;
public readonly int Count => _count;
public readonly ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (index < 0 || index >= _count)
{
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
}
return ref _buffer[index];
}
}
public readonly bool IsCreated
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer != null;
}
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeArray<T>*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Constructs an UnsafeArray with a default size of 1 and uses the Persistent allocator.
/// </summary>
public UnsafeArray()
: this(1, Allocator.Persistent)
{
}
public UnsafeArray(int count, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
if (count <= 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "Count must be greater than zero.");
}
_handle = (AllocationHandle*)Unsafe.AsPointer(ref handle);
_buffer = (T*)handle.Alloc(_handle->Allocator, (uint)count * (uint)sizeof(T), (uint)AlignOf<T>(), allocationOption);
_count = count;
}
/// <summary>
/// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation type.
/// </summary>
/// <param name="count">Specifies the number of elements to allocate in the array, which must be greater than zero.</param>
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
public UnsafeArray(int count, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(count, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
/// <summary>
/// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. This does not copy the data.
/// </summary>
/// <param name="buffer">A pointer to the memory location that holds the elements of the array.</param>
/// <param name="count">The total size of the data.</param>
/// <remarks>
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
/// </remarks>
public UnsafeArray(void* buffer, int count)
{
_buffer = (T*)buffer;
_count = count;
_handle = (AllocationHandle*)Unsafe.AsPointer(ref AllocationManager.EmptyAllocator.Handle);
}
/// <inheritdoc/>
public void Resize(int newSize)
{
if (newSize == _count)
{
return;
}
_buffer = (T*)_handle->Realloc(_handle->Allocator, _buffer, (uint)newSize, (uint)AlignOf<T>());
_count = newSize;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Clear()
{
MemClear(_buffer, (nuint)(_count * sizeof(T)));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _buffer;
}
/// <inheritdoc/>
public void Dispose()
{
if (!IsCreated)
{
return;
}
_handle->Free(_handle->Allocator, _buffer);
_handle = null;
_buffer = null;
_count = 0;
}
}

View File

@@ -0,0 +1,223 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeCollection<KeyValuePair<TKey, TValue>>
where TKey : unmanaged, IEquatable<TKey> where TValue : unmanaged
{
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
{
internal HashMapHelper<TKey>.Enumerator _enumerator;
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()
{
}
}
private HashMapHelper<TKey> _hashMap;
public readonly int Count => _hashMap.Count;
public readonly int Capacity => _hashMap.Capacity;
public readonly bool IsCreated => _hashMap.IsCreated;
/// <summary>
/// Gets and sets values by key.
/// </summary>
/// <remarks>Getting a key that is not present will throw. Setting a key that is not already present will add the key.</remarks>
/// <param name="key">The key to look up.</param>
/// <value>The value associated with the key.</value>
/// <exception cref="ArgumentException">For getting, thrown if the key was not present.</exception>
public TValue this[TKey key]
{
get
{
if (!_hashMap.TryGetValue<TValue>(key, out var result))
{
throw new ArgumentException($"Key: {key} is not present.");
}
return result;
}
set
{
var idx = _hashMap.Find(key);
if (-1 != idx)
{
UnsafeUtilities.WriteArrayElement(_hashMap.Buffer, idx, value);
return;
}
TryAdd(key, value);
}
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => new Enumerator((HashMapHelper<TKey>*)UnsafeUtilities.AddressOf(ref _hashMap));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public UnsafeHashMap(int capacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
_hashMap = new HashMapHelper<TKey>(capacity, sizeof(TValue), HashMapHelper<TKey>.MINIMAL_CAPACITY, ref handle, allocationOption);
}
public UnsafeHashMap(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(capacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
/// <summary>
/// Adds a new key-value pair.
/// </summary>
/// <remarks>If the key is already present, this method returns false without modifying the hash map.</remarks>
/// <param name="key">The key to add.</param>
/// <param name="item">The value to add.</param>
/// <returns>True if the key-value pair was added.</returns>
public bool TryAdd(TKey key, TValue item)
{
var idx = _hashMap.TryAdd(key);
if (idx != -1)
{
UnsafeUtilities.WriteArrayElement(_hashMap.Buffer, idx, item);
return true;
}
return false;
}
/// <summary>
/// Adds a new key-value pair.
/// </summary>
/// <remarks>If the key is already present, this method throws without modifying the hash map.</remarks>
/// <param name="key">The key to add.</param>
/// <param name="item">The value to add.</param>
/// <exception cref="ArgumentException">Thrown if the key was already present.</exception>
public void Add(TKey key, TValue item)
{
var result = TryAdd(key, item);
if (!result)
{
throw new ArgumentException($"An item with the same key has already been added: {key}");
}
}
/// <summary>
/// Removes a particular key and its value.
/// </summary>
/// <param name="item">The value to remove.</param>
/// <returns>True if the value was present.</returns>
public bool Remove(TKey key)
{
return -1 != _hashMap.TryRemove(key);
}
/// <summary>
/// Returns the value associated with a key.
/// </summary>
/// <param name="key">The key to look up.</param>
/// <param name="item">Outputs the value associated with the key. Outputs default if the key was not present.</param>
/// <returns>True if the key was present.</returns>
public bool TryGetValue(TKey key, out TValue item)
{
return _hashMap.TryGetValue(key, out item);
}
/// <summary>
/// Returns true if a given key is present in this hash map.
/// </summary>
/// <param name="key">The key to look up.</param>
/// <returns>True if the key was present.</returns>
public bool ContainsKey(TKey key)
{
return -1 != _hashMap.Find(key);
}
/// <summary>
/// Sets the capacity to match what it would be if it had been originally initialized with all its entries.
/// </summary>
public void TrimExcess() => _hashMap.TrimExcess();
public void Resize(int newSize)
{
_hashMap.Resize(newSize);
}
public void Clear()
{
_hashMap.Clear();
}
/// <summary>
/// Retrieves an array of keys from the hash map.
/// </summary>
/// <returns>An array containing the keys stored in the hash map.</returns>
public UnsafeArray<TKey> GetKeyArray(Allocator allocator) => _hashMap.GetKeyArray(allocator);
/// <summary>
/// Retrieves an array of values from the underlying hash map.
/// </summary>
/// <returns>An UnsafeArray containing the values stored in the hash map.</returns>
public UnsafeArray<TValue> GetValueArray(Allocator allocator) => _hashMap.GetValueArray<TValue>(allocator);
/// <summary>
/// Retrieves an array of key-value pairs from the hash map. The keys are of type TKey and the values are of type
/// TValue.
/// </summary>
/// <returns>Returns an UnsafeArray containing KeyValuePair objects.</returns>
public UnsafeArray<KeyValuePair<TKey, TValue>> GetKeyValueArrays(Allocator allocator) => _hashMap.GetKeyValueArrays<TValue>(allocator);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _hashMap.Buffer;
}
public void Dispose()
{
_hashMap.Dispose();
}
public void Test(ref HashMapHelper<TKey> t)
{
Console.WriteLine(t.Equals(_hashMap));
}
}

View File

@@ -0,0 +1,129 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
/// <summary>
/// A collection that provides fast, unsafe operations for managing a set of unmanaged types. It supports adding,
/// removing, and checking for values.
/// </summary>
/// <typeparam name="T">Represents an unmanaged type that can be compared for equality.</typeparam>
public unsafe struct UnsafeHashSet<T> : IUnsafeCollection<T>, IEnumerable<T>
where T : unmanaged, IEquatable<T>
{
public struct Enumerator : IEnumerator<T>
{
internal HashMapHelper<T>.Enumerator _enumerator;
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();
public void Reset() => _enumerator.Reset();
public readonly void Dispose()
{
}
}
private HashMapHelper<T> _hashMap;
public readonly int Count => _hashMap.Count;
public readonly int Capacity => _hashMap.Capacity;
public readonly bool IsCreated => _hashMap.IsCreated;
public IEnumerator<T> GetEnumerator() => new Enumerator((HashMapHelper<T>*)UnsafeUtilities.AddressOf(ref _hashMap));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public UnsafeHashSet(int capacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
_hashMap = new HashMapHelper<T>(capacity, 0, HashMapHelper<T>.MINIMAL_CAPACITY, ref handle, allocationOption);
}
public UnsafeHashSet(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(capacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
/// <summary>
/// Adds a new value (unless it is already present).
/// </summary>
/// <param name="item">The value to add.</param>
/// <returns>True if the value was not already present.</returns>
public bool Add(T item)
{
return -1 != _hashMap.TryAdd(item);
}
/// <summary>
/// Removes a particular value.
/// </summary>
/// <param name="item">The value to remove.</param>
/// <returns>True if the value was present.</returns>
public bool Remove(T item)
{
return -1 != _hashMap.TryRemove(item);
}
/// <summary>
/// Returns true if a particular value is present.
/// </summary>
/// <param name="item">The value to check for.</param>
/// <returns>True if the value was present.</returns>
public bool Contains(T item)
{
return -1 != _hashMap.Find(item);
}
/// <summary>
/// Sets the capacity to match what it would be if it had been originally initialized with all its entries.
/// </summary>
public void TrimExcess() => _hashMap.TrimExcess();
/// <summary>
/// Returns an array with a copy of this set's values (in no particular order).
/// </summary>
/// <param name="allocator">The allocator to use.</param>
/// <returns>An array with a copy of the set's values.</returns>
public UnsafeArray<T> ToNativeArray(Allocator allocator)
{
return _hashMap.GetKeyArray(allocator);
}
public void Resize(int newSize)
{
_hashMap.Resize(newSize);
}
public void Clear()
{
_hashMap.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _hashMap.Buffer;
}
public void Dispose()
{
_hashMap.Dispose();
}
}

View File

@@ -0,0 +1,321 @@
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
/// <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>
public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
where T : unmanaged
{
public struct Enumerator : IEnumerator<T>
{
private UnsafeList<T>* _collection;
private int _index;
private T _value;
public Enumerator(UnsafeList<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = UnsafeUtilities.ReadArrayElement<T>(_collection->_array.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
}
public void Reset()
{
_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()
{
}
}
/// <summary>
/// A parallel writer for an UnsafeList.
/// </summary>
/// <remarks>
/// Use <see cref="AsParallelWriter"/> to create a parallel writer for a list.
/// </remarks>
public unsafe struct ParallelWriter
{
/// <summary>
/// The UnsafeList to write to.
/// </summary>
public UnsafeList<T>* listData;
internal unsafe ParallelWriter(UnsafeList<T>* list)
{
listData = list;
}
/// <summary>
/// Adds a value to a collection without resizing it, ensuring capacity is checked before insertion.
/// </summary>
/// <param name="value">The value to be added to the collection.</param>
public void AddNoResize(T value)
{
var idx = Interlocked.Increment(ref listData->_count) - 1;
listData->CheckNoResizeCapacity(idx, 1);
UnsafeUtilities.WriteArrayElement(listData->_array.GetUnsafePtr(), idx, value);
}
/// <summary>
/// Adds a specified number of elements from a pointer to a buffer without resizing the underlying storage.
/// </summary>
/// <param name="ptr">Points to the source data to be copied into the buffer.</param>
/// <param name="count">Indicates the number of elements to be added from the source data.</param>
public void AddRangeNoResize(T* ptr, int count)
{
var idx = Interlocked.Add(ref listData->_count, count) - count;
listData->CheckNoResizeCapacity(idx, count);
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(listData->_array.GetUnsafePtr(), idx), ptr, (uint)(count * sizeof(T)));
}
}
private UnsafeArray<T> _array;
private int _count;
public readonly int Count => _count;
public readonly int Capacity => _array.Count;
public readonly bool IsCreated => _array.IsCreated;
public readonly ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _array[index];
}
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeList<T>*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public ParallelWriter AsParallelWriter() => new((UnsafeList<T>*)UnsafeUtilities.AddressOf(ref this));
public UnsafeList() : this(1, Allocator.Persistent)
{
}
public UnsafeList(int capacity, Allocator allocator, AllocationOption allocationType = AllocationOption.None)
{
_array = new UnsafeArray<T>(capacity, allocator, allocationType);
}
private readonly void CheckNoResizeCapacity(int count)
{
CheckNoResizeCapacity(count, Count);
}
private readonly void CheckNoResizeCapacity(int index, int count)
{
if (index + count > Capacity)
{
throw new Exception(
$"AddNoResize assumes that list capacity is sufficient (Capacity {Capacity}, Size {Count}), requested count {count}!"
);
}
}
private readonly void CheckIndexCount(int index, int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException($"Value for count {count} must be positive.");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException($"Value for index {index} must be positive.");
}
if (index > Count)
{
throw new ArgumentOutOfRangeException($"Value for index {index} is out of bounds.");
}
if (index + count > Count)
{
throw new ArgumentOutOfRangeException($"Value for count {count} is out of bounds.");
}
}
public void Add(T value)
{
if (_count >= Capacity)
{
Resize(Capacity + (int)(Capacity * 0.5f));
}
UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
_count++;
}
public void AddNoResize(T value)
{
CheckNoResizeCapacity(1);
UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
_count++;
}
public void AddRange(Span<T> values, int count)
{
var newSize = _count + count;
if (newSize > Capacity)
{
Resize(Capacity + count);
}
fixed (T* ptr = values)
{
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), ptr, (uint)(count * sizeof(T)));
}
_count += count;
}
public void AddRangeNoResize(ReadOnlySpan<T> values)
{
CheckNoResizeCapacity(values.Length);
fixed (T* ptr = values)
{
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), ptr, (uint)(values.Length * sizeof(T)));
}
_count += values.Length;
}
public void AddRangeNoResize(T* ptr, int count)
{
CheckNoResizeCapacity(count);
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), ptr, (uint)(count * sizeof(T)));
_count += count;
}
public void RemoveRange(int start, int length)
{
CheckIndexCount(start, length);
if (length <= 0)
{
return;
}
var copyFrom = Math.Min(start + length, _count);
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), start),
UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), copyFrom),
(uint)((_count - copyFrom) * sizeof(T))
);
_count -= length;
}
public void RemoveAt(int index)
{
RemoveRange(index, 1);
}
public void RemoveRangeSwapBack(int start, int length)
{
CheckIndexCount(start, length);
if (length <= 0)
{
return;
}
var copyFrom = Math.Min(_count - length, start + length);
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), start),
UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), copyFrom),
(uint)((_count - copyFrom) * sizeof(T))
);
_count -= length;
}
public void RemoveAtSwapBack(int index)
{
RemoveRangeSwapBack(index, 1);
}
public void Resize(int newSize)
{
_array.Resize(newSize);
if (_count > newSize)
{
_count = newSize;
}
}
public void Clear()
{
_array.Clear();
_count = 0;
}
/// <summary>
/// Returns a pointer to the underlying data of the array in an unsafe manner. This method is optimized for
/// performance.
/// </summary>
/// <returns>A pointer to the array's data.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _array.GetUnsafePtr();
}
/// <summary>
/// Converts the current array to an UnsafeArray representation using its pointer and count.
/// </summary>
/// <returns>Returns a new UnsafeArray instance initialized with the array's unsafe pointer and its count.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly UnsafeArray<T> AsUnsafeArray()
{
return new UnsafeArray<T>(_array.GetUnsafePtr(), _count);
}
public void Dispose()
{
_array.Dispose();
_count = 0;
}
}

View File

@@ -0,0 +1,174 @@
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
/// <summary>
/// A structure that implements a queue using unmanaged types for efficient memory management.
/// </summary>
/// <typeparam name="T">Represents the type of elements stored in the queue, which must be an unmanaged type for performance and safety.</typeparam>
public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
where T : unmanaged
{
public struct Enumerator : IEnumerator<T>
{
private UnsafeQueue<T>* _collection;
private int _index;
private T _value;
public Enumerator(UnsafeQueue<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = UnsafeUtilities.ReadArrayElement<T>(_collection->_array.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
}
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;
}
public readonly void Dispose()
{
}
}
private UnsafeArray<T> _array;
private int _count;
private int _offset;
public readonly int Count => _count;
public readonly int Capacity => _array.Count;
public readonly bool IsCreated => _array.IsCreated;
public readonly T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _array[index];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _array[index] = value;
}
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeQueue<T>*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public UnsafeQueue() : this(1, Allocator.Persistent)
{
}
public UnsafeQueue(int capacity, Allocator allocator, AllocationOption allocationType = AllocationOption.None)
{
_array = new UnsafeArray<T>(capacity, allocator, allocationType);
}
/// <summary>
/// Adds an element to the end of a collection, resizing if the current capacity is reached. The new element is
/// stored in a circular buffer.
/// </summary>
/// <param name="value">The item to be added to the collection.</param>
public void Enqueue(T value)
{
if (_count >= Capacity)
{
Resize(Capacity + (int)(Capacity * 0.5f));
}
UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), (_offset + _count) % Capacity, value);
_count++;
}
/// <summary>
/// Removes and returns the element at the front of the queue. If the queue is empty, an exception is thrown.
/// </summary>
/// <returns>The element that was removed from the front of the queue.</returns>
/// <exception cref="InvalidOperationException">Thrown when attempting to dequeue from an empty queue.</exception>
public T Dequeue()
{
if (_count == 0)
{
throw new InvalidOperationException("Queue is empty.");
}
var value = UnsafeUtilities.ReadArrayElement<T>(_array.GetUnsafePtr(), _offset);
_offset = (_offset + 1) % Capacity;
_count--;
return value;
}
/// <summary>
/// Attempts to remove and return an item from a collection. Returns a boolean indicating success or failure.
/// </summary>
/// <param name="value">The output variable that will hold the dequeued item if the operation is successful.</param>
/// <returns>True if an item was successfully dequeued, otherwise false.</returns>
public bool TryDequeue(out T value)
{
if (_count == 0)
{
value = default;
return false;
}
value = Dequeue();
return true;
}
public void Resize(int newSize)
{
_array.Resize(newSize);
if (_count > newSize)
{
_count = newSize;
}
}
public void Clear()
{
_array.Clear();
_count = 0;
_offset = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _array.GetUnsafePtr();
}
public void Dispose()
{
_array.Dispose();
_count = 0;
_offset = 0;
}
}

View File

@@ -0,0 +1,107 @@
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
where T : unmanaged
{
private UnsafeArray<T> _array;
private int _count;
public readonly int Count => _count;
public readonly bool IsCreated => _array.IsCreated;
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public UnsafeStack() : this(1, Allocator.Persistent)
{
}
public UnsafeStack(int initialSize, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
{
_array = new UnsafeArray<T>(initialSize, allocator, allocationOption);
}
public void Push(T value)
{
if (_count >= _array.Count)
{
Resize(_array.Count + (int)(_array.Count * 0.5f));
}
UnsafeUtilities.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
_count++;
}
public T Pop()
{
if (_count == 0)
{
throw new InvalidOperationException("Stack is empty.");
}
_count--;
return _array[_count];
}
public bool TryPop(out T value)
{
if (_count == 0)
{
value = default;
return false;
}
_count--;
value = _array[_count];
return true;
}
public readonly T Peek()
{
if (_count == 0)
{
throw new InvalidOperationException("Stack is empty.");
}
return _array[_count - 1];
}
public void Resize(int newSize)
{
_array.Resize(newSize);
if (_count > newSize)
{
_count = newSize;
}
}
public void Clear()
{
_array.Clear();
_count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _array.GetUnsafePtr();
}
public void Dispose()
{
_array.Dispose();
_count = 0;
}
}

View File

@@ -0,0 +1,52 @@
using Misaki.HighPerformance.LowLevel.Collections;
namespace Misaki.HighPerformance.LowLevel.Contracts;
using unsafe AllocFunc = delegate* unmanaged<void*, nuint, nuint, AllocationOption, void*>;
using unsafe FreeFunc = delegate* unmanaged<void*, void*, void>;
using unsafe ReallocFunc = delegate* unmanaged<void*, void*, nuint, nuint, void*>;
public unsafe readonly struct AllocationHandle
{
public void* Allocator
{
get;
}
public AllocFunc Alloc
{
get;
}
public ReallocFunc Realloc
{
get;
}
public FreeFunc Free
{
get;
}
public AllocationHandle(void* allocator, AllocFunc alloc, ReallocFunc realloc, FreeFunc free)
{
Allocator = allocator;
Alloc = alloc;
Realloc = realloc;
Free = free;
}
}
/// <summary>
/// Represents an allocator interface for managing memory allocations.
/// </summary>
/// <remarks>
/// The allocator must be static or pined to a specific memory region.
/// </remarks>
public unsafe interface IAllocator
{
public ref AllocationHandle Handle
{
get;
}
}

View File

@@ -0,0 +1,46 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Diagnostics;
using System.Text;
namespace Misaki.HighPerformance.LowLevel.Exceptions;
public class MemoryLeakException(params AllocationManager.AllocationInfo[] Infos) : Exception
{
private static string GetMessage(StackTrace? stackTrace)
{
if (stackTrace == null)
{
return "No stack trace available.";
}
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("Memory leak detected at: ");
for (var i = 0; i < stackTrace.FrameCount; i++)
{
var frame = stackTrace.GetFrame(i);
if (frame != null)
{
stringBuilder.AppendLine($"File: {frame.GetFileName()}, Method: {DiagnosticMethodInfo.Create(frame)?.Name}, Line: {frame.GetFileLineNumber()}");
}
}
return stringBuilder.ToString();
}
public override string Message
{
get
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"Found {Infos.Length} memory lakes!");
foreach (var info in Infos)
{
stringBuilder.AppendLine(GetMessage(info.StackTrace));
}
return stringBuilder.ToString();
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel;
public readonly struct FunctionPointer<T>
where T : Delegate
{
private readonly nint _ptr;
private readonly T _delegate;
/// <summary>
/// Gets the native function pointer associated with this function pointer instance.
/// </summary>
public readonly nint Pointer => _ptr;
/// <summary>
/// Gets the delegate instance associated with the specified function pointer.
/// </summary>
/// <remarks>This property uses <see
/// cref="Marshal.GetDelegateForFunctionPointer{TDelegate}"/> to convert the function
/// pointer to a delegate. Ensure that the function pointer is valid and compatible with the delegate type
/// <typeparamref name="T"/>.</remarks>
public T Invoke => _delegate;
/// <summary>
/// Creates a new instance of this function pointer with the following native pointer.
/// </summary>
/// <param name="ptr"></param>
public FunctionPointer(nint ptr)
{
_ptr = ptr;
_delegate = Marshal.GetDelegateForFunctionPointer<T>(ptr);
}
}

View File

@@ -0,0 +1,499 @@
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Contracts;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Helpers;
public unsafe struct HashMapHelper<TKey> : IDisposable
where TKey : unmanaged, IEquatable<TKey>
{
internal unsafe struct Enumerator
{
public HashMapHelper<TKey>* buffer;
public int index;
public int bucketIndex;
public int nextIndex;
public unsafe Enumerator(HashMapHelper<TKey>* data)
{
buffer = data;
index = -1;
bucketIndex = 0;
nextIndex = -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
return buffer->MoveNext(ref bucketIndex, ref nextIndex, out index);
}
public void Reset()
{
index = -1;
bucketIndex = 0;
nextIndex = -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public KeyValuePair<TKey, TValue> GetCurrent<TValue>()
where TValue : unmanaged
{
return new KeyValuePair<TKey, TValue>(buffer->_keys[index], UnsafeUtilities.ReadArrayElement<TValue>(buffer->_buffer, index));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TKey GetCurrentKey()
{
if (index != -1)
{
return buffer->_keys[index];
}
return default;
}
public void Dispose()
{
}
}
// This buffer has 4 parts: TValue, TKey, Next, Buckets.
private byte* _buffer;
internal TKey* _keys;
internal int* _next;
internal int* _buckets;
private int _count;
private int _capacity;
private int _bucketCapacity;
private int _allocatedIndex;
private int _firstFreeIndex;
private readonly int _sizeOfTValue;
private readonly int _log2MinGrowth;
private AllocationHandle* _handle;
public const int MINIMAL_CAPACITY = 64;
public readonly byte* Buffer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer;
}
public readonly int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count;
}
public readonly int Capacity
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _capacity;
}
public readonly bool IsCreated
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _buffer != null;
}
public readonly bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => !IsCreated || _count == 0;
}
private static int CalculateDataSize(int capacity, int bucketCapacity, int sizeOfTValue, out int outKeyOffset, out int outNextOffset, out int outBucketOffset)
{
var sizeOfTKey = sizeof(TKey);
var sizeOfInt = sizeof(int);
var valuesSize = sizeOfTValue * capacity;
var keysSize = sizeOfTKey * capacity;
var nextSize = sizeOfInt * capacity;
var bucketSize = sizeOfInt * bucketCapacity;
var totalSize = valuesSize + keysSize + nextSize + bucketSize;
outKeyOffset = 0 + valuesSize;
outNextOffset = outKeyOffset + keysSize;
outBucketOffset = outNextOffset + nextSize;
return totalSize;
}
public HashMapHelper(int capacity, int sizeOfTValue, uint minGrowth, ref AllocationHandle handle, AllocationOption allocationOption)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
}
if (sizeOfTValue <= 0)
{
throw new ArgumentOutOfRangeException(nameof(sizeOfTValue), "Size of TValue must be greater than zero.");
}
_capacity = CalcCapacityCeilPow2(capacity);
_bucketCapacity = _capacity * 2;
_sizeOfTValue = sizeOfTValue;
_log2MinGrowth = BitOperations.Log2(minGrowth);
_handle = (AllocationHandle*)Unsafe.AsPointer(ref handle);
var totalSize = CalculateDataSize(_capacity, _bucketCapacity, sizeOfTValue,
out var keyOffset, out var nextOffset, out var bucketOffset);
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, allocationOption);
Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly int CalcCapacityCeilPow2(int capacity)
{
capacity = Math.Max(Math.Max(1, _count), capacity);
var newCapacity = Math.Max(capacity, 1 << _log2MinGrowth);
var result = MathUtilities.CeilPow2(newCapacity);
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly int GetBucket(in TKey key)
{
var h = (uint)key.GetHashCode();
return (int)(h & (uint)(_bucketCapacity - 1));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void CheckIndexOutOfBounds(int idx)
{
if ((uint)idx >= (uint)_capacity)
{
throw new InvalidOperationException($"Index {idx} is out of bounds for the hash map with capacity {_capacity}.");
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AllocateBuffer(int totalSize, int keyOffset, int nextOffset, int bucketOffset, AllocationOption allocationOption)
{
var alignSize = sizeof(TKey) > sizeof(int) ? AlignOf<TKey>() : AlignOf<int>();
_buffer = (byte*)_handle->Alloc(_handle->Allocator, (uint)totalSize, (uint)alignSize, allocationOption);
_keys = (TKey*)(_buffer + keyOffset);
_next = (int*)(_buffer + nextOffset);
_buckets = (int*)(_buffer + bucketOffset);
}
internal void ResizeExact(int newCapacity, int newBucketCapacity)
{
var totalSize = CalculateDataSize(newCapacity, newBucketCapacity, _sizeOfTValue,
out var keyOffset, out var nextOffset, out var bucketOffset);
var oldBuffer = _buffer;
var oldKeys = _keys;
var oldNext = _next;
var oldBuckets = _buckets;
var oldBucketCapacity = _bucketCapacity;
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, AllocationOption.None);
_capacity = newCapacity;
_bucketCapacity = newBucketCapacity;
Clear();
for (int i = 0, num = oldBucketCapacity; i < num; ++i)
{
for (var idx = oldBuckets[i]; idx != -1; idx = oldNext[idx])
{
var newIdx = TryAdd(oldKeys[idx]);
MemCpy(_buffer + _sizeOfTValue * newIdx, oldBuffer + _sizeOfTValue * idx, (nuint)_sizeOfTValue);
}
}
_handle->Free(_handle->Allocator, oldBuffer);
}
internal void Resize(int newCapacity)
{
newCapacity = Math.Max(newCapacity, _count);
var newBucketCapacity = MathUtilities.CeilPow2(newCapacity * 2);
if (_capacity == newCapacity && _bucketCapacity == newBucketCapacity)
{
return;
}
ResizeExact(newCapacity, newBucketCapacity);
}
public void TrimExcess()
{
var capacity = CalcCapacityCeilPow2(_count);
ResizeExact(capacity, capacity * 2);
}
public int Find(in TKey key)
{
if (_allocatedIndex <= 0)
{
return -1;
}
// First find the slot based on the hash
var bucket = GetBucket(key);
var entryIdx = _buckets[bucket];
if ((uint)entryIdx < (uint)_capacity)
{
var nextPtrs = _next;
while (!UnsafeUtilities.ReadArrayElement<TKey>(_keys, entryIdx).Equals(key))
{
entryIdx = nextPtrs[entryIdx];
if ((uint)entryIdx >= (uint)_capacity)
{
return -1;
}
}
return entryIdx;
}
return -1;
}
public int TryAdd(in TKey key)
{
var k = key;
if (Find(in key) != -1)
{
return -1;
}
// Allocate an entry from the free list
int idx;
int* next;
if (_allocatedIndex >= _capacity && _firstFreeIndex < 0)
{
var newCap = CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth));
Resize(newCap);
}
idx = _firstFreeIndex;
if (idx >= 0)
{
_firstFreeIndex = _next[idx];
}
else
{
idx = _allocatedIndex++;
}
CheckIndexOutOfBounds(idx);
UnsafeUtilities.WriteArrayElement(_keys, idx, key);
var bucket = GetBucket(key);
// Add the index to the hash-map
next = _next;
next[idx] = _buckets[bucket];
_buckets[bucket] = idx;
_count++;
return idx;
}
public int TryRemove(in TKey key)
{
if (_capacity == 0)
{
return -1;
}
var removed = 0;
// First find the slot based on the hash
var bucket = GetBucket(key);
var prevEntry = -1;
var entryIdx = _buckets[bucket];
while (entryIdx >= 0 && entryIdx < _capacity)
{
if (UnsafeUtilities.ReadArrayElement<TKey>(_keys, entryIdx).Equals(key))
{
removed++;
// Found matching element, remove it
if (prevEntry < 0)
{
_buckets[bucket] = _next[entryIdx];
}
else
{
_next[prevEntry] = _next[entryIdx];
}
// And free the index
var nextIdx = _next[entryIdx];
_next[entryIdx] = _firstFreeIndex;
_firstFreeIndex = entryIdx;
entryIdx = nextIdx;
break;
}
else
{
prevEntry = entryIdx;
entryIdx = _next[entryIdx];
}
}
_count -= removed;
return 0 != removed ? removed : -1;
}
public bool TryGetValue<TValue>(in TKey key, out TValue item)
where TValue : unmanaged
{
var idx = Find(key);
if (idx != -1)
{
item = UnsafeUtilities.ReadArrayElement<TValue>(_buffer, idx);
return true;
}
item = default;
return false;
}
public bool MoveNextSearch(ref int bucketIndex, ref int nextIndex, out int index)
{
for (int i = bucketIndex, num = _bucketCapacity; i < num; ++i)
{
var idx = _buckets[i];
if (idx != -1)
{
index = idx;
bucketIndex = i + 1;
nextIndex = _next[idx];
return true;
}
}
index = -1;
bucketIndex = _bucketCapacity;
nextIndex = -1;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext(ref int bucketIndex, ref int nextIndex, out int index)
{
if (nextIndex != -1)
{
index = nextIndex;
nextIndex = _next[nextIndex];
return true;
}
return MoveNextSearch(ref bucketIndex, ref nextIndex, out index);
}
internal UnsafeArray<TKey> GetKeyArray(Allocator allocator)
{
var result = new UnsafeArray<TKey>(_count, allocator);
for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; i++)
{
var bucket = _buckets[i];
while (bucket != -1)
{
result[count++] = UnsafeUtilities.ReadArrayElement<TKey>(_keys, bucket);
bucket = _next[bucket];
}
}
return result;
}
internal UnsafeArray<TValue> GetValueArray<TValue>(Allocator allocator)
where TValue : unmanaged
{
var result = new UnsafeArray<TValue>(_count, allocator);
for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; ++i)
{
var bucket = _buckets[i];
while (bucket != -1)
{
result[count++] = UnsafeUtilities.ReadArrayElement<TValue>(_buffer, bucket);
bucket = _next[bucket];
}
}
return result;
}
public UnsafeArray<KeyValuePair<TKey, TValue>> GetKeyValueArrays<TValue>(Allocator allocator)
where TValue : unmanaged
{
var result = new UnsafeArray<KeyValuePair<TKey, TValue>>(_count, allocator);
for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; i++)
{
var bucket = _buckets[i];
while (bucket != -1)
{
result[count] = new(UnsafeUtilities.ReadArrayElement<TKey>(_keys, bucket),
UnsafeUtilities.ReadArrayElement<TValue>(_buffer, bucket));
count++;
bucket = _next[bucket];
}
}
return result;
}
public void Clear()
{
MemSet(_buckets, 0xff, (nuint)_bucketCapacity * sizeof(int));
MemSet(_next, 0xff, (nuint)_capacity * sizeof(int));
_count = 0;
_firstFreeIndex = -1;
_allocatedIndex = 0;
}
public void Dispose()
{
if (IsCreated)
{
_handle->Free(_handle->Allocator, _buffer);
_buffer = null;
_keys = null;
_next = null;
_buckets = null;
_count = 0;
_capacity = 0;
_bucketCapacity = 0;
}
}
}

View File

@@ -0,0 +1,352 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace Misaki.HighPerformance.LowLevel.Helpers;
public static unsafe partial class MemoryUtilities
{
[DoesNotReturn]
private static void ThrowMustBeNullTerminatedString()
{
throw new ArgumentException("Arg_MustBeNullTerminatedString");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> LoadVector128(ref byte start, nuint offset)
=> Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.AddByteOffset(ref start, offset));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<byte> LoadVector256(ref byte start, nuint offset)
=> Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.AddByteOffset(ref start, offset));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint GetByteVector128SpanLength(nuint offset, int length)
=> (uint)((length - (int)offset) & ~(Vector128<byte>.Count - 1));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint GetByteVector256SpanLength(nuint offset, int length)
=> (uint)((length - (int)offset) & ~(Vector256<byte>.Count - 1));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint GetByteVector512SpanLength(nuint offset, int length)
=> (uint)((length - (int)offset) & ~(Vector512<byte>.Count - 1));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe nuint UnalignedCountVector128(byte* searchSpace)
{
var unaligned = (nint)searchSpace & (Vector128<byte>.Count - 1);
return (uint)((Vector128<byte>.Count - unaligned) & (Vector128<byte>.Count - 1));
}
/// <summary>
/// Searches for the first occurrence of a null byte (0x00) in a given byte array.
/// </summary>
/// <param name="searchSpace">A pointer to the byte array where the search will be performed.</param>
/// <returns>Returns the index of the first null byte found in the array..</returns>
/// <exception cref="ArgumentException">Thrown if the byte array is not null-terminated.</exception>"
public static unsafe int IndexOfNullByte(byte* searchSpace)
{
const int Length = int.MaxValue;
const uint uValue = 0; // Use uint for comparisons to avoid unnecessary 8->32 extensions
nuint offset = 0; // Use nuint for arithmetic to avoid unnecessary 64->32->64 truncations
var lengthToExamine = (nuint)(uint)Length;
if (Vector128.IsHardwareAccelerated)
{
// Avx2 branch also operates on Sse2 sizes, so check is combined.
lengthToExamine = UnalignedCountVector128(searchSpace);
}
SequentialScan:
while (lengthToExamine >= 8)
{
lengthToExamine -= 8;
if (uValue == searchSpace[offset])
goto Found;
if (uValue == searchSpace[offset + 1])
goto Found1;
if (uValue == searchSpace[offset + 2])
goto Found2;
if (uValue == searchSpace[offset + 3])
goto Found3;
if (uValue == searchSpace[offset + 4])
goto Found4;
if (uValue == searchSpace[offset + 5])
goto Found5;
if (uValue == searchSpace[offset + 6])
goto Found6;
if (uValue == searchSpace[offset + 7])
goto Found7;
offset += 8;
}
if (lengthToExamine >= 4)
{
lengthToExamine -= 4;
if (uValue == searchSpace[offset])
goto Found;
if (uValue == searchSpace[offset + 1])
goto Found1;
if (uValue == searchSpace[offset + 2])
goto Found2;
if (uValue == searchSpace[offset + 3])
goto Found3;
offset += 4;
}
while (lengthToExamine > 0)
{
lengthToExamine -= 1;
if (uValue == searchSpace[offset])
goto Found;
offset += 1;
}
// We get past SequentialScan only if IsHardwareAccelerated is true; and remain length is greater than Vector length.
// However, we still have the redundant check to allow the JIT to see that the code is unreachable and eliminate it when the platform does not
// have hardware accelerated. After processing Vector lengths we return to SequentialScan to finish any remaining.
if (Vector512.IsHardwareAccelerated)
{
if (offset < Length)
{
if ((((uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
{
// Not currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
// with no upper bound e.g. String.strlen.
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
// This ensures we do not fault across memory pages while searching for an end of string.
var search = Vector128.Load(searchSpace + offset);
// Same method as below
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector128<byte>.Count;
}
else
{
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
}
if ((((uint)searchSpace + offset) & (nuint)(Vector512<byte>.Count - 1)) != 0)
{
// Not currently aligned to Vector512 (is aligned to Vector256); this can cause a problem for searches
// with no upper bound e.g. String.strlen.
// Start with a check on Vector256 to align to Vector512, before moving to processing Vector256.
// This ensures we do not fault across memory pages while searching for an end of string.
var search = Vector256.Load(searchSpace + offset);
// Same method as below
var matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector256<byte>.Count;
}
else
{
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
}
lengthToExamine = GetByteVector512SpanLength(offset, Length);
if (lengthToExamine > offset)
{
do
{
var search = Vector512.Load(searchSpace + offset);
var matches = Vector512.Equals(Vector512<byte>.Zero, search).ExtractMostSignificantBits();
// Note that MoveMask has converted the equal vector elements into a set of bit flags,
// So the bit position in 'matches' corresponds to the element offset.
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector512<byte>.Count;
continue;
}
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
} while (lengthToExamine > offset);
}
lengthToExamine = GetByteVector256SpanLength(offset, Length);
if (lengthToExamine > offset)
{
var search = Vector256.Load(searchSpace + offset);
// Same method as above
var matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector256<byte>.Count;
}
else
{
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
}
lengthToExamine = GetByteVector128SpanLength(offset, Length);
if (lengthToExamine > offset)
{
var search = Vector128.Load(searchSpace + offset);
// Same method as above
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector128<byte>.Count;
}
else
{
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
}
if (offset < Length)
{
lengthToExamine = (Length - offset);
goto SequentialScan;
}
}
}
else if (Vector256.IsHardwareAccelerated)
{
if (offset < Length)
{
if ((((uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
{
// Not currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
// with no upper bound e.g. String.strlen.
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
// This ensures we do not fault across memory pages while searching for an end of string.
var search = Vector128.Load(searchSpace + offset);
// Same method as below
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector128<byte>.Count;
}
else
{
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
}
lengthToExamine = GetByteVector256SpanLength(offset, Length);
if (lengthToExamine > offset)
{
do
{
var search = Vector256.Load(searchSpace + offset);
var matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
// Note that MoveMask has converted the equal vector elements into a set of bit flags,
// So the bit position in 'matches' corresponds to the element offset.
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector256<byte>.Count;
continue;
}
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
} while (lengthToExamine > offset);
}
lengthToExamine = GetByteVector128SpanLength(offset, Length);
if (lengthToExamine > offset)
{
var search = Vector128.Load(searchSpace + offset);
// Same method as above
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
if (matches == 0)
{
// Zero flags set so no matches
offset += (nuint)Vector128<byte>.Count;
}
else
{
// Find bitflag offset of first match and add to current offset
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
}
if (offset < Length)
{
lengthToExamine = (Length - offset);
goto SequentialScan;
}
}
}
else if (Vector128.IsHardwareAccelerated)
{
if (offset < Length)
{
lengthToExamine = GetByteVector128SpanLength(offset, Length);
while (lengthToExamine > offset)
{
var search = Vector128.Load(searchSpace + offset);
// Same method as above
var compareResult = Vector128.Equals(Vector128<byte>.Zero, search);
if (compareResult == Vector128<byte>.Zero)
{
// Zero flags set so no matches
offset += (nuint)Vector128<byte>.Count;
continue;
}
// Find bitflag offset of first match and add to current offset
var matches = compareResult.ExtractMostSignificantBits();
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
}
if (offset < Length)
{
lengthToExamine = (Length - offset);
goto SequentialScan;
}
}
}
ThrowMustBeNullTerminatedString();
Found: // Workaround for https://github.com/dotnet/runtime/issues/8795
return (int)offset;
Found1:
return (int)(offset + 1);
Found2:
return (int)(offset + 2);
Found3:
return (int)(offset + 3);
Found4:
return (int)(offset + 4);
Found5:
return (int)(offset + 5);
Found6:
return (int)(offset + 6);
Found7:
return (int)(offset + 7);
}
}

View File

@@ -0,0 +1,181 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Helpers;
public static unsafe partial class MemoryUtilities
{
[StructLayout(LayoutKind.Sequential)]
private struct AlignOfHelper<T>
where T : struct
{
public byte dummy;
public T data;
}
/// <summary>
/// Allocates a block of memory of the specified size in bytes.
/// </summary>
/// <param name="size">Specifies the number of bytes to allocate in memory.</param>
/// <returns>Returns a pointer to the allocated memory block.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* Malloc(nuint size)
{
return NativeMemory.Alloc(size);
}
/// <summary>
/// Allocates a block of memory with a specified size and alignment.
/// </summary>
/// <param name="size">Specifies the total number of bytes to allocate for the memory block.</param>
/// <param name="alignment">Defines the required alignment for the allocated memory address.</param>
/// <returns>Returns a pointer to the allocated memory block.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* AlignedAlloc(nuint size, nuint alignment)
{
return NativeMemory.AlignedAlloc(size, alignment);
}
/// <summary>
/// Resizes a previously allocated memory block to a new size. It returns a pointer to the reallocated memory.
/// </summary>
/// <param name="ptr">The pointer to the memory block that needs to be resized.</param>
/// <param name="size">The new size for the memory block after resizing.</param>
/// <returns>A pointer to the reallocated memory block, or null if the operation fails.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* Realloc(void* ptr, nuint size)
{
return NativeMemory.Realloc(ptr, size);
}
/// <summary>
/// Reallocates memory to a specified size with a given alignment. It returns a pointer to the newly allocated
/// memory.
/// </summary>
/// <param name="ptr">The pointer to the existing memory block that needs to be reallocated.</param>
/// <param name="size">The new size for the memory allocation.</param>
/// <param name="alignment">The required alignment for the new memory allocation.</param>
/// <returns>A pointer to the reallocated memory block, or null if the allocation fails.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* AlignedRealloc(void* ptr, nuint size, nuint alignment)
{
return NativeMemory.AlignedRealloc(ptr, size, alignment);
}
/// <summary>
/// Releases the allocated memory pointed to by the given pointer. This helps in managing memory usage effectively.
/// </summary>
/// <param name="ptr">The pointer to the memory block that needs to be freed.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Free(void* ptr)
{
if (ptr == null)
{
return;
}
NativeMemory.Free(ptr);
}
/// <summary>
/// Releases memory that was allocated with alignment requirements. It ensures proper deallocation of aligned memory
/// blocks.
/// </summary>
/// <param name="ptr">The pointer to the memory block that needs to be freed.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AlignedFree(void* ptr)
{
if (ptr == null)
{
return;
}
NativeMemory.AlignedFree(ptr);
}
/// <summary>
/// Clears a block of memory by setting it to zero. It initializes a specified number of bytes at a given memory
/// address.
/// </summary>
/// <param name="ptr">Specifies the memory address where the clearing operation will begin.</param>
/// <param name="size">Indicates the number of bytes to be cleared in the memory block.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MemClear(void* ptr, nuint size)
{
if (ptr == null || size == 0)
{
return;
}
NativeMemory.Clear(ptr, size);
}
/// <summary>
/// Sets a block of memory to a specified byte value for a given size.
/// </summary>
/// <param name="ptr">The memory address where the byte value will be set.</param>
/// <param name="size">The number of bytes to set to the specified value.</param>
/// <param name="value">The byte value to which the memory block will be initialized.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MemSet(void* ptr, byte value, nuint size)
{
if (ptr == null || size == 0)
{
return;
}
NativeMemory.Fill(ptr, size, value);
}
/// <summary>
/// Copies a block of memory from a source location to a destination location.
/// </summary>
/// <param name="source">Indicates the memory address from which data will be copied.</param>
/// <param name="destination">Specifies the memory address where the copied data will be stored.</param>
/// <param name="size">Defines the number of bytes to be copied from the source to the destination.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void MemCpy(void* source, void* destination, nuint size)
{
if (source == null || destination == null || size == 0)
{
return;
}
NativeMemory.Copy(source, destination, size);
}
/// <summary>
/// Calculates the size in bytes of a specified unmanaged type.
/// </summary>
/// <typeparam name="T">Represents an unmanaged type for which the size is being calculated.</typeparam>
/// <returns>Returns the size of the specified type as an unsigned integer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nuint SizeOf<T>()
where T : unmanaged
{
return (nuint)sizeof(T);
}
/// <summary>
/// Calculates the alignment size of a specified unmanaged type.
/// </summary>
/// <typeparam name="T">Represents an unmanaged type for which the alignment size is being calculated.</typeparam>
/// <returns>Returns the difference in size between a helper structure and the specified type.</returns>
public static nuint AlignOf<T>()
where T : unmanaged
{
return (nuint)(sizeof(AlignOfHelper<T>) - sizeof(T));
}
/// <summary>
/// Calculates the alignment size difference between a specified struct and a helper struct.
/// </summary>
/// <typeparam name="T">Represents a value type that is used to determine the alignment size.</typeparam>
/// <returns>Returns the size difference in bytes as an integer.</returns>
public static int MarshalAlignOf<T>()
where T : struct
{
return Marshal.SizeOf<AlignOfHelper<T>>() - Marshal.SizeOf<T>();
}
}

View File

@@ -0,0 +1,138 @@
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Helpers;
/// <summary>
/// Provides extension methods for copying elements between unsafe collections and spans, converting collections to
/// arrays or lists, and searching for values.
/// </summary>
public unsafe static class UnsafeCollectionExtensions
{
/// <summary>
/// Copies elements from a source UnsafeCollection to a destination Span, ensuring both have the same size.
/// </summary>
/// <typeparam name="T">Specifies the type of elements being copied, which must be unmanaged.</typeparam>
/// <param name="source">Represents the source collection from which elements are copied.</param>
/// <param name="destination">Represents the target span where elements are copied to.</param>
/// <exception cref="ArgumentException">Thrown when the sizes of the source collection and destination span do not match.</exception>
public static void CopyTo<T>(this IUnsafeCollection<T> source, Span<T> destination)
where T : unmanaged
{
if (source.Count > destination.Length)
{
throw new ArgumentException("Source collection is larger than the destination span.");
}
fixed (T* ptr = destination)
{
SystemUnsfae.CopyBlock(ptr, source.GetUnsafePtr(), (uint)(source.Count * sizeof(T)));
}
}
/// <summary>
/// Copies a range of elements from a source collection to a destination span, ensuring both are adequately sized.
/// </summary>
/// <typeparam name="T">Specifies the type of elements being copied, which must be a value type.</typeparam>
/// <param name="source">The collection from which elements are copied.</param>
/// <param name="destination">The span where the elements will be copied to.</param>
/// <param name="sourceIndex">The starting index in the source collection for the copy operation.</param>
/// <param name="destinationIndex">The starting index in the destination span where the elements will be placed.</param>
/// <param name="length">The number of elements to copy from the source to the destination.</param>
/// <exception cref="ArgumentException">Thrown when the specified range exceeds the bounds of the source collection or destination span.</exception>
public static void CopyTo<T>(this IUnsafeCollection<T> source, Span<T> destination, int sourceIndex, int destinationIndex, int length)
where T : unmanaged
{
if (sourceIndex + length > source.Count || destinationIndex + length > destination.Length)
{
throw new ArgumentException("Source collection or destination span is too small for the specified range.");
}
fixed (T* ptr = destination)
{
SystemUnsfae.CopyBlock(ptr + destinationIndex, (byte*)source.GetUnsafePtr() + sourceIndex * sizeof(T), (uint)(length * sizeof(T)));
}
}
/// <summary>
/// Copies elements from a source span to a destination unsafe collection, ensuring both have the same size.
/// </summary>
/// <typeparam name="T">Specifies the type of elements being copied, which must be unmanaged.</typeparam>
/// <param name="destination">Represents the unsafe collection that will receive the copied elements.</param>
/// <param name="source">Represents the span containing the elements to be copied to the unsafe collection.</param>
/// <exception cref="ArgumentException">Thrown when the source span and destination collection have different sizes.</exception>
public static void CopyFrom<T>(this IUnsafeCollection<T> destination, Span<T> source)
where T : unmanaged
{
if (destination.Count > source.Length)
{
throw new ArgumentException("Destination collection is larger than the source span.");
}
fixed (T* ptr = source)
{
SystemUnsfae.CopyBlock(destination.GetUnsafePtr(), ptr, (uint)(source.Length * sizeof(T)));
}
}
/// <summary>
/// Copies a specified range of elements from a source span to a destination collection.
/// </summary>
/// <typeparam name="T">Represents the type of elements being copied, which must be unmanaged.</typeparam>
/// <param name="destination">The collection where elements will be copied to.</param>
/// <param name="source">The span containing the elements to be copied.</param>
/// <param name="sourceIndex">The starting index in the source span from which to begin copying.</param>
/// <param name="destinationIndex">The starting index in the destination collection where the elements will be placed.</param>
/// <param name="length">The number of elements to copy from the source span to the destination collection.</param>
/// <exception cref="ArgumentException">Thrown when the specified range exceeds the bounds of the source span or destination collection.</exception>
public static void CopyFrom<T>(this IUnsafeCollection<T> destination, Span<T> source, int sourceIndex, int destinationIndex, int length)
where T : unmanaged
{
if (sourceIndex + length > source.Length || destinationIndex + length > destination.Count)
{
throw new ArgumentException("Source span or destination collection is too small for the specified range.");
}
fixed (T* ptr = source)
{
SystemUnsfae.CopyBlock((byte*)destination.GetUnsafePtr() + destinationIndex * sizeof(T), ptr + sourceIndex, (uint)(length * sizeof(T)));
}
}
/// <summary>
/// Converts an UnsafeCollection into a Span for efficient memory access.
/// </summary>
/// <typeparam name="T">Represents a type that can be stored in unmanaged memory.</typeparam>
/// <param name="source">The UnsafeCollection instance to be converted into a Span.</param>
/// <returns>A Span that provides a view over the elements of the UnsafeCollection.</returns>
public static Span<T> AsSpan<T>(this IUnsafeCollection<T> source)
where T : unmanaged
{
return new(source.GetUnsafePtr(), source.Count);
}
public static UnsafeArray<T> ToUnsafeArray<T>(this T[] source, Allocator allocator)
where T : unmanaged
{
var array = new UnsafeArray<T>(source.Length, allocator);
fixed (T* ptr = source)
{
MemCpy(array.GetUnsafePtr(), ptr, (uint)(source.Length * sizeof(T)));
}
return array;
}
public static UnsafeList<T> ToUnsafeList<T>(this List<T> source, Allocator allocator)
where T : unmanaged
{
var list = new UnsafeList<T>(source.Count, allocator);
fixed (T* ptr = CollectionsMarshal.AsSpan(source))
{
MemCpy(list.GetUnsafePtr(), ptr, (uint)(source.Count * sizeof(T)));
}
return list;
}
}

View File

@@ -0,0 +1,97 @@
using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Helpers;
public static unsafe class UnsafeUtilities
{
/// <summary>
/// Converts a pointer to a reference of a specified type.
/// </summary>
/// <typeparam name="T">Specifies the type of the reference to be created from the pointer.</typeparam>
/// <param name="ptr">Represents the memory address to be converted into a reference.</param>
/// <returns>Returns a reference of the specified type pointing to the given memory address.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T AsRef<T>(void* ptr)
{
return ref SystemUnsfae.AsRef<T>(ptr);
}
/// <summary>
/// Returns the address of a specified variable in memory.
/// </summary>
/// <typeparam name="T">Represents the type of the variable whose address is being retrieved.</typeparam>
/// <param name="value">The variable whose memory address is to be obtained.</param>
/// <returns>A pointer to the memory address of the specified variable.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void* AddressOf<T>(ref T value)
{
return SystemUnsfae.AsPointer(ref value);
}
/// <summary>
/// Reads an element from an unmanaged array at a specified index using a pointer.
/// </summary>
/// <typeparam name="T">Specifies the type of elements in the unmanaged array.</typeparam>
/// <param name="ptr">Points to the start of the unmanaged array from which the element is read.</param>
/// <param name="index">Indicates the position of the element to be accessed within the array.</param>
/// <returns>Returns a pointer to the element located at the specified index.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T* ReadArrayElementUnsafe<T>(void* ptr, int index) where T : unmanaged
{
return (T*)((byte*)ptr + index * sizeof(T));
}
/// <summary>
/// Reads an element from an unmanaged array using a pointer and index, returning a reference to the element.
/// </summary>
/// <typeparam name="T">Specifies the type of the elements in the unmanaged array.</typeparam>
/// <param name="ptr">Points to the start of the unmanaged array from which the element is read.</param>
/// <param name="index">Indicates the position of the element to be accessed in the array.</param>
/// <returns>A reference to the specified element in the unmanaged array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T ReadArrayElementRef<T>(void* ptr, int index) where T : unmanaged
{
return ref AsRef<T>(ReadArrayElementUnsafe<T>(ptr, index));
}
/// <summary>
/// Reads an element from an array at a specified index using a pointer to the array.
/// </summary>
/// <typeparam name="T">Specifies the type of the elements in the array, which must be unmanaged.</typeparam>
/// <param name="ptr">Points to the start of the array from which an element will be read.</param>
/// <param name="index">Indicates the position of the element to be accessed within the array.</param>
/// <returns>The element located at the specified index in the array.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T ReadArrayElement<T>(void* ptr, int index) where T : unmanaged
{
return *ReadArrayElementUnsafe<T>(ptr, index);
}
/// <summary>
/// Writes a value to a specified index of an unmanaged array using a pointer.
/// </summary>
/// <typeparam name="T">Specifies the type of the value being written to the array, which must be an unmanaged type.</typeparam>
/// <param name="ptr">Points to the beginning of the unmanaged array where the value will be written.</param>
/// <param name="index">Indicates the position in the array where the value should be stored.</param>
/// <param name="value">Represents the value to be written to the specified index of the array.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteArrayElement<T>(void* ptr, int index, T value) where T : unmanaged
{
*ReadArrayElementUnsafe<T>(ptr, index) = value;
}
/// <summary>
/// Converts an UnsafeArray of one unmanaged type to another unmanaged type without copying the elements.
/// </summary>
/// <typeparam name="TIn">Represents the type of elements in the input array.</typeparam>
/// <typeparam name="TOut">Represents the type of elements in the output array.</typeparam>
/// <param name="array">The input array containing elements of the specified input type.</param>
/// <returns>An UnsafeArray containing elements of the specified output type.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static UnsafeArray<TOut> CastArray<TIn, TOut>(UnsafeArray<TIn> array)
where TIn : unmanaged where TOut : unmanaged
{
return new UnsafeArray<TOut>(array.GetUnsafePtr(), array.Count * sizeof(TIn) / sizeof(TOut));
}
}

View File

@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Misaki.HighPerformance\Misaki.HighPerformance.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Buffer\FixedString.tt">
<LastGenOutput>FixedString.cs</LastGenOutput>
<Generator>TextTemplatingFileGenerator</Generator>
</None>
<None Update="Buffer\FixedStackString.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>FixedStackString.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Compile Update="Buffer\FixedString.cs">
<DependentUpon>FixedString.tt</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="Buffer\FixedStackString.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>FixedStackString.tt</DependentUpon>
</Compile>
</ItemGroup>
</Project>