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]); // 99Strings 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 spanMost 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
awaitoryield 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
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.
stackallocgives GC-free temporary buffers;ArrayPoollets you reuse large arrays.- Spans are ref structs: no fields in classes, no lambdas, no
await— useMemory<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.
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 pass1.What does `"hello world".AsSpan(6, 5)` allocate?
2.Why can't a Span<T> be stored in a class field?
3.What is `stackalloc` used for?
4.What does ArrayPool<T>.Shared help with?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)