You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Span<T> & ReadOnlySpan<T>

Access memory slices efficiently without allocation or copying

Last generated

Span<T> & ReadOnlySpan<T>

Master high-performance memory management with free flashcards and spaced repetition practice. This lesson covers Span<T> fundamentals, ReadOnlySpan<T> usage, stack-only constraints, and zero-allocation slicingβ€”essential concepts for building efficient C# applications that minimize heap pressure and garbage collection overhead.

Welcome to Memory Views πŸ’»

In modern C# applications, performance often comes down to how efficiently you manage memory. Traditional arrays and strings work well for most scenarios, but they come with hidden costs: heap allocations, garbage collection pauses, and unnecessary copying. Enter Span<T> and ReadOnlySpan<T>β€”revolutionary types introduced in C# 7.2 that provide a safe, high-performance way to work with contiguous memory without allocations.

🎯 What makes Span special? It's a ref struct that lives entirely on the stack, representing a view into memory that can point to arrays, stack-allocated memory, or even unmanaged memory. No heap allocations, no GC pressure, just blazing-fast memory access.

Core Concepts πŸ”

What is Span<T>?

Span<T> is a ref struct that provides a type-safe, memory-safe representation of a contiguous region of arbitrary memory. Think of it as a lightweight window into existing dataβ€”it doesn't own the memory, it just provides access to it.

<pre> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MEMORY OWNERSHIP VS VIEWS β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ Traditional Array (OWNS memory): β”‚ β”‚ β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β” β”‚ β”‚ β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚ 5 β”‚ (heap) β”‚ β”‚ β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Span<T> (VIEW into memory): β”‚ β”‚ β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β” β”‚ β”‚ β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚ 5 β”‚ (original) β”‚ β”‚ β””β”€β”€β”€β”΄β”€β”¬β”€β”΄β”€β”¬β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └───┴─ Span points here β”‚ β”‚ (no copy, no allocation) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ </pre>

Key characteristics:

  • Stack-only: Cannot be boxed, cannot be fields in classes, cannot be used in async methods
  • Zero-allocation slicing: Create sub-views without copying data
  • Type-safe: Provides bounds checking and type safety
  • Unified API: Works with arrays, stack memory, and native memory

ReadOnlySpan<T> - Immutable Views

ReadOnlySpan<T> is the read-only counterpart to Span<T>. It provides the same performance benefits but guarantees that the underlying data cannot be modified through the span.

string text = "Hello, World!";
ReadOnlySpan<char> span = text.AsSpan();
// span[0] = 'h'; // ❌ Compile error!

πŸ’‘ Tip: Use ReadOnlySpan<T> by default when you don't need to modify data. It communicates intent and enables the compiler to optimize better.

Stack-Only Constraint (ref struct)

The ref struct constraint is what makes Span<T> so fastβ€”but it comes with limitations:

