Spans in C#: Your Best Friend for Efficient Coding

от автора

As Midjourney see Mister Span

As Midjourney see Mister Span

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?

  1. Faster Performance: Spans help reduce memory allocations and garbage collection pressure. They allow you to work with data directly and efficiently.

  2. Safer Code: Spans prevent common errors like buffer overruns and provide bounds-checking.

  3. 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


ссылка на оригинал статьи https://habr.com/ru/articles/831844/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *