C# Span vs Array: Choosing the Right Type
c# span vs array: Learn the practical differences between Span<T> and arrays in C#. Understand memory allocation, performance, and when to use each type.
When you need to work with a contiguous block of data in C#, the first type that usually comes to mind is T[]. Arrays have been part of the language since the beginning and are straightforward to use. However, modern C# offers Span<T>, a ref struct that provides a type-safe view over any contiguous memory, whether it lives on the managed heap, the stack, or even in unmanaged memory. The choice between c# span vs array affects memory allocation, method signatures, and how your code interacts with the garbage collector.
What Is Span<T> and How Is It Different from an Array
An array is a reference type that owns its elements. When you create an array, the runtime allocates the entire block on the managed heap, and the array variable holds a reference to that block. This means arrays always involve heap allocation, and the GC tracks their lifetime.
int[] numbers = new int[1000];
Span<T> is a ref struct that acts as a lightweight window over contiguous memory. It does not allocate the memory itself; it simply points to an existing region and exposes an API similar to an array. Because Span<T> is a ref struct, it can only live on the stack. It cannot be used as a field in a class, cannot be captured by a lambda, and cannot be used with async methods.
Span<int> numbers = stackalloc int[1000];
This distinction changes how you design APIs. A method that accepts an array can be called with any array, but it forces the caller to have an array at hand. A method that accepts a Span<T> can be called with arrays, spans over stack memory, or spans over unmanaged memory.
Memory Allocation: When Does Each Type Allocate?
Arrays always allocate on the managed heap. Even a small array like int[] numbers = { 1, 2, 3 }; creates a new object that the GC must track. The allocation cost is small for short-lived arrays, but frequent allocations can increase GC pressure in high-throughput paths.
Span<T> itself is a stack-only value type. Creating a Span<int> from an existing array does not allocate any new managed memory. The span simply stores a reference to the array and a length. If the source of the span is stackalloc, no heap allocation occurs at all.
int[] sourceArray = new int[100]; Span<int> arraySpan = sourceArray.AsSpan(); // no allocation Span<int> stackSpan = stackalloc int[100]; // no heap allocation
The AsSpan() extension method is zero-cost in most cases; it creates a span that points directly into the existing array. This means that for operations like slicing, Span<T> offers a way to avoid allocating a new array each time you need a subset.
Slicing and Subarray Operations Without Allocation
When you need a portion of an array, the classic approach is Array.Copy or LINQ's .Skip().Take(). Both create a new array and copy the elements. This is fine for one-off operations but problematic in performance-sensitive code where slicing happens repeatedly.
int[] data = { 0, 1, 2, 3, 4, 5 }; int[] slice = data[2..4]; // creates a new array
With Span<T>, the same operation creates a new view over the same memory without copying anything.
Span<int> dataSpan = data; Span<int> slice = dataSpan.Slice(2, 2); // no copy
The slice refers to the original array's elements. Any modification to slice modifies data, which is expected and often desirable. However, it also means that if you need a separate copy, you must explicitly copy the slice into a new array or a Memory<T> buffer.
When to Use Span<T> in Method Signatures
If you are writing a method that should operate on contiguous data and you do not need to store the data beyond the method call, Span<T> is often the better parameter type. This allows callers to pass arrays, stack-allocated buffers, or unmanaged memory.
int Sum(Span<int> values) { int total = 0; foreach (var v in values) { total += v; } return total; }
This method can be called with an array, a stack-allocated buffer, or a slice from another span. If the signature used int[], the caller would be forced to allocate an array even if they have data already in another form.
There are practical limitations. A Span<T> cannot be used in async methods because the span may be on the stack and the execution context changes after an await. For asynchronous operations, consider using Memory<T> instead, which is a regular struct that can be stored on the heap.
Performance Considerations Beyond Allocation
Allocation is not the entire story. Accessing array elements by index is very fast because the JIT can optimize bounds checks in many cases. Span<T> is designed to provide similar performance, and in most scenarios it is equivalent. There are subtle differences in how the bounds checks are generated depending on the context and the runtime version, but the practical takeaway is that using Span<T> does not mean giving up performance.
The real performance win comes from avoiding allocation and copying. When working with large arrays, copying a subset to a new array costs time and memory. Span<T> eliminates that copy. Similarly, using stackalloc avoids the GC entirely for short-lived buffers.
One maintenance consideration: if your method returns a Span<T> that points into a heap-allocated array, the caller must ensure the span is not used after the array is collected. Since spans are stack-only and have a short lifetime, this is rarely a problem in practice. The compiler helps by preventing spans from being stored in heap objects.
Practical Example: Binary Parsing with Span<T>
Consider a scenario where you need to parse a binary packet from a buffer. With an array, you might create subarrays for the header and payload, copying data unnecessarily. With Span<T>, you can pass the buffer around and slice it without allocation.
int ParseHeader(ReadOnlySpan<byte> packet) { // The first 4 bytes are the length return BinaryPrimitives.ReadInt32LittleEndian(packet.Slice(0, 4)); } void ProcessPacket(byte[] data) { var span = data.AsSpan(); int length = ParseHeader(span); var payload = span.Slice(4, length); // process payload without copying }
This pattern is common in network protocols and file parsers where performance matters. Using Span<T> avoids the allocation of a new array for each field, which reduces GC pressure and improves throughput. However, it also requires careful attention to the lifetime of the underlying buffer. If the byte[] is reused, you must not inadvertently keep a span longer than the data's validity.
Constraints and Compatibility When Using Span<T>
Because Span<T> is a ref struct, you cannot use it everywhere an array can be used. For example, a List<T> cannot store a Span<T> because it stores items on the heap. Similarly, a class that needs to hold a span as a field must use Memory<T> instead.
Memory<T> is the heap-friendly counterpart to Span<T>. It represents a contiguous region of memory that can be stored in fields, used in async methods, and passed across calls. You can create a Memory<T> from an array, and then create a Span<T> from the Memory<T> when you need the fast stack-only view.
Memory<int> memory = new int[100]; Span<int> span = memory.Span;
When designing library APIs, consider whether the caller is likely to have heap arrays or memory from a pool. If you want to support stackalloc, you need Span<T>, but that forces the caller to be in a stack context. For public APIs that might be called from async code, Memory<T> is the safer choice.
Final Recommendation: Choosing Based on the Context
The decision of whether to use Span<T> or an array in a particular spot comes down to what you need to do with the data and the lifetime of that data.
Use an array when you need to store a collection over a longer period, when you need to pass it to APIs that only accept arrays, or when you are working in an async context where a span cannot be used. Arrays are also the natural choice when the size is fixed and known at compile time and when you want the simplicity of a reference type that can be null and shared.
Use Span<T> when you are working with data that exists only inside a method call, when you need to slice or process contiguous buffers without copying, or when you want to use stackalloc for temporary buffers. Span is ideal for hot paths like parsers, serializers, and low-level operations where GC allocation is a bottleneck.
Keep in mind that Span<T> is not a replacement for arrays in every situation. It is an additional tool that provides more control over memory. For many everyday applications, arrays are perfectly fine. The cost and complexity of using Span<T> are justified only when you have measured or identified allocation and copying as a problem.