Performance review
Skill Sarmkadan/dotnet-senior-skills/skills/performance-review
Review .NET code for allocation pressure, string handling, Span/pooling opportunities, LINQ costs, and caching - with explicit guidance on when performance work is and is not justified. Use when reviewing hot paths, optimizing .NET code, or evaluating performance claims.From its SKILL.md
npx -y skills add Sarmkadan/dotnet-senior-skills --skill performance-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 29 days oldThe repository was created 29 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
4.5 KB, ~1.0k tokens by cl100k_base, as published. Nobody here has run it
Performance Review (.NET)
First: does this code path earn optimization?
Optimize code that is (a) per-request or per-item in a hot loop, and (b) shown hot by a profiler or allocation trace - dotnet-trace, dotnet-counters, PerfView, or a BenchmarkDotNet micro-benchmark for the disputed snippet. Reject performance PRs justified by vibes, and equally reject "premature optimization" as an excuse for gratuitous waste in known-hot paths (serializers, middleware, per-row parsing). Startup code, admin endpoints, and once-a-day jobs get readability, not Spans.
The usual ranking of real wins: eliminate I/O (N+1, chatty HTTP, missing cache) >> reduce allocations >> micro-optimize CPU. A Span<T> refactor is noise next to an uncached per-request database call.
Allocation review flags
- Closures in hot paths: a lambda capturing locals allocates a closure object per call. Use static lambdas with state parameters where the API offers them:
ConcurrentDictionary.GetOrAdd(key, static (k, arg) => Create(k, arg), arg). - LINQ in per-item loops: each chained operator allocates an enumerator/iterator.
items.Where(...).Select(...).ToList()once per request is fine; inside a loop over 100k rows, write theforeach. AlsoAny()on anICollection- use.Count > 0(no enumerator). - params / interface enumeration:
params object[]allocates an array per call (logging!);foreachoverIEnumerable<T>boxes the enumerator when the concrete type's is a struct - iterate the concreteList<T>in hot code. - Boxing: value types passed as
object/non-generic interfaces, string interpolation of structs into loggers. Use structured logging templates -_logger.LogInformation("Order {Id}", id)- which also skip formatting entirely when the level is off; interpolated$"..."pays even when filtered. Wrap expensive log-value computation inif (_logger.IsEnabled(LogLevel.Debug)).
Strings
- Concatenation in a loop is O(n^2):
StringBuilder, orstring.Createwhen the final length is known. - Parsing/slicing hot text:
ReadOnlySpan<char>+span.Slice/IndexOfinstead ofSubstringchains - zero allocations vs one string per slice.int.Parse(span)overloads exist for exactly this. - Case-insensitive compare:
string.Equals(a, b, StringComparison.OrdinalIgnoreCase), nevera.ToLower() == b.ToLower()(two allocations plus culture pitfalls).
Span, Memory, pooling
Span<T>/stackallocfor small (<=1KB) transient buffers in synchronous code.Spancannot live acrossawait; useMemory<T>there.ArrayPool<T>.Shared.Rentfor large transient buffers (I/O, encoding). Always return infinally, never return a buffer you still reference, and remember rented arrays are not cleared and may be oversized - use the length you asked for, not.Length.- Repeated serialization targets:
RecyclableMemoryStreamor pooledIBufferWriter<byte>instead ofnew MemoryStream()per message. - Structs: fine and beneficial small (<= ~16-24 bytes) and readonly; large mutable structs copied through method calls are a deoptimization.
readonly structprevents defensive copies underin.
Collections and data
- Pre-size when count is known:
new List<T>(count),new Dictionary<K,V>(count)- growth is repeated array copies. - Lookup in a loop over another collection: build a
Dictionary/HashSetfirst;list.ContainsinsideWhereis the in-memory N+1. IEnumerable<T>returned and enumerated twice re-executes the pipeline (or the query). Materialize once at the decision point.
Caching (the actual big lever)
IMemoryCachefor per-instance hot reference data; set size limits or explicit expirations - an unbounded cache is a memory leak with a nicer name.- Cache stampede: on expiry of a popular key, N concurrent requests all recompute. .NET 9+
HybridCachehandles this (built-in stampede protection, plus L1/L2); otherwise a per-key semaphore/Lazy<Task<T>>pattern. - Cache DTOs/immutable objects, never tracked EF entities (they capture a disposed context and cross-request state).
Verify, then merge
Any PR claiming a performance improvement includes the before/after evidence: BenchmarkDotNet table for micro, or trace/latency numbers for macro. "Should be faster" is not a review artifact.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.