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 viaIAsyncEnumerable<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
- Async/await & Cancellation, IAsyncEnumerable, Concurrency Primitives
- Span, Pooling, Boxing, Large Struct Caveats
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 |
+---------------------------+ +--------------------------------+
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
structinobject, prefer generics). - Use
StringBuilderfor heavy concatenation; preferArrayPool<T>andSpan<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)
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);
}
GC awareness
- Allocation spikes can trigger GC; keep hot paths allocation-light.
- Prefer struct enumerators and
readonly structfor 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/awaitis for I/O-bound work; for CPU-bound tasks useTask.Runcarefully. - 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);
}
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;
}
}
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();
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
awaitor be captured in lambdas; useMemory<T>/ReadOnlyMemory<T>for async boundaries.
ReadOnlySpan<char> span = "1234".AsSpan();
int total = 0;
foreach (var ch in span) total += (ch - '0');
Large struct caveats
- Large structs incur copy cost when passed/returned; prefer classes or use
inparameters 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;
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)