DEV Community

Seigo Kitamura
Seigo Kitamura

Posted on

Part 2 - Performance & Concurrency Essentials in C#: Memory, Async, and High-Performance Primitives

Excerpt

This post covers the C# performance essentials for enterprise backends: memory semantics (stack vs heap, value vs reference, GC), async/await with cancellation, streaming via IAsyncEnumerable<T>, concurrency primitives, Span<T>, pooling, and pitfalls with boxing and large structs.


Performance & Concurrency Essentials


Memory Model: Stack vs Heap, Value vs Reference, GC

Why it matters:
Efficient memory management reduces GC pressure and improves performance.

Diagram: Memory at a glance

+---------------------------+           +--------------------------------+
|           Stack           |           |              Heap              |
|---------------------------|           |--------------------------------|
| int x = 42                |           | new User() { Name = "Alice" } |
| Point p (struct inline)   |   --->    |  reference held on the stack  |
| local frames, by-value    |           | objects, arrays, strings      |
+---------------------------+           +--------------------------------+
Enter fullscreen mode Exit fullscreen mode

Essentials

  • Value types (struct, record struct) are copied by value and often stored inline; cheap for tiny models.
  • Reference types (class, record) live on the heap; managed by the GC.
  • Reduce boxing/unboxing (avoid storing struct in object, prefer generics).
  • Use StringBuilder for heavy concatenation; prefer ArrayPool<T> and Span<T>/Memory<T> for parsing and allocations.

Boxing pitfall

struct Counter { public int Value; }
object o = new Counter { Value = 5 }; // boxing (allocates)
Counter c = (Counter)o;               // unboxing (copy)
Enter fullscreen mode Exit fullscreen mode

ArrayPool & Span for parsing

using System.Buffers;

var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(4096);
try {
    var span = new Span<byte>(buffer, 0, 4096);
    // parse span...
}
finally {
    pool.Return(buffer);
}
Enter fullscreen mode Exit fullscreen mode

GC awareness

  • Allocation spikes can trigger GC; keep hot paths allocation-light.
  • Prefer struct enumerators and readonly struct for tight loops when practical.

Async/await & Cancellation, IAsyncEnumerable, Concurrency Primitives

Why it matters:
Modern services must scale and stay responsive. Async and concurrency patterns are essential for I/O and CPU-bound workloads.

Guidelines

  • async/await is for I/O-bound work; for CPU-bound tasks use Task.Run carefully.
  • Always pass CancellationToken; prefer timeouts and propagate cancellation across layers.
  • For streaming, use IAsyncEnumerable<T>; avoid buffering large sets in memory.

Cancellation & timeout

public async Task<string> FetchAsync(HttpClient http, string url, CancellationToken ct) {
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    cts.CancelAfter(TimeSpan.FromSeconds(5));
    return await http.GetStringAsync(url, cts.Token).ConfigureAwait(false);
}
Enter fullscreen mode Exit fullscreen mode

Streaming results

public async IAsyncEnumerable<int> GetNumbersAsync([EnumeratorCancellation] CancellationToken ct = default) {
    for (int i = 0; i < 100; i++) {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(10, ct);
        yield return i;
    }
}
Enter fullscreen mode Exit fullscreen mode

Concurrency primitives

// SemaphoreSlim: limit concurrent access
var sem = new SemaphoreSlim(5);
await sem.WaitAsync(ct);
try {
    // critical section
} finally {
    sem.Release();
}

// Channels: producer-consumer without manual locks
var channel = Channel.CreateUnbounded<string>();
_ = Task.Run(async () => {
    await foreach (var item in channel.Reader.ReadAllAsync(ct)) {
        // process item
    }
});
await channel.Writer.WriteAsync("work-item", ct);
channel.Writer.Complete();
Enter fullscreen mode Exit fullscreen mode

Library tip

  • In libraries, prefer ConfigureAwait(false) to avoid deadlocks in legacy sync contexts.

Span, Pooling, Boxing, Large Struct Caveats

Span notes

  • Span<T> is a stack-only ref struct for high-performance slicing—no heap allocations.
  • Cannot cross await or be captured in lambdas; use Memory<T>/ReadOnlyMemory<T> for async boundaries.
ReadOnlySpan<char> span = "1234".AsSpan();
int total = 0;
foreach (var ch in span) total += (ch - '0');
Enter fullscreen mode Exit fullscreen mode

Large struct caveats

  • Large structs incur copy cost when passed/returned; prefer classes or use in parameters for read-only refs.
public readonly struct BigValue {
    public readonly int A, B, C, D, E, F, G, H;
}

int Sum(in BigValue v) => v.A + v.B + v.C + v.D + v.E + v.F + v.G + v.H;
Enter fullscreen mode Exit fullscreen mode

Pooling

  • Prefer ArrayPool<T> or object pools in hot paths to reduce GC pressure.

CTA

Next up: Part 3 — Production-Ready Practices (resource management, testing/diagnostics, logging/telemetry, security & reliability). Share your performance tips in the comments!


Series Navigation

Top comments (0)