Case 18: The Ultimate Frontier - Minimizing GC Pressure in Hot Paths
The Story
Imagine a high-performance service, for example, one that processes messages from a message queue, parses data from a network stream, or handles image processing.
There is a method that runs thousands of times per second. This method needs to process a small part of a large byte array. Although the processing logic is very fast, the application still experiences pauses and uses more CPU than expected.
The Problem Code (Creating Lots of Garbage)
A common way to do this is to use LINQ methods or Array.Copy to extract a sub-array.
csharp
public void ProcessLargeBuffer(byte[] buffer)
{
// Assume we are looping through 1000 messages in the buffer
for (int i = 0; i < 1000; i++)
{
int offset = i * 128;
int length = 128;
// MISTAKE: Allocating a NEW byte array for each sub-message.
// This operation both copies data and creates garbage on the heap.
byte[] messagePayload = buffer.Skip(offset).Take(length).ToArray();
HandleMessage(messagePayload);
}
}Why is this a problem?
- Heap Allocation: The
.ToArray()line creates a brand newbyte[]object on the heap in every single loop. If the loop runs 1000 times, it creates 1000 new objects. - Data Copying: Not only does it allocate memory, but it also has to copy 128 bytes of data from the original
bufferto the newmessagePayload. - Pressure on the Garbage Collector (GC):
- Your application is creating a "garbage storm." Thousands of small
byte[]arrays are created and then immediately discarded. - The Garbage Collector has to work extremely hard to clean up this mess.
- Every time the GC runs, it uses CPU and can pause all of your application's threads for a short time (this is called a "GC pause" or "stop-the-world"). For applications that require low latency, these pauses are unacceptable.
- Your application is creating a "garbage storm." Thousands of small
The Solution: Use Span<T> to Avoid Memory Allocation
Span<T> is a special struct in .NET that allows you to create a "window" or a "view" over an existing piece of memory (like an array) without allocating new memory on the heap and without copying any data.
Here is the better code:
csharp
public void ProcessLargeBuffer(byte[] buffer)
{
// Create a Span that covers the entire original buffer
Span<byte> bufferSpan = buffer;
for (int i = 0; i < 1000; i++)
{
int offset = i * 128;
int length = 128;
// OPTIMIZED: Use .Slice() to create a "view".
// This operation does NOT allocate memory on the heap.
Span<byte> messagePayloadSpan = bufferSpan.Slice(offset, length);
HandleMessage(messagePayloadSpan);
}
}
// The handling method now accepts a Span<byte>
public void HandleMessage(Span<byte> payload)
{
// ... process the data directly on the original memory
}What We Gained
- Zero Heap Allocation: The
.Slice()operation is almost free. It creates no new objects on the heap. - No Data Copying:
messagePayloadSpanis simply a pointer and a length, pointing to the original data in thebuffer. - Reduced GC Pressure: Because no garbage is created, the GC does not need to run as often. This reduces CPU usage, eliminates pauses, and makes the application's throughput higher and more stable.
Conclusion
Span<T>and related types (Memory<T>,ReadOnlySpan<T>) are advanced optimization tools for high-performance scenarios where minimizing memory allocation is critical.- They are not tools you need every day in a simple CRUD API, but they are a "secret weapon" when building libraries, parsers, or high-performance services that need to process raw data.
- Using
Span<T>allows you to write C# code with performance that rivals low-level languages like C++.