Performance Optimization with Span<T> and Memory<T> in C#
Learn performance optimization in C# using Span<T> and Memory<T> to manage memory efficiently and process data faster.
With C# 7.2 and later, the Span<T> and Memory<T> types optimize memory access in high-performance data processing scenarios by preventing unnecessary copies. Thanks to these types, you can work over large arrays, strings, or byte buffers without extra memory allocations.
What Is Span<T>?
Span<T> is a stack-based type that operates over memory using the concept of slicing.
It holds a reference to a portion of an array, string, or unmanaged memory without copying.
However, it lives on the stack; therefore it cannot be used in async methods or in constructs that escape to the heap.
using System;
class Program
{
static void Main()
{
int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> slice = numbers.AsSpan(1, 3); // 20,30,40
slice[0] = 99;
Console.WriteLine(string.Join(", ", numbers));
// Output: 10, 99, 30, 40, 50 — because it's pointing to the same memory
}
}
Access Without Copies via Slice
The key benefit of Span<T> is accessing subranges without copying data.
For example, when you want to work on a specific part of a large byte array, you don’t need to create a new array.
byte[] buffer = new byte[1000];
Span<byte> header = buffer.AsSpan(0, 128); // header section
Span<byte> data = buffer.AsSpan(128); // remaining data
// Different parts of the same array with no memory copy
header.Clear(); // clears only the first 128 bytes
stackalloc: Temporary Memory Without Heap Allocation
With stackalloc you can allocate temporary memory on the stack instead of the heap.
This memory is cleaned up automatically and is not tracked by the GC (Garbage Collector).
It’s very efficient for high-frequency, small-data processing scenarios.
Span<int> numbers = stackalloc int[5];
for (int i = 0; i < numbers.Length; i++)
numbers[i] = i * 10;
Console.WriteLine(string.Join(", ", numbers.ToArray()));
Note: stackalloc works with Span<T> only and should be used with limited sizes.
There’s a risk of stack overflow with large data.
What Is Memory<T>?
Memory<T> is the heap-based, async-safe counterpart of Span<T>.
While Span<T> only lives on the stack, Memory<T> can live on the heap
and can be safely used in asynchronous scenarios like async/await.
using System;
class Program
{
static async Task Main()
{
byte[] data = new byte[1024];
Memory<byte> memory = data.AsMemory();
await ProcessAsync(memory.Slice(100, 200));
}
static async Task ProcessAsync(Memory<byte> data)
{
await Task.Delay(100); // simulate async work
var span = data.Span;
span.Fill(255); // direct memory access
Console.WriteLine("Segment has been filled.");
}
}
You can access Span<T> directly via the Span property of Memory<T>.
ReadOnlySpan<T> and ReadOnlyMemory<T>
If you want the data to be read-only, use ReadOnlySpan<T> or ReadOnlyMemory<T>.
This gives you copy-free access while preventing modification.
ReadOnlySpan<char> span = "Hello World".AsSpan();
Console.WriteLine(span.Slice(6)); // World
String Processing with Span
Using Span<char>, you can perform string slicing much faster than with Substring().
No new string object is created; you simply view a portion of the original string.
ReadOnlySpan<char> text = "1234-5678-9012-3456";
ReadOnlySpan<char> last4 = text.Slice(text.Length - 4);
Console.WriteLine($"Last 4 digits: {last4.ToString()}");
Span vs ArraySegment
ArraySegment<T> represents a portion of an array but works only with arrays.
Span<T>, on the other hand, can work over many data sources such as memory, strings, and pointers.
Also, because Span<T> is a ref struct, it reduces GC pressure.
int[] numbers = { 1, 2, 3, 4, 5 };
var segment = new ArraySegment<int>(numbers, 1, 3);
Span<int> slice = numbers.AsSpan(1, 3);
Performance Comparison
The following example shows the difference between classic string slicing and using Span<T>.
Substring() creates a new string object, while AsSpan() just views existing memory.
string text = "CSharp Performance Test";
var part1 = text.Substring(7, 10); // creates a new string
ReadOnlySpan<char> part2 = text.AsSpan(7, 10); // no copy
Console.WriteLine(part1);
Console.WriteLine(part2.ToString());
Especially in high-volume string or byte processing, Span significantly reduces GC pressure.
Things to Watch When Using Span and Memory
Span<T>lives on the stack; it cannot escape in async methods or lambdas.Memory<T>lives on the heap; it should be preferred in async scenarios.- The lifetime of the underlying data must outlive the
Span(otherwise memory corruption can occur). - Use
stackalloconly for small data (e.g., < 1 KB). - Highly effective in high-performance text, networking, JSON, and binary data processing.
Example: Slicing a Byte Array
For example, when parsing a network packet or reading file headers, processing large byte arrays without copying increases performance. The following example processes a data packet by slicing it into header and body.
using System;
class Program
{
static void Main()
{
byte[] packet = new byte[1024];
new Random().NextBytes(packet);
Span<byte> span = packet.AsSpan();
Span<byte> header = span.Slice(0, 128);
Span<byte> body = span.Slice(128);
Console.WriteLine($"Header length: {header.Length}");
Console.WriteLine($"Body length: {body.Length}");
}
}
TL;DR
- Span<T>: stack-based, copy-free, fast memory slice. (Short-lived, not usable with async.)
- Memory<T>: heap-based, async-friendly, safe memory reference.
- ReadOnlySpan / ReadOnlyMemory: read-only access that preserves data integrity.
- stackalloc: eliminates GC pressure for small temporary data.
- Operate on large data without copying to significantly reduce CPU and GC load.
Related Articles
Arrays in C#
Learn arrays in C#, including declaration, indexing, looping through elements, and common array operations with examples.
C# Basic Data Types
Basic data types in C#: numeric, text, logical, object-based, and nullable types.
Generics in C# (List<T>, Dictionary<TKey,TValue>)
Learn generics in C#, including List<T> and Dictionary<TKey,TValue>, to write type-safe and reusable code with examples.
Memory Management and the Garbage Collector in C#
Learn memory management and the garbage collector in C# to understand object lifetime, allocation, and cleanup processes.
Structs in C# – Differences from Classes
Learn the key differences between structs and classes in C#, including memory model, inheritance, boxing, and performance.
The Concept of Source Generators in C# (C# 9+)
Learn source generators in C# to generate code at compile time and improve performance with modern development techniques.
Unsafe Code and Pointers in C#
Learn unsafe code and pointers in C# to work with memory addresses, low-level operations, and advanced performance scenarios.