Blog
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 like a window that you position on a memory location, whatever this memory is located on the heap, the stack or even be formed of unmanaged memory.
Span<T>, which was introduced with C# 7.2, is a ref struct, which also began with C# 7.2. A ref struct is allocated on the stack and can't escape on the heap which introduces some limitations to their usage :
- You can't use a Span<T> or a ReadOnlySpan<T> field in a class, or even in non-ref-like struct (it also means that you can't use these two types in places where they might implicitly become fields on classes, by capturing them into lambdas or by declaring them into an async method for example)
- Span<T> and ReadOnlySpan<T> can't be boxed (it also means that you can't use them as generic arguments because in some situation they can be stored to the heap or they can getting boxed)
However, Span<T> can be used as a type of method arguments or return values.
If you need an equivalent of Span<T> but that can be stored to the heap Memory<T> is probably the option that will suits the best for you but I won't cover this type in this article.
Span<T> has a variant type called System.ReadOnlySpan<T> which enabled read-only access and works with immutable data types like System.String.
Span<T> and ReadOnlySpan<T> can be represented like this:
When to 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 by using it with strings and arrays, which are probably the most common usecases for "standard" developers.
Using ReadOnlySpan<T> with System.String
Assume the following scenario: you receive an invoice number which is composed by 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 informations.
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 store in memory so your memory allocation will be pretty much significant (here it won't be massive but the same method in a loop could have an impact on the performance o the memory consumption).
Now we can rewrite this method using ReadOnlySpan<T> and by replacing the .Substring() by some .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 less important 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 as Span<T> as you want from Slice you can see than they add no new memory allocation:
Notice that Span<T> and ReadOnlySpan<T> have a static property Empty you can use if you want an empty variable and a property IsEmpty:
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 lots of developer sees Span<T> like a new fashion-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 array is still needed and Span<T> is a just a convenient view on it. But Span<T> has some superpowers that arrays and even ArraySegment
You can instanciate 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 shorter like this now:
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, you can use it in his instanciation:
ReadOnlySpan<int> invoiceData = new (invoiceDataArray, 1, 2); // Equivalent to: new (invoiceDataArray, start:1, length:2)
To get back to our invoice example, out method could be write 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
I took our two previous methods which were parsing some invoice scenario from string invoice references and took 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 but above all it consumes much less memory with no memory allocated againt 128 bytes.
There are still a lot of things to say about this subject but I hope to 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! ;)