# Csharp Performance

> Guides advanced .NET performance optimization: Span<T> and Memory<T> usage, BenchmarkDotNet, allocation profiling with dotMemory, source generators, and high-performance patterns. Use when the user asks about C# performance, Span, Memory, BenchmarkDotNet, allocation profiling, source generators, high-performance .NET. Do NOT use when the user asks about C# async (use `csharp-async-patterns`), C# modern idioms (use `csharp-modern-idioms`), general performance testing (use `performance-testing`).

- Skill: `ferroxlabs/csharp-performance` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ferroxlabs/csharp-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ferroxlabs/csharp-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: FerroxLabs (https://skillmd.com/u/ferroxlabs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ferroxlabs/csharp-performance

---

# C# Performance Optimization

## When to Use

**Use this skill when the user:**
- Asks about reducing heap allocations in C# -- including boxing, closure captures, LINQ overhead, or excessive `new` expressions
- Wants to use `Span<T>`, `Memory<T>`, `ReadOnlySpan<T>`, or `ArrayPool<T>` to avoid heap pressure in hot paths
- Needs to set up BenchmarkDotNet to measure method-level throughput, memory allocations, or JIT behavior
- Asks about profiling tools -- dotMemory, PerfView, dotTrace, EventPipe, or `dotnet-counters` -- to find allocation hotspots or GC pressure
- Wants to use source generators, `Unsafe`, `MemoryMarshal`, or `NativeMemory` for ultra-low-overhead patterns
- Is hitting GC pauses (Gen2 collections, LOH fragmentation) and needs to redesign object lifecycles
- Asks about value types, `ref struct`, `readonly struct`, struct layout, or `[StructLayout]` packing
- Wants to optimize string handling -- `StringPool`, `SearchValues<T>`, `Regex` compilation, or `CompositeFormat`
- Asks about SIMD via `System.Numerics.Vector<T>` or `System.Runtime.Intrinsics` for data-parallel computation
- Is writing library code targeting `netstandard2.1` or `net8+` and needs to understand API availability trade-offs

**Do NOT use this skill when:**
- The user asks about `async`/`await`, `ValueTask`, `IAsyncEnumerable`, or `ConfigureAwait` -- use `csharp-async-patterns`
- The user asks about C# 10--13 language features, pattern matching, records, or init-only setters in general -- use `csharp-modern-idioms`
- The user needs load testing, k6, Gatling, or HTTP-level throughput testing -- use `performance-testing`
- The user asks about EF Core query optimization, N+1 queries, or compiled queries -- use `efcore-performance`
- The user is asking about general algorithm complexity theory without a C# implementation context -- use a general algorithms skill
- The user asks about containerization, Kubernetes resource limits, or cloud scaling -- those are infrastructure concerns

---

## Process

### Step 1: Establish a Profiling-First Foundation

Before writing a single line of optimized code, build a measurement baseline.

- **Select the right tool for the bottleneck type:**
  - Allocation profiling: JetBrains dotMemory, or `dotnet-gcdump` + PerfView for allocation call trees
  - CPU hotspot profiling: JetBrains dotTrace (Timeline mode), PerfView (CPU stacks), or `perf` on Linux with `dotnet-trace`
  - GC diagnostics: `dotnet-counters monitor --counters System.Runtime` to watch `gen-0-gc-count`, `gen-1-gc-count`, `gen-2-gc-count`, `loh-size`, and `alloc-rate` in real time
  - Quick micro-benchmarks: BenchmarkDotNet with `[MemoryDiagnoser]` and `[DisassemblyDiagnoser]`
  - Production telemetry: EventPipe via DiagnosticPort or OpenTelemetry metrics with `System.Runtime` meters

- **Run the application under a realistic workload** -- never profile with toy data. For web services, replay production traffic via a captured HTTP archive or use a load generator targeting p95 latency scenarios.

- **Capture a baseline snapshot before ANY change.** In dotMemory: take a "Get Snapshot" after stabilization. In PerfView: collect a `.etl` file for 30--60 seconds under load. In BenchmarkDotNet: commit the baseline results to version control as a `.csv` or use `--exporters json`.

- **Identify the top-3 allocation sites by bytes retained, not just bytes allocated.** Allocated bytes tell you about throughput pressure; retained bytes identify leaks and LOH survivors.

- **Check the GC mode.** Server GC (`<GarbageCollectionAdaptationMode>`) allocates per-logical-core heaps and is nearly always correct for ASP.NET Core services. Workstation GC is correct for desktop or CLI tools. Mixing them causes unexpected pause behavior.

---

### Step 2: Classify the Bottleneck with a Decision Framework

Once profiling data exists, classify the root cause before selecting an optimization.

- **Allocation pressure / GC churn:** Gen0 collection rate > 10/sec in a server app, or `alloc-rate` consistently above 100 MB/s with no corresponding throughput -- reduce allocations, pool objects, use `Span<T>`.

- **LOH fragmentation:** Objects >= 85,000 bytes land on the Large Object Heap and are not compacted by default. Symptoms: `loh-size` growing monotonically, `gen-2-gc-count` spiking. Fix: pool large buffers via `ArrayPool<byte>.Shared`, or enable LOH compaction with `GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce` before a single critical GC.

- **CPU hotspot in tight loop:** If a single method consumes >5% of CPU samples and is called millions of times, consider: algorithmic improvement first, then `Span<T>` to remove bounds checks, then SIMD vectorization, then `unsafe` pointer arithmetic as a last resort.

- **String allocation dominance:** Strings are immutable reference types. If string operations dominate allocations, evaluate: `string.Create<TState>()`, `StringBuilderPool`, `ReadOnlySpan<char>` slicing instead of `Substring`, `SearchValues<char>` for membership tests, and `CompositeFormat` (net8+) to avoid repeated format string parsing.

- **Reflection or dynamic dispatch overhead:** If profiling shows `Invoke`, `GetMethod`, `CreateInstance`, or interface dispatch in hot paths, replace with source generators, cached delegates, `MethodTable` tricks via `Unsafe.As`, or `DynamicMethod`-based IL emit (only if source generators are not viable).

- **Contention / false sharing:** If CPU utilization is high but throughput is low and you see lock contentions in profiling, check: lock striping, `ConcurrentDictionary`, `Interlocked` operations, and cache-line-aligned struct layout using `[StructLayout(LayoutKind.Explicit)]` with 64-byte padding fields.

---

### Step 3: Reduce Allocations with Span, Memory, and Pooling

This is the highest-leverage optimization category in modern .NET.

- **`Span<T>` basics:** `Span<T>` is a `ref struct` that wraps a contiguous region of memory -- stack, heap, or native. Because it is a `ref struct`, it cannot be boxed, stored on the heap, or used across `await` points. Use it for synchronous, stack-confined processing of arrays, strings (`ReadOnlySpan<char>`), or stack-allocated memory (`stackalloc`).

- **`stackalloc` thresholds:** Stack space is ~1 MB on most OS thread configurations. A practical safe limit for `stackalloc` is 256--1024 bytes for value types. For anything larger, fall back to `ArrayPool<T>.Shared.Rent(size)`. Always use the pattern:
  ```csharp
  const int StackAllocThreshold = 256;
  byte[]? pooled = null;
  Span<byte> buffer = size <= StackAllocThreshold
      ? stackalloc byte[size]
      : (pooled = ArrayPool<byte>.Shared.Rent(size));
  try { /* use buffer */ }
  finally { if (pooled is not null) ArrayPool<byte>.Shared.Return(pooled); }
  ```

- **`Memory<T>` for async contexts:** When you need to pass a buffer across an `await` boundary, use `Memory<T>` or `ReadOnlyMemory<T>`. These are regular structs (not `ref struct`) that wrap the same contiguous memory. Call `.Span` to get a `Span<T>` inside a synchronous scope.

- **`ArrayPool<T>` correctness rules:** Always `Return` the rented array even on exception (use `try/finally`). Never use a returned array -- zero it first with `clearArray: true` if it contains sensitive or stale data. Never cache `Span<T>` views of a rented array beyond the return call.

- **`MemoryPool<T>` vs `ArrayPool<T>`:** `MemoryPool<T>` returns an `IMemoryOwner<T>` that implements `IDisposable` -- better for encapsulating ownership in `using` blocks. `ArrayPool<T>` is lower overhead and appropriate when lifetime is tightly controlled in a single method.

- **`MemoryMarshal` for zero-copy reinterpretation:** `MemoryMarshal.Cast<TFrom, TTo>()` reinterprets a `Span<TFrom>` as `Span<TTo>` without copying -- useful for reading network protocol headers from a `byte[]` buffer as a struct.

- **Avoid `ToArray()` and `ToString()` in hot paths.** These always allocate. Prefer accepting `ReadOnlySpan<char>` parameters in your own APIs, and use `TryFormat(Span<char>, ...)` overloads on numeric types.

---

### Step 4: Benchmark with BenchmarkDotNet Correctly

BenchmarkDotNet is the standard C# micro-benchmarking framework. Misuse produces misleading results.

- **Minimum viable benchmark setup:**
  ```csharp
  [MemoryDiagnoser]
  [DisassemblyDiagnoser(maxDepth: 3)]
  [SimpleJob(RuntimeMoniker.Net80)]
  public class ParseBenchmarks
  {
      private readonly string _input = new string('a', 1024);

      [Benchmark(Baseline = true)]
      public int SubstringParse() => ParseWithSubstring(_input);

      [Benchmark]
      public int SpanParse() => ParseWithSpan(_input.AsSpan());
  }
  ```
  Run with `dotnet run -c Release --project Benchmarks`. Never run benchmarks in `Debug` configuration -- the JIT does not optimize debug builds and results are meaningless.

- **`[Params]` for realistic size coverage:**
  ```csharp
  [Params(16, 256, 4096, 65536)]
  public int InputSize { get; set; }
  ```
  Always include at least one small size (fits in L1 cache, ~32 KB), one medium (fits in L2/L3, ~256 KB--8 MB), and one large size (exceeds cache, forces memory bandwidth). Performance characteristics often invert across these tiers.

- **`[GlobalSetup]` for pre-warming:** Allocate test data in `[GlobalSetup]` to exclude setup cost from measurements. Never allocate inside `[Benchmark]` methods unless allocation is the thing being measured.

- **Interpret `[MemoryDiagnoser]` output:** The `Allocated` column shows bytes allocated per operation (not total). A benchmark showing `0 B` means no managed heap allocation occurred -- this is the target for `Span<T>`-based hot paths. `Gen 0`, `Gen 1`, `Gen 2` columns show GC collections per 1000 operations.

- **Statistical validity:** BenchmarkDotNet runs a configurable number of warmup iterations (default 15) and measured iterations (default 100). For high-variance results, increase with `[SimpleJob(warmupCount: 30, iterationCount: 200)]`. Check the `Error` and `StdDev` columns -- a StdDev/Mean ratio above 5% indicates environmental noise; run on an isolated machine or use `--affinity` CPU pinning.

- **`[DisassemblyDiagnoser]`** produces the JIT-emitted x86/ARM64 assembly. Use it to verify: bounds-check elimination (look for absent `cmp`/`jae` pairs), SIMD vectorization (look for `vmovdqu`, `vpaddw`, `vpcmpeqb`), and that the JIT did not box value types (look for `call [mscorlib]System.Object::ToString`).

---

### Step 5: Eliminate JIT and Runtime Overhead with Advanced Patterns

After allocation reduction, these techniques address CPU-level costs.

- **`readonly struct`:** Declare structs `readonly` when all fields are `readonly`. This allows the JIT to pass the struct by reference internally without defensive copies. Without `readonly`, any call to a non-`readonly` method on a struct copied from a `readonly` field or `in` parameter triggers a silent defensive copy.

- **`in` parameters for large structs:** Passing a struct larger than 16 bytes by value copies it. Use `in` to pass by readonly reference: `void Process(in LargeStruct s)`. Combine with `ref readonly` returns to avoid copies on return paths.

- **`ref` returns and `ref` locals:** Enables zero-copy access to array elements or struct fields:
  ```csharp
  ref int element = ref data[index]; // no copy
  element += delta;                  // modifies in-place
  ```

- **`[SkipLocalsInit]` attribute:** By default, the CLR zero-initializes all local variables. `[SkipLocalsInit]` (applied to a method or module) skips this, saving cycles when you have many large stack-allocated buffers. Only safe when you guarantee initialization before first read. Apply at method level, not module level, unless you audit every method.

- **Aggressive inlining:** The JIT inlines methods below approximately 32 IL bytes automatically. For hot path methods just above this threshold, add `[MethodImpl(MethodImplOptions.AggressiveInlining)]`. Avoid applying this to large methods -- it increases code size and can harm instruction-cache efficiency. Verify with `[DisassemblyDiagnoser]` that inlining actually occurred.

- **`AggressiveOptimization`:** `[MethodImpl(MethodImplOptions.AggressiveOptimization)]` tells the JIT to spend more time optimizing a method (Tier 2 immediately). Use on known-hot, stable methods.

- **Avoid virtual dispatch in hot loops:** Interface calls and virtual method calls require an indirect jump through the MethodTable. In tight loops processing known concrete types, prefer: generics with struct constraints (the JIT monomorphizes the generic, eliminating virtual dispatch), or sealed classes (the JIT can devirtualize sealed types).

---

### Step 6: Apply SIMD and Vectorization

SIMD (Single Instruction, Multiple Data) processes multiple data elements per CPU instruction -- 4x to 32x throughput for bulk numeric or byte operations.

- **Check hardware support first:**
  ```csharp
  if (Vector.IsHardwareAccelerated)
      ProcessVectorized(data);
  else
      ProcessScalar(data);
  ```

- **`System.Numerics.Vector<T>`** is the portable SIMD API. `Vector<T>.Count` gives the number of elements per vector (16 for `Vector<byte>` on AVX2, 8 for `Vector<int>`). Use it for sum-of-squares, dot products, element-wise operations on arrays:
  ```csharp
  var sum = Vector<int>.Zero;
  int vectorSize = Vector<int>.Count;
  int i = 0;
  for (; i <= data.Length - vectorSize; i += vectorSize)
      sum += new Vector<int>(data, i);
  int result = Vector.Dot(sum, Vector<int>.One);
  // handle tail elements [i..data.Length)
  ```

- **`System.Runtime.Intrinsics`** gives direct access to AVX2, SSE4.2, ARM NEON, etc. Only use when `Vector<T>` cannot express the operation (e.g., byte shuffle, population count, horizontal min/max). Always guard with `Avx2.IsSupported` or `Sse42.IsSupported` and provide a scalar fallback.

- **`SearchValues<T>` (net8+):** For searching a small fixed set of characters or bytes in a large span -- e.g., URL parsing, CSV tokenization -- `SearchValues<char>` compiles to vectorized scanning automatically. Faster than `IndexOfAny` for sets of 5+ elements.
  ```csharp
  private static readonly SearchValues<char> s_delimiters =
      SearchValues.Create(",;\t\r\n");
  int idx = span.IndexOfAny(s_delimiters);
  ```

- **Always handle tail elements.** Vectorized loops process `N - (N % vectorWidth)` elements. The remaining `N % vectorWidth` elements must be processed with a scalar tail loop. Failing to do so is a silent correctness bug.

---

### Step 7: Use Source Generators to Eliminate Reflection

Reflection-based serialization, logging, and mapping are among the most common hidden allocation sources in .NET applications.

- **System.Text.Json source generation:** Replace runtime reflection with compile-time generated serializers:
  ```csharp
  [JsonSerializable(typeof(OrderDto))]
  [JsonSerializable(typeof(List<OrderDto>))]
  internal partial class AppJsonContext : JsonSerializerContext { }

  // Usage:
  string json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
  ```
  This eliminates reflection, reduces startup time, and is AOT-compatible. In hot-path HTTP handlers this can reduce serialization allocations by 60--80%.

- **Microsoft.Extensions.Logging compile-time log messages:** Replace `_logger.LogInformation("User {UserId} logged in", userId)` (which boxes the userId and allocates a string) with:
  ```csharp
  [LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} logged in")]
  private static partial void LogUserLoggedIn(ILogger logger, int userId);
  ```
  The generated code avoids boxing and only evaluates the message string if the log level is enabled.

- **Incremental source generators for custom hot paths:** Write incremental source generators (using `IIncrementalGenerator`) for repetitive patterns like: fast property mapping (instead of AutoMapper reflection), fast enum-to-string conversion (instead of `Enum.GetName`), or pre-compiled regular expressions via `[GeneratedRegex]`:
  ```csharp
  [GeneratedRegex(@"^\d{4}-\d{2}-\d{2}$", RegexOptions.Compiled)]
  private static partial Regex DatePattern();
  ```
  `[GeneratedRegex]` compiles the regex at build time, eliminating the runtime compilation cost and reducing allocations during matching.

- **Avoid `Activator.CreateInstance` in hot paths.** Cache a compiled `Func<T>` delegate instead:
  ```csharp
  private static readonly Func<MyService> _factory =
      Expression.Lambda<Func<MyService>>(
          Expression.New(typeof(MyService))).Compile();
  ```
  Or use a source generator to emit a `new MyService()` call directly.

---

### Step 8: Validate, Document, and Protect the Optimization

Optimizations rot without discipline. Build guardrails.

- **Commit BenchmarkDotNet results to version control.** Store the `BenchmarkDotNet.Artifacts` folder output (or a summary CSV) alongside the code. Review benchmark diffs in PR descriptions.

- **Write regression tests for performance.** In CI, run benchmarks with `--filter *` and compare against the baseline using `--join` mode, or write a unit test using `dotnet-benchmark` assert extensions that fails if throughput drops by more than 10%.

- **Document WHY the optimization exists.** Add an XML doc comment or a `// PERF:` comment block explaining: what profiling showed, what the allocation was, what the fix is, and what the measured improvement was. Example:
  ```csharp
  // PERF: Uses stackalloc + Span<byte> instead of byte[] to avoid heap allocation.
  // Profiling (dotMemory, 2024-01-15) showed this path allocated 4 KB/request
  // at 50K RPS = 200 MB/s allocation pressure. Fix reduces to 0 B/request.
  ```

- **Run the full test suite.** Span-based optimizations frequently introduce subtle bounds bugs. Ensure unit tests cover: empty input, single-element input, exact-vector-width input, and input sizes that are not powers of two.

- **Check for thread safety regressions.** Pooled objects (`ArrayPool`, `MemoryPool`) introduce reuse -- verify that returned buffers are not accessed after returning them. Use `dotnet-asan` or `Span<T>` "poison" wrappers in test builds.

---

## Output Format

When delivering a C# performance analysis or optimization recommendation, structure the response as follows:

```
## Performance Analysis: [Component Name]

### Profiling Evidence
| Metric                  | Baseline          | Target            | Tool Used        |
|-------------------------|-------------------|-------------------|------------------|
| Allocation rate          | 250 MB/s          | < 10 MB/s         | dotnet-counters  |
| Gen0 collections/sec     | 45/sec            | < 2/sec           | dotnet-counters  |
| P99 latency              | 18 ms             | < 5 ms            | dotTrace         |
| Allocated per request    | 12,400 B          | < 200 B           | BenchmarkDotNet  |
| LOH size                 | 2.1 GB (growing)  | Stable < 50 MB    | dotMemory        |

### Bottleneck Classification
- **Root cause:** [e.g., Repeated string Substring() calls in JSON tokenizer hot path]
- **Category:** Allocation pressure -- string heap churn
- **Impact scope:** Called 500,000x per second under p95 load

### Optimization Plan

| Priority | Technique                  | Expected Gain       | Complexity | Risk    |
|----------|----------------------------|---------------------|------------|---------|
| 1        | ReadOnlySpan<char> slicing | -90% allocations    | Low        | Low     |
| 2        | SearchValues<char>         | -40% CPU in scan    | Low        | Low     |
| 3        | ArrayPool<byte> for buffers| Eliminate LOH churn | Medium     | Medium  |
| 4        | Source-gen serialization   | -60% serial alloc   | Medium     | Low     |

### Implementation

#### Before (problematic pattern)
\`\`\`csharp
// PROBLEM: Substring allocates a new string on every call
public int FindField(string input, int startIndex)
{
    string slice = input.Substring(startIndex);   // heap allocation
    return int.Parse(slice.Split(',')[0]);         // two more allocations
}
\`\`\`

#### After (optimized pattern)
\`\`\`csharp
// PERF: Zero-allocation span-based parsing
// Measured: 0 B/call vs 184 B/call before (BenchmarkDotNet, net8.0)
public int FindField(ReadOnlySpan<char> input, int startIndex)
{
    ReadOnlySpan<char> slice = input[startIndex..];  // no allocation
    int comma = slice.IndexOf(',');
    ReadOnlySpan<char> field = comma >= 0 ? slice[..comma] : slice;
    return int.Parse(field, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
\`\`\`

### BenchmarkDotNet Results

| Method         | Mean      | Error    | StdDev   | Gen0   | Allocated |
|----------------|-----------|----------|----------|--------|-----------|
| FindFieldOld   | 245.3 ns  | 1.42 ns  | 1.33 ns  | 0.0458 | 184 B     |
| FindFieldNew   |  38.7 ns  | 0.28 ns  | 0.26 ns  | -      | 0 B       |
| Ratio          | baseline  |          |          |        | 100%      |
|                | 6.3x faster|         |          |        | 0%        |

### Validation Checklist
- [ ] Unit tests pass for: empty span, single-char, no-comma, exact-buffer-size inputs
- [ ] BenchmarkDotNet baseline committed to repo
- [ ] PERF comment added explaining rationale and measurement date
- [ ] `[MemoryDiagnoser]` confirms 0 B allocated on hot path
- [ ] Load test confirms p99 improvement in staging environment
```

---

## Rules

1. **Never use `string.Substring()` in hot paths.** It allocates a new `string` on every call. Replace with `ReadOnlySpan<char>` slicing (`span[start..end]`). If you must produce a `string` at the boundary, call `span.ToString()` exactly once.

2. **Never benchmark in `Debug` configuration.** The C# compiler and JIT disable optimizations in Debug builds. Always use `dotnet run -c Release` or `dotnet build -c Release` before benchmarking. Results from Debug builds are not only meaningless -- they actively mislead.

3. **Never store a `Span<T>` in a field, async method, or iterator.** The compiler enforces this for `ref struct`, but understand WHY: the `Span<T>` may point to stack memory that is no longer valid after the method returns. Use `Memory<T>` when cross-scope storage is needed.

4. **Never return a rented `ArrayPool<T>` buffer more than once.** Double-return corrupts the pool's free list and causes subsequent callers to receive the same buffer, leading to data corruption that is extremely difficult to debug. Use `IMemoryOwner<T>` and `IDisposable` to enforce single-return semantics.

5. **Never apply `[MethodImpl(MethodImplOptions.AggressiveInlining)]` without verifying with `[DisassemblyDiagnoser]`.** The JIT may refuse to inline even with the hint (e.g., method is too large, contains try/catch, or is a P/Invoke). The attribute does not guarantee inlining -- verify it happened.

6. **Never use `Enum.GetName()`, `Enum.ToString()`, or `.ToString("G")` in hot paths.** These use reflection and allocate. Use a `[GeneratedRegex]`-style source generator, a static `ReadOnlySpan<char>`-indexed lookup table, or the `FastEnum` pattern (switch expression over known values).

7. **Always handle the tail when vectorizing.** Vectorized loops must follow with a scalar tail loop for `data.Length % Vector<T>.Count` remaining elements. Omitting the tail loop is a silent correctness bug that only manifests on inputs not divisible by the vector width.

8. **Never optimize a method that is not in a profiler-identified hot path.** The 80/20 rule is mild in .NET -- typically 1--3% of code paths account for 90%+ of allocation and CPU cost. Optimizing the wrong 97% wastes engineering time and introduces maintenance burden with zero measurable benefit.

9. **Never use `object` pooling (e.g., `ObjectPool<T>`) as a substitute for proper allocation design.** Object pools add complexity and thread-safety overhead. They are appropriate for expensive-to-construct objects (e.g., `StringBuilder`, `MemoryStream`, protocol parsers) but not for small DTOs -- use structs or `Span<T>`-based patterns instead.

10. **Never measure latency with `DateTime.Now` or `Stopwatch` inside the benchmarked code.** Use BenchmarkDotNet exclusively for micro-benchmarks -- it handles warmup, JIT tier transitions, OS scheduling noise, and statistical aggregation. For macro/integration latency, use distributed tracing (OpenTelemetry `ActivitySource`) with percentile histograms, not wall-clock diffs.

---

## Edge Cases

### Large Object Heap Fragmentation
Symptom: `loh-size` growing indefinitely in dotMemory, Gen2 GC frequency spiking, but no object leaks visible. Cause: `byte[]` or `string` allocations >= 85,000 bytes (the LOH threshold) that have long-enough lifetimes to survive at least one GC, fragmenting the LOH address space. Fix: Pool all large buffers with `ArrayPool<byte>.Shared` -- rented arrays stay in pool memory which is already on the LOH and is reused. For strings, avoid large concatenations; use `StringBuilder` with a pooled backing store. If you need LOH compaction once, call `GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce` before `GC.Collect(2, GCCollectionMode.Forced)` in a maintenance window -- do not do this on a hot path.

### `ref struct` Incompatibility with Existing APIs
A method returning `Span<T>` or accepting `Span<T>` cannot implement an interface method, be used as a generic type argument in non-`ref struct` generics, or be stored in a class field. If you need to pass `Span<T>`-based processing results to a component that only accepts `IEnumerable<T>` or `T[]`: create a seam -- have the `Span<T>` processing layer call a callback/delegate with the result rather than returning it, or materialize to a pooled array with explicit lifetime control using `IMemoryOwner<T>`. Do not fight the type system by casting -- it indicates an architectural boundary issue.

### Benchmarking Multi-Core / Concurrent Paths
BenchmarkDotNet defaults to single-threaded execution. If the optimized code is accessed concurrently (e.g., a shared cache, a `ConcurrentQueue<T>`, a channel reader), single-threaded benchmarks will not reveal cache-line contention or lock convoy effects. Use `[ThreadingDiagnoser]` and `[Benchmark]` with `OperationsPerInvoke` set to a batch size, and run the benchmark from multiple threads by using `Parallel.For` inside the benchmark method. Alternatively, use load testing tools (k6, wrk2) against a local server instance for concurrency-sensitive paths.

### Source Generator Conflicts and Incremental Build Issues
When multiple source generators run in the same compilation, they can produce conflicting partial class members or duplicate type names. Symptoms: `CS0101 The namespace already contains a definition` or `CS0111 Member is already defined with same parameter types`. Fix: ensure each generator emits into distinct `partial` methods with unique names, use `hintName` uniqueness in `SourceProductionContext.AddSource()`, and add `#nullable enable` at the top of generated files. Incremental generators must use value equality on their `SyntaxValueProvider` to avoid full re-generation on every keystroke -- verify with the `IncrementalGeneratorInitializationContext` `RegisterSourceOutput` vs `RegisterImplementationSourceOutput` distinction.

### `.NET Framework` vs `.NET 8+` API Availability
`Span<T>` and `Memory<T>` exist in `netstandard2.1` but NOT in `netstandard2.0` or `.NET Framework 4.x`. `SearchValues<T>` is net8+ only. `[GeneratedRegex]` is net7+. `[LoggerMessage]` source generation is net6+. If targeting `netstandard2.0` for library compatibility: use `Microsoft.Bcl.Memory` NuGet package for `Span<T>` and `Memory<T>` backport. Use conditional compilation (`#if NET8_0_OR_GREATER`) to enable advanced paths on modern runtimes while providing a correct (slower) fallback on older targets. Never use `#if` to silently skip correctness -- only to swap implementations.

### SIMD Producing Incorrect Results on Different Hardware
`Vector<T>.Count` varies by CPU: 4 elements for `Vector<int>` on SSE2, 8 on AVX2, 16 on AVX-512. Code that hardcodes a vector width (e.g., `new Vector<int>(data, i)` when `data.Length` was sized for `Count == 8` only) will produce incorrect results on machines with different SIMD widths. Always derive loop bounds dynamically from `Vector<int>.Count`, and always write and test the scalar tail path. Run BenchmarkDotNet on at least two machines with different SIMD capability levels before shipping vectorized code to production.

### GC Interaction with `fixed` Statements and `GCHandle`
When pinning managed memory with `fixed` or `GCHandle.Alloc(obj, GCHandleType.Pinned)` for P/Invoke or unsafe operations, pinned objects block GC heap compaction. If many objects are pinned simultaneously (e.g., in a tight loop with native interop), heap fragmentation increases rapidly. Fix: pin for the minimum possible duration -- enter `fixed`, do the native call, exit `fixed` immediately. For long-duration pinning (e.g., a buffer passed to an async I/O operation), prefer `NativeMemory.Alloc()` (net6+) or `MemoryPool<byte>` backed by pinned managed memory to minimize impact on the GC heap.

### Struct Defensive Copies from `readonly` Violations
The most insidious struct performance bug: a struct field on a `readonly` context (a `readonly` field, an `in` parameter, a `foreach` iteration variable) that has a non-`readonly` method called on it causes the JIT to silently emit a defensive copy of the entire struct before each call. Symptoms: profiling shows unexpected memory writes in what should be a read-only path; performance scales with struct size. Diagnosis: enable Roslyn analyzer `IDE0064` or use `[DisassemblyDiagnoser]` and look for unexpected `lea`/`mov` sequences. Fix: declare the struct `readonly`, or mark methods that do not mutate state as `readonly` methods (`public readonly int ComputeHash() { ... }`).

---

## Example

**User input:** "I have an ASP.NET Core API endpoint that parses incoming CSV-like log lines and extracts three fields. Under load (20K RPS), dotnet-counters shows 180 MB/s allocation rate and Gen0 GC running 80 times per second. The method is called on every request. How do I optimize it?"

---

### Performance Analysis: Log Line Parser Hot Path

#### Profiling Evidence

| Metric                | Baseline           | Target             | Tool Used          |
|-----------------------|--------------------|--------------------|--------------------|
| Allocation rate        | 180 MB/s           | < 5 MB/s           | dotnet-counters    |
| Gen0 collections/sec   | 80/sec             | < 5/sec            | dotnet-counters    |
| Allocated per request  | ~9,000 B           | < 64 B             | BenchmarkDotNet    |
| P95 latency            | 22 ms              | < 4 ms             | dotTrace           |
| Throughput             | 14K RPS (degraded) | 20K+ RPS           | load test          |

#### Bottleneck Classification

- **Root cause:** `string.Split()` called on every log line allocates a `string[]` and N new `string` objects per call. With 20K RPS and average 6 fields per line, this generates ~120,000 string allocations per second.
- **Category:** Allocation pressure -- `string` heap churn causing Gen0 GC storm
- **Impact scope:** Called synchronously on every request in the middleware pipeline

#### Original Problematic Code

```csharp
// PROBLEM: Three allocations per call minimum: Split array + two Substring results
// Under 20K RPS = ~240,000 allocations/sec, ~9 KB/request
public static (string Timestamp, string Level, string Message) ParseLogLine(string line)
{
    var parts = line.Split('|');          // allocates string[] + N string objects
    return (
        parts[0].Trim(),                  // allocates trimmed string
        parts[1].Trim(),                  // allocates trimmed string
        parts[2]                          // references existing string (ok)
    );
}
```

#### Why This Is Expensive

Every `string.Split('|')` allocates:
1. A `string[]` of length N (24 + N * 8 bytes on 64-bit)
2. N new `string` objects, one per segment
3. Each `Trim()` call potentially allocates another `string` if whitespace is present

At 20K RPS with average 5-field lines: approximately 160,000 `string` objects per second hitting the GC.

#### Optimized Implementation

```csharp
// PERF: Zero-allocation log line parser using ReadOnlySpan<char>
// Measured: 0 B/call vs ~9,200 B/call before (BenchmarkDotNet, net8.0, 2024-01-20)
// dotnet-counters alloc-rate: 180 MB/s -> 1.2 MB/s at 20K RPS

public ref struct LogFields
{
    public ReadOnlySpan<char> Timestamp;
    public ReadOnlySpan<char> Level;
    public ReadOnlySpan<char> Message;
}

public static bool TryParseLogLine(
    ReadOnlySpan<char> line,
    out LogFields fields)
{
    fields = default;

    int first = line.IndexOf('|');
    if (first < 0) return false;

    int second = line[(first + 1)..].IndexOf('|');
    if (second < 0) return false;
    second += first + 1; // adjust to absolute index

    fields.Timestamp = line[..first].Trim();
    fields.Level      = line[(first + 1)..second].Trim();
    fields.Message    = line[(second + 1)..];
    return true;
}

// Caller: accept ReadOnlySpan<char> from request body or Pipe reader
// to avoid ever materializing a string if downstream processing can work with spans.
// If a string IS needed for logging/storage, call span.ToString() exactly once at boundary.
```

#### ASP.NET Core Integration Pattern

```csharp
// In the controller or minimal API handler:
// Use PipeReader to read body as ReadOnlySequence<byte>, then process without string allocation
app.MapPost("/ingest", async (HttpContext ctx) =>
{
    var reader = ctx.Request.BodyReader;
    ReadResult result = await reader.ReadAsync();
    ReadOnlySequence<byte> buffer = result.Buffer;

    // Process each line without allocating strings
    foreach (var segment in buffer)
    {
        // MemoryMarshal.Cast to char if UTF-8 encoding is guaranteed ASCII subset
        // Otherwise use a pooled decoder:
        Span<char> charBuffer = stackalloc char[512]; // safe: lines are bounded
        int charCount = Encoding.UTF8.GetChars(segment.Span, charBuffer);
        ReadOnlySpan<char> line = charBuffer[..charCount];

        if (TryParseLogLine(line, out LogFields fields))
        {
            // Process fields -- all are spans, zero allocations
            ProcessFields(fields.Timestamp, fields.Level, fields.Message);
        }
    }

    reader.AdvanceTo(buffer.End);
    return Results.Ok();
});
```

#### BenchmarkDotNet Setup and Results

```csharp
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class LogParserBenchmarks
{
    private readonly string _line = "2024-01-20T10:30:00Z|INFO|User authenticated successfully";

    [Benchmark(Baseline = true)]
    public (string, string, string) ParseWithSplit()
    {
        var parts = _line.Split('|');
        return (parts[0].Trim(), parts[1].Trim(), parts[2]);
    }

    [Benchmark]
    public bool ParseWithSpan()
    {
        return TryParseLogLine(_line.AsSpan(), out _);
    }
}
```

| Method         | Mean      | Error    | StdDev   | Ratio | Gen0   | Allocated | Alloc Ratio |
|----------------|-----------|----------|----------|-------|--------|-----------|-------------|
| ParseWithSplit | 187.4 ns  | 1.12 ns  | 1.05 ns  | 1.00  | 0.0286 | 120 B     | 1.00        |
| ParseWithSpan  |  31.2 ns  | 0.18 ns  | 0.17 ns  | 0.17  | --     | 0 B       | 0.00        |

**Result: 6x faster, 0 bytes allocated per call vs 120 bytes.**

At 20K RPS: allocation rate drops from ~2.4 GB/min to 0 MB/min for this path.

#### Source Generator Addition for Structured Logging

Replace the log write that would re-allocate strings for the logger:

```csharp
// Before: boxes level string, allocates interpolated string
_logger.LogInformation("Parsed log: timestamp={Timestamp} level={Level}", ts, level);

// After: compile-time generated, no boxing, no allocation when log level disabled
[LoggerMessage(Level = LogLevel.Information,
    Message = "Parsed log: timestamp={Timestamp} level={Level}")]
private static partial void LogParsedEntry(
    ILogger logger,
    ReadOnlySpan<char> timestamp,  // net8+: LoggerMessage supports Span params
    ReadOnlySpan<char> level);
```

#### Validation Checklist

- [x] Unit tests cover: empty line, single-field line, no second delimiter, leading/trailing whitespace in fields, Unicode characters in Message field, exactly 512-char line (stack boundary)
- [x] BenchmarkDotNet results committed to `benchmarks/LogParser_net8_results.json`
- [x] `[MemoryDiagnoser]` confirms `0 B` allocated on optimized path
- [x] Load test at 20K RPS in staging: Gen0 GC rate dropped from 80/sec to 3/sec
- [x] `PERF:` comment block added to `TryParseLogLine` with profiling date and before/after numbers
- [x] `dotnet-counters` monitoring confirmed `alloc-rate` dropped from 180 MB/s to 1.2 MB/s (residual from unrelated paths)
- [x] No regressions in integration test suite (`dotnet test -c Release`)

