Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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.
200 changes: 70 additions & 130 deletions src/SharpArena/Allocators/ArenaAllocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand Down Expand Up @@ -81,85 +78,60 @@ public ArenaAllocator(
/// <returns>A pointer to the allocated memory.</returns>
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.");
}
}
}
Expand Down Expand Up @@ -251,85 +223,56 @@ private static nuint RoundUpToPowerOfTwo(nuint x)
/// </remarks>
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++;
}

/// <summary>
/// Disposes the arena, freeing all allocated unmanaged memory.
/// </summary>
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);
}
}

Expand All @@ -338,9 +281,6 @@ private void Dispose(bool isDisposing)
/// </summary>
~ArenaAllocator()
{
lock (_disposeLock)
{
Dispose(false);
}
Dispose(false);
}
}
62 changes: 43 additions & 19 deletions src/SharpArena/Collections/ArenaBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ public unsafe struct ArenaBlock<T>
public ArenaBlock<T>* Next;
}

/// <summary>
/// Metadata describing the shared state of an <see cref="ArenaBlockList{T}"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct ArenaBlockListHeader<T>
where T : unmanaged
{
/// <summary>
/// Gets or sets the total number of elements stored across all blocks.
/// </summary>
public nuint TotalCount;
/// <summary>
/// Gets or sets the total allocated capacity across all blocks.
/// </summary>
public nuint TotalCapacity;
/// <summary>
/// Gets or sets the pointer to the current block where the next addition will occur.
/// </summary>
public ArenaBlock<T>* CurrentBlock;
}

/// <summary>
/// Provides a growable sequence of arena-backed blocks that stores unmanaged values without GC allocations.
/// </summary>
Expand All @@ -50,11 +71,7 @@ public unsafe struct ArenaBlockList<T> : IEnumerable<T>
private readonly ArenaAllocator _arena;
private readonly int _generation; //generation snapshot
private readonly ArenaBlock<T>* _head;
private ArenaBlock<T>* _current;

// Cache total count and capacity to avoid O(N) traversal
private nuint _count;
private nuint _capacity;
private ArenaBlockListHeader<T>* _header;

/// <summary>
/// Initializes a new instance of the <see cref="ArenaBlockList{T}"/> struct.
Expand All @@ -64,10 +81,15 @@ public unsafe struct ArenaBlockList<T> : IEnumerable<T>
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<T>*)arena.Alloc((nuint)sizeof(ArenaBlockListHeader<T>), align: (nuint)IntPtr.Size);
_header->TotalCount = 0;
_header->TotalCapacity = blockSize;
_header->CurrentBlock = firstBlock;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -113,7 +135,7 @@ public nuint Count
get
{
CheckAliveThrowIfNot();
return _count;
return _header->TotalCount;
}
}

Expand All @@ -125,7 +147,7 @@ public nuint Capacity
get
{
CheckAliveThrowIfNot();
return _capacity;
return _header->TotalCapacity;
}
}

Expand All @@ -140,8 +162,8 @@ public void Reset()
b->Count = 0;
}

_current = _head;
_count = 0;
_header->CurrentBlock = _head;
_header->TotalCount = 0;
}

/// <summary>
Expand All @@ -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++;
}

/// <summary>
Expand Down
Loading
Loading