<table> <tr><th>βœ… Allowed</th><th>❌ Not Allowed</th></tr> <tr><td>Local variables</td><td>Class/struct fields</td></tr> <tr><td>Method parameters</td><td>Boxing to object</td></tr> <tr><td>Return values</td><td>Async/await methods</td></tr> <tr><td>Stack arrays (stackalloc)</td><td>Lambda captures</td></tr> <tr><td>Synchronous LINQ</td><td>Generic type arguments (pre-C# 11)</td></tr> </table>

// βœ… Valid uses
public void Process(Span<int> data) { }

public Span<int> GetSlice(int[] array) 
{
    return array.AsSpan(0, 10);
}

// ❌ Invalid uses
public class Container
{
    private Span<int> _data; // ❌ Can't be a field!
}

public async Task ProcessAsync(Span<int> data) // ❌ Can't use in async!
{
    await Task.Delay(100);
}

Creating Spans

You can create Span<T> from multiple sources:

1. From arrays:

int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array; // Implicit conversion
Span<int> fullSpan = array.AsSpan();
Span<int> slice = array.AsSpan(1, 3); // Elements [1,2,3]

2. From stack memory (stackalloc):

Span<int> stackSpan = stackalloc int[10];
stackSpan[0] = 42;

3. From strings:

string text = "Hello";
ReadOnlySpan<char> charSpan = text.AsSpan();
ReadOnlySpan<char> substring = text.AsSpan(0, 3); // "Hel"

4. From Memory<T>:

Memory<int> memory = new int[] { 1, 2, 3 };
Span<int> span = memory.Span;

Slicing and Subviews

One of Span<T>'s most powerful features is zero-allocation slicing. You can create views into subsets of data without copying:

int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Span<int> span = numbers;

// Multiple ways to slice:
Span<int> first5 = span.Slice(0, 5);     // [0,1,2,3,4]
Span<int> last5 = span.Slice(5);         // [5,6,7,8,9]
Span<int> middle = span.Slice(3, 4);     // [3,4,5,6]

// Using range operators (C# 8.0+):
Span<int> rangeSlice = span[2..7];       // [2,3,4,5,6]
Span<int> fromStart = span[..3];         // [0,1,2]
Span<int> toEnd = span[5..];             // [5,6,7,8,9]

<pre> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SLICING WITHOUT ALLOCATION β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ Original Array: β”‚ β”‚ β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β” β”‚ β”‚ β”‚ 0 β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚ 5 β”‚ 6 β”‚ 7 β”‚ β”‚ β”‚ β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Slice(2, 4): β”‚ β”‚ β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β” β”‚ β”‚ β”‚ 0 β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚ 5 β”‚ 6 β”‚ 7 β”‚ β”‚ β”‚ β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”¬β”€β”΄β”€β”¬β”€β”΄β”€β”¬β”€β”΄β”€β”¬β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜ β”‚ β”‚ View into [2,3,4,5] β”‚ β”‚ (no copy!) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ </pre>

Common Operations

Span<T> provides a rich set of operations:

Accessing elements:

Span<int> span = stackalloc int[] { 1, 2, 3 };
int first = span[0];
span[1] = 42;
int length = span.Length;

Copying:

Span<int> source = stackalloc int[] { 1, 2, 3 };
Span<int> dest = stackalloc int[3];
source.CopyTo(dest);

// Or use TryCopyTo for safety:
if (!source.TryCopyTo(dest))
{
    Console.WriteLine("Destination too small");
}

Filling:

Span<int> span = stackalloc int[10];
span.Fill(42); // All elements = 42
span.Clear();  // All elements = 0

Searching:

ReadOnlySpan<char> text = "Hello World";
int index = text.IndexOf('W');           // 6
bool contains = text.Contains('o');      // true
int lastIndex = text.LastIndexOf('o');   // 7

Real-World Examples πŸš€

Example 1: String Parsing Without Allocations

Traditional string manipulation creates many temporary objects. With ReadOnlySpan<char>, you can parse strings with zero allocations:

public static class CsvParser
{
    public static void ParseLine(ReadOnlySpan<char> line)
    {
        while (line.Length > 0)
        {
            int commaIndex = line.IndexOf(',');
            
            ReadOnlySpan<char> field = commaIndex >= 0
                ? line.Slice(0, commaIndex)
                : line;
            
            // Process field (no string allocation!)
            ProcessField(field);
            
            // Move to next field
            line = commaIndex >= 0 
                ? line.Slice(commaIndex + 1) 
                : ReadOnlySpan<char>.Empty;
        }
    }
    
    private static void ProcessField(ReadOnlySpan<char> field)
    {
        // Trim whitespace without allocation
        field = field.Trim();
        
        // Parse as integer if needed
        if (int.TryParse(field, out int value))
        {
            Console.WriteLine($"Number: {value}");
        }
        else
        {
            Console.WriteLine($"Text: {field.ToString()}");
        }
    }
}

// Usage:
string csvLine = "John,30,Engineer,New York";
CsvParser.ParseLine(csvLine.AsSpan()); // Zero allocations!

πŸ’‘ Performance win: Traditional Split(',') would allocate a string array plus substrings. This approach allocates nothing.

Example 2: Buffer Processing with Stackalloc

When you need temporary buffers for small data, stackalloc with Span<T> is incredibly efficient:

public static string BytesToHex(byte[] bytes)
{
    const int maxStackAlloc = 256;
    
    // Use stack for small arrays, heap for large
    Span<char> buffer = bytes.Length <= maxStackAlloc / 2
        ? stackalloc char[bytes.Length * 2]
        : new char[bytes.Length * 2];
    
    for (int i = 0; i < bytes.Length; i++)
    {
        byte b = bytes[i];
        buffer[i * 2] = GetHexChar(b >> 4);
        buffer[i * 2 + 1] = GetHexChar(b & 0x0F);
    }
    
    return new string(buffer);
}

private static char GetHexChar(int value)
{
    return (char)(value < 10 ? '0' + value : 'A' + value - 10);
}

// Usage:
byte[] data = { 0xFF, 0xA3, 0x12 };
string hex = BytesToHex(data); // "FFA312" - buffer on stack!

⚑ Performance note: For arrays up to 128 bytes, stackalloc is typically faster than heap allocation and doesn't trigger GC.

Example 3: Efficient Array Manipulation

Span<T> makes working with array segments much cleaner:

public static void ReverseWords(char[] sentence)
{
    Span<char> span = sentence;
    
    // Reverse entire sentence first
    span.Reverse();
    
    // Then reverse each word
    int start = 0;
    for (int i = 0; i <= span.Length; i++)
    {
        if (i == span.Length || span[i] == ' ')
        {
            // Reverse the word from start to i-1
            span.Slice(start, i - start).Reverse();
            start = i + 1;
        }
    }
}

// Usage:
char[] text = "Hello World from Span".ToCharArray();
ReverseWords(text);
Console.WriteLine(new string(text)); // "Span from World Hello"

πŸ”§ Try this: Implement a similar algorithm using traditional string methodsβ€”you'll see how many allocations it requires!

Example 4: Working with Binary Data

Span<T> is perfect for low-level binary operations:

public static class BinaryUtils
{
    public static int ReadInt32BigEndian(ReadOnlySpan<byte> buffer)
    {
        if (buffer.Length < 4)
            throw new ArgumentException("Buffer too small");
        
        return (buffer[0] << 24) | 
               (buffer[1] << 16) | 
               (buffer[2] << 8) | 
               buffer[3];
    }
    
    public static void WriteInt32BigEndian(Span<byte> buffer, int value)
    {
        if (buffer.Length < 4)
            throw new ArgumentException("Buffer too small");
        
        buffer[0] = (byte)(value >> 24);
        buffer[1] = (byte)(value >> 16);
        buffer[2] = (byte)(value >> 8);
        buffer[3] = (byte)value;
    }
    
    // Process packets without allocations
    public static void ProcessPacket(ReadOnlySpan<byte> packet)
    {
        if (packet.Length < 8)
            return;
        
        ReadOnlySpan<byte> header = packet.Slice(0, 4);
        ReadOnlySpan<byte> payload = packet.Slice(4);
        
        int packetId = ReadInt32BigEndian(header);
        Console.WriteLine($"Packet ID: {packetId}, Payload: {payload.Length} bytes");
    }
}

// Usage:
byte[] networkData = new byte[100];
BinaryUtils.WriteInt32BigEndian(networkData.AsSpan(), 12345);
BinaryUtils.ProcessPacket(networkData);

Common Mistakes ⚠️

Mistake 1: Trying to Store Span in a Field

// ❌ WRONG - Won't compile!
public class DataProcessor
{
    private Span<int> _buffer; // Error: ref struct can't be a field
    
    public DataProcessor(int[] data)
    {
        _buffer = data;
    }
}

// βœ… RIGHT - Store the underlying array or Memory<T>
public class DataProcessor
{
    private readonly int[] _buffer;
    
    public DataProcessor(int[] data)
    {
        _buffer = data;
    }
    
    public void Process()
    {
        Span<int> span = _buffer; // Create span when needed
        // Use span...
    }
}

// βœ… ALTERNATIVE - Use Memory<T> for storage
public class DataProcessor
{
    private readonly Memory<int> _buffer;
    
    public DataProcessor(int[] data)
    {
        _buffer = data;
    }
    
    public void Process()
    {
        Span<int> span = _buffer.Span;
        // Use span...
    }
}

Mistake 2: Using Span in Async Methods

// ❌ WRONG - Span can't be used in async methods
public async Task ProcessAsync(Span<int> data)
{
    await Task.Delay(100); // Error!
    // Process data...
}

// βœ… RIGHT - Use Memory<T> instead
public async Task ProcessAsync(Memory<int> data)
{
    await Task.Delay(100);
    Span<int> span = data.Span; // Get span when needed
    // Process span...
}

// βœ… ALTERNATIVE - Process before await
public async Task ProcessAsync(int[] data)
{
    Span<int> span = data;
    int result = ProcessSpan(span); // Synchronous span usage
    
    await Task.Delay(100);
    
    // Use result...
}

private int ProcessSpan(Span<int> span)
{
    // All span operations here
    return span.Length;
}

Mistake 3: Dangling References with Stackalloc

// ❌ WRONG - Returning stack memory!
public Span<int> CreateBuffer()
{
    Span<int> buffer = stackalloc int[10];
    return buffer; // Dangerous! Stack will be unwound!
}

// βœ… RIGHT - Use array for returns
public Span<int> CreateBuffer()
{
    int[] buffer = new int[10];
    return buffer; // Safe - array is on heap
}

// βœ… RIGHT - Process stack data immediately
public int ProcessData()
{
    Span<int> buffer = stackalloc int[10];
    // Fill buffer...
    return ComputeSum(buffer); // Use it before returning
}

Mistake 4: Not Checking Bounds Before Slicing

// ❌ WRONG - No bounds checking
public void ProcessSubstring(string text, int start, int length)
{
    ReadOnlySpan<char> span = text.AsSpan(start, length); // Throws if out of bounds!
    // Process...
}

// βœ… RIGHT - Validate parameters
public void ProcessSubstring(string text, int start, int length)
{
    if (start < 0 || start + length > text.Length)
    {
        throw new ArgumentOutOfRangeException();
    }
    
    ReadOnlySpan<char> span = text.AsSpan(start, length);
    // Process...
}

// βœ… ALTERNATIVE - Use range operators with bounds check
public void ProcessSubstring(string text, int start, int length)
{
    if (start < 0 || start + length > text.Length)
        return;
    
    ReadOnlySpan<char> span = text.AsSpan()[start..(start + length)];
    // Process...
}

Mistake 5: Unnecessary ToString() Calls

// ❌ WRONG - Allocates string unnecessarily
public bool StartsWithHello(string text)
{
    ReadOnlySpan<char> span = text.AsSpan(0, 5);
    string substr = span.ToString(); // Allocation!
    return substr == "Hello";
}

// βœ… RIGHT - Use SequenceEqual
public bool StartsWithHello(string text)
{
    if (text.Length < 5) return false;
    
    ReadOnlySpan<char> span = text.AsSpan(0, 5);
    return span.SequenceEqual("Hello".AsSpan()); // No allocation!
}

// βœ… ALTERNATIVE - Use StartsWith extension
public bool StartsWithHello(string text)
{
    return text.AsSpan().StartsWith("Hello".AsSpan());
}

Performance Characteristics πŸ“Š

<table> <tr><th>Operation</th><th>Traditional</th><th>Span<T></th><th>Benefit</th></tr> <tr><td>Substring</td><td>O(n) + allocation</td><td>O(1) + zero allocation</td><td>⚑ Much faster</td></tr> <tr><td>Array slice</td><td>Array.Copy + allocation</td><td>View creation</td><td>⚑ Zero copy</td></tr> <tr><td>Buffer operations</td><td>Heap allocation + GC</td><td>Stack allocation</td><td>⚑ No GC pressure</td></tr> <tr><td>Element access</td><td>Bounds check</td><td>Bounds check</td><td>βœ… Same safety</td></tr> <tr><td>String comparison</td><td>Allocates temp strings</td><td>Direct comparison</td><td>⚑ Zero allocation</td></tr> </table>

🧠 Memory device: Remember "SPAN" = Stack Performance And No-allocation!

Span vs Memory<T> πŸ†š

While Span<T> is powerful, sometimes you need Memory<T>:

<table> <tr><th>Feature</th><th>Span<T></th><th>Memory<T></th></tr> <tr><td><strong>Location</strong></td><td>Stack only (ref struct)</td><td>Heap (regular struct)</td></tr> <tr><td><strong>Async support</strong></td><td>❌ No</td><td>βœ… Yes</td></tr> <tr><td><strong>Field storage</strong></td><td>❌ No</td><td>βœ… Yes</td></tr> <tr><td><strong>Performance</strong></td><td>⚑ Fastest</td><td>⚑ Fast</td></tr> <tr><td><strong>Use case</strong></td><td>Sync hot paths</td><td>Async, storage</td></tr> <tr><td><strong>Get Span</strong></td><td>Direct use</td><td>.Span property</td></tr> </table>

// When to use each:
public void SynchronousProcessing(int[] data)
{
    Span<int> span = data; // βœ… Use Span for sync
    ProcessSync(span);
}

public async Task AsynchronousProcessing(int[] data)
{
    Memory<int> memory = data; // βœ… Use Memory for async
    await ProcessAsync(memory);
}

private async Task ProcessAsync(Memory<int> memory)
{
    await Task.Delay(100);
    Span<int> span = memory.Span; // Get Span when needed
    // Use span...
}

Key Takeaways 🎯

  1. Span<T> is a view, not ownershipβ€”it points to existing memory without copying or allocating

  2. Stack-only constraint (ref struct) enables incredible performance but limits usage scenarios

  3. Zero-allocation slicing makes substring and array segment operations essentially free

  4. ReadOnlySpan<T> should be your default choice when you don't need to modify data

  5. Use stackalloc with Span<T> for small temporary buffers (typically < 1KB)

  6. Memory<T> bridges the gap when you need async support or field storage

  7. Avoid ToString() on spans when possibleβ€”use SequenceEqual, StartsWith, or other span methods

  8. Check bounds before slicing to avoid runtime exceptions

  9. String parsing becomes allocation-free with ReadOnlySpan<char>

  10. Performance matters: In hot paths, Span<T> can reduce allocations by 90%+ compared to traditional approaches

πŸ“‹ Quick Reference Card

<div style="border: 2px solid #4fd1c5; border-radius: 8px; padding: 16px; margin: 16px 0; background: rgba(79, 209, 197, 0.1);"> <h4>πŸ’» Span<T> Cheat Sheet</h4> <table> <tr><td><strong>Create from array</strong></td><td><code>Span<int> s = array;</code></td></tr> <tr><td><strong>Create from stack</strong></td><td><code>Span<int> s = stackalloc int[10];</code></td></tr> <tr><td><strong>Slice</strong></td><td><code>span.Slice(start, length)</code> or <code>span[start..end]</code></td></tr> <tr><td><strong>Copy</strong></td><td><code>source.CopyTo(dest)</code></td></tr> <tr><td><strong>Fill</strong></td><td><code>span.Fill(value)</code></td></tr> <tr><td><strong>Clear</strong></td><td><code>span.Clear()</code></td></tr> <tr><td><strong>Search</strong></td><td><code>span.IndexOf(value)</code></td></tr> <tr><td><strong>Compare</strong></td><td><code>span.SequenceEqual(other)</code></td></tr> <tr><td><strong>String to span</strong></td><td><code>text.AsSpan()</code></td></tr> <tr><td><strong>For async</strong></td><td>Use <code>Memory<T></code> instead</td></tr> </table>

<strong>⚠️ Restrictions:</strong> <ul> <li>❌ No fields in classes</li> <li>❌ No async methods</li> <li>❌ No boxing</li> <li>❌ No lambda captures</li> <li>βœ… Local variables only</li> <li>βœ… Method parameters/returns</li> </ul> </div>

πŸ“š Further Study

  1. Microsoft Docs - Memory and Span Usage Guidelines: https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/
  2. Stephen Toub's Span<T> Deep Dive: https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
  3. High-Performance C# with Span<T>: https://www.youtube.com/watch?v=byvoPD15CXs

πŸŽ“ Practice tip: Try refactoring string-heavy code in your projects to use ReadOnlySpan<char>. Profile before and afterβ€”you'll be amazed at the performance gains and reduced GC pressure!

πŸ”¬ Advanced challenge: Implement a zero-allocation JSON parser for simple key-value pairs using ReadOnlySpan<char>. Compare memory allocations with existing libraries using a profiler.