C# feature highlight: Span<T>

Span<T> is a type-safe and memory-safe representation of a contiguous region of arbitrary memory. You can think of it as a window that you position on a memory location, whether this memory is located on the heap, on the stack, or is even made of unmanaged memory.

Span<T>, which was introduced with C# 7.2, is a ref struct, which also started with C# 7.2. A ref struct is allocated on the stack and can't escape to the heap, which introduces some limitations to its usage:


However, Span<T> can be used as a type of method arguments or return values.

If you need an equivalent of Span<T> that can be stored on the heap, Memory<T> is probably the option that will suit you best, but I won't cover this type in this article.

Span<T> has a variant type called System.ReadOnlySpan<T> which enables read-only access and works with immutable data types like System.String.

Span<T> and ReadOnlySpan<T> can be represented like this:

When should you use Span<T> and ReadOnlySpan<T>? These two types are very useful tools when you need to improve the speed and/or when you need to reduce the allocations of your code. Let's see some examples using them with strings and arrays, which are probably the most common use cases for "standard" developers.

Using ReadOnlySpan<T> with System.String

Assume the following scenario: you receive an invoice number which is composed of the year of the invoice, the month, an id for the customer and the amount of the invoice (let's say "2022-06-123-600" for example) and you want to parse the string to deconstruct these 4 pieces of information.

Without using Span<T> you could do something like this:


private static readonly string _invoiceReference = "2022-06-123-600";

public static (int year, int month, int customerId, int amount) ParseInvoiceReferenceWithSubstring()
{
    string year = _invoiceReference.Substring(0, 4);
    string month = _invoiceReference.Substring(5, 2);
    string customerId = _invoiceReference.Substring(8, 3);
    string amount = _invoiceReference[12..];

    int parsedYear = Int32.Parse(year);
    int parsedMonth = Int32.Parse(month);
    int parsedCustomerId = Int32.Parse(customerId);
    int parsedAmount = Int32.Parse(amount);

    return (parsedYear, parsedMonth, parsedCustomerId, parsedAmount);
}

This method will work but, because System.String is an immutable type, each time you extract a part of _invoiceReference you create a new string that will be stored in memory, so your memory allocation will be pretty significant (here it won't be massive but the same method in a loop could have an impact on the performance or the memory consumption).

Now we can rewrite this method using ReadOnlySpan<T>, replacing the .Substring() calls with .Slice() methods:


private static readonly string _invoiceReference = "2022-06-123-600";

public static(int year, int month, int customerId, int amount) ParseInvoiceReferenceWithSpanT()
{
    ReadOnlySpan<char> invoiceReferenceAsSpan = _invoiceReference;
    ReadOnlySpan<char> year = invoiceReferenceAsSpan.Slice(0, 4);
    ReadOnlySpan<char> month = invoiceReferenceAsSpan.Slice(5, 2);
    ReadOnlySpan<char> customerId = invoiceReferenceAsSpan.Slice(8, 3);
    ReadOnlySpan<char> amount = invoiceReferenceAsSpan.Slice(12); // We don't specify a length so it will slice until the end of the original Span<T>

    int parsedYear = Int32.Parse(year);
    int parsedMonth = Int32.Parse(month);
    int parsedCustomerId = Int32.Parse(customerId);
    int parsedAmount = Int32.Parse(amount);

    return (parsedYear, parsedMonth, parsedCustomerId, parsedAmount);
}

As you can see, the code is very close to our first method but the memory allocation is far lower here because when you slice a Span<T> you don't reallocate what the original memory contains, you just position a tighter window on the original Span<T>:

You can add as many Span<T> as you want from Slice and you can see that they add no new memory allocation:

Notice that Span<T> and ReadOnlySpan<T> have a static Empty property you can use if you want an empty variable, and an IsEmpty property:


ReadOnlySpan<char> invoiceReferenceAsSpan = _invoiceReference ?? ReadOnlySpan<char>.Empty;
ReadOnlySpan<char> year = invoiceReferenceAsSpan.IsEmpty ? ReadOnlySpan<char>.Empty : invoiceReferenceAsSpan.Slice(0, 4);

Using Span<T> with arrays

Span<T> is pretty close to arrays and a lot of developers see Span<T> as a new fashionable way to do arrays, but it's not that simple. This confusion comes from the fact that Span<T> is a view on some data and most of the time this data is represented through an array. So arrays are still needed and Span<T> is just a convenient view on them. But Span<T> has some superpowers that arrays and even ArraySegment don't have:

You can instantiate a Span<T> directly from an Array:


var invoiceDataArray = new int[4] { 2022, 06, 123, 600 };
ReadOnlySpan<int> invoiceData = new ReadOnlySpan<int>(invoiceDataArray);

You can also write this line more concisely now, like this:


ReadOnlySpan<int> invoiceData = new (invoiceDataArray);

Another option is to use the implicit cast from Array[T] to Span<T>:


ReadOnlySpan<int> invoiceData = invoiceDataArray;

As we saw, Span<T> supports slicing, and you can use it in its instantiation:


ReadOnlySpan<int> invoiceData = new (invoiceDataArray, 1, 2); // Equivalent to: new (invoiceDataArray, start:1, length:2)

To get back to our invoice example, our method could be written with something like this:


private static readonly string _invoiceReference = "2022-06-123-600";

public static (int year, int month, int customerId, int amount) ParseArrayWithSpanT()
{
    var invoiceDataArray = new int[4] { 2022, 06, 123, 600 };
    ReadOnlySpan<int> invoiceData = invoiceDataArray;

    return (invoiceData[0], invoiceData[1], invoiceData[2], invoiceData[3]);
}

Benchmarking our string scenario

Because Span doesn't allocate new memory when it slices, it has great performance regarding execution speed and memory allocation, but why don't we benchmark it to see exactly what's going on?

I took our two previous methods, which were parsing our invoice scenario from string invoice references, and put them in a basic benchmark class. This is my program class:


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Validators;

var benchmarkConfig = new ManualConfig()
        .WithOptions(ConfigOptions.DisableOptimizationsValidator)
        .AddValidator(JitOptimizationsValidator.DontFailOnError)
        .AddLogger(ConsoleLogger.Default)
        .AddColumnProvider(DefaultColumnProviders.Instance);

BenchmarkRunner.Run<InvoiceParserBenchmarks>(benchmarkConfig);

public class InvoiceParser
{
    public (int year, int month, int customerId, int amount) ParseInvoiceReferenceWithSubstring(string invoiceReference)
    {
        string year = invoiceReference.Substring(0, 4);
        string month = invoiceReference.Substring(5, 2);
        string customerId = invoiceReference.Substring(8, 3);
        string amount = invoiceReference[12..];

        int parsedYear = Int32.Parse(year);
        int parsedMonth = Int32.Parse(month);
        int parsedCustomerId = Int32.Parse(customerId);
        int parsedAmount = Int32.Parse(amount);

        return (parsedYear, parsedMonth, parsedCustomerId, parsedAmount);
    }

    public (int year, int month, int customerId, int amount) ParseInvoiceReferenceWithSpanT(ReadOnlySpan<char> invoiceReference)
    {
        ReadOnlySpan<char> year = invoiceReference.Slice(0, 4);
        ReadOnlySpan<char> month = invoiceReference.Slice(5, 2);
        ReadOnlySpan<char> customerId = invoiceReference.Slice(8, 3);
        ReadOnlySpan<char> amount = invoiceReference.Slice(12);

        int parsedYear = Int32.Parse(year);
        int parsedMonth = Int32.Parse(month);
        int parsedCustomerId = Int32.Parse(customerId);
        int parsedAmount = Int32.Parse(amount);

        return (parsedYear, parsedMonth, parsedCustomerId, parsedAmount);
    }

    public (int year, int month, int customerId, int amount) ParseArrayWithSpanT()
    {
        var invoiceDataArray = new int[4] { 2022, 06, 123, 600 };
        ReadOnlySpan<int> invoiceData = invoiceDataArray;

        return (invoiceData[0], invoiceData[1], invoiceData[2], invoiceData[3]);
    }
}

[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
public class InvoiceParserBenchmarks
{
    private static readonly string invoiceReference = "2022-06-123-600";
    private static readonly InvoiceParser invoiceParser = new ();

    [Benchmark]
    public void ParseInvoiceReferenceWithSubstring()
    {
        invoiceParser.ParseInvoiceReferenceWithSubstring(invoiceReference);
    }

    [Benchmark]
    public void ParseInvoiceReferenceWithSpanT()
    {
        invoiceParser.ParseInvoiceReferenceWithSpanT(invoiceReference);
    }
}

This is the result of the benchmark (in debug mode on a developer local machine):

As you can see, the method using Span<T> is slightly faster but, above all, it consumes much less memory, with no memory allocated against 128 bytes.

There are still a lot of things to say about this subject but I hope I have enlightened you a little on it, and I invite you to learn more about this powerful stuff; I have put a list of links below the article.

Keep coding! ;)

June 17, 2022
  • .NET
  • Span<T>
  • C# 7.2
  • ref struct
  • ReadOnlySpan<T>