C# ReadOnlySpan<T> for High-Performance String Parsing Without Allocations
Learn how ReadOnlySpan<T> enables zero-allocation string slicing in C# for high-performance parsing of large datasets without GC pressure.
01 Apr 2026, 12:38 UTC

The Hidden Allocation Cost in String Parsing
When processing large CSV files, log files, or network protocols in C#, the typical approach uses string.Substring() or string.Split(). Each call creates a new string object on the managed heap. In high-throughput scenarios processing millions of records, these short-lived allocations trigger frequent Garbage Collection cycles. Modern .NET GC is mostly concurrent, but excessive allocations still cause latency spikes and memory pressure.
ReadOnlySpan: Stack-Only Memory Views
ReadOnlySpan<T> provides a type-safe, memory-safe representation of a contiguous memory region without copying data. It's implemented as a ref struct, meaning it lives exclusively on the stack and cannot be boxed or stored as a field in a class. When you slice a span, you're creating a new span that points to a different offset within the original buffer—no data is copied, no heap allocation occurs.
Work Example: Parsing Key-Value Pairs
Consider parsing a configuration string like "timeout:30|retry:true|maxAttempts:5". Using Substring creates multiple temporary strings. Using ReadOnlySpan<char> parses in place:
// Requires: .NET Core 2.1+ or .NET 5/6/7/8
public static Dictionary<string, string> ParseConfig(ReadOnlySpan<char> input)
{
var result = new Dictionary<string, string>();
var pairs = input.Split('|');
foreach (var pair in pairs)
{
int colonIndex = pair.IndexOf(':');
if (colonIndex <= 0 || colonIndex == pair.Length - 1) continue;
var key = pair.Slice(0, colonIndex).Trim();
var value = pair.Slice(colonIndex + 1).Trim();
result[key.ToString()] = value.ToString();
}
return result;
}
// Usage example
string config = "timeout:30|retry:true|maxAttempts:5";
var parsed = ParseConfig(config.AsSpan());
Execution Details
- Where to run: Any C# method in .NET 6+ project
- Permissions: No special permissions required for managed spans
- Expected result:
Slicereturns newReadOnlySpan<char>pointing to original string memory - Risk: Using spans with unmanaged memory requires careful lifecycle management to avoid accessing freed memory
Ref Struct Limitations: The Trade-Offs
The performance benefits come with strict compiler restrictions. You cannot use ReadOnlySpan<T> in these scenarios:
- Async methods: Cannot cross
awaitboundaries (state machines move locals to heap) - Class fields: Cannot declare as field in class or non-ref struct
- Generics: Cannot use as generic type argument
Async Alternative: Memory<T>
For async workflows, use ReadOnlyMemory<T> which can live on the heap. Convert to span using the .Span property in synchronous contexts where high-performance slicing is needed.
Verification Method
To verify allocation reduction, use Visual Studio Diagnostic Tools or JetBrains dotMemory. Compare 100,000 Substring() calls against 100,000 Slice() calls. You'll see a sawtooth pattern in heap memory for Substring(), while Slice() shows nearly flat managed heap allocations. Note: This approach works best for short-lived parsing operations; for long-term storage, convert to string using .ToString() when needed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.