| name | csharp-dotnet-cli-optimization |
| description | Use when diagnosing or optimizing generic C#/.NET performance, GC pressure, allocations, heap or stack usage, LINQ overhead, boxing, Span/Memory, stackalloc, pooling, or hot-path code with CLI-first tools such as dotnet-counters, dotnet-trace, dotnet-stack, dotnet-gcdump, dotnet-dump, or BenchmarkDotNet. |
| metadata | {"category":"technique","triggers":["dotnet-counters","dotnet-trace","dotnet-dump","dotnet-gcdump","dotnet-stack","benchmarkdotnet","allocations","gc pressure","memory leak","hot path","linq","stackalloc","span","memory","boxing","heap","stack","latency","throughput","slow","hang","deadlock"]} |
C#/.NET CLI Optimization
CLI-first guidance for generic C# 14 / .NET 10 performance work.
Use the references on demand:
- Read references/memory-model-gc.md for stack vs heap, generations, LOH, pinning, server vs workstation GC, and GC tuning limits.
- Read references/code-patterns.md for LINQ, Span/Memory, stackalloc, structs, boxing, pooling, strings, and analyzer-backed code patterns.
- Read references/README.md for dated sources and freshness notes.
When to Use
- A .NET process is slow, allocation-heavy, CPU-heavy, or memory-hungry
- A live process appears stuck, hung, or deadlocked
- The user asks how heap, stack, GC, boxing, or LINQ overhead actually works in .NET
- The user wants concrete bad vs good code patterns after measurement has identified a hot path
- The task needs a decision between counters, traces, stacks, GC dumps, dumps, or a benchmark
NOT for:
- ASP.NET Core, EF Core, MAUI, Orleans, Unity, Avalonia, WPF, WinForms, Blazor, or other framework-specific playbooks
- Visual Studio, Rider, VS Code, PerfView, speedscope, or any GUI-first workflow
- speculative rewrites such as "replace everything with Span" before measurement
Iron Rule
ALWAYS measure first, change second, and re-measure third.
NEVER claim an optimization without before/after evidence from the same scenario.
| Rationalization | Reality |
|---|
| "This is obviously slow" | The runtime, JIT, and libraries often invalidate intuition. |
| "struct means stack" | Value types are stored inline. They are not "always on the stack". |
| "All LINQ is slow" | .NET 10 improved many LINQ paths. Measure before rewriting. |
| "GC.Collect will fix it" | Forced collection usually treats symptoms, not cause. |
Investigation Order
- Use
dotnet-counters for live triage.
- If the process is stuck, capture
dotnet-stack immediately.
- If CPU or allocation hot paths matter, collect
dotnet-trace.
- If heap growth matters more than call paths, collect
dotnet-gcdump.
- If you need SOS heap inspection or a postmortem, collect
dotnet-dump.
- Only after live evidence points to a candidate routine, apply patterns from the reference docs.
- If the change is truly local and isolated, use BenchmarkDotNet to compare implementations.
- Re-run the original live capture to prove the real workload improved.
Which Reference to Load
| User question | Read first |
|---|
| "How do stack and heap really work in .NET?" | references/memory-model-gc.md |
| "Why is GC pausing or why is LOH churn hurting us?" | references/memory-model-gc.md |
| "How should I optimize this LINQ?" | references/code-patterns.md |
| "Can I move this to the stack with stackalloc or Span?" | references/code-patterns.md |
| "Should this be a struct, ref struct, readonly struct, or class?" | references/code-patterns.md and references/memory-model-gc.md |
| "Why is this boxing?" | references/code-patterns.md |
Tool Selection
| Question | Tool | What it answers | Typical next step |
|---|
| Is the live process allocating, GCing, or saturating CPU? | dotnet-counters | Live counters and trend direction | Capture a trace or GC dump if suspicious |
| Is the process hung or deadlocked right now? | dotnet-stack | Current managed stack snapshot | Collect a dump if you need deeper postmortem evidence |
| Which call paths consume CPU or allocate heavily? | dotnet-trace | Sampled execution and runtime events | Confirm hot paths, then isolate code |
| Which object types dominate managed heap usage? | dotnet-gcdump | Heap composition and type totals | Decide whether to redesign lifetimes or collect a full dump |
| Do I need SOS heap inspection or thread state? | dotnet-dump | Full dump plus CLI analysis | Run analyze -c commands |
| Did a code change improve one isolated routine? | BenchmarkDotNet | Reproducible microbenchmark comparison | Re-run live diagnostics in the real scenario |
Pattern Guardrails
- Do not answer "put it on the stack" as a blanket goal. Explain lifetime, copies, boxing, and escape rules instead.
- Do not suggest
stackalloc for unbounded sizes, large buffers, or loop-carried allocations.
- Do not recommend
Span<T> for data that must cross await, escape to the heap, or live in object fields. Switch to Memory<T> or ReadOnlyMemory<T> for that.
- Do not recommend converting every
class to a struct. Large, mutable, identity-bearing, or frequently boxed types often get worse.
- Do not blanket-rewrite LINQ to loops. Use analyzer-backed fixes first, and remember .NET 10 substantially improved many LINQ paths.
- Do not recommend pooling without ownership rules. Returned pooled arrays must not be reused by the caller.
- Do not recommend
GC.Collect() except for rare, justified lifecycle boundaries, and only with measurement.
Analyzer Radar
When performance diagnostics point to code patterns rather than runtime configuration, consult the current performance analyzers, especially:
CA1826, CA1827, CA1829, CA1836, CA1851, CA1860 for LINQ and enumeration
CA1845, CA1846, CA1858 for string and span-friendly APIs
CA1834, CA1865-CA1867 for StringBuilder char overloads
CA1870 for cached SearchValues<T>
These rules are clues, not goals. Apply them where the measured hot path justifies it.
Minimal Commands
dnx dotnet-counters monitor --process-id <PID>
dotnet-counters monitor -p <PID> --counters System.Runtime
dotnet-stack report -p <PID>
dotnet-trace collect -p <PID> --duration 00:00:30
dotnet-trace report <trace.nettrace> topN
dotnet-gcdump collect -p <PID>
dotnet-gcdump report <file.gcdump>
dotnet-dump collect -p <PID> --type Heap
dotnet-dump analyze <dump> -c "dumpheap -stat" -c "exit"
Minimal BenchmarkDotNet pattern:
using BenchmarkDotNet.Attributes;
[MemoryDiagnoser]
public class CandidateBench
{
[Benchmark(Baseline = true)]
public int Original() => OriginalImpl();
[Benchmark]
public int Candidate() => CandidateImpl();
}
dotnet run -c Release
Output
When using this skill, report:
- the measured symptom and the evidence used to identify it
- the chosen tool or code pattern and why it fits this bottleneck
- the relevant tradeoff, such as allocation vs copy cost, deferred vs eager execution, or stack vs pool
- the before/after result, or say explicitly if the recommendation is still unverified