Eliminating Heap Allocations with ReadOnlySpan<T> Slicing in C#
Learn how to use ReadOnlySpan<T> to eliminate unnecessary heap allocations during string parsing in C#, reducing GC pressure and improving application latency.
10 Jun 2026, 03:18 UTC

The Problem: Allocation Bloat in String Parsing
High-throughput C# applications often suffer from latency spikes caused by temporary string allocations. When using string.Substring() or string.Split(), the .NET runtime creates a new string object on the managed heap for every fragment. In scenarios like parsing large log files or CSV data, this generates thousands of short-lived objects, increasing pressure on the Garbage Collector (GC) and triggering frequent Generation 0 collections.
The solution is ReadOnlySpan<T>. Instead of copying data into a new object, a span provides a type-safe, memory-safe view into a contiguous region of existing memory. By "slicing" the original data, you can process subsets of a string or array without allocating any additional heap memory.
Mechanism: How ReadOnlySpan<T> Works
ReadOnlySpan<T> is implemented as a ref struct. Unlike standard structs, a ref struct is guaranteed to be allocated only on the stack. Internally, it consists of a reference (pointer) to the start of the memory region and a length. When you call the Slice() method, the runtime simply calculates a new pointer and length based on the original buffer; it does not copy the underlying data.
Practical Example: Zero-Allocation Parsing
The following example demonstrates how to extract a value from a key-value pair (e.g., "UserID:12345") without creating new strings for the key or the value. This assumes .NET Core 2.1 or later, where ReadOnlySpan<char> overloads for parsing were introduced.
using System;
public class DataParser
{
public static void ProcessLine(string input)
{
if (string.IsNullOrEmpty(input))
return;
// Convert string to ReadOnlySpan<char> without heap allocation
ReadOnlySpan<char> span = input.AsSpan();
int separatorIndex = span.IndexOf(':');
if (separatorIndex == -1)
return;
// Slice creates a view of the existing memory, not a new string
ReadOnlySpan<char> key = span.Slice(0, separatorIndex);
ReadOnlySpan<char> value = span.Slice(separatorIndex + 1);
// Compare spans directly to avoid allocating a string for comparison
if (key.Equals("UserID", StringComparison.Ordinal))
{
// Parse directly from the span to an integer
if (int.TryParse(value, out int id))
{
Console.WriteLine($"Found ID: {id}");
}
}
}
}
Key Implementation Details
AsSpan(): An extension method that creates a span pointing to the string's existing memory.Slice(int start, int length): Returns a new span representing a window of the original memory. This is an O(1) operation.- Direct Parsing: Modern .NET versions provide
int.TryParseand other numeric parsing methods that acceptReadOnlySpan<char>, eliminating the need to callToString().
Constraints and Common Mistakes
Because ReadOnlySpan<T> is a ref struct to ensure it never escapes to the heap, the compiler enforces strict usage rules.
1. Storage Restrictions (CS8343)
You cannot use a span as a field in a class or a non-ref struct. If you attempt to declare private ReadOnlySpan<char> _buffer; inside a class, the compiler will throw error CS8343. Spans must exist only as local variables or method parameters.
2. Async/Await Boundaries
Spans cannot be used across await boundaries. When a method awaits, the compiler generates a state machine that moves local variables to the heap to preserve state. Since ref structs cannot be moved to the heap, they cannot be captured in an async method's state machine.
3. Stackalloc Lifetime Risks
When using stackalloc to create a buffer on the stack, that memory is reclaimed immediately when the method returns. Returning a ReadOnlySpan<T> that points to stackalloc memory will result in a pointer to invalid memory, leading to undefined behavior or access violations.
Comparison: Substring vs. Slicing
| Feature | string.Substring() | ReadOnlySpan.Slice() |
|---|---|---|
| Allocation | Creates new string on Heap | Stack-only view (Zero allocation) |
| GC Impact | Increases Gen 0 pressure | No impact on GC |
| Complexity | O(n) due to data copying | O(1) pointer arithmetic |
| Storage | Can be stored in classes/lists | Local variables/parameters only |
Verification and Testing
To verify the reduction in memory pressure, use a memory profiler (such as the .NET Memory Profiler or Visual Studio Diagnostic Tools). Compare a loop using string.Split against a loop using ReadOnlySpan<char>. You should observe a significant decrease in Allocated Bytes per Second and a reduction in the frequency of Gen 0 Garbage Collections.
To verify safety constraints, attempt to pass a ReadOnlySpan<T> into a List<T> or use it as a generic type argument; the compiler should prevent this, confirming the ref struct restrictions are active.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.