Using ReadOnlySpan in .NET

2024-06-12

In .NET, one of the important features for performance and memory management is ReadOnlySpan<T>. In this article, we will explore what ReadOnlySpan<T> is, how to use it, and its technical details. Additionally, we will look at how it translates to IL code and compare it to other collection types.

What is ReadOnlySpan<T>?

ReadOnlySpan<T> is a type introduced in .NET Core 2.1 that provides a type-safe and memory-safe representation of a contiguous region of arbitrary memory. It is a read-only version of Span<T> and allows for efficient memory access without making copies or allocating new objects.

How Does ReadOnlySpan<T> Work?

ReadOnlySpan<T> represents contiguous regions of memory and allows for safe and efficient access to that memory. It is stored on the stack and cannot be stored on the heap, making it lightweight and performant. ReadOnlySpan<T> consists of a starting address and a length, which together represent a specific block of data.

Compilation and Runtime Details

When ReadOnlySpan<T> is compiled, it carries a starting address (pointer) and a length. These two pieces of information allow access to the data block represented by ReadOnlySpan<T>. Since ReadOnlySpan<T> works with value types instead of reference types, it is stored on the stack, avoiding heap allocation and enhancing performance.

ReadOnlySpan<T> and IL Code

When using ReadOnlySpan<T>, the generated Intermediate Language (IL) code is straightforward and optimized for performance. Let’s look at an example of using ReadOnlySpan<int> and how it translates to IL code:

public void ReadOnlySpanExample()
{
    int[] array = { 1, 2, 3, 4, 5 };
    ReadOnlySpan<int> readOnlySpan = array;

    foreach (var item in readOnlySpan)
    {
        Console.WriteLine(item);
    }
}

The IL code for this example is as follows:

.method public hidebysig instance void ReadOnlySpanExample() cil managed
{
  .maxstack 2
  .locals init (
    [0] int32[] array,
    [1] valuetype [System.Memory]System.ReadOnlySpan`1<int32> readOnlySpan,
    [2] valuetype [System.Memory]System.ReadOnlySpan`1<int32> slice,
    [3] int32 index,
    [4] int32 item
  )
  IL_0000: nop
  IL_0001: ldc.i4.5
  IL_0002: newarr [mscorlib]System.Int32
  IL_0007: dup
  IL_0008: ldtoken field int32[] modopt([mscorlib]System.Runtime.CompilerServices.IsVolatile) '<PrivateImplementationDetails>'::'AF2085A68B19A8D07ADCC3053E5A7A1125089C85'
  IL_000d: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
  IL_0012: stloc.0
  IL_0013: ldloc.0
  IL_0014: call valuetype [System.Memory]System.ReadOnlySpan`1<!0> [System.Memory]System.ReadOnlySpan`1<int32>::op_Implicit(!0[])
  IL_0019: stloc.1
  IL_001a: ldloca.s 1
  IL_001c: call instance !0 [System.Memory]System.ReadOnlySpan`1<int32>::get_Item(int32)
  IL_0021: stloc.s 4
  IL_0023: ldloc.s 4
  IL_0025: call void [mscorlib]System.Console::WriteLine(int32)
  IL_002a: nop
  IL_002b: ret
}

As seen, the usage of ReadOnlySpan<T> involves direct memory access with minimal memory allocation.

Technical Details and Memory Usage

  • Stack Allocation: ReadOnlySpan<T> can only be stored on the stack and cannot be stored on the heap.
  • Minimized Memory Allocation: It minimizes memory allocation and provides efficient access to data blocks.
  • Value Types Only: ReadOnlySpan<T> works only with value types, not reference types.
  • Inlined Operations: Operations on ReadOnlySpan<T> are often inlined, enhancing performance.

Advantages of Using ReadOnlySpan<T>

  1. Performance: Allows data manipulation without memory copying.
  2. Safety: Prevents accidental modification of data.
  3. Flexibility: Can work with various data sources like arrays, strings, and stackalloc.

How to Use ReadOnlySpan<T>

Using ReadOnlySpan<T> is straightforward. Here are some basic usage examples:

Using with Arrays
int[] array = { 1, 2, 3, 4, 5 };
ReadOnlySpan<int> readOnlySpan = array;

foreach (var item in readOnlySpan)
{
    Console.WriteLine(item);
}
Using with Strings
string str = "Hello, World!";
ReadOnlySpan<char> readOnlySpan = str;

Console.WriteLine(readOnlySpan.Slice(0, 5).ToString()); // Output: Hello
Using with stackalloc

stackalloc allows for high-performance stack allocation. This method is ideal for small, short-lived data structures. Here’s an example using ReadOnlySpan<T> with stackalloc:

Span<int> span = stackalloc int[5] { 1, 2, 3, 4, 5 };
ReadOnlySpan<int> readOnlySpan = span;

foreach (var item in readOnlySpan)
{
    Console.WriteLine(item);
}

In this example, we use stackalloc to allocate a 5-element int array on the stack and use it with ReadOnlySpan<int>. stackalloc ensures that the array is allocated directly on the stack, providing high performance and low memory usage.

Using with Substrings
string str = "Hello, World!";
ReadOnlySpan<char> readOnlySpan = str.AsSpan();

ReadOnlySpan<char> helloSpan = readOnlySpan.Slice(0, 5);
Console.WriteLine(helloSpan.ToString()); // Output: Hello

Comparing ReadOnlySpan<T> with Other Collection Types

Array:

  • Heap Allocation: Arrays are allocated on the heap and have a fixed size in memory.
  • Mutable: Elements in arrays can be changed.
  • High Performance: Arrays offer high performance, especially for small datasets.
  • Immutability: ReadOnlySpan<T> offers similar performance to arrays but prevents accidental modification of data.

List:

  • Heap Allocation: Lists are allocated on the heap and can grow dynamically.
  • Mutable: Lists are suitable for adding, removing, and updating elements.
  • Dynamic Growth: Lists can dynamically change in size.
  • Increased Memory Usage: Lists require additional memory for dynamic growth. ReadOnlySpan<T> eliminates this overhead and ensures efficient memory usage.

Memory:

  • Heap Allocation Possible: Memory<T> can be stored both on the heap and the stack.
  • Slicing: Memory<T> can be used to slice and manipulate data.
  • Flexibility: Memory<T> is more flexible and suited for complex scenarios.
  • Performance: ReadOnlySpan<T> is more performant as it is only stored on the stack.

Span:

  • Mutable: Span<T> allows modification of data.
  • Stack Allocation: Span<T> is allocated on the stack and thus is highly performant.
  • Data Manipulation: Span<T> is ideal for data manipulation.
  • Read-Only: ReadOnlySpan<T> is similar to Span<T> but ensures data cannot be modified, providing added safety.

Comparing ReadOnlySpan with Other Collection Types

Limitations of ReadOnlySpan<T>

  • Stack Only: ReadOnlySpan<T> can only be stored on the stack and cannot be stored on the heap.
  • Value Types Only: ReadOnlySpan<T> works only with value types, not reference types.

Conclusion

ReadOnlySpan<T> is a powerful tool for performance and safety in .NET. It is particularly useful in high-performance applications and when working with large datasets.