Skip to main content

C# Span<T> and ReadOnlySpan<T>

Introduction

Memory management is a crucial aspect of any application, especially when performance is a priority. In C#, the Span<T> and ReadOnlySpan<T> types were introduced in .NET Core 2.1 (and later in .NET Standard 2.1) as powerful tools to work with contiguous memory regions without creating unnecessary allocations.

These types provide a way to work with arrays, strings, and other memory sequences in a high-performance manner by representing a continuous block of memory without copying the data. This is particularly useful for operations that would otherwise create temporary arrays or strings, leading to unnecessary garbage collection pressure.

What is Span<T>?

Span<T> is a ref struct that represents a contiguous region of arbitrary memory. It's similar to an array but with some key differences:

  1. It's a value type that can only live on the stack (not the heap)
  2. It's a window into existing memory, not a copy
  3. It enables zero-allocation operations on arrays, strings, and other memory
  4. It can represent memory from managed arrays, native memory, or the stack

Think of Span<T> as a view into memory that lets you work with the data without copying it.

Basic Usage of Span<T>

Let's start with some basic examples of how to use Span<T>:

csharp
using System;

public class SpanBasics
{
public static void Main()
{
// Creating a Span from an array
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Span<int> entireSpan = numbers;

// Creating a Span from part of an array
Span<int> partialSpan = numbers.AsSpan(3, 3); // Start at index 3, length 3

Console.WriteLine("Original array:");
foreach (var num in numbers)
Console.Write($"{num} ");

Console.WriteLine("\n\nEntire span:");
foreach (var num in entireSpan)
Console.Write($"{num} ");

Console.WriteLine("\n\nPartial span (index 3, length 3):");
foreach (var num in partialSpan)
Console.Write($"{num} ");

// Modifying the Span modifies the underlying array
partialSpan[0] = 42;

Console.WriteLine("\n\nArray after modifying span:");
foreach (var num in numbers)
Console.Write($"{num} ");
}
}

Output:

Original array:
1 2 3 4 5 6 7 8 9

Entire span:
1 2 3 4 5 6 7 8 9

Partial span (index 3, length 3):
4 5 6

Array after modifying span:
1 2 3 42 5 6 7 8 9

What is ReadOnlySpan<T>?

ReadOnlySpan<T> is very similar to Span<T>, but as the name suggests, it provides read-only access to a contiguous memory block. It's especially useful for operations that don't need to modify the underlying data, and it can be created from immutable data sources like string literals.

Working with ReadOnlySpan<T>

Here's how you can use ReadOnlySpan<T>:

csharp
using System;

public class ReadOnlySpanBasics
{
public static void Main()
{
// Creating a ReadOnlySpan from a string
string text = "Hello, Span world!";
ReadOnlySpan<char> span = text.AsSpan();

// Slicing a ReadOnlySpan
ReadOnlySpan<char> greeting = span.Slice(0, 5); // "Hello"

Console.WriteLine($"Original text: {text}");
Console.WriteLine($"Greeting: {greeting.ToString()}");

// Working with string literals directly
ReadOnlySpan<char> directSpan = "This is a string literal".AsSpan();

// Find position of a character
int position = directSpan.IndexOf('s');
Console.WriteLine($"First 's' is at position: {position}");

// ReadOnlySpan cannot be modified
// The following would cause a compilation error:
// span[0] = 'h';
}
}

Output:

Original text: Hello, Span world!
Greeting: Hello
First 's' is at position: 3

Performance Benefits

The main advantage of Span<T> and ReadOnlySpan<T> is performance. Let's compare traditional string manipulation with ReadOnlySpan<T>:

csharp
using System;
using System.Diagnostics;

public class PerformanceComparison
{
public static void Main()
{
const int iterations = 1000000;
string testString = "The quick brown fox jumps over the lazy dog";

// Traditional substring approach
Stopwatch sw1 = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
string word = testString.Substring(16, 3); // "fox"
if (word[0] != 'f')
Console.WriteLine("Unexpected result!");
}
sw1.Stop();

// Using ReadOnlySpan approach
Stopwatch sw2 = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
ReadOnlySpan<char> span = testString.AsSpan(16, 3); // "fox"
if (span[0] != 'f')
Console.WriteLine("Unexpected result!");
}
sw2.Stop();

