C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
84: Span<T> and Memory<T> for Performance
Imagine you have a massive, 50-foot-long scroll of parchment containing a giant list of data. You need to give a specific section of that list to a colleague so they can analyze it. In the "old way" of doing things, you'd take a pair of scissors, cut out the relevant section, and hand them a brand new, smaller piece of paper. That's fine for one person, but if you do that a thousand times a second, you're going to end up with a floor covered in paper scraps (or in our world, a Garbage Collector that's working overtime and freezing your app).
Now, imagine a different approach. Instead of cutting the scroll, you just hand your colleague a viewfinder—a small cardboard frame. You slide that frame over the specific part of the scroll they need and say, "Look only at what's inside this box." You haven't created any new paper; you've just defined a window into the existing data.
That's exactly what Span<T> and Memory<T> do for your memory:
- The Scroll is your original array, string, or unmanaged memory buffer.
- The Viewfinder is the
Span<T>. It doesn't own the data; it just points to a start position and a length. - Sliding the frame is what we call "slicing." You can move the window around without ever copying the underlying data.
Stop Copying Everything
I've spent way too many hours profiling apps where the biggest bottleneck was string.Substring(). Every time you call Substring, .NET allocates a brand new string on the heap. If you're parsing a 100MB log file and slicing it into pieces, you're generating massive amounts of garbage for the GC to clean up.
With ReadOnlySpan<char>, you can parse that same log file without a single new allocation. Here is how that looks in practice:
string logEntry = "2023-10-01|ERROR|Database connection failed";
// The old way: This creates 3 new strings on the heap
string date = logEntry.Substring(0, 10);
string level = logEntry.Substring(11, 5);
string message = logEntry.Substring(17);
// The high-performance way: No new strings created
ReadOnlySpan<char> span = logEntry.AsSpan();
ReadOnlySpan<char> dateSpan = span.Slice(0, 10);
ReadOnlySpan<char> levelSpan = span.Slice(11, 5);
ReadOnlySpan<char> messageSpan = span.Slice(17);
// You can still use them for comparisons or parsing
if (levelSpan.SequenceEqual("ERROR"))
{
// Handle error...
}
The Stack-Only Nature of Span
Here is the catch: Span<T> is a ref struct. In plain English, that means it must live on the stack. It cannot be a field in a class, it cannot be boxed, and it definitely cannot be used inside an async method (because async methods transform your code into a state machine that lives on the heap).
I've seen a lot of developers get frustrated when the compiler screams at them for trying to use a Span in an await block. This isn't a bug; it's a safety feature to prevent the "viewfinder" from pointing to memory that has already been cleared or moved while the method was suspended.
When you need the heap: Memory<T>
This is where Memory<T> comes in. Think of Memory<T> as a "stored version" of a span. It's a regular struct, so it can live on the heap, be a field in a class, or be passed across async boundaries. It's slightly less performant than Span, but it's far more flexible.
The workflow usually looks like this: You store your data as Memory<T>, and the moment you need to actually do work with it (like a loop or a calculation), you call .Span to get a high-speed Span<T> for the local operation.
public class LogProcessor
{
private Memory<char> _buffer;
public async Task ProcessLogAsync(Memory<char> data)
{
_buffer = data;
await Task.Delay(10); // We can hold Memory<T> across an await
// Now we convert it to a Span to do the actual work
ReadOnlySpan<char> span = _buffer.Span;
if (span.StartsWith("ERROR"))
{
Console.WriteLine("Found an error!");
}
}
}
A quick word on Slicing
The real magic is in Slice(). Whether you are using Span or Memory, slicing doesn't copy. It just adjusts the pointer and the length. If you have an array of 1 million integers and you slice it to get the last 10, you aren't creating a new array of 10; you're just creating a tiny object that says "start at index 999,990 and stop after 10 elements." This is why these types are game-changers for high-throughput systems.
📋 Practical Task
Exercise: The Zero-Allocation CSV Field Extractor
You are building a high-frequency trading parser where every microsecond and every byte of GC pressure matters. Your goal is to extract a specific field from a CSV line without using string.Split or string.Substring, as both of those create unnecessary heap allocations.
The Challenge:
Write a method called ExtractField that takes a string and an index, and returns a ReadOnlySpan<char> representing the value of that field.
Requirements:
- Do NOT use
Split(). - Do NOT use
Substring(). - Use
AsSpan()andSlice()to locate the field. - Assume the CSV is comma-separated.
Starter Code:
public static ReadOnlySpan<char> ExtractField(string line, int fieldIndex)
{
// Your implementation here
}
// Test Case:
string csvLine = "Apple,Banana,Cherry,Date";
var result = ExtractField(csvLine, 2);
// result should be a span pointing to "Cherry"
There are no comments for now.