Performance: allocation, Span and benchmarking

Measure first, then reduce allocations on the hot path with Span and pooling rather than rewriting code on intuition.

Measure before changing anything

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]                       // allocations per operation
[SimpleJob(warmupCount: 3, iterationCount: 10)]
public class Parsing
{
    private string _line = "";

    [GlobalSetup]
    public void Setup() =>
        _line = string.Join(",", Enumerable.Range(0, 64).Select(i => i.ToString()));

    [Benchmark(Baseline = true)]
    public int Split()
    {
        var parts = _line.Split(',');
        var sum = 0;
        foreach (var p in parts) sum += int.Parse(p);
        return sum;
    }

    [Benchmark]
    public int Span()
    {
        var sum = 0;
        var span = _line.AsSpan();
        while (!span.IsEmpty)
        {
            var comma = span.IndexOf(',');
            var slice = comma < 0 ? span : span[..comma];
            sum += int.Parse(slice);
            span = comma < 0 ? default : span[(comma + 1)..];
        }
        return sum;
    }
}

// Program.cs
// BenchmarkRunner.Run<Parsing>();
Result columnWhat it tells youWhy it matters
MeanAverage time per operationThe headline number
StdDevSpread of measurementsA large value means the result is not trustworthy
RatioTime relative to the baselineThe comparison that matters
Gen0Gen 0 collections per 1000 operationsThe allocation signal
AllocatedBytes allocated per operationThe direct lever on GC pressure
Alloc RatioAllocation relative to the baselineShows whether a change actually helped
  • Always run benchmarks in Release, with a baseline, and never trust a single run or a stopwatch in a test.
  • Optimise the code that the profiler says is hot. A faster string parser in a function called once per request changes nothing.
  • An allocation reduction that does not move the end-to-end number is churn. Measure the call site, not just the microbenchmark.

Span, Memory and pooling

using System.Buffers;
using System.Runtime.InteropServices;

// Span<T> is a stack-only view: no allocation, no copy, but it cannot be stored
// on the heap, used in an async method, or captured by a lambda.
public static int SumCsv(ReadOnlySpan<char> line)
{
    var sum = 0;
    while (true)
    {
        var comma = line.IndexOf(',');
        var token = comma < 0 ? line : line[..comma];
        if (!token.IsEmpty) sum += int.Parse(token);
        if (comma < 0) return sum;
        line = line[(comma + 1)..];
    }
}

// Memory<T> when you need to cross an await boundary or store the slice
public static async Task ProcessAsync(Memory<byte> buffer, Stream stream,
                                      CancellationToken ct)
{
    var read = await stream.ReadAsync(buffer, ct);
    Consume(buffer.Span[..read]);
}

// ArrayPool for large buffers, with a finally that always returns the array
public static string Compress(byte[] input)
{
    var pool = ArrayPool<byte>.Shared;
    var buffer = pool.Rent(input.Length * 2);
    try
    {
        var written = Deflate(input, buffer);
        return Convert.ToBase64String(buffer, 0, written);
    }
    finally
    {
        // clearArray: false is faster but leaves data readable by the next renter
        pool.Return(buffer, clearArray: false);
    }
}

// CollectionsMarshal for in-place access to a List<T> without copying
public static void BumpFirst(List<int> values)
{
    ref var first = ref CollectionsMarshal.AsSpan(values)[0];
    first++;
}
⚠️
Do not return a Span from a method, store it in a field, or use it inside an async method — the compiler rejects most of it, but the cases it allows are lifetime bugs. When you need a view to outlive the current stack frame, use Memory, or copy.

FAQ

Is string pooling worth it?
Rarely. Replacing Split with AsSpan and IndexOf removes an allocation with no shared state and no ceremony, while string interning adds a global lock and a permanent memory cost.
When is Span not the answer?
When the data must be stored, sent across an await, or passed to an API that takes a string. Converting a Span back to a string allocates, which cancels the benefit.

JSON serialisation with System.Text.Json Diagnostics and observability in production

Last refreshed 2026-09-18.