Efficiently Working With Arrays And Memory In C# Using Span<T>

When it comes to working with arrays and other collections of data in C#, developers often face challenges in terms of performance and memory management. In particular, copying or allocating additional memory can be a costly operation, especially for large arrays. To address these issues, C# 7.2 introduced the 'Span<T>' type, which provides a way to work with arrays and memory in a more efficient and performant manner.

What is Span<T>?

'Span<T>' is a type in C# that was introduced in version 7.2 of the language. The 'Span<T>' type is defined in the 'System' namespace and is typically used for efficient manipulation of arrays and other collections of data. It represents a contiguous sequence of elements of type T stored in memory. In other words, 'Span<T>' provides a way to view and manipulate data stored in memory as a sequence of values of a specific type.

One of the key features of 'Span<T>' is that it provides a view of memory, rather than a copy of it. This means that 'Span<T>' instances do not allocate any additional memory on the heap. Instead, they refer to existing memory, such as an array or a block of unmanaged memory.

'Span<T>' provides a number of useful methods for working with arrays and other collections of data, including indexing, slicing, and conversion to and from other types. These methods make it easy to work with 'Span<T>' instances in a way that is similar to working with arrays.

Benefits of using Span<T>

One of the primary benefits of using 'Span<T>' is improved performance. By avoiding the need to copy or allocate additional memory, Span<T>' can provide significant performance improvements, especially for large arrays or collections of data. Additionally, Span<T>' can be used for interoperability with unmanaged memory and for working with external APIs that expect contiguous blocks of memory. There many other benefits are as below:

  1. Improved performance: 'Span<T>' can help improve the performance of array and memory manipulation operations by avoiding the need to copy or allocate additional memory.
  2. Memory safety: 'Span<T>' provides a type-safe and bounds-checked way to access memory, helping to prevent common memory-related errors such as buffer overflows and null reference exceptions.
  3. Interoperability: 'Span<T>' can be used for working with unmanaged memory and for interoperability with external APIs that expect contiguous blocks of memory.
  4. Lower memory usage: By avoiding the need to copy or allocate additional memory, 'Span<T>' can help reduce the memory usage of applications that work with large arrays or collections of data.
  5. Simplified code: 'Span<T>' provides a simple and consistent way to work with arrays and memory, reducing the amount of boilerplate code required for common operations.
  6. Parallelism: 'Span<T>' can be used to efficiently work with data in parallel, thanks to its efficient memory access and type-safe operations.
  7. Reduced GC pressure: By reducing the amount of memory allocation and copying, Span<T> can help reduce the pressure on the garbage collector, leading to better performance and reduced memory fragmentation.

Let's see the example that demonstrates how Span<T> can be more useful than arrays for array manipulation:

public class SpanVsArray
{
    private const int arraySize = 1000000;
    private readonly int[] array = new int[arraySize];
    private readonly byte[] memory = new byte[arraySize * sizeof(int)];

    public SpanVsArray()
    {
        var rand = new Random();
        for (int i = 0; i < arraySize; i++)
        {
            int value = rand.Next();
            array[i] = value;
            MemoryMarshal.Write(memory.AsSpan(i * sizeof(int)), ref value);
        }
    }

    [Benchmark]
    public long ArraySum()
    {
        long sum = 0;
        for (int i = 0; i < arraySize; i++)
        {
            sum += array[i];
        }
        return sum;
    }

    [Benchmark]
    public long SpanSum()
    {
        Span<byte> spanMemory = memory.AsSpan();
        long sum = 0;
        var span = MemoryMarshal.Cast<byte, int>(spanMemory);
        for (int i = 0; i < arraySize; i++)
        {
            sum += span[i];
        }
        return sum;
    }
}

In this example, the SpanVsArray class contains two methods that perform the sum operation on the array and the memory block using for loops. The array and memory fields are initialized with random values in the constructor, and the spanMemory field is initialized with the memory block. The ArraySum and SpanSum methods are annotated with the Benchmark attribute, which tells BenchmarkDotNet to measure the performance of these methods.

create a new class that uses BenchmarkDotNet to run the benchmarks. Here's an example:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        var summary = BenchmarkRunner.Run<SpanVsArray>();
    }
}

When we run this example, we should see output that shows the performance of the ArraySum and SpanSum methods:

As you can see, the SpanSum method is more  faster than the ArraySum method in this example. This confirms that Span<T> can provide significant performance improvements over arrays in certain scenarios, particularly when working with large arrays and when memory layout is important for performance.

NOTE: For BenchmarkDotNet you need to add below  package to your project.

Install-Package BenchmarkDotNet

Here is a list of some of the important methods and properties available in the Span<T> class along with a brief description:

  1. Span<T>.IsEmpty: Gets an empty Span<T>.
  2. Span<T>.Length: Gets the length of the Span<T>.
  3. Span<T>.Slice(int start): Returns a Span<T> that starts at the specified index and includes all elements to the end of the Span<T>.
  4. Span<T>.Slice(int start, int length): Returns a Span<T> that starts at the specified index and includes the specified number of elements.
  5. Span<T>.ToArray(): Copies the elements of the Span<T> to a new array.
  6. Span<T>.CopyTo(Span<T> destination): Copies the elements of the Span<T> to the specified destination Span<T>.
  7. Span<T>.SequenceEqual(ReadOnlySpan<T> other): Returns a value indicating whether the elements of the Span<T> are equal to the elements of the specified ReadOnlySpan<T>.
  8. Span<T>.Fill(T value): Sets all elements in the Span<T> to the specified value.
  9. Span<T>.IndexOf(T value): Searches for the specified value in the Span<T> and returns the index of the first occurrence, or -1 if the value is not found.
  10. Span<T>.LastIndexOf(T value): Searches for the specified value in the Span<T> and returns the index of the last occurrence, or -1 if the value is not found.
  11. Span<T>.GetEnumerator(): Returns an enumerator that iterates through the elements of the Span<T>.
  12. Span<T>.TryCopyTo(Span<T> destination): Attempts to copy the elements of the Span<T> to the specified destination Span<T>. Returns true if the operation succeeded, or false if the Span<T> is too large to fit in the destination Span<T>.
  13. Span<T>.TryGet(ref T value): Attempts to get the first element of the Span<T>. Returns true if the operation succeeded, or false if the Span<T> is empty.

These methods and properties allow you to perform a wide range of operations on Span<T> instances

In conclusion, the Span<T> type in C# provides a powerful and efficient way to work with arrays and memory. By avoiding the need to copy or allocate additional memory, Span<T> can provide significant performance improvements, especially for large arrays or collections of data. Additionally, Span<T> can be used for interoperability with unmanaged memory and for working with external APIs that expect contiguous blocks of memory. By leveraging Span<T>, developers can improve the performance and memory efficiency of their C# applications.

I hope you will find this article helpful. If you have any suggestions, then please feel free to ask in the comment section.

Thank you.


Similar Articles