Memory management in C# has come a long way. Back in the day, if you wanted to slice an array or manipulate
buffers, you’d end up copying data, allocating new arrays, and hoping the garbage collector didn’t slow you down.
But with the introduction of Span<T>
in .NET Core 2.1, things changed - dramatically.
Span<T>
gives you a way to work with slices of memory without allocating anything new. It’s
fast, safe, and surprisingly flexible. But it also comes with some quirks - like being a ref struct
,
which means it behaves differently than most types you’re used to.
In this article, we’ll walk through everything you need to know about Span<T>
and its sibling
Memory<T>
. We’ll cover stack allocation, slicing, interop with unmanaged memory, async
limitations, and real-world use cases. By the end, you’ll be slicing memory like a pro - and avoiding the common
pitfalls that trip up even experienced developers.
What Is Span<T>
?
Span<T>
is a ref struct that represents a contiguous region of memory. It doesn’t own
the memory - it just gives you a view over it. That view can be over an array, a portion of the stack, or even
unmanaged memory.
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Span<int> numbersSpan = numbers.AsSpan();
numbersSpan[2] = 100;
Console.WriteLine(numbers[2]); // Output: 100
Notice how we didn’t copy anything. The span just wraps the array. Any changes to the span affect the original array - and vice versa. That’s zero-copy memory access, and it’s a big deal for performance.
Why Span<T>
Matters
Before Span<T>
, if you wanted to work with a slice of an array, you’d probably do something
like:
int[] slice = numbers.Skip(2).Take(3).ToArray();
That creates a new array and copies the data. It’s fine for small stuff, but in performance-critical code - like parsers, network buffers, or image processing - those allocations add up fast.
Span<T>
solves this by letting you work with slices directly. It also enables stack allocation
using stackalloc
, which avoids heap pressure entirely.
Span<T>
lives in the System
namespace and is available
in .NET Core 2.1+, .NET 5+, and .NET 6+.
Understanding ref struct
Here’s where things get interesting. Span<T>
is a ref struct
, which means it has
some special rules:
- It can’t be boxed or cast to
object
ordynamic
. - It can’t be captured by lambdas or local functions.
- It can’t be used in async methods or returned from methods (unless using
ref
).
These restrictions exist to keep the span safe. Since it might point to stack memory, the runtime needs to make sure it doesn’t outlive the memory it references. So no boxing, no async, no sneaky closures.
Slicing Memory with Span<T>
One of the coolest things about spans is slicing. You can create a view over part of the memory - without allocating anything new.
Span<int> slice = numbersSpan.Slice(1, 3);
foreach (var number in slice)
{
Console.WriteLine(number);
}
That prints the second, third, and fourth elements of the array. The slice is just a view - any changes to it affect the original array.
Stack Allocation with stackalloc
Want to allocate memory on the stack? Use stackalloc
. It’s fast, avoids garbage collection, and is
perfect for short-lived buffers.
Span<int> stackNumbers = stackalloc int[5] { 1, 2, 3, 4, 5 };
stackNumbers[0] = 10;
Console.WriteLine(stackNumbers[0]); // Output: 10
Just be careful - stack memory is limited. Don’t go allocating huge buffers unless you enjoy stack overflows.
stackalloc
for small, short-lived buffers. For larger data, stick to
the heap.
Introducing Memory<T>
Span<T>
is great, but it can’t be stored in fields or used in async methods. That’s where
Memory<T>
comes in.
Memory<T>
is a heap-based alternative that provides similar functionality. You can store it,
pass it around, and even use it across async boundaries.
Memory<int> memory = new int[5];
Span<int> span = memory.Span;
span[0] = 42;
Console.WriteLine(memory.Span[0]); // Output: 42
Use Memory<T>
when you need to persist memory or use it in async code. It’s slower than
Span<T>
, but way more flexible.
Working with ReadOnlySpan<T>
If you only need to read memory, use ReadOnlySpan<T>
. It gives you all the slicing and indexing
features - but prevents modification.
ReadOnlySpan<char> readOnly = "Hello, World!".AsSpan();
Console.WriteLine(readOnly[0]); // Output: H
This is perfect for string parsing, file reading, or any API that shouldn’t mutate data.
Interop with Unmanaged Memory
Need to work with unmanaged memory? Span<T>
can wrap pointers - safely.
unsafe
{
int* ptr = stackalloc int[3] { 1, 2, 3 };
Span<int> span = new Span<int>(ptr, 3);
Console.WriteLine(span[1]); // Output: 2
}
This gives you the power of pointers with the safety of spans. Just remember to use unsafe
carefully
- and only when you really need it.
Pinning Memory with GCHandle
Sometimes you need to pass managed memory to native APIs. That means pinning it - so the GC doesn’t move it.
byte[] buffer = new byte[1024];
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr ptr = handle.AddrOfPinnedObject();
// Pass ptr to native API
}
finally
{
handle.Free();
}
Span<T>
and Memory<T>
work great with pinned memory. Just don’t forget to
free the handle - or you’ll leak memory.
Performance Considerations
Span<T>
is designed for performance, but there are a few things to keep in mind:
- Every access is bounds-checked - but the JIT often removes redundant checks.
- Copying spans is cheap - but avoid doing it repeatedly in tight loops.
- Since it’s a struct,
Span<T>
is passed by value - be mindful of that in performance-critical code.
Span<T>
for short-lived, high-performance operations.
For longer-lived or async scenarios, use Memory<T>
.
Real-World Use Cases
Span<T>
is useful in tons of scenarios:
- Parsing CSV, JSON, or XML without allocations
- Efficient buffer manipulation in networking
- Compression and encryption algorithms
- Interop with native libraries
- Image and audio processing
Let’s look at a simple CSV parsing example:
string csvLine = "John,Doe,30";
ReadOnlySpan<char> span = csvLine.AsSpan();
int commaIndex = span.IndexOf(',');
ReadOnlySpan<char> firstName = span.Slice(0, commaIndex);
Console.WriteLine(firstName.ToString()); // Output: John
This avoids creating new strings until absolutely necessary - reducing GC pressure and improving performance.
Span in .NET Libraries
Microsoft has integrated Span<T>
into many .NET libraries:
System.Memory
for memory utilitiesSystem.Buffers
for pooled memorySystem.IO.Pipelines
for high-performance I/O
That means you can use spans throughout your application - from parsing to networking to file I/O.
Summary
Span<T>
and Memory<T>
are game-changers for memory access in C#. They let
you work with slices of memory without allocations, while maintaining safety and performance.
Use Span<T>
for synchronous, short-lived operations. Use Memory<T>
when you
need to store memory or use it asynchronously. Use ReadOnlySpan<T>
for immutable access. And
use stackalloc
for fast, temporary buffers.
With these tools, you can write faster, safer, and more modern C# code. Whether you’re building parsers, working
with native libraries, or optimizing performance, Span<T>
and Memory<T>
give
you the control and efficiency you need.