ref struct and stackalloc are your secret weapons for obliterating heap allocations in C#, leading to significantly faster and more memory-efficient code.
Let’s see this in action. Imagine you’re processing a buffer of data, perhaps reading from a file or network stream. A common, but inefficient, approach might look like this:
public void ProcessDataInefficient(byte[] data)
{
// This creates a new object on the heap
var buffer = new byte[data.Length];
Array.Copy(data, buffer, data.Length);
// ... process buffer ...
}
Every time ProcessDataInefficient is called, a new byte[] array is allocated on the managed heap. For high-throughput scenarios, this can quickly become a performance bottleneck due to the overhead of allocation and subsequent garbage collection.
Now, let’s refactor this using ref struct and stackalloc:
public ref struct DataProcessor
{
private readonly Span<byte> _buffer;
public DataProcessor(int size)
{
// stackalloc allocates memory directly on the stack
_buffer = new Span<byte>(stackalloc byte[size]);
}
public Span<byte> GetBuffer() => _buffer;
public void Process()
{
// ... process _buffer using Span<T> operations ...
// No heap allocations here!
_buffer[0] = 1;
_buffer[1] = 2;
}
}
public class Example
{
public void ProcessDataEfficient(byte[] data)
{
// Create an instance of our ref struct
var processor = new DataProcessor(data.Length);
// Copy data into the stack-allocated buffer
data.CopyTo(processor.GetBuffer());
// Process the data using the ref struct
processor.Process();
}
}
The DataProcessor is a ref struct. This means it’s allocated on the stack, not the heap. The stackalloc keyword within its constructor directly carves out memory on the stack for the byte array. Span<byte> is then used to safely access this stack-allocated memory. Crucially, Span<T> is also a ref struct, further reinforcing the stack-bound nature of this operation.
The core problem this pattern solves is the cost of heap allocation and garbage collection. When you allocate objects on the heap, the .NET runtime needs to track them, mark them as reachable or unreachable, and eventually reclaim their memory. This process, managed by the Garbage Collector (GC), has a performance cost. For applications that need to perform many small allocations rapidly, or applications with very strict latency requirements (like high-frequency trading or real-time game engines), the GC pauses can become unacceptable. By moving allocations to the stack, you eliminate this overhead entirely. The stack is a much simpler memory management model: memory is automatically reclaimed when the scope in which it was allocated goes out of scope.
The true power comes from understanding how Span<T> and ref struct interact. Span<T> is a ref struct that provides a safe, type-safe view into contiguous memory. This memory can reside on the heap, on the stack, or even be unmanaged. When you combine Span<T> with stackalloc, you get a stack-allocated buffer that you can work with using all the familiar, high-performance Span<T> APIs (like Slice, Fill, SequenceEqual, etc.) without any heap allocation. This is a massive win for performance-critical code paths.
The constraint that ref structs cannot be boxed, cannot be used as generic type arguments (unless they are constrained to struct), and cannot be fields of a class or non-ref struct are all direct consequences of them living on the stack. If a ref struct were allowed to be a field of a heap-allocated object, its lifetime would become tied to the heap object, and it would need to be allocated on the heap itself. Similarly, boxing would require moving it to the heap. These restrictions ensure that ref structs always live on the stack and are automatically cleaned up when their containing scope exits.
The next hurdle you’ll likely encounter is when you need to pass these stack-allocated buffers across method boundaries that don’t have the same stack lifetime.