DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Introducing Span<T> in .NET

Meta Description:

Discover the power of Span<T> in .NET for efficient memory management. Learn how to slice arrays without copying, improve performance, and ensure immutability with ReadOnlySpan<T>. Includes detailed examples, comparisons, and best practices for modern .NET development.

When working with large arrays or datasets, you may need to process a specific portion of the data. Traditionally, slicing an array creates new arrays, which increases memory usage and can hurt performance. To solve this, .NET introduces Span<T>, a memory-efficient type that allows you to work with contiguous regions of memory without unnecessary allocations.


What is Span<T>?

Span<T> is a stack-only type that provides a safe and efficient way to represent a contiguous block of memory. It allows you to:

  • Work with slices of an array or buffer without creating new arrays.
  • Reference memory without copying data.
  • Reduce allocations and improve performance.

Key Features

  • No Copying: Slicing a Span<T> doesn’t create new arrays; it points to the same memory.
  • Implicit Conversion: Arrays (T[]) are automatically converted to Span<T> for ease of use.
  • Stack-Based: Optimized for short-lived, high-performance operations.

Limitations

  • Cannot be used in asynchronous methods.
  • Cannot be stored as a class field.
  • Only valid within the current stack frame.

Why Use Span<T>?

  1. Memory Efficiency: Avoid creating new arrays while working with slices of data.
  2. Performance: Reduces heap allocations and garbage collection pressure.
  3. Ease of Use: Simplifies operations with array slicing and manipulation.

Using Span<T>: A Practical Example

Suppose you have a large array of integers representing sales data. You want to process the last 100 values to perform validation.

Without Span<T>

using System;

class Program
{
    static void Main()
    {
        int[] salesData = new int[1000];
        Random random = new Random();
        for (int i = 0; i < salesData.Length; i++)
        {
            salesData[i] = random.Next(1, 100);
        }

        // Slicing the array (creates a new array)
        int[] last100Sales = new int[100];
        Array.Copy(salesData, salesData.Length - 100, last100Sales, 0, 100);

        // Validate the sliced array
        bool isValid = ValidateWithoutSpan(last100Sales);
        Console.WriteLine($"Validation result: {isValid}");
    }

    static bool ValidateWithoutSpan(int[] dataSlice)
    {
        foreach (int value in dataSlice)
        {
            if (value > 90)
            {
                return false; // Validation failed
            }
        }
        return true; // Validation passed
    }
}
Enter fullscreen mode Exit fullscreen mode

Drawbacks

  • Extra Allocations: Array.Copy creates a new array.
  • Performance Overhead: More memory usage and increased garbage collection pressure.

With Span<T>

using System;

class Program
{
    static void Main()
    {
        int[] salesData = new int[1000];
        Random random = new Random();
        for (int i = 0; i < salesData.Length; i++)
        {
            salesData[i] = random.Next(1, 100);
        }

        // Create a span directly from the array
        Span<int> salesSpan = salesData;

        // Slice the last 100 values
        Span<int> last100Sales = salesSpan[^100..];

        // Validate the sliced span
        bool isValid = ValidateWithSpan(last100Sales);
        Console.WriteLine($"Validation result: {isValid}");
    }

    static bool ValidateWithSpan(Span<int> dataSlice)
    {
        foreach (int value in dataSlice)
        {
            if (value > 90)
            {
                return false; // Validation failed
            }
        }
        return true; // Validation passed
    }
}
Enter fullscreen mode Exit fullscreen mode

Advantages

  • No Extra Allocations: The slice directly references the original array.
  • Better Performance: Reduced memory usage and faster processing.

Ensuring Data Integrity with ReadOnlySpan<T>

If you want to ensure that the original data remains unchanged, use ReadOnlySpan<T>. It guarantees immutability by preventing modifications to the data.

using System;

class Program
{
    static void Main()
    {
        int[] salesData = new int[1000];
        Random random = new Random();
        for (int i = 0; i < salesData.Length; i++)
        {
            salesData[i] = random.Next(1, 100);
        }

        // Create a read-only span
        ReadOnlySpan<int> salesSpan = salesData;

        // Slice the last 100 values
        ReadOnlySpan<int> last100Sales = salesSpan[^100..];

        // Validate the read-only span
        bool isValid = ValidateReadOnlySpan(last100Sales);
        Console.WriteLine($"Validation result: {isValid}");
    }

    static bool ValidateReadOnlySpan(ReadOnlySpan<int> dataSlice)
    {
        foreach (int value in dataSlice)
        {
            if (value > 90)
            {
                return false; // Validation failed
            }
        }
        return true; // Validation passed
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits of ReadOnlySpan<T>

  • Prevents accidental modifications to the data.
  • Ensures immutability for safer operations.

Comparing Performance: Span<T> vs. Without Span<T>

Here’s a performance comparison for processing slices:

using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        int[] data = new int[10_000_000];
        Random random = new Random();
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = random.Next(1, 100);
        }

        Stopwatch stopwatch = new Stopwatch();

        // Without Span<T>
        stopwatch.Start();
        ProcessWithoutSpan(data);
        stopwatch.Stop();
        Console.WriteLine($"Without Span<T>: {stopwatch.ElapsedMilliseconds} ms");

        // With Span<T>
        stopwatch.Restart();
        ProcessWithSpan(data);
        stopwatch.Stop();
        Console.WriteLine($"With Span<T>: {stopwatch.ElapsedMilliseconds} ms");
    }

    static void ProcessWithoutSpan(int[] data)
    {
        for (int i = 0; i < 100; i++)
        {
            int[] slice = new int[100];
            Array.Copy(data, i * 100, slice, 0, 100);
            ValidateWithoutSpan(slice);
        }
    }

    static void ProcessWithSpan(int[] data)
    {
        Span<int> span = data;
        for (int i = 0; i < 100; i++)
        {
            Span<int> slice = span.Slice(i * 100, 100);
            ValidateWithSpan(slice);
        }
    }

    static bool ValidateWithoutSpan(int[] dataSlice)
    {
        foreach (int value in dataSlice)
        {
            if (value > 90) return false;
        }
        return true;
    }

    static bool ValidateWithSpan(Span<int> dataSlice)
    {
        foreach (int value in dataSlice)
        {
            if (value > 90) return false;
        }
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Using Span<T>

  1. Use for Stack-Based Operations: Span<T> is ideal for short-lived operations within a stack frame.
  2. Prefer ReadOnlySpan<T> for Immutability: Use ReadOnlySpan<T> when data should not be modified.
  3. Avoid in Async Methods: Use Memory<T> for asynchronous workflows.
  4. Minimize Conversions: Convert collections (e.g., List<T>) to arrays once, then use spans for further processing.
  5. Leverage Range Syntax: Use slicing ([^start..end]) for simplicity and efficiency.

Conclusion

Span<T> provides a high-performance, memory-efficient way to handle slices of arrays or other memory regions. By avoiding unnecessary allocations and leveraging stack-based operations, you can significantly improve the performance of your .NET applications. Use ReadOnlySpan<T> for immutability and Memory<T> for asynchronous scenarios to maximize the utility of this powerful tool.

Top comments (0)