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:
Performance and Allocation Costs: Copying data means additional heap allocations, which increases GC pressure and reduces performance.
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 aref 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 aSpan<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
andSystem.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.