In this post, we will see what is Span<T> in C# and how we can use it to improve performance.
But first of all, what is a Span<T>?
From Microsoft web site:
“System.Span<T> is a new value type at the heart of .NET. It enables the representation of contiguous regions of arbitrary memory, regardless of whether that memory is associated with a managed object, is provided by native code via interop, or is on the stack. And it does so while still providing safe access with performance characteristics like that of arrays.”
In a nutshell, with Span<T>, is possible to improve memory usage in scenarios where we would traditionally have to create new objects or arrays.
In fact, with Span<T>, we can directly manipulate the relevant part of memory without additional allocations. It’s particularly useful in scenarios where we need to slice and dice arrays or strings frequently.
Let’s see some examples.
MANIPULATE ARRAY:
In this example, we have an array of integers and we want to process a subset of this array.
Traditionally, we would have to create a new array to hold the subset, which means additional memory allocation.
Let’s see how this can be avoided using Span<T>:
[BENCHMARK.CS]
using BenchmarkDotNet.Attributes;
namespace TestSpan;
[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class BenchMark
{
private int[] numbers;
[GlobalSetup]
public void Setup()
{
// Initialization of the numbers array
numbers = Enumerable.Range(1, 10).ToArray();
}
// Benchmark for measuring the performance of the Array.Copy method
[Benchmark]
public int[] SliceWithArrayCopy()
{
// Define an array to hold the subset
int[] subset = new int[5];
// Copy a slice of the 'numbers' array to 'subset' array
Array.Copy(numbers, 2, subset, 0, 5);
// Return the subset
return subset;
}
// Benchmark for measuring the performance of the Span.Slice method
[Benchmark]
public Span<int> SliceWithSpan()
{
// Define a Span to wrap the 'numbers' array
Span<int> numbersSpan = numbers;
// Return a slice of the 'numbers' Span
return numbersSpan.Slice(2, 5);
}
}
[PROGRAM.CS]
using BenchmarkDotNet.Running;
using TestSpan;
Console.WriteLine("Start Benchmark");
BenchmarkRunner.Run<BenchMark>();
We have done and now, if we run the application, the following will be the result:
STRING MANIPULATION:
In this example, we want to extract a substring from a string.
A common way to do this, is using the SUBSTRING method but, this can result in additional memory allocations as a new string object is created each time we use SUBSTRING.
Instead, with Span<T>, we can work with substrings without creating new strings.
That’s how:
[BENCHMARK.CS]
using BenchmarkDotNet.Attributes;
namespace TestSpan;
[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class BenchMark
{
private string sentence;
[GlobalSetup]
public void Setup()
{
// Initialization of the sentence
sentence = "The quick brown fox jumps over the lazy dog";
}
// Benchmark for measuring the performance of the String.Substring method
[Benchmark]
public string SliceWithStringSubstring()
{
// Return a substring of 'sentence' from the 5th index, with a length of 5 characters
return sentence.Substring(4, 5);
}
// Benchmark for measuring the performance of the ReadOnlySpan.Slice method
[Benchmark]
public ReadOnlySpan<char> SliceWithStringAsSpan()
{
// Create a ReadOnlySpan to wrap the 'sentence' string
ReadOnlySpan<char> sentenceSpan = sentence.AsSpan();
// Return a slice of the 'sentence' ReadOnlySpan from the 5th index, with a length of 5 characters
return sentenceSpan.Slice(4, 5);
}
}
[PROGRAM.CS]
using BenchmarkDotNet.Running;
using TestSpan;
Console.WriteLine("Start Benchmark");
BenchmarkRunner.Run<BenchMark>();
We have done and now, if we run the application, the following will be the result:
PARSING NUMERIC DATA:
In this example, we have a large number of numeric strings that need to be converted to integers.
In a traditional approach, we might use int.Parse() or int.TryParse().
While these methods are straightforward and effective, they incur the overhead of creating temporary strings when working with substrings.
Instead with Span<T>, we can directly parse numbers from a part of a string, no substring required.
That’s how:
[BENCHMARK.CS]
using BenchmarkDotNet.Attributes;
namespace TestSpan;
[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class BenchMark
{
private string data;
[GlobalSetup]
public void Setup()
{
// Initialization of the data string
data = "1234567890";
}
// Benchmark for measuring the performance of parsing an integer using String.Substring and int.Parse methods
[Benchmark]
public int ParseWithSubstring()
{
// Parse an integer from a substring of 'data' starting from the 3rd character with a length of 3 characters
return int.Parse(data.Substring(2, 3));
}
// Benchmark for measuring the performance of parsing an integer using Span<T> and int.Parse methods
[Benchmark]
public int ParseWithSpan()
{
// Convert the 'data' string into a ReadOnlySpan<char> and parse an integer directly from a slice starting from the 3rd character with a length of 3 characters
return int.Parse(data.AsSpan(2, 3));
}
}
[PROGRAM.CS]
using BenchmarkDotNet.Running;
using TestSpan;
Console.WriteLine("Start Benchmark");
BenchmarkRunner.Run<BenchMark>();
We have done and now, if we run the application, the following will be the result: