Understanding Span and Memory in C#

C# has steadily evolved over the years, not just as a language but as a full ecosystem designed to support high-performance, safe, and productive programming. Among the most impactful additions in recent years are Span<T> and Memory<T>. These types fundamentally change how we think about working with data in memory, especially in performance-critical scenarios.

In this article, we'll take a deep dive into what Span<T> and Memory<T> are, why they were added to C#, how they work under the hood, and how you can use them effectively. By the end, you’ll understand not just the "how" but the "why" behind these powerful features — and be ready to apply them to your own code.

1. A Little Background: The Problem They Solve

Before Span<T> and Memory<T>, developers primarily worked with arrays and collections. Need a subset of an array? Copy it into a new array. Need to parse a portion of a string? Extract a substring — which creates an entirely new string object.

This approach has two big problems:

  1. Performance and Allocation Costs: Copying data means additional heap allocations, which increases GC pressure and reduces performance.

  2. Limited Control: Working with raw pointers is unsafe and difficult to get right. Yet, for truly high-performance scenarios, unsafe code was often the only option.

What we needed was a safe, efficient way to work with slices of data — without making unnecessary copies — and without giving up memory safety.

Enter Span<T> and Memory<T>.

2. What is Span?

Span<T> is a stack-only type that represents a contiguous region of arbitrary memory. Think of it as a window into existing memory — whether that memory is an array, stack-allocated data, or even unmanaged memory.

Key Properties of Span:

  • Zero-allocation: Creating a Span<T> does not allocate new memory — it just provides a view over existing memory.

  • Bounds-checked: Like arrays, spans are safe and throw exceptions if you try to go out of range.

  • Stack-only: Span<T> is a ref struct, meaning it must live on the stack and cannot be boxed, captured by lambdas, or stored in fields of classes.

Creating and Using Spans

Let's look at a simple example:

int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> slice = numbers.AsSpan(1, 3); // A view over elements 2, 3, 4

slice[0] = 99;
Console.WriteLine(string.Join(", ", numbers)); // Output: 1, 99, 3, 4, 5

Notice what happened — we modified the span, and it updated the underlying array. No copying occurred.

Why This Matters

This ability to work with data slices without allocating new arrays is a huge performance win in scenarios like:

  • Parsing incoming data buffers

  • Manipulating sections of large arrays

  • Writing high-performance libraries where allocation cost matters

3. What is Memory?

While Span<T> is great, its stack-only nature can be limiting. What if you need to hold onto the data beyond the current stack frame — for example, in an asynchronous method?

That’s where Memory<T> comes in.

Key Properties of Memory:

  • Similar to Span<T> but heap-allocatable.

  • Can be stored in fields, passed around, and survive beyond the current stack frame.

  • Provides a .Span property to get a Span<T> view.

Example:

Memory<int> memory = new int[] { 10, 20, 30, 40, 50 };
Span<int> span = memory.Span;

span[2] = 99;
Console.WriteLine(string.Join(", ", memory.Span.ToArray())); // 10, 20, 99, 40, 50

You get the same slicing and zero-copy benefits, but with the ability to store and reuse this reference across asynchronous calls or object fields.

4. When Were They Added?

Both Span<T> and Memory<T> were introduced around C# 7.2 and .NET Core 2.1 (2018). They are also available via the System.Memory NuGet package for older frameworks.

Microsoft introduced them as part of a broader push toward high-performance, allocation-free APIs — a direction that's especially important in cloud, microservices, and real-time systems.

5. Why They Were Added

The primary motivations:

  • Performance: Avoid copying data unnecessarily.

  • Safety: Provide a safe, managed alternative to unsafe pointers.

  • Expressiveness: Make slicing, slicing-without-copying, and working with buffers a first-class, easy-to-use pattern.

  • Support for Pipelines: They underpin many of the high-performance APIs in .NET today, such as System.IO.Pipelines and System.Text.Json.

6. Practical Use Cases

Let’s look at some real scenarios where these types shine.

Example 1: Parsing a CSV Row

Imagine processing a large CSV file. You don't want to create new strings for every field if you don't have to.

ReadOnlySpan<char> line = "John,Doe,30".AsSpan();
int commaIndex = line.IndexOf(',');
var firstName = line.Slice(0, commaIndex);

Console.WriteLine(firstName.ToString()); // Output: John

Here, we didn't allocate a new string until we explicitly called .ToString().

Example 2: High-Performance String Building

Span<char> buffer = stackalloc char[50];
var pos = 0;

"Hello".AsSpan().CopyTo(buffer.Slice(pos));
pos += 5;
" World".AsSpan().CopyTo(buffer.Slice(pos));
pos += 6;

string result = new string(buffer.Slice(0, pos));
Console.WriteLine(result); // Output: Hello World

Using stack allocation and spans here avoids heap allocations for intermediate strings.

Example 3: Working with Memory in Async Code

async Task ProcessDataAsync(Memory<byte> memory)
{
    await Task.Delay(100); // simulate async work
    Span<byte> span = memory.Span;
    span[0] = 42; // modify data safely
}

With Memory<T>, we can hold onto a reference to data and still process it safely after an await.

7. Pitfalls and Things to Watch For

While powerful, Span<T> and Memory<T> have some important rules:

  • Span can’t be stored in fields of classes (it’s a ref struct).

  • Be careful with stackalloc: The memory is only valid until the end of the current scope.

  • Slicing does not copy data: Remember that changes to the slice affect the original memory.

8. Performance Benefits in Practice

Benchmarks have shown significant improvements when replacing array slicing/copying with spans — often resulting in 2x–4x faster performance and far fewer allocations. This is why many core .NET libraries, such as System.Text.Json, rely heavily on spans internally.

9. Summary

Span<T> and Memory<T> represent a major leap forward for high-performance programming in C#. They give you:

  • Safe, zero-allocation access to slices of memory.

  • Flexibility to work on the stack (Span<T>) or on the heap (Memory<T>).

  • Better performance and lower GC pressure.

If you're writing libraries, parsing data, working with large datasets, or just want to write more efficient C#, these types are essential tools to learn.

In short:

  • Use Span<T> when you need fast, stack-bound, sliceable memory views.

  • Use Memory<T> when you need a heap-storable, async-friendly version of the same.

Together, they allow C# developers to write code that is both fast and safe — without resorting to unsafe pointers or excessive memory allocations.

Ready to level up your C# performance? Start using Span<T> and Memory<T> in your next project — and watch how they help you write cleaner, faster, and more memory-efficient code.

If you want to keep the discussion going, head over to our Facebook group or Skool Community.

Previous
Previous

Mastering readonly struct in C#: Safer, Faster, and More Efficient Code

Next
Next

Why C# Is Still a Great Language in 2025