Ever felt like your application is doing more work than necessary just to move a few bytes around? You might be missing out on one of C#’s most powerful performance tools: Span<T>
. Let me show you how this little slice of memory magic can shave milliseconds off your hot paths.
What Is Span?
Span<T>
is a value type introduced in C# 7.2 that represents a contiguous region of arbitrary memory. It’s like a super-efficient array wrapper that doesn’t allocate on the heap.
Unlike arrays or lists, Span<T>
is stack-allocated and provides a safe, fast way to work with slices of data—whether that’s arrays, strings, or unmanaged memory.
Key characteristics:
- No heap allocations
- Fast slicing and slicing operations
- Type-safe and memory-safe
- Works with
stackalloc
for high-performance scenarios
The Performance Boost: Why Use Span?
Let’s be blunt: allocations are expensive. Every time you create a new array or list, you add pressure on the garbage collector.
With Span<T>
, you:
- Avoid allocations for temporary data structures
- Work directly with memory for tasks like parsing, encoding, and transformations
- Minimize copying of data
Here’s a microbenchmark scenario:
string input = "1234567890";
ReadOnlySpan<char> span = input;
ReadOnlySpan<char> sliced = span.Slice(2, 5);
Console.WriteLine(sliced.ToString()); // Output: 34567
No new string is created—you’re working on the same memory.
Getting Started: Syntax & Basic Usage
Span<int> numbers = stackalloc int[5] { 1, 2, 3, 4, 5 };
Span<int> slice = numbers.Slice(1, 3);
foreach (var number in slice)
Console.WriteLine(number);
Explanation:
stackalloc
allocates memory on the stack (not heap!)Slice(1, 3)
returns elements 2, 3, and 4- No garbage collector involvement
Want to operate on parts of a byte array?
byte[] buffer = new byte[100];
Span<byte> bufferSpan = buffer.AsSpan(10, 50);
Advanced Examples & Use Cases
Parsing integers from a large file buffer:
ReadOnlySpan<byte> line = Encoding.UTF8.GetBytes("42,65,89,100");
int sum = 0;
foreach (var part in line.ToString().Split(','))
sum += int.Parse(part);
Console.WriteLine(sum);
Better yet:
ReadOnlySpan<byte> line = Encoding.UTF8.GetBytes("42,65,89,100");
ReadOnlySpan<char> span = Encoding.UTF8.GetString(line).AsSpan();
int sum = 0;
int comma;
do
{
comma = span.IndexOf(',');
var segment = comma != -1 ? span.Slice(0, comma) : span;
sum += int.Parse(segment);
span = comma != -1 ? span.Slice(comma + 1) : Span<char>.Empty;
} while (!span.IsEmpty);
Console.WriteLine(sum);
Avoids allocations by skipping string.Split
.
Unsafe interop/memory manipulation:
unsafe
{
int* ptr = stackalloc int[3] { 1, 2, 3 };
Span<int> span = new Span<int>(ptr, 3);
span[0] = 10;
Console.WriteLine(span[0]); // Output: 10
}
Best Practices for Using Span
- Use
Span<T>
when you need high-performance, temporary views over data - Combine with
stackalloc
for max performance - Use
ReadOnlySpan<T>
when you don’t need to modify data - Ideal for parsers, encoders, formatters, protocol readers
- Use in performance-critical hot paths to reduce allocations
Common Pitfalls & Limitations
- Can’t be stored in fields of classes (unless in ref structs)
- Can’t be boxed or used with async methods
- Short-lived: only live as long as the stack frame
- Misusing
Span<T>
can cause undefined behavior or exceptions
FAQ: Common Span Questions
No. Spans can’t be captured across await
. Use memory pooling or arrays instead.
You can’t unless it’s returned immediately from a ref struct
or within the same stack frame.
Span<T>
is stack-only and fast; Memory<T>
is heap-safe and can be used in async methods.
Conclusion: Span is Small but Mighty
If you’re chasing those last drops of performance or want to avoid heap allocations without resorting to unsafe code, Span<T>
is your friend. I’ve used it in data pipelines, serializers, and even to shave off GC pressure in tight loops. It’s one of those underused gems in C#.
Give it a try—your performance metrics will thank you.
What’s the most clever way you’ve used Span<T>
in your code? Share it in the comments below!