# Performance and Benchmarking in .NET

## 1. Measure First

Performance work starts with a **goal** (p99 < 200 ms, 5,000 req/s per instance) and **measurements**, not guesses:

| Question | Tool |
|---|---|
| Is a function faster after my change? | **BenchmarkDotNet** micro-benchmarks |
| Where does CPU time go in the app? | `dotnet-trace` + PerfView/Speedscope, Visual Studio/Rider profilers |
| Is memory/GC the problem? | `dotnet-counters` (allocation rate, GC count, % time in GC), `dotnet-gcdump` |
| Which requests are slow and why? | APM / OpenTelemetry traces |
| How does it behave under load? | k6, NBomber, bombardier |

## 2. BenchmarkDotNet

```csharp
[MemoryDiagnoser]
public class ParsingBenchmarks
{
    private readonly string _line = "10,20,30,40,50";
    [Benchmark(Baseline = true)] public int Split() => _line.Split(',').Sum(int.Parse);
    [Benchmark] public int Span() => SumCsv(_line);
}
// dotnet run -c Release  →  Mean, Error, StdDev, Allocated per operation
```

It handles warm-up, JIT tiers, many iterations and statistics. Always run in **Release**, without a debugger, and compare against a **baseline**. A `Stopwatch` around one call is not a benchmark.

## 3. Common Wins (When Measured)

- **Fewer allocations**: avoid string concatenation in loops (use `StringBuilder` with capacity or `string.Create`), avoid LINQ in hot paths, reuse buffers (`ArrayPool`), pool objects (`ObjectPool<T>`), use spans for parsing.
- **Right data structures**: `Dictionary`/`HashSet` lookups instead of list scans; `FrozenDictionary` for read-mostly data; pre-size collections.
- **Avoid exceptions for control flow**: `TryParse`, result types.
- **Async I/O** everywhere on servers; batching database calls; caching.
- **Serialization**: `System.Text.Json` with source generators avoids reflection.
- **Logging**: use structured logging with `LoggerMessage` source generators; check `IsEnabled` before building expensive messages.

## 4. Runtime Settings

- **Server GC** for throughput; consider **DATAS** to reduce memory in containers.
- **Dynamic PGO** (on by default since .NET 8) optimizes hot paths using runtime profiles.
- **ReadyToRun** / **Native AOT** for startup-sensitive apps.
- Keep up with .NET versions: each release brings significant performance improvements for free.

## 5. Reading Results Honestly

Look at the distribution (p95/p99), not just averages; check allocations alongside time; make sure the benchmark does the same work as production (realistic input sizes); beware of the JIT eliminating dead code (return results from benchmarks).

## Common Pitfalls

1. **Optimizing without profiling**: speeding up code that isn't the bottleneck.
2. **Benchmarking in Debug** or with a debugger attached.
3. **Micro-optimizing** at the cost of readability where it doesn't matter.
4. **Ignoring allocations**: GC time shows up as latency spikes.
5. **Caching without bounds or invalidation**.

## Quick Reference

```text
goal → measure → profile → fix the biggest bottleneck → measure again
BenchmarkDotNet (Release, [MemoryDiagnoser], baseline) · dotnet-counters / dotnet-trace / gcdump
wins: fewer allocations (StringBuilder, spans, pools), right collections, no exceptions as control flow,
async I/O, STJ source generators, LoggerMessage · Server GC + DATAS · PGO · upgrade .NET
```
