With .NET Core 2.1 and C# 7.2, a new type of struct was introduced: Span<T>
and ReadOnlySpan<T>
. These types represent a contiguous region of arbitrary memory, with T
specifying the type of elements in the memory (e.g., arrays, strings, chars, integers, etc.). They can be thought of as a “window” into an existing dataset already allocated in memory, allowing efficient operations without additional allocations. What makes them particularly efficient is that they are stack-only structures, meaning they are allocated on the stack rather than the heap.
What Does It Mean to Be a Stack-Only Structure?
To understand this, let’s first clarify the difference between the stack and the heap.
The stack is a region of memory used for storing:
- local variables
- Temporary data such as method parameters
- Immediate values like primitive types (e.g.,
int
,float
, Span<T>). - Small object allocations
When the method completes, its stack frame (all data allocated during its execution) is automatically removed from the stack.
The heap is used to store:
- Complex objects (e.g., classes), arrays, List.
- Data that must persist beyond the lifecycle of a method (e.g., shared objects).
- Large object allocations.
Memory in the heap requires the garbage collector to clean up unused objects.
Limits and Features of Span<T>
Now that we understand the distinction between stack and heap memory, let’s explore the scenarios and boundaries within which Span<T>
is most effectively used. Let’s begin with its advantages:
Performance: since it is allocated only on the stack, Span<T>
is faster
Slicing and memory efficiency : while slicing Span<T>
does not create a new object, slicing operations on an array (e.g., with LINQ methods like .Take()
or .Skip()
or manually creating a subset) result in a copy of the data. However, Span<T>
avoids this overhead by simply referencing the relevant portion of the original memory.
string[] array = { "uno", "due", "tre", "quattro", "cinque" };
Span<string> span = array.AsSpan();
//Slicing
Span<string> slicedSpanArray = span[1..4]; // { "due", "tre", "quattro" }
slicedSpanArray[0] = "DUE"; //Modifica anche l'array originale
Console.WriteLine(array[1]); // "DUE"
Limitations:
scope: Span<T>
is confined to the current scope (e.g., a method). It cannot escape the stack or be stored in a way that extends its lifetime. For example: you cannot use Span<T>
as a property of a class or store it in a field that outlives the current context.
limited size: The stack is small (generally 1MB per thread), so it cannot handle very large data.
Comparing Span<T>
with arrays and List<T>
Feature | Array | List<T> | Span<T> |
Data container | Yes | Yes | No (points to memory data) |
Allocation | Heap | Heap | Stack |
Data modification | Yes | Yes | Yes (if allowed) |
Slice | Creates a new array | Creates a new list | No new object is created |
Thread-safe | No | No | No |
Example Code
Let’s look at a practical example. We start with an integer array containing 10 elements. Then, we create a Span<T>
that represents 5 elements, starting at index 2 of the original array. As previously explained, no copy of the array is created; instead, the Span<T>
provides a view into the original memory. After processing the span, we can observe that any changes made to the span are reflected in the original array.
var originalArray = new int[] { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
//Create a span from originalArray, starting at index 2 for 5 elements
var sliceSpan = new Span<int>(array, 2, 5);
//Iterate over the sliceSpan and apply a transformation to its elements
for (int ctr = 0; ctr < sliceSpan.Length; ctr++)
sliceSpan[ctr] *= 2;
//Iterate over the original array
foreach (var value in originalArray)
Console.Write($"{value}, ");
Console.WriteLine();
//Index: 0 1 2 3 4 5 6 7 8 9
//originalArray: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20
//Output: 2, 4, 12, 16, 20, 24, 28, 16, 18, 20
Conclusioni
Span<T>
is a powerful tool for optimizing operations that demand high performance and minimal memory allocations. However, it is not a universal solution. Its use is best suited for scenarios where the constraints of the stack—such as its limited size and scope—are not an issue. For most other use cases, where flexibility and long-lived objects are required, arrays and lists remain the safer and more versatile choice.