Unleashing High-Performance C# with Span<T>

Unleashing High-Performance C# with Span<T>

As modern applications demand more efficiency and speed, reducing memory allocations and optimizing processing times are crucial for performance-critical systems.

System.Span<T>, is a value type in .Net, that represents a contiguous region of memory that significantly enhances performance while maintaining safety. Unlike arrays, Span<T> does not require heap allocation, which makes it extremely efficient for scenarios where you want to avoid garbage collection overhead. It supports slicing, which allows you to manipulate sub-regions of memory without copying data, hence improving performance.


using Span<T>

by checking the Span source implementation from Visual Studio or visiting .NET Source Browser website https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Span.cs,d2517139cac388e8 you’ll find that the Span<t> contains two properties:

ref T & Length

Span fields

ref T: we can here pass a value (like an array), and the ref will refer to a reference of the first element of the passed data source. It allows Span<T> to access the data without copying it. which means that the span<T> here is a representation of the underlying memory, and it’s not a way to instantiate blocks in memory

when to use Span<T>?

  • when it’s crucial to minimize memory allocations
  • when we need to slice the data without creating additional copies

we can see the power of the Span easily while manipulating a string or an array

In this scenario, we want to extract a specific column (e.g., the third column) from each line in a CSV string. The CSV string is large, and the goal is to extract the data efficiently without unnecessary memory allocations.

we will use the split method, then we will use the Span, and we will run a benchmark to see the differences between them. (this sample is not a real sample, just to show the power of the Span<T> ??)

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Order;

BenchmarkRunner.Run<BenchmarkDemoWithSpan>();

[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class BenchmarkCsvProcessing
{
    [Params(1000, 5000)]
    public int N;

    private string _csvData;

    [GlobalSetup]
    public void GlobalSetup()
    {
        _csvData = string.Join("\n", Enumerable.Range(1, N).Select(i => $"1,2,{i},4,5,6"));
    }

    [Benchmark]
    public string[] Split()
    {
        string[] results = new string[N];
        var lines = _csvData.Split('\n');

        for (int i = 0; i < lines.Length; i++)
        {
            var columns = lines[i].Split(',');
            results[i] = columns[2]; // Extracting the third column
        }

        return results;
    }

    [Benchmark(Baseline = true)]
    public string[] Span()
    {
        string[] results = new string[N];
        var lines = _csvData.Split('\n');

        for (int i = 0; i < lines.Length; i++)
        {
            var span = lines[i].AsSpan();
            int firstComma = span.IndexOf(',');
            span = span.Slice(firstComma + 1);
            int secondComma = span.IndexOf(',');
            span = span.Slice(secondComma + 1);
            int thirdComma = span.IndexOf(',');
            results[i] = span.Slice(0, thirdComma).ToString(); // Extracting the third column
        }

        return results;
    }
}
        

here is the result,

benchmark result

another sample is to extract a segment of an array (to modify it later)

using Array.Copy() & Span<t>

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<BenchmarkDemoWithSpan>();

[MemoryDiagnoser]
public class BenchmarkDemoWithSpan
{
    private int[] _array;

    [Params(1000, 5000, 10000)]
    public int Size { get; set; }

    [GlobalSetup]
    public void SetUp()
    {
        _array = Enumerable.Range(0, Size).ToArray();
    }

    // Method without Span<T>
    [Benchmark(Baseline = true)]
    public int[] CopyAndModifyWithoutSpan()
    {
        int[] segment = new int[Size / 4];
        Array.Copy(_array, Size / 2, segment, 0, Size / 4);

        return segment;
    }

    // Method with Span<T>
    [Benchmark]
    public Span<int>  ModifyWithSpan()
    {
        Span<int> segment = _array.AsSpan().Slice(Size / 2, Size / 4);
        return segment;
    }
}        

the result :

Conclusion

From these examples, it's clear that using Span<T> can have a significant impact on performance and memory allocation.

Limitations

While Span<T> is a powerful tool for performance optimization, it comes with several limitations that developers need to be aware of.

Span<T> is a stack-only type, meaning it cannot be stored on the heap. This restricts its usage in scenarios that involve asynchronous operations or require data to persist beyond the lifetime of a stack frame. For example, Span<T> cannot be used as a class field, across async and await boundaries, or in any situation where it might need to be boxed, as it cannot be converted to object, dynamic, or any interface type.

Additionally, because Span<T> does not implement IEnumerable<T>, it does not support LINQ operations, which might limit its usability in some contexts. While alternative libraries like SpanLinq or NetFabric.Hyperlinq can be used to perform LINQ-like operations on spans, these require additional effort and dependencies.

要查看或添加评论,请登录

Mohammed Shaker的更多文章

社区洞察

其他会员也浏览了