.NET Memory Efficiency, Zero Allocation
A guide for APIs that minimize GC pressure and enable high-performance memory management.
Quick Reference: See QUICKREF.md for essential patterns at a glance.
- Core Concepts
-
.NET CLR GC Heap Memory Optimization
-
Understanding Stack allocation vs Heap allocation
-
Stack-only types through ref struct
- Key APIs
API Purpose NuGet
Span<T> , Memory<T>
Stack-based memory slicing BCL
ArrayPool<T>.Shared
Reduce GC pressure through array reuse BCL
DefaultObjectPool<T>
Object pooling Microsoft.Extensions.ObjectPool
MemoryCache
In-memory caching System.Runtime.Caching
- Span, ReadOnlySpan
3.1 Basic Usage
// Zero Allocation when parsing strings public void ParseData(ReadOnlySpan<char> input) { // String manipulation without Heap allocation var firstPart = input.Slice(0, 10); var secondPart = input.Slice(10); }
// Array slicing public void ProcessArray(int[] data) { Span<int> span = data.AsSpan(); Span<int> firstHalf = span[..^(span.Length / 2)]; Span<int> secondHalf = span[(span.Length / 2)..]; }
3.2 String Processing Optimization
// ❌ Bad example: Substring allocates new string string part = text.Substring(0, 10);
// ✅ Good example: AsSpan has no allocation ReadOnlySpan<char> part = text.AsSpan(0, 10);
3.3 Using with stackalloc
public void ProcessSmallBuffer() { // Allocate small buffer on Stack (no Heap allocation) Span<byte> buffer = stackalloc byte[256]; FillBuffer(buffer); }
- ArrayPool
Reduces GC pressure by reusing large arrays.
4.1 Basic Usage
namespace MyApp.Services;
public sealed class DataProcessor { public void ProcessLargeData(int size) { // Rent array (minimize Heap allocation) var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
// Use buffer (only use up to requested size)
ProcessBuffer(buffer.AsSpan(0, size));
}
finally
{
// Must return
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
4.2 clearArray Option
// Initialize before returning when handling sensitive data ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
- ObjectPool
Reuses expensive objects.
namespace MyApp.Services;
using Microsoft.Extensions.ObjectPool;
public sealed class HeavyObjectProcessor { private readonly ObjectPool<HeavyObject> _pool;
public HeavyObjectProcessor()
{
var policy = new DefaultPooledObjectPolicy<HeavyObject>();
_pool = new DefaultObjectPool<HeavyObject>(policy, maximumRetained: 100);
}
public void Process()
{
var obj = _pool.Get();
try
{
obj.DoWork();
}
finally
{
_pool.Return(obj);
}
}
}
- Memory
Unlike Span, can be stored in fields or used in async methods.
public sealed class AsyncProcessor { private Memory<byte> _buffer;
public AsyncProcessor(int size)
{
_buffer = new byte[size];
}
// Memory<T> can be used in async methods
public async Task ProcessAsync()
{
await FillBufferAsync(_buffer);
ProcessData(_buffer.Span);
}
}
- Required NuGet Package
<ItemGroup> <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.*" /> </ItemGroup>
- Important Notes
⚠️ Span Constraints
-
Span<T> , ReadOnlySpan<T> cannot be used with async-await
-
Cannot be boxed (ref struct)
-
Cannot be stored as class field (use Memory)
-
Cannot be captured in lambdas/closures
⚠️ ArrayPool Return Required
-
Arrays rented with Rent() must be returned with Return()
-
Use try-finally pattern
-
Memory leak occurs if not returned
⚠️ Rented Size vs Actual Size
// Array larger than requested may be returned var buffer = ArrayPool<byte>.Shared.Rent(100); // buffer.Length >= 100 (not exactly 100)
// Use only requested size when actually using ProcessBuffer(buffer.AsSpan(0, 100));
- References
-
Span - Microsoft Docs
-
ArrayPool - Microsoft Docs