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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Zero-alloc arena allocator + collections for high-perf parsers.
Check out the samples directory for complete, end-to-end examples of building a math expression parser with zero managed allocations (outside the arena).
**Now with Blazor WebAssembly demo — zero managed allocations in the browser**

- [**SimpleMathParser**](samples/SimpleMathParser/) [![Try SimpleMathParser](https://img.shields.io/badge/Try%20it-SimpleMathParser-blue)](samples/SimpleMathParser/): A standard .NET console app using `ArenaList`, `ArenaPtrStack`, and `ArenaString` to tokenize and evaluate expressions.
- [**SimpleMathParser**](samples/SimpleMathParser/) [![Try SimpleMathParser](https://img.shields.io/badge/Try%20it-SimpleMathParser-blue)](samples/SimpleMathParser/): A standard .NET console app using `ArenaList`, `ArenaPtrStack`, and `ArenaUtf16String` to tokenize and evaluate expressions.
- [**BlazorMathSymbolicCalculator**](samples/BlazorMathSymbolicCalculator/) [![Try BlazorMathSymbolicCalculator](https://img.shields.io/badge/Try%20it-BlazorMathSymbolicCalculator-purple)](samples/BlazorMathSymbolicCalculator/): A symbolic calculator Blazor WebAssembly app evaluating math equations in-browser natively without garbage collection.

### Reusable Parser Example
Expand Down Expand Up @@ -43,14 +43,14 @@ arena.Reset();

SharpArena includes several collection types designed to be backed by the arena allocator. These collections allocate memory from the arena and become invalid when the arena is reset or disposed. They avoid normal GC allocations.

### ArenaString
### ArenaUtf16String
A non-owning view of UTF-16 text stored in unmanaged (arena) memory.

```csharp
using SharpArena.Collections;

// Clone a managed string or span into the arena
var str = ArenaString.Clone("Hello, World!", arena);
var str = ArenaUtf16String.Clone("Hello, World!", arena);

// Can be implicitly cast to ReadOnlySpan<char>
ReadOnlySpan<char> span = str;
Expand All @@ -59,8 +59,8 @@ ReadOnlySpan<char> span = str;
Console.WriteLine(str.ToString());
```

`ArenaString` equality and hash codes are both content-based (character data + length), so separately cloned but equal text compares and hashes the same.
**When to use:** Use `ArenaString` when you need to store substrings, tokens, or parsed text during parsing or processing without creating `System.String` allocations for every token.
`ArenaUtf16String` equality and hash codes are both content-based (character data + length), so separately cloned but equal text compares and hashes the same.
**When to use:** Use `ArenaUtf16String` when you need to store substrings, tokens, or parsed text during parsing or processing without creating `System.String` allocations for every token.

### ArenaList
A dynamic array (list) backed by the arena for unmanaged structs.
Expand Down Expand Up @@ -119,7 +119,7 @@ To achieve maximum performance and zero overhead on the hot path, `ArenaAllocato

- **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.
- **Single-Threaded Collections:** Collections like `ArenaList<T>` and `ArenaUtf16String` 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.
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.3" PrivateAssets="all" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\SimpleMathParser\MathParser.cs" Link="MathParser.cs" />
</ItemGroup>
Expand Down
8 changes: 4 additions & 4 deletions samples/SimpleMathParser/MathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ public static ArenaList<Token> Tokenize(ReadOnlySpan<char> input, ArenaAllocator
{
i++;
}
var val = ArenaString.Clone(input[start..i], arena);
var val = ArenaUtf16String.Clone(input[start..i], arena);
tokens.Add(new Token(TokenType.Number, val));
continue;
}

var singleChar = ArenaString.Clone(input.Slice(i, 1), arena);
var singleChar = ArenaUtf16String.Clone(input.Slice(i, 1), arena);
switch (c)
{
case '+': tokens.Add(new Token(TokenType.Plus, singleChar)); break;
Expand Down Expand Up @@ -237,11 +237,11 @@ public Token(TokenType type, char* valuePtr, int valueLen)
}

/// <summary>
/// Initializes a new instance of the <see cref="Token"/> struct using an <see cref="ArenaString"/>.
/// Initializes a new instance of the <see cref="Token"/> struct using an <see cref="ArenaUtf16String"/>.
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="str">The arena string containing the token text.</param>
public Token(TokenType type, ArenaString str)
public Token(TokenType type, ArenaUtf16String str)
{
Type = type;
ValuePtr = str.RawPtr;
Expand Down
2 changes: 1 addition & 1 deletion samples/SimpleMathParser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ It implements a simple Shunting Yard algorithm for tokenizing and evaluating bas

## What it Demonstrates
- Using `ArenaAllocator` to manage memory for all parser state.
- Tokenizing input strings with minimal overhead using `ArenaString`.
- Tokenizing input strings with minimal overhead using `ArenaUtf16String`.
- Storing token streams efficiently with `ArenaList<T>`.
- Using `ArenaPtrStack<T>` to manage operator stacks without GC pressure.
- Gracefully handling and reporting syntax errors.
Expand Down
37 changes: 20 additions & 17 deletions src/SharpArena/Allocators/ArenaAllocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public unsafe class ArenaAllocator : IDisposable
private int _generation = 0;

private nuint _peakBytes;
private nuint _allocatedBytes;

/// <summary>
/// Returns the current generation (incremented between Reset())
Expand All @@ -31,23 +32,7 @@ public unsafe class ArenaAllocator : IDisposable
/// <summary>
/// Gets the total number of bytes currently allocated from the arena.
/// </summary>
public nuint AllocatedBytes
{
get
{
nuint total = 0;
var current = _current;
for (var seg = _first; seg != null; seg = seg->Next)
{
total += seg->Offset;
if (seg == current)
{
break;
}
}
return total;
}
}
public nuint AllocatedBytes => _allocatedBytes;

/// <summary>
/// Gets the peak number of bytes allocated from the arena over its lifetime.
Expand Down Expand Up @@ -102,8 +87,10 @@ public ArenaAllocator(
}

align = AlignUp(align, (nuint)IntPtr.Size);
var oldOffset = seg->Offset;
if (seg->TryAlloc(size, align, out var ptr))
{
_allocatedBytes += (seg->Offset - oldOffset);
return ptr;
}

Expand All @@ -114,6 +101,19 @@ public ArenaAllocator(
throw new ObjectDisposedException(nameof(ArenaAllocator));
}

if (seg->Next != null)
{
seg = seg->Next;
_current = seg;
oldOffset = seg->Offset;
if (seg->TryAlloc(size, align, out ptr))
{
_allocatedBytes += (seg->Offset - oldOffset);
return ptr;
}
continue;
}

var nextSize = NextSegmentSize(seg->Size, size);

if (nextSize < size)
Expand All @@ -131,8 +131,10 @@ public ArenaAllocator(
_current = newSeg;
seg = newSeg;

oldOffset = seg->Offset;
if (seg->TryAlloc(size, align, out ptr))
{
_allocatedBytes += (seg->Offset - oldOffset);
return ptr;
}

Expand Down Expand Up @@ -246,6 +248,7 @@ public void Reset()
seg->Offset = 0;
}

_allocatedBytes = 0;
_current = _first;
_generation++;
}
Expand Down
22 changes: 17 additions & 5 deletions src/SharpArena/Collections/ArenaBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
// ReSharper disable UnusedMember.Global
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

namespace SharpArena.Collections;

Expand Down Expand Up @@ -211,9 +210,6 @@ IEnumerator IEnumerable.GetEnumerator()
return new Enumerator(_head, _arena);
}

/// <summary>
/// Provides a value-type enumerator for iterating over the block list contents.
/// </summary>
/// <summary>
/// Provides a value-type enumerator for iterating over the block list contents.
/// </summary>
Expand All @@ -236,9 +232,20 @@ internal Enumerator(ArenaBlock<T>* head, ArenaAllocator arena)
_current = default;
}

/// <summary>
/// Gets the element in the collection at the current position of the enumerator.
/// </summary>
public T Current => _current;

/// <summary>
/// Gets the element in the collection at the current position of the enumerator.
/// </summary>
object IEnumerator.Current => _current!;

/// <summary>
/// Advances the enumerator to the next element of the collection.
/// </summary>
/// <returns><see langword="true"/> if the enumerator was successfully advanced to the next element; <see langword="false"/> if the enumerator has passed the end of the collection.</returns>
public bool MoveNext()
{
ThrowIfArenaDead();
Expand Down Expand Up @@ -269,6 +276,9 @@ private void ThrowIfArenaDead()
}
}

/// <summary>
/// Sets the enumerator to its initial position, which is before the first element in the collection.
/// </summary>
public void Reset()
{
ThrowIfArenaDead();
Expand All @@ -277,7 +287,9 @@ public void Reset()
_current = default;
}

/// <inheritdoc />
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
// Nothing to dispose
Expand Down
Loading
Loading