Span: Zero-Copy Memory Operations
August 27, 2024
Span<T>
is a stack-allocated ref struct that provides type-safe access to contiguous memory without allocation. It’s essentially a pointer with length and bounds checking.
Available since: .NET Core 2.1 / .NET Standard 2.1 / C# 7.2
Core Concept
// All these create spans over the same memory
int[] array = {1, 2, 3, 4, 5};
Span<int> span1 = array;
Span<int> span2 = array.AsSpan();
Span<int> span3 = new Span<int>(array, 1, 3); // elements 2,3,4
Stack Allocation
// Allocates on stack, not heap
Span<byte> buffer = stackalloc byte[256];
ProcessData(buffer);
void ProcessData(Span<byte> data)
{
// Works with any contiguous memory source
for (int i = 0; i < data.Length; i++)
data[i] = (byte)(i % 256);
}
Slicing Without Copying
ReadOnlySpan<char> text = "Hello, World!";
ReadOnlySpan<char> hello = text[..5]; // "Hello"
ReadOnlySpan<char> world = text[7..12]; // "World"
// No string allocations occurred
Parsing Performance
// Old way - allocates substring
string ExtractValue(string input)
{
int start = input.IndexOf('=') + 1;
return input.Substring(start);
}
// New way - zero allocations
ReadOnlySpan<char> ExtractValue(ReadOnlySpan<char> input)
{
int start = input.IndexOf('=') + 1;
return input[start..];
}
// If you need to parse the extracted value
int ExtractNumberValue(ReadOnlySpan<char> input)
{
ReadOnlySpan<char> numberSpan = ExtractValue(input);
return int.Parse(numberSpan); // Parse directly from span
}
Memory Pattern Matching
static bool StartsWithIgnoreCase(ReadOnlySpan<char> text, ReadOnlySpan<char> prefix)
{
return text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
}
// Usage
ReadOnlySpan<char> data = GetLargeText();
if (StartsWithIgnoreCase(data, "HTTP/"))
{
// Process without allocating substrings
}
Ref Structs and Stack-Only Types
Span<T>
is a ref struct, meaning it can only live on the stack, never on the heap. This enables zero-allocation memory views but comes with constraints.
// Ref struct - stack allocated, contains references to memory
public readonly ref struct TextParser
{
private readonly ReadOnlySpan<char> _text;
public TextParser(ReadOnlySpan<char> text) => _text = text;
public ReadOnlySpan<char> ExtractWord(int index)
{
// Can safely return spans - all stack allocated
int start = FindWordStart(index);
int end = FindWordEnd(start);
return _text[start..end];
}
}
// Usage - everything stays on stack
ReadOnlySpan<char> input = "Hello world from spans";
var parser = new TextParser(input);
ReadOnlySpan<char> word = parser.ExtractWord(1); // "world"
Ref Struct Constraints
public class DataProcessor
{
// ILLEGAL - ref structs cannot be fields
// private Span<byte> _buffer;
// ILLEGAL - cannot be in async methods
// public async Task ProcessAsync(Span<byte> data) { }
// ILLEGAL - cannot be generic type arguments
// List<Span<byte>> spans;
// LEGAL - local variables and parameters only
public void ProcessData(Span<byte> data)
{
Span<byte> localBuffer = stackalloc byte[256];
ProcessBuffer(data, localBuffer);
}
private void ProcessBuffer(Span<byte> input, Span<byte> output)
{
// Both spans are stack-allocated references
input.CopyTo(output);
}
}
Custom Ref Structs with Spans
public readonly ref struct BufferWriter
{
private readonly Span<byte> _buffer;
private readonly int _position;
public BufferWriter(Span<byte> buffer, int position = 0)
{
_buffer = buffer;
_position = position;
}
public BufferWriter WriteInt32(int value)
{
BinaryPrimitives.WriteInt32LittleEndian(_buffer[_position..], value);
return new BufferWriter(_buffer, _position + 4);
}
public BufferWriter WriteBytes(ReadOnlySpan<byte> data)
{
data.CopyTo(_buffer[_position..]);
return new BufferWriter(_buffer, _position + data.Length);
}
}
// Usage - fluent API with zero allocations
Span<byte> buffer = stackalloc byte[1024];
var writer = new BufferWriter(buffer)
.WriteInt32(42)
.WriteBytes("Hello"u8);
ReadOnlySpan vs Span
ReadOnlySpan<T>
provides immutable access to memory, preventing accidental modifications and enabling broader usage scenarios.
// ReadOnlySpan - immutable view
public static int CountWords(ReadOnlySpan<char> text)
{
int count = 0;
bool inWord = false;
foreach (char c in text) // Can iterate directly
{
if (char.IsWhiteSpace(c))
inWord = false;
else if (!inWord)
{
inWord = true;
count++;
}
}
return count;
}
// Works with any source
string text = "Hello world";
char[] array = {'H', 'i'};
ReadOnlySpan<char> fromString = text; // Implicit conversion
ReadOnlySpan<char> fromArray = array;
ReadOnlySpan<char> fromLiteral = "Direct literal";
int words1 = CountWords(fromString);
int words2 = CountWords("Inline text"); // Direct usage
ReadOnlySpan String Operations
public static bool IsValidEmail(ReadOnlySpan<char> email)
{
int atIndex = email.IndexOf('@');
if (atIndex <= 0 || atIndex == email.Length - 1)
return false;
ReadOnlySpan<char> local = email[..atIndex];
ReadOnlySpan<char> domain = email[(atIndex + 1)..];
return domain.IndexOf('.') > 0 &&
!local.StartsWith('.') &&
!domain.EndsWith('.');
}
// Zero allocations for validation
bool valid = IsValidEmail("user@example.com");
Span vs ReadOnlySpan Conversion
Span<char> mutableSpan = stackalloc char[100];
ReadOnlySpan<char> readOnlyView = mutableSpan; // Implicit conversion
// Cannot go backwards - compilation error
// Span<char> mutable = readOnlySpan; // ERROR
public void ProcessData(Span<char> buffer)
{
// Can pass mutable span to readonly methods
int length = CalculateLength(buffer); // Span -> ReadOnlySpan
// Modify after readonly operations
buffer[0] = char.ToUpper(buffer[0]);
}
private int CalculateLength(ReadOnlySpan<char> text)
{
return text.TrimEnd().Length;
}
ReadOnlySpan with String Literals
// UTF-8 string literals create ReadOnlySpan<byte>
ReadOnlySpan<byte> utf8Data = "Hello, 世界"u8;
// Useful for protocol headers, magic numbers
ReadOnlySpan<byte> pngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
public static bool IsPngFile(ReadOnlySpan<byte> fileData)
{
return fileData.StartsWith(pngHeader);
}
The ref struct limitation ensures Span<T>
operations remain on the stack, preventing accidental heap allocations while enabling safe, high-performance memory operations. Use ReadOnlySpan<T>
when you only need read access - it’s safer and works in more scenarios.