Reducing memory allocations and GC pressure in .NET: streaming, Span<T>, BenchmarkDotNet

Streaming instead of buffering: large exports with IAsyncEnumerable, files with Stream rather than ReadToEnd() / byte[].

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ======================================================================
// Q: How do you reduce memory allocations and GC pressure?
//
// - Streaming instead of buffering: large exports with IAsyncEnumerable, files
//   with Stream rather than ReadToEnd() / byte[].
// - Span<T> / ReadOnlySpan<char> for parsing without new strings, ArrayPool<T>
//   for temporary buffers.
// - Objects > 85 KB go to the Large Object Heap, which is collected rarely and
//   expensively.
// - System.Text.Json source generators for faster serialization without
//   reflection (also needed for Native AOT).
// - Memory leaks: usually static collections or singletons that keep growing,
//   event handlers that are never removed, a cache without a size limit.
// Don't optimize allocations in code that runs 10 times a day. Span and
// ArrayPool make sense in hot paths (parsing ERP files with millions of lines,
// middleware), once profiling has shown it.
// ======================================================================

// Streaming export: does not load 100,000 orders into memory
app.MapGet("/api/orders/export", (AppDbContext db) =>
    db.Orders.AsNoTracking()
      .Select(o => new OrderExportDto(o.Id, o.OrderNumber, o.TotalNet))
      .AsAsyncEnumerable());                          // JSON is written while rows are read

// Benchmark: measure before changing code
[MemoryDiagnoser]
public class SkuParsingBenchmarks
{
    private const string Line = "SKU-12345;10;19.90";

    [Benchmark(Baseline = true)]
    public decimal WithSplit() =>
        decimal.Parse(Line.Split(';')[2], CultureInfo.InvariantCulture);       // array + 3 strings

    [Benchmark]
    public decimal WithSpan()
    {
        ReadOnlySpan<char> s = Line;                                         // 0 allocations
        return decimal.Parse(s[(s.LastIndexOf(';') + 1)..], CultureInfo.InvariantCulture);
    }
}
// dotnet run -c Release