C# tutorials > Modern C# Features > C# 6.0 and Later > Explain `Span<T>` and `ReadOnlySpan<T>` and their importance for performance.

Explain `Span<T>` and `ReadOnlySpan<T>` and their importance for performance.

Understanding Span<T> and ReadOnlySpan<T> in C#

Span<T> and ReadOnlySpan<T> are value types introduced in C# 7.2 that provide a safe and efficient way to represent contiguous regions of arbitrary memory. They are crucial for writing high-performance code, especially when dealing with arrays, strings, and other data structures that require manipulation without unnecessary memory allocations or copying.

This tutorial will delve into the concepts behind Span<T> and ReadOnlySpan<T>, demonstrate their usage with code examples, and explain their benefits for performance optimization.

What are Span<T> and ReadOnlySpan<T>?

Span<T> is a struct that provides a type-safe, contiguous view of memory. It can represent a portion of an array, a string, or even unmanaged memory. Importantly, Span<T> does not own the memory it points to; it's just a window into existing memory.

ReadOnlySpan<T> is a similar struct, but it provides read-only access to the underlying memory. It is used when you need to ensure that the data being accessed is not modified.

Both Span<T> and ReadOnlySpan<T> are ref struct types, which means they can only live on the stack and cannot be stored on the heap. This restriction helps prevent memory leaks and ensures that they remain lightweight.

Basic Usage: Creating a Span from an Array

This code demonstrates how to create a Span<int> from an existing integer array. Modifying the Span directly modifies the underlying array, highlighting the fact that the Span is a view, not a copy. The code also shows how to create a ReadOnlySpan<int> and the fact that modifying it is not allowed, enforced at compile time.

using System;

public class SpanExample
{
    public static void Main(string[] args)
    {
        int[] numbers = { 1, 2, 3, 4, 5 };

        // Create a Span<int> that represents the entire array
        Span<int> span = new Span<int>(numbers);

        // Modify the first element using the Span
        span[0] = 10;

        Console.WriteLine($"The first element is now: {numbers[0]}"); // Output: 10

        // Create a ReadOnlySpan<int> from the array
        ReadOnlySpan<int> readOnlySpan = numbers;

        // Attempting to modify readOnlySpan will result in a compile-time error
        // readOnlySpan[0] = 20; // Error: Property or indexer 'ReadOnlySpan<int>.this[int]' cannot be assigned to -- it is read only

        Console.WriteLine($"The first element using ReadOnlySpan: {readOnlySpan[0]}"); // Output: 10
    }
}

Slicing Spans

Slicing allows you to create new Span<T> instances that represent a smaller portion of the original memory. The Slice() method creates a new Span or ReadOnlySpan that starts at a specified index and optionally has a specified length. This is extremely useful for parsing and manipulating data without copying.

using System;

public class SpanSliceExample
{
    public static void Main(string[] args)
    {
        char[] text = "Hello, World!".ToCharArray();

        // Create a Span<char> from the char array
        Span<char> span = new Span<char>(text);

        // Create a slice of the Span from index 7 to the end
        Span<char> worldSpan = span.Slice(7);

        Console.WriteLine(worldSpan.ToString()); // Output: World!

        // Create a slice of the Span from index 0 with a length of 5
        ReadOnlySpan<char> helloSpan = span.Slice(0, 5);

        Console.WriteLine(helloSpan.ToString()); // Output: Hello
    }
}

Importance for Performance

Span<T> and ReadOnlySpan<T> improve performance because they avoid unnecessary memory allocations and copying. When you use methods like Substring() on a string, a new string object is created, incurring a memory allocation and data copy. With Span<T>, you can operate directly on the existing memory, reducing overhead. This is particularly beneficial in scenarios involving:

  • String parsing and manipulation
  • Working with large arrays
  • Interfacing with native code
  • High-performance data processing

Real-Life Use Case: Parsing CSV Data

This example demonstrates parsing a comma-separated value (CSV) line using ReadOnlySpan<char>. Instead of creating multiple substrings, we use Slice() and IndexOf() to efficiently extract the different fields directly from the original string. This avoids unnecessary string allocations, leading to improved performance, especially when parsing large CSV files.

using System;