Console.WriteLine($"Traditional substring: {sw1.ElapsedMilliseconds}ms");
Console.WriteLine($"Using ReadOnlySpan: {sw2.ElapsedMilliseconds}ms");
}
}

Output (results will vary):

Traditional substring: 58ms
Using ReadOnlySpan: 4ms

The ReadOnlySpan<T> approach is significantly faster because:

  1. It avoids creating new string objects
  2. It doesn't allocate memory on the heap
  3. It reduces garbage collection pressure

Practical Use Cases

1. String Parsing Without Allocations

csharp
public static bool TryParseHeader(ReadOnlySpan<char> headerLine, out string name, out string value)
{
name = null;
value = null;

int colonIndex = headerLine.IndexOf(':');
if (colonIndex <= 0)
return false;

ReadOnlySpan<char> nameSpan = headerLine.Slice(0, colonIndex).Trim();
ReadOnlySpan<char> valueSpan = headerLine.Slice(colonIndex + 1).Trim();

name = nameSpan.ToString();
value = valueSpan.ToString();
return true;
}

// Usage
string header = "Content-Type: application/json";
if (TryParseHeader(header.AsSpan(), out var name, out var value))
{
Console.WriteLine($"{name} = {value}");
}

2. Working with Binary Data

csharp
public static int ReadInt32(ReadOnlySpan<byte> data, int startIndex)
{
ReadOnlySpan<byte> intBytes = data.Slice(startIndex, 4);
return BitConverter.ToInt32(intBytes);
}

// Usage
byte[] fileData = new byte[1024];
// Assume fileData is filled from file reading
int value = ReadInt32(fileData, 128);

3. Array Manipulation

csharp
public static void ReverseArray<T>(Span<T> array)
{
array.Reverse();
}

// Usage
int[] numbers = { 1, 2, 3, 4, 5 };
ReverseArray(numbers);
Console.WriteLine(string.Join(", ", numbers)); // Outputs: 5, 4, 3, 2, 1

Limitations of Span<T>

Since Span<T> is a ref struct, it has some notable limitations:

  1. It cannot be stored as a field in a class or a non-ref struct
  2. It cannot be used in async methods
  3. It cannot be used in iterators (methods with yield return)
  4. It cannot be boxed or used as a generic type argument
csharp
// This will NOT compile
class MyClass
{
// Error: Cannot use ref struct as field
private Span<int> span;
}

// This will NOT compile
async Task ProcessAsync(Span<int> span)
{
// Error: Cannot use ref struct in async method
await Task.Delay(100);
}

For these scenarios, Memory<T> and ReadOnlyMemory<T> (heap-based counterparts of Span<T> and ReadOnlySpan<T>) are more appropriate.

Converting Between Types

You can easily convert between various types:

csharp
// Array to Span
int[] array = { 1, 2, 3 };
Span<int> span = array;

// Span to ReadOnlySpan (implicit conversion)
ReadOnlySpan<int> roSpan = span;

// String to ReadOnlySpan
string str = "Hello";
ReadOnlySpan<char> charSpan = str.AsSpan();

// Memory to Span
Memory<byte> memory = new byte[100];
Span<byte> memorySpan = memory.Span;

Summary

Span<T> and ReadOnlySpan<T> are powerful tools in C# for working with memory efficiently:

  • They provide a view over existing memory without copying data
  • They significantly reduce allocations and improve performance
  • They work with arrays, strings, and other memory sources
  • They're especially useful for parsing, slicing, and manipulating data

The main trade-off is that they have restrictions (ref struct limitations) which means they're best used in performance-critical, localized scenarios rather than as fields or in async code.

Additional Resources

Exercises

  1. Write a function that counts the occurrences of a character in a string using ReadOnlySpan<char> instead of string methods.
  2. Create a function that checks if a string is a palindrome using ReadOnlySpan<char> without allocating additional memory.
  3. Implement a simple CSV parser that uses Span<T> to extract values from a line without creating substrings.
  4. Compare the performance of parsing a large text file line by line using traditional strings vs. using ReadOnlySpan<char>.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)