diff --git a/.skills/csharp-dotnet-cli-optimization/SKILL.md b/.skills/csharp-dotnet-cli-optimization/SKILL.md new file mode 100644 index 00000000..71acf832 --- /dev/null +++ b/.skills/csharp-dotnet-cli-optimization/SKILL.md @@ -0,0 +1,167 @@ +--- +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 +version: 0.2.0 +--- + +# 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](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](references/code-patterns.md) for LINQ, Span/Memory, stackalloc, structs, boxing, pooling, strings, and analyzer-backed code patterns. +- Read [references/README.md](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 + +1. Use `dotnet-counters` for live triage. +2. If the process is stuck, capture `dotnet-stack` immediately. +3. If CPU or allocation hot paths matter, collect `dotnet-trace`. +4. If heap growth matters more than call paths, collect `dotnet-gcdump`. +5. If you need SOS heap inspection or a postmortem, collect `dotnet-dump`. +6. Only after live evidence points to a candidate routine, apply patterns from the reference docs. +7. If the change is truly local and isolated, use BenchmarkDotNet to compare implementations. +8. 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` for data that must cross `await`, escape to the heap, or live in object fields. Switch to `Memory` or `ReadOnlyMemory` 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` + +These rules are clues, not goals. Apply them where the measured hot path justifies it. + +## Minimal Commands + +```bash +dnx dotnet-counters monitor --process-id +dotnet-counters monitor -p --counters System.Runtime +dotnet-stack report -p +dotnet-trace collect -p --duration 00:00:30 +dotnet-trace report topN +dotnet-gcdump collect -p +dotnet-gcdump report +dotnet-dump collect -p --type Heap +dotnet-dump analyze -c "dumpheap -stat" -c "exit" +``` + +Minimal BenchmarkDotNet pattern: + +```csharp +using BenchmarkDotNet.Attributes; + +[MemoryDiagnoser] +public class CandidateBench +{ + [Benchmark(Baseline = true)] + public int Original() => OriginalImpl(); + + [Benchmark] + public int Candidate() => CandidateImpl(); +} +``` + +```bash +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 diff --git a/.skills/csharp-dotnet-cli-optimization/references/README.md b/.skills/csharp-dotnet-cli-optimization/references/README.md new file mode 100644 index 00000000..b1c634b7 --- /dev/null +++ b/.skills/csharp-dotnet-cli-optimization/references/README.md @@ -0,0 +1,68 @@ +--- +description: Dated source ledger for csharp-dotnet-cli-optimization. +metadata: + tags: [sources, diagnostics, gc, linq, span, stackalloc, boxing] +--- + +# Sources + +## Current primary sources + +- [dotnet-counters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters), Microsoft Learn, updated `2025-10-02` + - Canonical CLI docs for live counters, `monitor`, `collect`, and `dnx` one-shot execution on .NET 10.0.100+. +- [dotnet-trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace), Microsoft Learn, updated `2026-03-20` + - Canonical CLI docs for `collect`, `report`, and the preview `collect-linux` path plus its limits. +- [dotnet-dump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump), Microsoft Learn, updated `2026-03-04` + - Canonical CLI docs for dump collection, dump types, and `analyze -c`. +- [dotnet-gcdump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-gcdump), Microsoft Learn, updated `2025-12-17` + - Canonical CLI docs for GC dump collection, `report`, and the induced full Gen 2 GC caveat. +- [Fundamentals of garbage collection](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals), Microsoft Learn, updated `2025-10-22` + - Current official overview of generations, allocation, and managed heap behavior. +- [Runtime configuration options for garbage collection](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector), Microsoft Learn, updated `2025-11-22` + - Current official source for server vs workstation GC, background GC, heap limits, LOH threshold, and modern GC configuration behavior. +- [stackalloc expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc), Microsoft Learn, updated `2026-01-24` + - Current official guidance for stack allocation limits, loop avoidance, initialization, and Span-based usage. +- [ref struct types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct), Microsoft Learn, updated `2026-01-20` + - Current official guidance for stack-only semantics and escape restrictions. +- [Structure types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct), Microsoft Learn, updated `2026-01-14` + - Current official source for readonly structs, pass-by-reference guidance, and boxing conversions. +- [Value types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types), Microsoft Learn, updated `2026-01-20` + - Current official source for copy semantics and inline storage behavior. +- [Boxing and Unboxing](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing), Microsoft Learn, updated `2025-10-13` + - Current official source for boxing semantics and cost. +- [Memory and Span usage guidelines](https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines), Microsoft Learn, updated `2025-04-11` + - Current official guidance for choosing `Span` vs `Memory` and ownership rules. +- [Lambda expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions), Microsoft Learn, updated `2026-01-24` + - Current official source for capture semantics and `static` lambdas. +- [What's new in C# 14](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14), Microsoft Learn, updated `2025-11-19` + - Current official confirmation of first-class span conversions in C# 14. +- [Performance rules](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings), Microsoft Learn, updated `2025-10-29` + - Current official index of analyzer-backed performance rules, including `CA1870`. +- [Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/), Stephen Toub, published `2025-09-10` + - High-trust expert source showing real .NET 10 runtime and LINQ improvements. Use it to avoid stale folklore such as "all LINQ is slow". + +## Specific analyzer pages used for code-pattern guidance + +- [CA1827](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1827), updated `2023-11-14` +- [CA1845](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1845), updated `2024-11-12` +- [CA1846](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1846), updated `2023-12-16` +- [CA1851](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1851), updated `2023-11-14` +- [CA1858](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1858), current official analyzer page +- [CA1860](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1860), current official analyzer page + +## Older but still canonical sources used cautiously + +- [Reduce memory allocations using new C# features](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/), Microsoft Learn, updated `2023-10-17` + - Still useful for `ref`, `in`, readonly struct, and copy-avoidance guidance, but older than the core 2025-2026 docs. +- [Intermediate materialization](https://learn.microsoft.com/en-us/dotnet/standard/linq/intermediate-materialization), Microsoft Learn, updated `2022-09-02` + - Still canonical for LINQ materialization semantics. +- [Deferred execution and lazy evaluation](https://learn.microsoft.com/en-us/dotnet/standard/linq/deferred-execution-lazy-evaluation), Microsoft Learn, updated `2022-09-29` + - Still canonical for LINQ deferred-execution semantics. +- [dotnet-stack](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-stack), Microsoft Learn, updated `2023-03-14` + - Used only for narrow stack-snapshot guidance because the page is stale compared to the other diagnostics docs. + +## Explicit exclusions + +- Framework-specific tutorials were intentionally excluded. +- GUI-first analysis flows were intentionally excluded. +- MCC-specific paths, code, and hot-path examples were intentionally excluded. diff --git a/.skills/csharp-dotnet-cli-optimization/references/code-patterns.md b/.skills/csharp-dotnet-cli-optimization/references/code-patterns.md new file mode 100644 index 00000000..4e18edea --- /dev/null +++ b/.skills/csharp-dotnet-cli-optimization/references/code-patterns.md @@ -0,0 +1,381 @@ +--- +description: Code-level performance patterns for csharp-dotnet-cli-optimization. +metadata: + tags: [linq, span, stackalloc, boxing, pooling, strings, analyzers] +--- + +# Code Patterns + +Use this reference after a profile or benchmark identifies a hot path. Do not apply these patterns speculatively. + +## Table Of Contents + +- [LINQ And Enumeration](#linq-and-enumeration) +- [Stack Allocation, Span, And Memory](#stack-allocation-span-and-memory) +- [Structs, Boxing, And Copies](#structs-boxing-and-copies) +- [Buffer Reuse And Advanced Helpers](#buffer-reuse-and-advanced-helpers) +- [Strings](#strings) +- [What Not To Suggest](#what-not-to-suggest) + +## LINQ And Enumeration + +### Property or indexer over LINQ when the concrete collection is known + +Wrong: + +```csharp +if (items.Count() > 0) +{ + return items.First(); +} +``` + +Better: + +```csharp +if (items.Count > 0) +{ + return items[0]; +} +``` + +Use `Count`, `Length`, `IsEmpty`, or an indexer when you already have a concrete collection with that API. Relevant analyzers: `CA1826`, `CA1829`, `CA1836`, `CA1860`. + +### `Any()` over `Count() > 0` when all you know is `IEnumerable` + +Wrong: + +```csharp +if (source.Count() != 0) +{ + Process(source); +} +``` + +Better: + +```csharp +if (source.Any()) +{ + Process(source); +} +``` + +Relevant analyzer: `CA1827`. + +### Avoid multiple enumeration of deferred queries + +Wrong: + +```csharp +var query = source.Where(Filter); +return query.Count() + query.Last().Id; +``` + +Better: + +```csharp +var materialized = source.Where(Filter).ToArray(); +return materialized.Length + materialized[^1].Id; +``` + +Materialize once only when you truly need multiple passes or random access and can afford the extra memory. Relevant analyzer: `CA1851`. + +### Avoid premature materialization + +Wrong: + +```csharp +var projected = source.ToList().Select(Map); +``` + +Better: + +```csharp +var projected = source.Select(Map); +``` + +Keep deferred execution unless you need a snapshot, repeated traversal, indexing, or a boundary between expensive stages. + +### Do not blanket-rewrite LINQ to loops + +- .NET 10 improved many LINQ operations substantially. +- Start with analyzer-backed fixes and measurement. +- Replace LINQ with hand-written loops only when a benchmark or trace shows that the remaining cost matters. + +### Use `TryGetNonEnumeratedCount` when count is optional + +```csharp +if (source.TryGetNonEnumeratedCount(out int count)) +{ + LogCount(count); +} +``` + +This avoids forcing enumeration when the underlying type already knows its size. + +## Stack Allocation, Span, And Memory + +### `stackalloc` only for small, bounded, temporary buffers + +Wrong: + +```csharp +for (int i = 0; i < items.Length; i++) +{ + Span buffer = stackalloc byte[4096]; + Use(buffer); +} +``` + +Better: + +```csharp +Span buffer = stackalloc byte[256]; +for (int i = 0; i < items.Length; i++) +{ + buffer.Clear(); + Use(buffer); +} +``` + +Guidance: + +- keep sizes conservative +- avoid `stackalloc` inside loops +- initialize the memory before use +- fall back to heap or pooling for larger or variable-sized buffers + +### Prefer span-based APIs over substring copies + +Wrong: + +```csharp +int.TryParse(line.Substring(7), out int value); +``` + +Better: + +```csharp +int.TryParse(line.AsSpan(7), out int value); +``` + +Relevant analyzers: `CA1845`, `CA1846`. + +### Use `Span` for sync work and `Memory` for async or heap-stored state + +Wrong: + +```csharp +// Wrong: Span cannot cross await safely. +public async Task ReadAsync(Span buffer) +{ + await socket.ReceiveAsync(buffer); + return buffer[0]; +} +``` + +Better: + +```csharp +public async Task ReadAsync(Memory buffer) +{ + await socket.ReceiveAsync(buffer); + return buffer.Span[0]; +} +``` + +`Span` is stack-only. If the lifetime crosses `await`, callbacks, or object storage, move to `Memory`. + +### `ref struct` is for stack-bound wrappers, not a general optimization badge + +- Use `ref struct` when the type itself contains spans or must never escape to the heap. +- Do not use it if you need arrays of that type, boxing, interface conversions, or heap fields. + +## Structs, Boxing, And Copies + +### Use `readonly struct` or `readonly record struct` for small immutable values + +Wrong: + +```csharp +public struct Measurement +{ + public double A; + public double B; + public void Normalize() => A /= B; +} +``` + +Better: + +```csharp +public readonly record struct Measurement(double A, double B); +``` + +Prefer value types for small, copyable, data-only values. Avoid large, mutable structs. + +### Pass large structs by `in` + +Wrong: + +```csharp +double Distance(Vector4 value) => value.X + value.Y + value.Z + value.W; +``` + +Better: + +```csharp +double Distance(in Vector4 value) => value.X + value.Y + value.Z + value.W; +``` + +This avoids copying large struct values on each call. + +### Avoid boxing in hot paths + +Wrong: + +```csharp +object boxed = valueStruct; +``` + +Wrong: + +```csharp +IFormattable f = valueStruct; +``` + +Better: + +```csharp +Use(in valueStruct); +``` + +Boxing allocates a heap object and copies the value. Interface conversions can box too. + +### Mark readonly members on structs + +- Non-readonly instance members on a readonly receiver can trigger defensive copies. +- Mark the whole struct `readonly` when possible, or mark readonly members explicitly. + +## Buffer Reuse And Advanced Helpers + +### Use `ArrayPool` when the buffer is too large or variable for `stackalloc` + +Wrong: + +```csharp +byte[] temp = new byte[inputLength]; +``` + +Better: + +```csharp +byte[] temp = ArrayPool.Shared.Rent(inputLength); +try +{ + Use(temp); +} +finally +{ + ArrayPool.Shared.Return(temp); +} +``` + +Rules: + +- return to the same pool once +- never use the buffer after return +- rented arrays may be larger than requested +- rented arrays are not guaranteed to be zeroed + +### Prevent accidental closure capture + +Wrong: + +```csharp +return values.Select(v => v * 2).ToArray(); +``` + +Better when no capture is needed: + +```csharp +return values.Select(static v => v * 2).ToArray(); +``` + +Use `static` lambdas or static local functions to prevent capture when the delegate does not need outer state. + +### Cache `SearchValues` for repeated searches + +Wrong: + +```csharp +int index = text.IndexOfAny(":/?&=".AsSpan()); +``` + +Better: + +```csharp +private static readonly SearchValues s_delims = + SearchValues.Create(":/?&=".AsSpan()); +``` + +```csharp +int index = text.IndexOfAny(s_delims); +``` + +Relevant analyzer: `CA1870`. + +### `CollectionsMarshal.AsSpan` is advanced and ownership-sensitive + +```csharp +Span span = CollectionsMarshal.AsSpan(list); +``` + +Use this only when: + +- you own the `List` +- you will not add or remove items while the span is in use +- a measured hot path justifies bypassing normal list APIs + +## Strings + +### `StartsWith` over `IndexOf(...) == 0` + +Wrong: + +```csharp +return text.IndexOf("abc", StringComparison.Ordinal) == 0; +``` + +Better: + +```csharp +return text.StartsWith("abc", StringComparison.Ordinal); +``` + +Relevant analyzer: `CA1858`. + +### `Append(char)` over `Append("x")` + +Wrong: + +```csharp +builder.Append("]"); +``` + +Better: + +```csharp +builder.Append(']'); +``` + +Relevant analyzers: `CA1834`, `CA1865-CA1867`. + +## What Not To Suggest + +- Do not suggest unsafe code first. +- Do not suggest pooling tiny objects by default. +- Do not suggest `stackalloc` because "heap bad, stack good". +- Do not suggest converting APIs to `Span` if the lifetime model does not fit. +- Do not suggest loop rewrites without a profile or benchmark showing LINQ still matters after simpler fixes. diff --git a/.skills/csharp-dotnet-cli-optimization/references/memory-model-gc.md b/.skills/csharp-dotnet-cli-optimization/references/memory-model-gc.md new file mode 100644 index 00000000..f64f6d81 --- /dev/null +++ b/.skills/csharp-dotnet-cli-optimization/references/memory-model-gc.md @@ -0,0 +1,117 @@ +--- +description: CLR memory model and GC guidance for csharp-dotnet-cli-optimization. +metadata: + tags: [clr, gc, heap, stack, loh, memory-model] +--- + +# CLR Memory Model And GC + +Use this reference when the user asks why allocations, boxing, heap growth, GC pauses, or stack-based techniques behave the way they do. + +## Core Model + +- Reference types allocate objects on the managed heap. Local variables and fields hold references to those objects. +- Value types store their data directly. A value type local is often stored in stack storage, but value types also live inline inside object fields and array elements. +- "`struct` means stack" is false. The useful distinction is inline storage plus copy semantics, not "stack forever". +- Boxing converts a value type to `object` or an interface by allocating a new heap object and copying the value into it. +- `ref struct` types, including `Span` and `ReadOnlySpan`, are stack-constrained wrappers that can't escape to the managed heap. +- `Memory` and `ReadOnlyMemory` are the heap-storable counterparts when data must live across `await`, callbacks, or object fields. + +## How The GC Works + +- The GC is generational: gen0 for young objects, gen1 as a buffer, gen2 for long-lived survivors. +- The large object heap (LOH) is used for allocations at or above 85,000 bytes by default. +- Background GC is enabled by default. It reduces pause impact for full collections but does not make them free. +- Server GC and workstation GC are process-level choices. The defaults are usually right unless measurement says otherwise. +- On modern 64-bit Windows and Linux, the GC internally uses regions, but the optimization model for application code is still about generations, allocation rate, survivor rate, LOH churn, and pinning. + +## What Usually Makes GC Expensive + +- High allocation rate on hot paths +- Objects surviving long enough to promote into older generations +- Large transient allocations that churn the LOH +- Excessive pinning that increases fragmentation +- Finalizers on objects that should have been deterministic `Dispose` calls instead + +## Wrong vs Better + +| Wrong | Better | Why | +|---|---|---| +| Assume a `struct` is always stack allocated | Explain whether it will be copied, boxed, stored inline, or escape | That is what actually drives cost | +| Allocate large temporary arrays repeatedly | Reuse, pool, or redesign the algorithm if measurement shows LOH churn | LOH allocations are cleared and collected with gen2 work | +| Call `GC.Collect()` to "fix" memory pressure | Lower allocation rate and object lifetime first | Forced GC usually adds pause time and hides the real problem | +| Pin many buffers for long periods | Minimize pin count and pin duration | Pinning can fragment the heap | +| Use finalizers for routine cleanup | Use `IDisposable`, `using`, and `SafeHandle` for unmanaged resources | Finalization is slower and delays reclamation | + +## GC Configuration Rules + +- Treat GC configuration changes as process-wide tuning, not local fixes. +- Prefer runtime defaults unless counters and traces show a clear reason to change them. +- Choose server GC for throughput-oriented workloads only after measurement. +- Use low-latency modes sparingly and for bounded windows. They reduce GC intrusiveness by letting memory grow and can increase fragmentation. +- If you are tuning in containers or hard memory limits, treat heap hard-limit settings as operational controls, not code-level optimizations. + +## Bad vs Good Examples + +Bad: + +```csharp +for (int i = 0; i < 10_000; i++) +{ + DoWork(new byte[200_000]); +} +GC.Collect(); +``` + +Better: + +```csharp +byte[] buffer = ArrayPool.Shared.Rent(200_000); +try +{ + for (int i = 0; i < 10_000; i++) + { + DoWork(buffer); + } +} +finally +{ + ArrayPool.Shared.Return(buffer); +} +``` + +Bad: + +```csharp +public sealed class NativeThing +{ + ~NativeThing() => ReleaseHandle(); +} +``` + +Better: + +```csharp +public sealed class NativeThing : IDisposable +{ + public void Dispose() + { + ReleaseHandle(); + GC.SuppressFinalize(this); + } +} +``` + +## Practical Heuristics + +- If counters show rising allocation rate and frequent gen0 collections, start by eliminating short-lived allocations. +- If gen2 collections or LOH size are the problem, look for survivor growth, pinned buffers, and large transient objects. +- If a change turns classes into structs, verify both allocation wins and copy costs. +- If the process is memory-constrained, inspect runtime GC settings before changing code blindly. + +## What Not To Claim + +- Do not claim that moving code to `struct` always reduces memory. +- Do not claim that stack allocation is always faster than pooling. +- Do not claim that background GC removes pause concerns. +- Do not claim that the GC is the problem unless counters, traces, or dumps support that diagnosis.