.NET Academy

Span<T> and allocation-free code

Work with slices of arrays and strings without copying, using Span<T> and ReadOnlySpan<T>.

11 min read

You will learn

  • Explain what a span is and why it avoids allocations
  • Slice arrays and strings with spans
  • Parse text from a ReadOnlySpan<char>
  • Know the restrictions of ref structs

Copies everywhere

Classic string and array code copies a lot:

string line = "2024-10-06,Ada,42.50";
string[] parts = line.Split(',');          // allocates an array + 3 new strings
string year = parts[0].Substring(0, 4);    // another new string
int y = int.Parse(year);

For one line that's fine. For a server parsing millions of lines or requests, those temporary strings become a mountain of garbage for the GC. Span<T> lets you work with a slice of existing memory without copying it.

What is a span?

A Span<T> is a lightweight view over a contiguous region of memory: just a reference to the start plus a length. It can point into an array, a string, stack memory or native memory — and slicing a span creates another view, not a copy.

int[] numbers = { 1, 2, 3, 4, 5, 6 };
Span<int> middle = numbers.AsSpan(2, 3);   // views 3, 4, 5 — no copy
 
middle[0] = 99;                            // writes through to the array!
Console.WriteLine(numbers[2]);             // 99

Strings are immutable, so they give you a ReadOnlySpan<char>:

ReadOnlySpan<char> text = "2024-10-06,Ada,42.50";
ReadOnlySpan<char> date = text[..10];      // "2024-10-06" — a view, no new string
int year = int.Parse(date[..4]);           // parse straight from the span

Most parsing and formatting APIs accept spans: int.Parse, double.TryParse, Guid.Parse, DateTime.Parse, IndexOf, StartsWith, Trim, SequenceEqual…

Parsing without allocating

decimal SumPrices(ReadOnlySpan<char> csv)
{
    decimal total = 0;
    while (!csv.IsEmpty)
    {
        int comma = csv.IndexOf(',');
        var field = comma >= 0 ? csv[..comma] : csv;
        total += decimal.Parse(field, CultureInfo.InvariantCulture);
        csv = comma >= 0 ? csv[(comma + 1)..] : ReadOnlySpan<char>.Empty;
    }
    return total;
}

No arrays, no substrings — zero allocations however long the input.

Stack buffers with stackalloc

For a small temporary buffer, allocate it on the stack — free for the GC:

Span<char> buffer = stackalloc char[32];
bool ok = DateTime.Now.TryFormat(buffer, out int written, "yyyy-MM-dd");
Console.WriteLine(buffer[..written].ToString());

The rules: spans live on the stack

Span<T> is a ref struct: it may point to stack memory, so the compiler ensures it never ends up on the heap. A span can't:

  • be a field of a class (or a normal struct),
  • be boxed or used as a generic type argument,
  • be captured by a lambda, or live across an await or yield return.

When you need a heap-friendly version (in a class field or an async method), use Memory<T> / ReadOnlyMemory<T> and call .Span where you do the work.

Reusing buffers: ArrayPool

When you need real arrays repeatedly (I/O buffers, for example), rent them from a pool instead of allocating:

byte[] buffer = ArrayPool<byte>.Shared.Rent(64 * 1024);
try
{
    int read = await stream.ReadAsync(buffer);
    // ... use buffer.AsSpan(0, read) ...
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

Measure first

Span-based code is a tool for hot paths — ASP.NET Core and System.Text.Json use it everywhere internally. In ordinary business code, clear and simple beats clever. Profile (Track 13) before optimizing.

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • Span<T> / ReadOnlySpan<T> are views over existing memory: slicing doesn't copy.
  • Parse directly from spans to avoid temporary strings and arrays.
  • stackalloc gives GC-free temporary buffers; ArrayPool lets you reuse large arrays.
  • Spans are ref structs: no fields in classes, no lambdas, no await — use Memory<T> there.

Exercise

Without calling string.Split or Substring, implement CountFields (how many fields a line has for the given separator — an empty line has 1 empty field) and SumCsv (the sum of comma-separated integers, 0 for an empty line). Slice the span and use int.Parse on spans.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

CountFields: count the separators — there's always one more field than separators. MemoryExtensions has line.Count(separator) in newer .NET; a foreach over the span works everywhere.

Hint 2

SumCsv: loop while the span isn't empty: int comma = line.IndexOf(','); take line[..comma] (or the whole rest), parse it with int.Parse(slice), then continue with line = line[(comma + 1)..].

Show solution
Console.WriteLine(CountFields("a,b,c", ','));
Console.WriteLine(SumCsv("10,20,5"));

int CountFields(ReadOnlySpan<char> line, char separator)
{
    int count = 1;
    foreach (char c in line)
    {
        if (c == separator) count++;
    }
    return count;
}

int SumCsv(ReadOnlySpan<char> line)
{
    int sum = 0;
    while (!line.IsEmpty)
    {
        int comma = line.IndexOf(',');
        ReadOnlySpan<char> field = comma >= 0 ? line[..comma] : line;
        sum += int.Parse(field);
        line = comma >= 0 ? line[(comma + 1)..] : ReadOnlySpan<char>.Empty;
    }
    return sum;
}

Quiz

4 questions · 3 correct to pass
  1. 1.What does `"hello world".AsSpan(6, 5)` allocate?

  2. 2.Why can't a Span<T> be stored in a class field?

  3. 3.What is `stackalloc` used for?

  4. 4.What does ArrayPool<T>.Shared help with?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)