Introduction
Memory management remains a critical challenge in 2026 for high-performance C# applications. With .NET 8, Microsoft has strengthened tools like Span
Prerequisites
- .NET 8 SDK installed
- Advanced knowledge of C# (async/await, generics)
- Visual Studio 2022 or Rider
- Basic understanding of the Garbage Collector
Create the benchmark project
dotnet new console -n MemoryPerf
cd MemoryPerf
dotnet add package BenchmarkDotNetInitialize a console project with BenchmarkDotNet to precisely measure the impact of memory optimizations.
Basic Span<T> Implementation
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class Program {
public static void Main() => BenchmarkRunner.Run<MemoryBenchmarks>();
}
[MemoryDiagnoser]
public class MemoryBenchmarks {
private byte[] data = new byte[10000];
[Benchmark(Baseline = true)]
public int SumWithArray() {
int sum = 0;
for (int i = 0; i < data.Length; i++) sum += data[i];
return sum;
}
[Benchmark]
public int SumWithSpan() {
var span = new Span<byte>(data);
int sum = 0;
for (int i = 0; i < span.Length; i++) sum += span[i];
return sum;
}
}This benchmark compares access via a classic array and Span
Using Memory<T> for async
using System;
using System.Threading.Tasks;
public class AsyncMemoryExample {
public async Task<int> ProcessAsync(Memory<byte> buffer) {
await Task.Delay(10);
int sum = 0;
foreach (var b in buffer.Span) sum += b;
return sum;
}
public async Task Run() {
byte[] data = new byte[5000];
var result = await ProcessAsync(data);
Console.WriteLine(result);
}
}Memory
Stackalloc and ref struct
using System;
public ref struct BufferWrapper {
public Span<byte> Data { get; }
public BufferWrapper(Span<byte> data) => Data = data;
}
public class Demo {
public unsafe void Process() {
Span<byte> stackBuffer = stackalloc byte[256];
var wrapper = new BufferWrapper(stackBuffer);
wrapper.Data[0] = 42;
Console.WriteLine(wrapper.Data[0]);
}
}stackalloc allows allocation on the stack. Combined with ref struct, it guarantees zero heap allocation for temporary buffers.
Optimization with ArrayPool
using System;
using System.Buffers;
public class PoolExample {
public void RentAndReturn() {
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(4096);
try {
buffer[0] = 1;
// traitement
} finally {
pool.Return(buffer);
}
}
}ArrayPool reduces allocations by recycling arrays. Always return the buffer in a finally block to avoid leaks.
Final Benchmark Measurement
using BenchmarkDotNet.Attributes;
[MemoryDiagnoser]
public class FinalBench {
[Benchmark]
public void OptimizedPath() {
var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(8192);
try {
var span = buffer.AsSpan(0, 8192);
span.Fill(0xFF);
} finally {
pool.Return(buffer);
}
}
}Final benchmark combining ArrayPool and Span to validate the drastic reduction in allocations and GC time.
Best Practices
- Always prefer Span
for synchronous processing on buffers - Use ArrayPool for reusable buffers of variable size
- Avoid capturing Span
in closures or async tasks - Always measure with BenchmarkDotNet before optimizing
- Document maximum size assumptions for stackalloc
Common Mistakes to Avoid
- Using Span
in async methods (compilation error) - Forgetting to return ArrayPool buffers (memory leak)
- Stackalloc of excessively large sizes (stack overflow)
- Ignoring race conditions on shared buffers
Further Reading
Deepen these techniques with our expert C# .NET training. Also discover the new features in .NET 9 for memory management and unsafe contexts.