← Back to Blog

Understanding Span and Memory in C#: High-Performance Memory Access

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 or dynamic.
  • 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.

Use 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.
Use 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 utilities
  • System.Buffers for pooled memory
  • System.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.