For years, high-performance .NET code meant fighting the garbage collector (GC). Every time you sliced a string, took a substring, or passed a portion of an array, you were likely allocating new memory. These small allocations add up, putting pressure on the GC and hurting your application’s throughput.
Enter Span<T> and Memory<T>, two revolutionary types that provide a unified and allocation-free way to work with contiguous memory. Understanding them is key to writing modern, high-performance C# code.
The Problem: Allocations Everywhere#
Let’s look at a classic example: parsing a full name from a string.
| |
The call to fullName.Split(' ') is the problem. It allocates a new array of strings (string[]) on the heap, plus a new string object for every part it finds. If you call this method in a tight loop, you’ll be creating a massive amount of garbage for the GC to collect.
What if we could just represent a “view” or a “slice” of the original string without allocating anything new?
Span<T>: The Allocation-Free Slice#
Span<T> is a ref struct that represents a contiguous block of memory. It’s like a pointer, but safe and managed. It can point to memory on the stack, in a managed array, or even to unmanaged memory.
Because it’s a ref struct, it has some important limitations:
- It can only live on the stack.
- It cannot be a field in a regular class or struct (only in other
ref structs). - It cannot be used in async methods across an
awaitboundary. - It cannot be boxed or assigned to
object.
These rules ensure that Span<T> can’t outlive the memory it’s pointing to, preventing memory corruption.
The Power of stackalloc#
One of the best features of Span<T> is that it allows you to use stack memory safely without the unsafe keyword.
| |
Rewriting GetFirstAndLastName with Span<T>#
Let’s rewrite our name-parsing method using ReadOnlySpan<char> (the Span<T> equivalent for immutable strings).
| |
Zero allocations.
The range operator ([..]) doesn’t create a new string. It simply creates a new ReadOnlySpan<char> that points to the same underlying memory as the original string.
Memory<T>: The Heap-Friendly Cousin#
The stack-only limitation of Span<T> is a deal-breaker for many scenarios, especially in async code. This is where Memory<T> comes in.
Memory<T> is a regular struct that can live on the heap. It serves as a “handle” to a block of memory. It’s slightly less performant than Span<T> because it has an extra layer of indirection, but it doesn’t have the same restrictions.
The magic is in the relationship between them: you can get a Span<T> from a Memory<T> at any time.
| |
The Golden Rule#
- Pass
Memory<T>around: Use it for your class fields, method parameters (especially for async methods), and any time you need to store a reference to a buffer on the heap. - Process with
Span<T>: When you are inside a synchronous method and ready to do the actual work, get a.Spanfrom yourMemory<T>and operate on that.
Practical Use Cases#
- High-Performance Parsing: Parsing text, binary data, or network protocols without creating intermediate strings or byte arrays.
- Image and Audio Processing: Working directly on raw pixel or audio data.
- API Design: Libraries like ASP.NET Core and System.Text.Json use
Span<T>andMemory<T>extensively in their internals to reduce allocations and improve throughput. For example, you can read a request body directly into aMemory<byte>buffer.
Conclusion#
Span<T> and Memory<T> are not just for library authors. They are powerful tools for any .NET developer looking to optimize their code. By thinking in terms of memory slices instead of object allocations, you can significantly reduce the pressure on the garbage collector, leading to faster, more efficient, and more scalable applications. If you’re not using them yet, it’s time to start.
Further Reading#
- Span Documentation - Official Microsoft documentation
- Memory and Span usage guidelines - Best practices
- C# 7.2 - Span and ref - Language reference
