Span
I guess I should start by writing about Span<T>, just like every .NET developer eventually does.
Several programming languages also have span-like types.
For example C++,
std::span<T>
Go, Rust and Zig have slice types for a similar purpose
[]T &[T] []T
What Is Span<T>
Span<T> is a type that contains a pointer and length value. It represents a view over region of memory without allocating on the heap.
The code snippet below from .NET Source Code shows the internal fields of Span<T>:
public readonly ref struct Span<T>
{
/// <summary>A byref or a native ptr.</summary>
internal readonly ref T _reference;
/// <summary>The number of elements this Span contains.</summary>
private readonly int _length;
}
You can iterate over that view of memory and modify. Of course, not all memory should be mutable, so .NET also provides ReadOnlySpan
So then it sound perfect, no allocation at all! Not exactly because Span comes with several restrictions.
Span<T> is a stack-only type (ref struct). This means it must live on the stack and cannot be boxed. Because its lifetime must always remain on the stack, it cannot be used in async methods.
For most .NET developers Span is mainly used for allocation-free and fast string processing. In this article, I want to focus on how I use Span in my daily work with raw data processing.
Use Case
I work with embedded systems and my job involves real-time monitoring. Reading raw data from any source(network, COM ports etc.) often involve allocations — which is “the main enemy of high-performance .NET code”.
Let’s say we have a protocol as shown below, where the phase currents U, V, and W are each 2 bytes long in little endian format.
public struct PhaseCurrents
{
public short URMS; //Little Endian
public short VRMS; //Little Endian
public short WRMS; //Little Endian
}
Each value can be extracted efficiently using Span<T> and BinaryPrimitives.
In .NET, System.Buffers.Binary.BinaryPrimitives lets you efficiently convert raw byte arrays into primitive types. Its most useful feature is that it is allocation-free, which is crucial for real-time data processing. It also works seamlessly with Span<T> for heap-free parsing.
private byte[] dataFromAnySource= new byte[] {0xAA, 0xBB, 0xCC, 0xDD, 0x1, 0x2};
PhaseCurrents currents= new();
currents.URMS= BinaryPrimitives.ReadInt16LittleEndian(dataFromAnySource.AsSpan(0,2));
currents.VRMS= BinaryPrimitives.ReadInt16LittleEndian(dataFromAnySource.AsSpan(2,2));
currents.WRMS= BinaryPrimitives.ReadInt16LittleEndian(dataFromAnySource.AsSpan(4,2));
Resources
Pro .NET Memory Management (2nd edition) Deep .NET: A Complete .NET Developer’s Guide to Span with Stephen Toub and Scott Hanselman