C# Memory Management: Using Span and Memory for Zero-Allocation
Source: Dev.to
Introduction: The Cost of the Garbage Collector (GC)
One of the biggest advantages of modern .NET development is managed memory. Developers do not need to concern themselves with manual memory allocation and deallocation like in C or C++. The .NET Garbage Collector (GC) handles memory cleanup on behalf of programmers, making development safer and more productive.
However, “managed code” does not mean free performance. When applications allocate many objects on the heap, the GC must run frequently to reclaim memory. This introduces several performance challenges:
- GC pressure – Frequent object allocations increase the workload for the garbage collector.
- Stop‑the‑world pauses – The GC temporarily pauses application threads to reclaim memory.
- Heap fragmentation – Repeated allocations and deallocations create inefficient memory layouts.
In most enterprise applications this overhead is acceptable, but in low‑latency environments even small pauses can be problematic.
Span and Memory
Span is a lightweight view over contiguous memory. It has the following characteristics:
- Can point to stack memory
- Can point to heap arrays
- Does not allocate memory
- Extremely fast
- Stack‑only structure
Example: Stack allocation with Span
Span<int> numbers = stackalloc int[5];The array is allocated directly on the stack, so no GC allocation occurs. This can lead to faster execution and automatic memory release. Because Span is stack‑only, it cannot be used in async methods.
Memory
Memory provides a heap‑safe wrapper around memory buffers and can be used where Span cannot (e.g., across await boundaries).
Memory<byte> buffer = new byte[1024];Zero‑Allocation Patterns: Practical Examples
Scenario: Extracting a “Product Code” from a long string
Using string.Substring() creates a new string object on the heap each time it is called. Processing a large file (e.g., 1 GB) can generate massive GC pressure.
Project Setup
Create a console application for benchmarking. Mixing benchmarks into a main Web API can skew results due to background service interference.
dotnet new console -n MemoryBenchmarks
cd MemoryBenchmarksDesigning the Benchmark Class
Create a file named StringPerformance.cs and use BenchmarkDotNet attributes to measure allocated memory.
using BenchmarkDotNet.Attributes;
namespace MyPerformanceLab
{
public class StringPerformance
{
private string TelemetryData = "example:data:12345";
private int Length = 5;
[Benchmark(Baseline = true)]
public string UsingSubstring()
{
int start = TelemetryData.IndexOf(':') + 1;
return TelemetryData.Substring(start, Length);
}
[Benchmark]
public ReadOnlySpan<char> UsingSpan()
{
ReadOnlySpan<char> span = TelemetryData.AsSpan();
int start = span.IndexOf(':') + 1;
return span.Slice(start, Length);
}
}
}Triggering the Run
Replace the default code in Program.cs with the BenchmarkRunner call.
using BenchmarkDotNet.Running;
using MyPerformanceLab;
class Program
{
static void Main(string[] args)
{
// This triggers the heavy‑duty measurement cycle
var summary = BenchmarkRunner.Run<StringPerformance>();
}
}Results
The benchmark typically shows that using Span is roughly 3× faster than using Substring, with significantly lower allocated memory.
You can download the full source code for these benchmarks from the author’s GitHub repository.