
Iâve been wanting to sort it out about String memory optimization and all these ways to improve the performance and memory usage for Collections and Arrays in C#(as you remember String is an array of chars, loaded a bit differently but anyway) code. I finally managed to find some time to dive deeper into the System.Span.
I have put together this guide to share what Iâve learned. Itâs filled with practical tips and examples to help you leverage Spans in your own projects. If you want to optimize your C# code, this guide is a great place to start!
So, you want to make your C# code run faster and use memory more efficiently? Meet Spans: the handy tool that simplifies handling blocks of memory and helps your applications perform better. Letâs dive into how Spans work, explore practical examples, understand their differences, and see how they can be used for JSON parsing, along with converting collections to and from Spans.
What Are Spans?
In C#, Span<T>
and ReadOnlySpan<T>
are structures that represent contiguous regions of memory. They allow you to work with slices of data without the overhead of additional memory allocations.
Span<T>
: Allows both reading and writing operations on memory.ReadOnlySpan<T>
: Used for read-only operations, ensuring the data cannot be modified.
Spans are particularly useful for performance-critical scenarios, as they enable direct access to data and efficient memory usage.
Why Should You Care About Spans?
Faster Performance: Spans help reduce memory allocations and garbage collection pressure. They allow you to work with data directly and efficiently.
Safer Code: Spans prevent common errors like buffer overruns and provide bounds-checking.
Versatility: They work with arrays, strings, and slices of other memory regions, making them adaptable for various data-handling scenarios.
How Spans Are Implemented
Under the hood, Spans are designed to be lightweight and fast:
Stack Allocation: Spans are typically allocated on the stack, which is faster and avoids heap allocation.
Memory Safety: They ensure safe access to memory, with built-in bounds checking to prevent out-of-bounds errors.
No Heap Overhead: Unlike arrays, Spans donât create additional heap allocations, reducing memory overhead and improving performance.
Differences Between Span and ReadOnlySpan
While both Span<T>
and ReadOnlySpan<T>
handle contiguous memory, they differ in their usage and capabilities:
Span<T>
:
Mutable: You can modify the contents of a
Span<T>
.Example: Changing elements in an array or buffer.
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = new Span<int>(numbers);
span[0] = 10; // Modifies the original array
Console.WriteLine(numbers[0]); // Outputs 10
ReadOnlySpan<T>
:
Immutable: You cannot modify the contents of a
ReadOnlySpan<T>
.Example: Reading values from a string or array without altering them.
string text = "Hello, World!";
ReadOnlySpan<char> readOnlySpan = text.AsSpan();
// readOnlySpan[0] = 'h'; // This line would cause a compilation error
Console.WriteLine(readOnlySpan.ToString()); // Outputs "Hello, World!"
Collection to Span Conversions
Spans are designed to work seamlessly with collections like arrays, making it easy to convert between collections and spans.
From Array to Span:
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> spanFromArray = new Span<int>(numbers);
From Span to Array:
Span<int> span = stackalloc int[] { 1, 2, 3, 4, 5 };
int[] arrayFromSpan = span.ToArray();
From String to ReadOnlySpan:
string text = "Hello, World!";
ReadOnlySpan<char> spanFromString = text.AsSpan();
From ReadOnlySpan to String:
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
string strFromSpan = span.ToString(); // Note: Converts to a new string
Practical Examples of Collection Conversion
Example: Working with Arrays and Spans
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array;
span[0] = 10; // Modifies the original array
Console.WriteLine(string.Join(", ", array)); // Outputs: 10, 2, 3, 4, 5
Example: Converting a Span to an Array
Span<int> span = stackalloc int[] { 10, 20, 30 };
int[] array = span.ToArray();
Console.WriteLine(string.Join(", ", array)); // Outputs: 10, 20, 30
Example: Extracting a Substring Using ReadOnlySpan
string text = "Hello, World!";
ReadOnlySpan<char> span = text.AsSpan();
ReadOnlySpan<char> helloSpan = span.Slice(0, 5);
Console.WriteLine(helloSpan.ToString()); // Outputs: HelloHello
Practical example: Writing own JSON Parser with Spans
Spans are especially useful for handling string data efficiently. So now lets try to write our own JSON parser which will work without creating unnecessary intermediate strings.
Simple JSON Parser
public void ParseJson(ReadOnlySpan<char> jsonData)
{
// Find the start of the value for a specific key
ReadOnlySpan<char> key = "name";
int keyStart = jsonData.IndexOf(key);
if (keyStart == -1)
{
Console.WriteLine("Key not found");
return;
}
// Move past the key and find the colon
int valueStart = jsonData.Slice(keyStart + key.Length).IndexOf(':') + keyStart + key.Length + 1;
int valueEnd = jsonData.Slice(valueStart).IndexOf(',');
if (valueEnd == -1) // If no comma, this is the last value
{
valueEnd = jsonData.Slice(valueStart).IndexOf('}');
}
// Extract and print the value
ReadOnlySpan<char> value = jsonData.Slice(valueStart, valueEnd);
Console.WriteLine(value.ToString().Trim('"')); // Remove quotes
}
The parser is great with atomic data types but doesn't support complex types like Arrays or inner Objects.
So let's add a Span-based Array parser:
public void ProcessJsonArray(ReadOnlySpan<char> jsonArray)
{
int currentIndex = 0;
while (currentIndex < jsonArray.Length)
{
int start = jsonArray.Slice(currentIndex).IndexOf('{');
if (start == -1) break; // No more objects
int end = jsonArray.Slice(currentIndex).IndexOf('}');
if (end == -1) break; // Incomplete object
ReadOnlySpan<char> jsonObject = jsonArray.Slice(currentIndex + start, end - start + 1);
ProcessJsonObject(jsonObject);
currentIndex += end + 1; // Move past the current object
}
}
And add nested objects support:
private void ProcessJsonObject(ReadOnlySpan<char> jsonObject)
{
// Simple key-value extraction, assuming keys and values are properly formatted
int colonIndex = jsonObject.IndexOf(':');
ReadOnlySpan<char> key = jsonObject.Slice(1, colonIndex - 2); // Skipping surrounding quotes
ReadOnlySpan<char> value = jsonObject.Slice(colonIndex + 1).Trim(); // Extract value and trim
Console.WriteLine($"Key: {key.ToString()}, Value: {value.ToString()}");
}
Putting It All Together: Parsing JSON Data
Hereâs how you might use all the above functions together to parse a complete JSON string:
public void ParseJson(ReadOnlySpan<char> jsonData)
{
int start = 0;
while (start < jsonData.Length)
{
int objectStart = jsonData.Slice(start).IndexOf('{');
if (objectStart == -1) break;
int objectEnd = jsonData.Slice(start).IndexOf('}');
if (objectEnd == -1) break;
ReadOnlySpan<char> jsonObject = jsonData.Slice(start + objectStart, objectEnd - objectStart + 1);
ProcessJsonObject(jsonObject);
start += objectEnd + 1;
}
}
Well done! Now we have our own memory-effective JSON Parser implementation and can finally forget about these Newtonsoft.Json nuget package update problemsâ¦probably didnât face it latest 3â4 years, because Microsoft wrote its own implementation, but if you faceââânow you know what to do!
Things to Watch Out For
Scope: Spans are stack-allocated and should be used within the method where theyâre created.
Pinning: When dealing with unmanaged memory, be cautious about pinning as it can affect garbage collection.
Compatibility: Ensure that your development environment supports Spans, especially with older frameworks.
Wrap-Up
Spans are a powerful feature in C# that can help you manage memory efficiently and safely. By understanding the differences between Span<T>
and ReadOnlySpan<T>
, and how to convert between collections and spans, you can write more efficient and cleaner code. Use Spans for task