public class CsvParser
{
    public static void Main(string[] args)
    {
        string csvLine = "John,Doe,30,Engineer";

        ReadOnlySpan<char> lineSpan = csvLine.AsSpan();
        int currentIndex = 0;

        // Parse the first name
        int commaIndex = lineSpan.Slice(currentIndex).IndexOf(',');
        ReadOnlySpan<char> firstName = lineSpan.Slice(currentIndex, commaIndex);
        Console.WriteLine($"First Name: {firstName.ToString()}");

        currentIndex += commaIndex + 1;

        // Parse the last name
        commaIndex = lineSpan.Slice(currentIndex).IndexOf(',');
        ReadOnlySpan<char> lastName = lineSpan.Slice(currentIndex, commaIndex);
        Console.WriteLine($"Last Name: {lastName.ToString()}");

        // Continue parsing other fields similarly
    }
}

Best Practices

  • Use ReadOnlySpan<T> when possible: If you don't need to modify the data, use ReadOnlySpan<T> to ensure data integrity and prevent accidental modifications.
  • Be mindful of the lifetime of the underlying memory: Since Span<T> doesn't own the memory, ensure that the underlying data structure remains valid for the duration of the Span's usage.
  • Avoid storing Span<T> in class fields: As Span<T> is a ref struct, it cannot be stored on the heap.

When to use them

Use Span<T> and ReadOnlySpan<T> in scenarios where performance is critical and you need to avoid unnecessary memory allocations and data copying, such as:

  • String manipulation and parsing
  • Working with large arrays or buffers
  • Interfacing with unmanaged memory
  • Implementing high-performance algorithms

Memory footprint

Span<T> is a value type (struct) consisting of two key components:

  • Pointer to the Memory Block: A managed pointer that points to the start of the memory block.
  • Length of the Span: An integer that specifies the number of elements the span represents.

Because it is a value type and not a reference type, it does not allocate memory on the heap. This means that creating and using spans generally results in lower memory overhead, contributing to better performance.

Alternatives

Before Span<T>, developers often used:

  • Array Slicing (Array.Copy, Array.Slice): Creates a new array, copying the desired elements. This incurs memory allocation and copying overhead.
  • String Substring (string.Substring): Creates a new string object, again involving memory allocation and copying.
  • Pointers and Unsafe Code: Can be used to directly access memory, but requires careful management and carries the risk of memory corruption and security vulnerabilities.

Span<T> provides a safer and more efficient alternative to these approaches.

Pros

  • Zero-copy abstraction: Avoids unnecessary memory allocations and data copying.
  • Type safety: Provides compile-time type checking and prevents common memory errors.
  • Performance: Improves performance by reducing memory overhead and enabling efficient data processing.
  • Safety: Offers a safer alternative to raw pointers with managed memory access.

Cons

  • Ref struct limitation: Span<T> is a ref struct, so it has restrictions on where it can be used (e.g., cannot be stored in class fields or used in async methods without careful consideration).
  • API Complexity: May require a change in coding style and understanding of the underlying memory management concepts.

Interview Tip

When discussing Span<T> and ReadOnlySpan<T> in an interview, emphasize their role in achieving zero-copy abstractions. Highlight the scenarios where they shine, such as string parsing, data processing, and interfacing with unmanaged memory. Be prepared to discuss the limitations imposed by the ref struct nature of Span<T> and the potential performance gains they offer. Give some usage examples like parsing a string to retrieve some value, also mention that span does not allocate memory in the heap, so this reduces memory pressure.

FAQ

  • Can I use `Span<T>` in async methods?

    Using Span<T> directly in async methods requires caution because async methods can be suspended and resumed later, potentially causing the underlying memory to be invalidated. To safely use Span<T> with async methods, you should typically convert the data to an array or other managed memory before passing it to the async method. You might pin the memory using fixed statement.
  • What happens if the underlying memory is modified while I'm using a `Span<T>`?

    Modifying the underlying memory while using a Span<T> can lead to unexpected behavior or memory corruption. It is crucial to ensure that the underlying data remains valid and consistent for the duration of the Span's usage. Use ReadOnlySpan<T> if you only need read access.
  • Is `Span<T>` only for arrays and strings?

    No, Span<T> can be used with any contiguous region of memory, including arrays, strings, and unmanaged memory. It provides a versatile way to work with memory efficiently and safely.