Boost C# Performance Using Span<T>
Using span can make our application superfast, provided we know how and when to use it. This article will explain very basics (and needful) of memory allocation in .NET C# and explore various ways to use Span.
What are Stack and Heap? How are they different?
Stack and Heap are pattern to allocate memory for our program and it’s crucial to understand the difference between Stack and Heap, before understanding Span<T>.
Imagine you are travelling via international flight to a different country on a vacation and you are working on luggage packing. Airline offers two types of luggage handling— check-in and carry-on. Here is how we will store our stuff between these two.
Carry-On (Stack allocation):
- Packing Order: You put things in and take them out in order — like your books or laptop. Very simple to put and take out items.
- Size: It’s small, so you can only carry a few things at a time.
- Speed: Easy to grab what you need quickly.
- Use: For things you need often.
- Maintain: We carry it, so we are the maintainer / responsible.
Check-in (Heap allocation):
- Packing Order: You can put lots of different things in it and doesn’t have any order.
- Size: It’s much bigger, so it can hold more stuff.
- Speed: Takes longer to find things because it’s more cluttered.
- Use: For things you want later like your favorite vacation shirt, goggles etc.
- Maintain: Airline does for you, they can delay delivery based on their internal system. You may be waiting at baggage claim for more than others.
As you can infer from above points, stack (carry-on) is much simpler to maintain and high performant compare to heap (check-in). However, heap is used when we need durable storage.
In terms of .NET, C# compiler is your airline provider and luggage system is nothing but your memory management. Simple types like int, float, char, function calls, struct etc. are stored in Stack. While complex types like an instance of a class, arrays, string (as it’s an array of characters) etc. are stored in Heap.
As you might have guess, Span<T> and ReadOnlySpan<T> are also Stack allocated, and now you might have slight idea on why they are better then String, right?
So, what if we declare Span<string>? Will it be stack or heap? Well, the thing is, it’s not allowed by our airline provider. You can’t have carry-on containing check-in luggage, right?
What is Span<T> really?
Let’s say we create an integer array like below:
int[] numbers = { 1, 2, 3, 4, 5 };
In C#, numbers will be created in a memory with heap structure. Now, when we create Span on this array, then it will have two details on this array.
- Pointer to the first element. i.e. address of “1”.
- Length of the array. i.e. 5 in our case.
Further, these details will be store in Stack, so no overload of memory management.
Now, let’s say we would like to change the value of first element. Using native array method we can use below code:
numbers[1] = 20;
Above code works, however, remember that in case of large data set, this could be like opening your check-in luggage, finding and replacing some stuff. Compare to this, Span will be faster.
We can create and use Span as below:
int[] numbers = { 1, 2, 3, 4, 5 };
// Create a span over the array
Span<int> numberSpan = numbers.AsSpan();
// Modify elements within the span
numberSpan[1] = 20;
numberSpan[3] = 40;
Console.WriteLine(string.Join(", ", numbers)); // Output: 1, 20, 3, 40, 5
Span<T> supports any type, but it's most effective with:
- Value Types: Such as int, float, char etc., for efficient memory access.
- Reference Types: Although technically supported, they don’t offer the same performance benefits due to additional indirection.
- Unmanaged Types: Types without pointers to managed objects, ideal for interop with native code.
The primary goal is to enable efficient manipulation of contiguous memory blocks (a block of memory addresses that are sequentially adjacent to each other), making it particularly useful for performance-critical scenarios.
Span<T> and ReadOnlySpan<T> are same with exception that former can change values while later is read-only.
Using Span vs String
Let’s say we need parse a comma separated values. Using string the code will look like:
string data = "apple,banana,cherry";
string[] fruits = data.Split(',');
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}
The same can be achieved using ReadOnlySpan<T> as below:
ReadOnlySpan<char> data = "apple,banana,cherry".AsSpan();
int start = 0;
while (true)
{
int commaIndex = data.Slice(start).IndexOf(',');
if (commaIndex == -1)
{
Console.WriteLine(data.Slice(start).ToString());
break;
}
Console.WriteLine(data.Slice(start, commaIndex).ToString());
start += commaIndex + 1;
}
Wait, you may feel that this is more code then compare to String and if we do simple performance measurement using Stopwatch then you may observe that ReadOnlySpan takes more time to execute. Let’s discuss reasons behind this and in which case ReadOnlySpan is better.
- Overhead: For small strings, the overhead of managing spans can outweigh the benefits.
- JIT Optimization: The JIT compiler optimizes string operations heavily, making them faster in some cases.
ReadOnlySpan (or Span) shines over String, If we have large data to process. Additionally, because of less memory management, parsing of complex data will also improved.
Limitations of Span<T>
Though Span looks promising, it may not be appropriate in some scenarios. This is because, Span is in Stack allocation, so it may not have huge capacity to store data. Don’t get confuse by large data. Span is great for handling large data processing, but not if we need to store it. This is because, Span can deal with chunk of large data extremely efficiently.
We can’t use Span for asynchronous programming — meaning if a function is declared as async and within the body of that function we try to declare Span, then compiler will throw an error.
Similarly, it’s invalid to use them with Iterators (yield returns).
Conclusion
We may intuitively feel to start using String when we need to perform string operations like Contains(), EndsWith(), StartsWith(), IndexOf(), LastIndexOf(), ToString(), and Trim() etc. However, it would be better if we start looking for opportunities of using Span (or ReadOnlySpan) for such operations considering the limitations and valid scenarios. Basically, we should better pack our luggage when flying for a safe and peaceful journey.