diff --git a/README.md b/README.md index 9d3209e..b6a2db6 100644 --- a/README.md +++ b/README.md @@ -108,5 +108,16 @@ Console.WriteLine(*ptr); * **Limitations:** Not available in WASM environments because it requires virtual memory OS APIs. * **When to use:** Better when you need to continuous blocks of very large memory without worrying about re-allocations or segment limits, and you are on a supported platform (Windows/Linux/macOS). +## Thread Safety + +> [!IMPORTANT] +> **SharpArena is strictly NOT thread-safe.** + +To achieve maximum performance and zero overhead on the hot path, `ArenaAllocator` and its associated collections do not use any synchronization primitives (locks, interlocked operations, or volatile reads). + +- **One Arena Per Thread:** You should create a separate `ArenaAllocator` instance for each thread or use a `[ThreadStatic]` field. +- **No Concurrent Access:** Do not call `Alloc`, `Reset`, or `Dispose` concurrently from multiple threads on the same instance. +- **Single-Threaded Collections:** Collections like `ArenaList` and `ArenaString` are intended to be used within the same thread that owns the arena. + ## Benchmarks See `bench/ArenaBench.md` for performance numbers compared to NativeMemory and Varena. diff --git a/src/SharpArena/Allocators/ArenaAllocator.cs b/src/SharpArena/Allocators/ArenaAllocator.cs index 2074bff..e3faaac 100644 --- a/src/SharpArena/Allocators/ArenaAllocator.cs +++ b/src/SharpArena/Allocators/ArenaAllocator.cs @@ -21,9 +21,6 @@ public unsafe class ArenaAllocator : IDisposable private bool _disposed; private int _generation = 0; - private readonly object _disposeLock = new(); - private int _activeAllocations; - private int _resetInProgress; private nuint _peakBytes; /// @@ -81,85 +78,60 @@ public ArenaAllocator( /// A pointer to the allocated memory. public void* Alloc(nuint size, nuint align = 8) { - SpinWait spinner = default; + if (_disposed) + { + throw new ObjectDisposedException(nameof(ArenaAllocator)); + } + + if (size == 0) + { + return null; + } + + var seg = _current; + if (seg is null) + { + throw new ObjectDisposedException(nameof(ArenaAllocator)); + } + + align = AlignUp(align, (nuint)IntPtr.Size); + if (seg->TryAlloc(size, align, out var ptr)) + { + return ptr; + } + while (true) { - if (Volatile.Read(ref _disposed)) + if (_disposed) { throw new ObjectDisposedException(nameof(ArenaAllocator)); } - Interlocked.Increment(ref _activeAllocations); - if (Volatile.Read(ref _disposed)) - { - Interlocked.Decrement(ref _activeAllocations); - throw new ObjectDisposedException(nameof(ArenaAllocator)); - } + var nextSize = NextSegmentSize(seg->Size, size); - if (Volatile.Read(ref _resetInProgress) != 0) + if (nextSize < size) { - Interlocked.Decrement(ref _activeAllocations); - spinner.SpinOnce(); - continue; + nextSize = AlignUp(size, DefaultPageSize); } - try + if (nextSize > _maxSegmentSize) { - if (size == 0) - { - return null; - } + nextSize = AlignUp(size, DefaultPageSize); // fallback if request > maxSegmentSize + } - var seg = _current; - if (seg is null) - { - throw new ObjectDisposedException(nameof(ArenaAllocator)); - } + var newSeg = AllocateSegment(nextSize); + seg->Next = newSeg; + _current = newSeg; + seg = newSeg; - align = AlignUp(align, (nuint)IntPtr.Size); - if (seg->TryAlloc(size, align, out var ptr)) - { - return ptr; - } - - while (true) - { - if (Volatile.Read(ref _disposed)) - { - throw new ObjectDisposedException(nameof(ArenaAllocator)); - } - - var nextSize = NextSegmentSize(seg->Size, size); - - if (nextSize < size) - { - nextSize = AlignUp(size, DefaultPageSize); - } - - if (nextSize > _maxSegmentSize) - { - nextSize = AlignUp(size, DefaultPageSize); // fallback if request > maxSegmentSize - } - - var newSeg = AllocateSegment(nextSize); - seg->Next = newSeg; - _current = newSeg; - seg = newSeg; - - if (seg->TryAlloc(size, align, out ptr)) - { - return ptr; - } - - if (nextSize == size) - { - throw new OutOfMemoryException("Failed to allocate memory in arena; request too large."); - } - } + if (seg->TryAlloc(size, align, out ptr)) + { + return ptr; } - finally + + if (nextSize == size) { - Interlocked.Decrement(ref _activeAllocations); + throw new OutOfMemoryException("Failed to allocate memory in arena; request too large."); } } } @@ -251,41 +223,24 @@ private static nuint RoundUpToPowerOfTwo(nuint x) /// public void Reset() { - lock (_disposeLock) + if (_disposed) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(ArenaAllocator)); - } - - Volatile.Write(ref _resetInProgress, 1); - try - { - SpinWait spinner = default; - while (Volatile.Read(ref _activeAllocations) != 0) - { - spinner.SpinOnce(); - } - - var currentAllocated = AllocatedBytes; - if (currentAllocated > _peakBytes) - { - _peakBytes = currentAllocated; - } + throw new ObjectDisposedException(nameof(ArenaAllocator)); + } - for (var seg = _first; seg != null; seg = seg->Next) - { - seg->Offset = 0; - } + var currentAllocated = AllocatedBytes; + if (currentAllocated > _peakBytes) + { + _peakBytes = currentAllocated; + } - _current = _first; - Interlocked.Increment(ref _generation); - } - finally - { - Volatile.Write(ref _resetInProgress, 0); - } + for (var seg = _first; seg != null; seg = seg->Next) + { + seg->Offset = 0; } + + _current = _first; + _generation++; } /// @@ -293,43 +248,31 @@ public void Reset() /// public void Dispose() { - lock (_disposeLock) - { - Dispose(true); - } + Dispose(true); } private void Dispose(bool isDisposing) { - lock (_disposeLock) + if (_disposed) { - if (_disposed) - { - return; - } - - _disposed = true; + return; + } - SpinWait spinner = default; - while (Volatile.Read(ref _activeAllocations) != 0) - { - spinner.SpinOnce(); - } + _disposed = true; - var head = _first; - _first = _current = null; + var head = _first; + _first = _current = null; - while (head != null) - { - var next = head->Next; - NativeAllocator.Free(head, _backend); - head = next; - } + while (head != null) + { + var next = head->Next; + NativeAllocator.Free(head, _backend); + head = next; + } - if (isDisposing) - { - GC.SuppressFinalize(this); - } + if (isDisposing) + { + GC.SuppressFinalize(this); } } @@ -338,9 +281,6 @@ private void Dispose(bool isDisposing) /// ~ArenaAllocator() { - lock (_disposeLock) - { - Dispose(false); - } + Dispose(false); } } diff --git a/src/SharpArena/Collections/ArenaBlock.cs b/src/SharpArena/Collections/ArenaBlock.cs index eb53c8b..20f07cf 100644 --- a/src/SharpArena/Collections/ArenaBlock.cs +++ b/src/SharpArena/Collections/ArenaBlock.cs @@ -36,6 +36,27 @@ public unsafe struct ArenaBlock public ArenaBlock* Next; } +/// +/// Metadata describing the shared state of an . +/// +[StructLayout(LayoutKind.Sequential)] +public unsafe struct ArenaBlockListHeader + where T : unmanaged +{ + /// + /// Gets or sets the total number of elements stored across all blocks. + /// + public nuint TotalCount; + /// + /// Gets or sets the total allocated capacity across all blocks. + /// + public nuint TotalCapacity; + /// + /// Gets or sets the pointer to the current block where the next addition will occur. + /// + public ArenaBlock* CurrentBlock; +} + /// /// Provides a growable sequence of arena-backed blocks that stores unmanaged values without GC allocations. /// @@ -50,11 +71,7 @@ public unsafe struct ArenaBlockList : IEnumerable private readonly ArenaAllocator _arena; private readonly int _generation; //generation snapshot private readonly ArenaBlock* _head; - private ArenaBlock* _current; - - // Cache total count and capacity to avoid O(N) traversal - private nuint _count; - private nuint _capacity; + private ArenaBlockListHeader* _header; /// /// Initializes a new instance of the struct. @@ -64,10 +81,15 @@ public unsafe struct ArenaBlockList : IEnumerable public ArenaBlockList(ArenaAllocator arena, nuint blockSize = DefaultBlockSize) { _arena = arena; - _head = _current = CreateBlock(arena, blockSize); _generation = arena.CurrentGeneration; - _count = 0; - _capacity = blockSize; + + var firstBlock = CreateBlock(arena, blockSize); + _head = firstBlock; + + _header = (ArenaBlockListHeader*)arena.Alloc((nuint)sizeof(ArenaBlockListHeader), align: (nuint)IntPtr.Size); + _header->TotalCount = 0; + _header->TotalCapacity = blockSize; + _header->CurrentBlock = firstBlock; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -113,7 +135,7 @@ public nuint Count get { CheckAliveThrowIfNot(); - return _count; + return _header->TotalCount; } } @@ -125,7 +147,7 @@ public nuint Capacity get { CheckAliveThrowIfNot(); - return _capacity; + return _header->TotalCapacity; } } @@ -140,8 +162,8 @@ public void Reset() b->Count = 0; } - _current = _head; - _count = 0; + _header->CurrentBlock = _head; + _header->TotalCount = 0; } /// @@ -151,17 +173,19 @@ public void Reset() public void Add(in T value) { CheckAliveThrowIfNot(); - if (_current->Count >= _current->Capacity) + var current = _header->CurrentBlock; + if (current->Count >= current->Capacity) { - var nextCapacity = _current->Capacity * 2; + var nextCapacity = current->Capacity * 2; var newBlock = CreateBlock(_arena, nextCapacity); - _current->Next = newBlock; - _current = newBlock; - _capacity += nextCapacity; + current->Next = newBlock; + _header->CurrentBlock = newBlock; + _header->TotalCapacity += nextCapacity; + current = newBlock; } - _current->Data[_current->Count++] = value; - _count++; + current->Data[current->Count++] = value; + _header->TotalCount++; } /// diff --git a/tests/SharpArena.Tests/Allocators/ArenaAllocatorTests.cs b/tests/SharpArena.Tests/Allocators/ArenaAllocatorTests.cs index 03f1510..43d0851 100644 --- a/tests/SharpArena.Tests/Allocators/ArenaAllocatorTests.cs +++ b/tests/SharpArena.Tests/Allocators/ArenaAllocatorTests.cs @@ -152,64 +152,6 @@ public void Alloc_ExceedingMaxSegmentSize_ShouldAllocateSuccessfully() Assert.NotEqual((nint)ptr, (nint)smallPtr); } - [Fact] - public void ConcurrentAlloc_And_Reset_ShouldNotCrash() - { - // Test beyond the basic _activeAllocations counter - using var arena = new ArenaAllocator(4096); - - var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; - int completedAllocations = 0; - - Parallel.Invoke(options, - () => - { - for (int i = 0; i < 10000; i++) - { - try - { - var ptr = arena.Alloc(16); - if (ptr != null) - { - Interlocked.Increment(ref completedAllocations); - } - } - catch (ObjectDisposedException) - { - // Dispose might have been called, but we are only testing Reset here - } - } - }, - () => - { - for (int i = 0; i < 10000; i++) - { - try - { - var ptr = arena.Alloc(32); - if (ptr != null) - { - Interlocked.Increment(ref completedAllocations); - } - } - catch (ObjectDisposedException) - { - } - } - }, - () => - { - for (int i = 0; i < 100; i++) - { - Thread.Sleep(1); - arena.Reset(); - } - } - ); - - Assert.True(completedAllocations > 0); - } - [Theory] [InlineData(8u)] [InlineData(16u)] diff --git a/tests/SharpArena.Tests/Collections/ArenaBlockTests.cs b/tests/SharpArena.Tests/Collections/ArenaBlockTests.cs index 1632ae0..51f1c56 100644 --- a/tests/SharpArena.Tests/Collections/ArenaBlockTests.cs +++ b/tests/SharpArena.Tests/Collections/ArenaBlockTests.cs @@ -130,4 +130,31 @@ public void CreateBlock_WithOverflowCapacity_ThrowsOutOfMemoryException() Assert.Throws(() => new ArenaBlockList(_arena, blockSize: overflowCapacity)); } + [Fact] + public void StructCopy_SharesState() + { + var list1 = new ArenaBlockList(_arena, blockSize: 2); + list1.Add(1); + + // Copy by value + var list2 = list1; + + list2.Add(2); + + // Both should see the same count and elements + Assert.Equal((nuint)2, list1.Count); + Assert.Equal((nuint)2, list2.Count); + + // Add more to trigger growth in list2 + list2.Add(3); // Should create a new block + + Assert.Equal((nuint)3, list1.Count); + Assert.Equal((nuint)3, list2.Count); + + var elements1 = list1.ToArray(); + var elements2 = list2.ToArray(); + + Assert.Equal(new[] { 1, 2, 3 }, elements1); + Assert.Equal(new[] { 1, 2, 3 }, elements2); + } }