mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
[skipci]Merge pull request #2992 from milutinke/skills/csharp-dotnet-cli-optimization
This commit is contained in:
commit
bd55fc3ed3
4 changed files with 733 additions and 0 deletions
167
.skills/csharp-dotnet-cli-optimization/SKILL.md
Normal file
167
.skills/csharp-dotnet-cli-optimization/SKILL.md
Normal file
|
|
@ -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<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
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```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
|
||||
68
.skills/csharp-dotnet-cli-optimization/references/README.md
Normal file
68
.skills/csharp-dotnet-cli-optimization/references/README.md
Normal file
|
|
@ -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<T> and Span<T> 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<T>` vs `Memory<T>` 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.
|
||||
|
|
@ -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<T>`
|
||||
|
||||
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<byte> buffer = stackalloc byte[4096];
|
||||
Use(buffer);
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
Span<byte> 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<T>` for sync work and `Memory<T>` for async or heap-stored state
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
// Wrong: Span<T> cannot cross await safely.
|
||||
public async Task<int> ReadAsync(Span<byte> buffer)
|
||||
{
|
||||
await socket.ReceiveAsync(buffer);
|
||||
return buffer[0];
|
||||
}
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
public async Task<int> ReadAsync(Memory<byte> buffer)
|
||||
{
|
||||
await socket.ReceiveAsync(buffer);
|
||||
return buffer.Span[0];
|
||||
}
|
||||
```
|
||||
|
||||
`Span<T>` is stack-only. If the lifetime crosses `await`, callbacks, or object storage, move to `Memory<T>`.
|
||||
|
||||
### `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<T>` when the buffer is too large or variable for `stackalloc`
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
byte[] temp = new byte[inputLength];
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
byte[] temp = ArrayPool<byte>.Shared.Rent(inputLength);
|
||||
try
|
||||
{
|
||||
Use(temp);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.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<T>` for repeated searches
|
||||
|
||||
Wrong:
|
||||
|
||||
```csharp
|
||||
int index = text.IndexOfAny(":/?&=".AsSpan());
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```csharp
|
||||
private static readonly SearchValues<char> s_delims =
|
||||
SearchValues.Create(":/?&=".AsSpan());
|
||||
```
|
||||
|
||||
```csharp
|
||||
int index = text.IndexOfAny(s_delims);
|
||||
```
|
||||
|
||||
Relevant analyzer: `CA1870`.
|
||||
|
||||
### `CollectionsMarshal.AsSpan` is advanced and ownership-sensitive
|
||||
|
||||
```csharp
|
||||
Span<int> span = CollectionsMarshal.AsSpan(list);
|
||||
```
|
||||
|
||||
Use this only when:
|
||||
|
||||
- you own the `List<T>`
|
||||
- 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<T>` if the lifetime model does not fit.
|
||||
- Do not suggest loop rewrites without a profile or benchmark showing LINQ still matters after simpler fixes.
|
||||
|
|
@ -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<T>` and `ReadOnlySpan<T>`, are stack-constrained wrappers that can't escape to the managed heap.
|
||||
- `Memory<T>` and `ReadOnlyMemory<T>` 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<byte>.Shared.Rent(200_000);
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
DoWork(buffer);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue