Files
Misaki.HighPerformance/Misaki.HighPerformance.Test/UnitTest/Buffer/TestArena.cs
Misaki a0deadc363 feat(memory): refactor allocation and add new queue
Refactored memory management by removing safety checks and introducing `MemoryHandle` for centralized tracking. Simplified allocation logic across allocators and enhanced `Dispose` methods for better resource cleanup.

Added `UnsafeChunkedQueue<T>`, a lock-free, dynamically resizing queue with chunk-based memory management, supporting parallel producers and consumers.

Updated unit tests to validate new queue functionality and ensure compatibility with refactored memory logic. Incremented assembly version to 1.6.12.

BREAKING CHANGE: Removed `#if MHP_ENABLE_SAFETY_CHECKS` blocks, altering memory validation behavior.
2026-04-10 14:44:48 +09:00

357 lines
11 KiB
C#

using Misaki.HighPerformance.LowLevel.Buffer;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
[TestClass]
public unsafe class TestArena
{
[TestMethod]
public void VirtualArena_AllocateReallocateResetDispose_Works()
{
var arena = new VirtualArena(100_000);
try
{
Assert.IsGreaterThanOrEqualTo(100_000u, arena.Reserved);
Assert.AreEqual(0u, (uint)arena.Allocated);
var p1 = (byte*)arena.Allocate(64, 16, AllocationOption.Clear);
Assert.IsTrue(p1 != null);
Assert.AreEqual((nuint)0u, ((nuint)p1) & 15u);
for (var i = 0; i < 64; i++)
{
Assert.AreEqual(0, p1[i]);
p1[i] = 0x5A;
}
var sameOnShrink = arena.Reallocate(p1, 64, 32, 16, AllocationOption.None);
Assert.AreEqual((nint)p1, (nint)sameOnShrink);
var p2 = (byte*)arena.Allocate(32, 8, AllocationOption.None);
Assert.IsTrue(p2 != null);
for (var i = 0; i < 64; i++)
{
p1[i] = (byte)(i + 1);
}
var moved = (byte*)arena.Reallocate(p1, 64, 128, 16, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 64; i++)
{
Assert.AreEqual((byte)(i + 1), moved[i]);
}
arena.Reset();
Assert.AreEqual(0u, (uint)arena.Allocated);
var pAfterReset = (byte*)arena.Allocate(32, 8, AllocationOption.None);
Assert.AreEqual((nint)arena.Buffer, (nint)pAfterReset);
}
finally
{
arena.Dispose();
}
Assert.IsTrue(arena.Buffer == null);
Assert.AreEqual(0u, (uint)arena.Reserved);
Assert.IsNull(arena.Allocate(16, 8, AllocationOption.None));
// Double dispose should be safe
arena.Dispose();
}
[TestMethod]
public void VirtualArena_ReallocateTailExtendsInPlace_AndClearsAdditionalMemory()
{
var arena = new VirtualArena(128 * 1024);
try
{
var ptr = (byte*)arena.Allocate(32, 8, AllocationOption.None);
Assert.IsNotNull(ptr);
for (var i = 0; i < 32; i++)
ptr[i] = 0x33;
var grown = (byte*)arena.Reallocate(ptr, 32, 96, 8, AllocationOption.Clear);
Assert.AreEqual((nint)ptr, (nint)grown);
for (var i = 0; i < 32; i++)
Assert.AreEqual(0x33, grown[i]);
for (var i = 32; i < 96; i++)
Assert.AreEqual(0, grown[i]);
}
finally
{
arena.Dispose();
}
}
[TestMethod]
public void VirtualArena_MultiThreadedAllocation_IsThreadSafe()
{
var mem = (VirtualArena*)NativeMemory.AllocZeroed((nuint)sizeof(VirtualArena));
*mem = new VirtualArena(8 * 1024 * 1024);
try
{
const int threadCount = 8;
const int iterations = 2000;
var ptrs = new ConcurrentDictionary<nint, byte>();
var errors = new ConcurrentQueue<Exception>();
var shared = (IntPtr)mem;
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
try
{
var arena = (VirtualArena*)shared;
for (var j = 0; j < iterations; j++)
{
var p = (byte*)arena->Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p);
Assert.AreEqual((nuint)0, ((nuint)p) & 7);
Assert.IsTrue(ptrs.TryAdd((nint)p, 0));
}
}
catch (Exception ex)
{
errors.Enqueue(ex);
}
});
}
foreach (var t in threads)
t.Start();
foreach (var t in threads)
t.Join();
if (errors.TryDequeue(out var err))
{
throw err;
}
Assert.AreEqual(threadCount * iterations, ptrs.Count);
}
finally
{
mem->Dispose();
NativeMemory.Free(mem);
}
}
[TestMethod]
public void Arena_AllocateReallocateResetDispose_Works()
{
var arena = new Arena(1024);
try
{
var p1 = (byte*)arena.Allocate(32, 8, AllocationOption.Clear);
Assert.IsNotNull(p1);
for (var i = 0; i < 32; i++)
Assert.AreEqual(0, p1[i]);
for (var i = 0; i < 32; i++)
p1[i] = 0x42;
var same = (byte*)arena.Reallocate(p1, 32, 64, 8, AllocationOption.Clear);
Assert.AreEqual((nint)p1, (nint)same);
for (var i = 0; i < 32; i++)
Assert.AreEqual(0x42, same[i]);
for (var i = 32; i < 64; i++)
Assert.AreEqual(0, same[i]);
var p2 = (byte*)arena.Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p2);
for (var i = 0; i < 64; i++)
same[i] = (byte)(255 - i);
var moved = (byte*)arena.Reallocate(same, 64, 96, 8, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 64; i++)
Assert.AreEqual((byte)(255 - i), moved[i]);
arena.Reset();
Assert.AreEqual(0u, (uint)arena.Offset);
var firstAfterReset = (byte*)arena.Allocate(8, 8, AllocationOption.None);
Assert.AreEqual((nint)arena.Buffer, (nint)firstAfterReset);
}
finally
{
arena.Dispose();
}
Assert.IsTrue(arena.Buffer == null);
Assert.IsNull(arena.Allocate(8, 8, AllocationOption.None));
arena.Dispose();
}
[TestMethod]
public void Arena_InvalidAlignment_Throws()
{
var arena = new Arena(256);
try
{
Assert.ThrowsExactly<ArgumentException>(() => arena.Allocate(8, 3, AllocationOption.None));
}
finally
{
arena.Dispose();
}
}
[TestMethod]
public void Arena_MultiThreadedAllocation_IsThreadSafe()
{
var mem = (Arena*)NativeMemory.AllocZeroed((nuint)sizeof(Arena));
*mem = new Arena(4 * 1024 * 1024);
try
{
const int threadCount = 8;
const int iterations = 2000;
var ptrs = new ConcurrentDictionary<nint, byte>();
var errors = new ConcurrentQueue<Exception>();
var shared = (IntPtr)mem;
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
try
{
var arena = (Arena*)shared;
for (var j = 0; j < iterations; j++)
{
var p = (byte*)arena->Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(p);
Assert.AreEqual((nuint)0, ((nuint)p) & 7);
Assert.IsTrue(ptrs.TryAdd((nint)p, 0));
}
}
catch (Exception ex)
{
errors.Enqueue(ex);
}
});
}
foreach (var t in threads)
t.Start();
foreach (var t in threads)
t.Join();
if (errors.TryDequeue(out var err))
{
throw err;
}
Assert.AreEqual(threadCount * iterations, ptrs.Count);
}
finally
{
mem->Dispose();
NativeMemory.Free(mem);
}
}
[TestMethod]
public void DynamicArena_GrowthReallocateResetDispose_Works()
{
var arena = new DynamicArena(64);
try
{
var p1 = (byte*)arena.Allocate(48, 8, AllocationOption.None);
Assert.IsNotNull(p1);
for (var i = 0; i < 48; i++)
p1[i] = (byte)(i + 1);
// Force growth to another node
var p2 = (byte*)arena.Allocate(128, 8, AllocationOption.Clear);
Assert.IsNotNull(p2);
Assert.AreNotEqual((nint)p1, (nint)p2);
for (var i = 0; i < 128; i++)
Assert.AreEqual(0, p2[i]);
var moved = (byte*)arena.Reallocate(p1, 48, 96, 8, AllocationOption.None);
Assert.IsNotNull(moved);
for (var i = 0; i < 48; i++)
Assert.AreEqual((byte)(i + 1), moved[i]);
arena.Reset();
var firstAfterReset = (byte*)arena.Allocate(16, 8, AllocationOption.None);
Assert.IsNotNull(firstAfterReset);
Assert.AreEqual((nint)p1, (nint)firstAfterReset);
}
finally
{
arena.Dispose();
}
Assert.IsNull(arena.Allocate(8, 8, AllocationOption.None));
arena.Dispose();
}
[TestMethod]
public void DynamicArena_MultiThreadedAllocation_IsThreadSafe()
{
var mem = (DynamicArena*)NativeMemory.AllocZeroed((nuint)sizeof(DynamicArena));
*mem = new DynamicArena(256);
try
{
const int threadCount = 8;
const int iterations = 1000;
var ptrs = new ConcurrentDictionary<nint, byte>();
var errors = new ConcurrentQueue<Exception>();
var shared = (IntPtr)mem;
var threads = new Thread[threadCount];
for (var i = 0; i < threadCount; i++)
{
threads[i] = new Thread(() =>
{
try
{
var arena = (DynamicArena*)shared;
for (var j = 0; j < iterations; j++)
{
var p = (byte*)arena->Allocate(24, 8, AllocationOption.None);
Assert.IsNotNull(p);
Assert.AreEqual((nuint)0, ((nuint)p) & 7);
Assert.IsTrue(ptrs.TryAdd((nint)p, 0));
}
}
catch (Exception ex)
{
errors.Enqueue(ex);
}
});
}
foreach (var t in threads)
t.Start();
foreach (var t in threads)
t.Join();
if (errors.TryDequeue(out var err))
{
throw err;
}
Assert.AreEqual(threadCount * iterations, ptrs.Count);
}
finally
{
mem->Dispose();
NativeMemory.Free(mem);
}
}
}