Skip to main content
The Power of Span<T> and Memory<T> in .NET

The Power of Span and Memory in .NET

A practical guide to understanding Span and Memory, two of the most important performance-related features in modern .NET, and how they help eliminate memory allocations.

  1. Posts/

The Power of Span and Memory in .NET

·905 words·5 mins· loading
👤

Chris Malpass

Author

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public (string, string) GetFirstAndLastName(string fullName)
{
    // 1. Allocates a string[] array
    // 2. Allocates new string objects for "First" and "Last"
    var parts = fullName.Split(' '); 
    
    var firstName = parts[0];
    var lastName = parts[1];
    return (firstName, lastName);
}

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 await boundary.
  • 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.

1
2
3
4
// Create a small buffer on the stack (no GC overhead!)
Span<byte> buffer = stackalloc byte[128];
// Use it just like an array
buffer[0] = 42;

Rewriting GetFirstAndLastName with Span<T>
#

Let’s rewrite our name-parsing method using ReadOnlySpan<char> (the Span<T> equivalent for immutable strings).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Note the 'ReadOnly' prefix, as strings are immutable.
public (ReadOnlySpan<char>, ReadOnlySpan<char>) GetFirstAndLastName_Span(ReadOnlySpan<char> fullName)
{
    var spaceIndex = fullName.IndexOf(' ');
    if (spaceIndex == -1)
    {
        // Return the whole name as the first name, and an empty span for the last.
        return (fullName, ReadOnlySpan<char>.Empty);
    }

    // Use C# Range syntax for cleaner slicing
    var firstName = fullName[..spaceIndex];
    var lastName = fullName[(spaceIndex + 1)..];

    return (firstName, lastName);
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public async Task ProcessDataAsync(Memory<byte> buffer)
{
    // 'buffer' can be stored in a class, passed around, and used across awaits.

    await Task.Delay(100); // Simulate async work

    // When you're ready to do the actual, high-performance processing,
    // you get a Span from the Memory.
    Span<byte> span = buffer.Span;

    // Now you can work with the data allocation-free in this synchronous context.
    for (int i = 0; i < span.Length; i++)
    {
        span[i] *= 2;
    }
}

The Golden Rule
#

  1. 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.
  2. Process with Span<T>: When you are inside a synchronous method and ready to do the actual work, get a .Span from your Memory<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> and Memory<T> extensively in their internals to reduce allocations and improve throughput. For example, you can read a request body directly into a Memory<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
#