
dotnet-performance-patterns
Optimizing .NET allocations/throughput. Span, ArrayPool, ref struct, sealed, stackalloc.
- Installs
- 117
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-performance-patterns- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads dotnet-performance-patterns automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
dotnet-performance-patterns
Performance-oriented architecture patterns for .NET applications. Covers zero-allocation coding with Span<T> and Memory<T>, buffer pooling with ArrayPool<T>, struct design for performance (readonly struct, ref struct, in parameters), sealed class devirtualization by the JIT, stack-based allocation with stackalloc, and string handling performance. Focuses on the why (performance rationale and measurement) rather than the how (language syntax).
Version assumptions: .NET 8.0+ baseline. Span<T> and Memory<T> are available from .NET Core 2.1+ but this skill targets modern usage patterns on .NET 8+.
Out of scope: C# language syntax for Span, records, pattern matching, and collection expressions -- see [skill:dotnet-csharp-modern-patterns]. Coding standards and naming conventions (including sealed class style guidance) -- see [skill:dotnet-csharp-coding-standards]. Microbenchmarking setup and measurement is owned by this epic's companion skill -- see [skill:dotnet-benchmarkdotnet]. Native AOT compilation pipeline and trimming -- see [skill:dotnet-native-aot]. Serialization format performance tradeoffs -- see [skill:dotnet-serialization]. Architecture patterns (caching, resilience, DI) -- see [skill:dotnet-architecture-patterns]. EF Core query optimization -- see [skill:dotnet-efcore-patterns].
Cross-references: [skill:dotnet-benchmarkdotnet] for measuring the impact of these patterns, [skill:dotnet-csharp-modern-patterns] for Span/Memory syntax foundation, [skill:dotnet-csharp-coding-standards] for sealed class style conventions, [skill:dotnet-native-aot] for AOT performance characteristics and trimming impact on pattern choices, [skill:dotnet-serialization] for serialization performance context.
Span<T> and Memory<T> for Zero-Allocation Scenarios
Why Span<T> Matters for Performance
Span<T> provides a safe, bounds-checked view over contiguous memory without allocating. It enables slicing arrays, strings, and stack memory without copying. For syntax details see [skill:dotnet-csharp-modern-patterns]; this section focuses on performance rationale.
Zero-Allocation String Processing
// BAD: Substring allocates a new string on each call
public static (string Key, string Value) ParseHeader_Allocating(string header)
{
var colonIndex = header.IndexOf(':');
return (header.Substring(0, colonIndex), header.Substring(colonIndex + 1).Trim());
}
// GOOD: ReadOnlySpan<char> slicing avoids all allocations
public static (ReadOnlySpan<char> Key, ReadOnlySpan<char> Value) ParseHeader_ZeroAlloc(
ReadOnlySpan<char> header)
{
var colonIndex = header.IndexOf(':');
return (header[..colonIndex], header[(colonIndex + 1)..].Trim());
}
Performance impact: for high-throughput parsing (HTTP headers, log lines, CSV rows), Span-based parsing eliminates GC pressure entirely. Measure with [MemoryDiagnoser] in [skill:dotnet-benchmarkdotnet] -- the Allocated column should read 0 B.
Memory<T> for Async and Storage Scenarios
Span<T> cannot be used in async methods or stored on the heap (it is a ref struct). Use Memory<T> when you need to:
- Pass buffers to async I/O methods
- Store a slice reference in a field or collection
- Return a memory region from a method for later consumption
public async Task<int> ReadAndProcessAsync(Stream stream, Memory<byte> buffer)
{
var bytesRead = await stream.ReadAsync(buffer);
var data = buffer[..bytesRead]; // Memory<T> slicing -- no allocation
return ProcessData(data.Span); // .Span for synchronous processing
}
private int ProcessData(ReadOnlySpan<byte> data)
{
var sum = 0;
foreach (var b in data)
sum += b;
return sum;
}
ArrayPool<T> for Buffer Pooling
Why Pool Buffers
Large array allocations (>= 85,000 bytes) go directly to the Large Object Heap (LOH), which is only collected in Gen 2 GC -- expensive and causes pauses. Even smaller arrays add GC pressure in hot paths. ArrayPool<T> rents and returns buffers to avoid repeated allocations.
Usage Pattern
using System.Buffers;
public int ProcessLargeData(Stream source)
{
var buffer = ArrayPool<byte>.Shared.Rent(minimumLength: 81920);
try
{
var bytesRead = source.Read(buffer, 0, buffer.Length);
// IMPORTANT: Rent may return a larger buffer than requested.
// Always use bytesRead or the requested length, never buffer.Length.
return ProcessChunk(buffer.AsSpan(0, bytesRead));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
// clearArray: true zeroes the buffer -- use when buffer held sensitive data
}
}
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
Using buffer.Length instead of requested size | Processes uninitialized bytes beyond actual data | Track requested/actual size separately |
| Forgetting to return the buffer | Pool exhaustion, falls back to allocation | Use try/finally or a using wrapper |
| Returning a buffer twice | Corrupts pool state | Null out the reference after return |
| Not clearing sensitive data | Security leak from pooled buffers | Pass clearArray: true to Return |
readonly struct, ref struct, and in Parameters
readonly struct -- Defensive Copy Elimination
The JIT must defensively copy non-readonly structs when accessed via in, readonly fields, or readonly methods to prevent mutation. Marking a struct readonly guarantees immutability, eliminating these copies:
// GOOD: readonly eliminates defensive copies on every access
public readonly struct Point3D
{
public double X { get; }
public double Y { get; }
public double Z { get; }
public Point3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);
// readonly struct: JIT knows this cannot mutate, no defensive copy needed
public double DistanceTo(in Point3D other)
{
var dx = X - other.X;
var dy = Y - other.Y;
var dz = Z - other.Z;
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
}
Without readonly, calling a method on a struct through an in parameter forces the JIT to copy the entire struct to protect against mutation. For large structs in tight loops, this eliminates significant overhead.
ref struct -- Stack-Only Types
ref struct types are constrained to the stack. They cannot be boxed, stored in fields, or used in async methods. This enables safe wrapping of Span<T>:
public ref struct SpanLineEnumerator
{
private ReadOnlySpan<char> _remaining;
public SpanLineEnumerator(ReadOnlySpan<char> text) => _remaining = text;
public ReadOnlySpan<char> Current { get; private set; }
public bool MoveNext()
{
if (_remaining.IsEmpty)
return false;
var newlineIndex = _remaining.IndexOf('\n');
if (newlineIndex == -1)
{
Current = _remaining;
_remaining = default;
}
else
{
Current = _remaining[..newlineIndex];
_remaining = _remaining[(newlineIndex + 1)..];
}
return true;
}
}
in Parameters -- Pass-by-Reference Without Mutation
Use in for large readonly structs passed to methods. The in modifier passes by reference (avoids copying) and prevents mutation:
// in parameter: pass by reference, no copy, no mutation allowed
public static double CalculateDistance(in Point3D a, in Point3D b)
=> a.DistanceTo(in b);
When to use in:
| Struct Size | Recommendation |
|---|---|
| <= 16 bytes | Pass by value (register-friendly, no indirection overhead) |
| > 16 bytes | Use in to avoid copy overhead |
| Any size, readonly struct | in is safe (no defensive copies) |
| Any size, non-readonly struct | Avoid in (defensive copies negate the benefit) |
Sealed Class Performance Rationale
JIT Devirtualization
When a class is sealed, the JIT can replace virtual method calls with direct calls (devirtualization) because no subclass override is possible. This enables further inlining:
// Without sealed: virtual dispatch through vtable
public class OpenService : IProcessor
{
public virtual int Process(int x) => x * 2;
}
// With sealed: JIT devirtualizes + inlines Process call
public sealed class SealedService : IProcessor
{
public int Process(int x) => x * 2;
}
public interface IProcessor { int Process(int x); }
Verify devirtualization with [DisassemblyDiagnoser] in [skill:dotnet-benchmarkdotnet]. See [skill:dotnet-csharp-coding-standards] for the project convention of defaulting to sealed classes.
Performance Impact
Devirtualization + inlining eliminates:
- vtable lookup -- indirect memory access to find the method pointer
- Call overhead -- the actual indirect call instruction
- Inlining barrier -- virtual calls cannot be inlined; sealed methods can
In tight loops and hot paths, the cumulative effect is measurable. For framework/library types that are not designed for extension, always prefer sealed.
stackalloc for Small Stack-Based Allocations
When to Use stackalloc
stackalloc allocates memory on the stack, avoiding GC entirely. Use for small, fixed-size buffers in hot paths:
public static string FormatGuid(Guid guid)
{
// 68 bytes on the stack -- well within safe limits
Span<char> buffer = stackalloc char[68];
guid.TryFormat(buffer, out var charsWritten, "D");
return new string(buffer[..charsWritten]);
}
Safety Guidelines
| Guideline | Rationale |
|---|---|
| Keep allocations small (< 1 KB typical, < 4 KB absolute maximum) | Stack space is limited (~1 MB default on Windows); overflow crashes the process |
| Use constant or bounded sizes only | Runtime-variable sizes risk stack overflow from malicious/unexpected input |
Prefer Span<T> assignment over raw pointer | Span provides bounds checking; raw pointers do not |
| Fall back to ArrayPool for large/variable sizes | Gracefully handle cases that exceed stack budget |
Hybrid Pattern: stackalloc with ArrayPool Fallback
public static string ProcessData(ReadOnlySpan<byte> input)
{
const int stackThreshold = 256;
char[]? rented = null;
Span<char> buffer = input.Length <= stackThreshold
? stackalloc char[stackThreshold]
: (rented = ArrayPool<char>.Shared.Rent(input.Length));
try
{
var written = Encoding.UTF8.GetChars(input, buffer);
return new string(buffer[..written]);
}
finally
{
if (rented is not null)
ArrayPool<char>.Shared.Return(rented);
}
}
This pattern is used throughout the .NET runtime libraries and is the recommended approach for methods that handle both small and large inputs.
String Interning and StringComparison Performance
String Comparison Performance
Ordinal comparisons are significantly faster than culture-aware comparisons because they avoid Unicode normalization:
// FAST: ordinal comparison (byte-by-byte)
bool isMatch = str.Equals("expected", StringComparison.Ordinal);
bool containsKey = dict.ContainsKey(key); // Dictionary<string, T> uses ordinal by default
// FAST: case-insensitive ordinal (no culture overhead)
bool isMatchIgnoreCase = str.Equals("expected", StringComparison.OrdinalIgnoreCase);
// SLOW: culture-aware comparison (Unicode normalization, linguistic rules)
bool isMatchCulture = str.Equals("expected", StringComparison.CurrentCulture);
Default guidance: Use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase for internal identifiers, dictionary keys, file paths, and protocol strings. Reserve culture-aware comparison for user-visible text sorting and display.
String Interning
The CLR interns compile-time string literals automatically. string.Intern() can reduce memory for runtime strings that repeat frequently:
// Intern frequently-repeated runtime strings to share a single instance
var normalized = string.Intern(headerName.ToLowerInvariant());
Caution: Interned strings are never garbage collected. Only intern strings from a bounded, known set (HTTP headers, XML element names). Never intern user input or unbounded data.
Efficient String Building
| Scenario | Recommended Approach | Why |
|---|---|---|
| 2-3 concatenations | String interpolation $"{a}{b}" | Compiler optimizes to string.Concat |
| Loop concatenation | StringBuilder | Avoids quadratic allocation |
| Known fixed parts | string.Create | Single allocation, Span-based writing |
| High-throughput formatting | Span<char> + TryFormat | Zero-allocation formatting |
// string.Create for single-allocation building
public static string FormatId(int category, int item)
{
return string.Create(11, (category, item), static (span, state) =>
{
state.category.TryFormat(span, out var catWritten);
span[catWritten] = '-';
state.item.TryFormat(span[(catWritten + 1)..], out _);
});
}
Performance Measurement Checklist
Before applying any optimization pattern, measure first. Premature optimization without data leads to complex code with no measurable benefit.
- Identify the hot path -- use [skill:dotnet-benchmarkdotnet] to establish a baseline
- Measure allocations -- enable
[MemoryDiagnoser]and check theAllocatedcolumn - Apply one pattern at a time -- change one thing, re-measure, compare to baseline
- Check AOT impact -- if targeting Native AOT ([skill:dotnet-native-aot]), verify patterns are trim-safe
- Verify with production-like data -- synthetic benchmarks can miss real-world allocation patterns
- Document the tradeoff -- every optimization trades readability or flexibility for speed; record the measured gain
Agent Gotchas
- Measure before optimizing -- never apply Span/ArrayPool/stackalloc without a benchmark showing the allocation or latency problem. Premature optimization produces unreadable code for no measurable gain.
- Do not use stackalloc with variable sizes from untrusted input -- stack overflow crashes the process with no exception handler. Always validate bounds or use the hybrid stackalloc/ArrayPool pattern.
- Always mark value types
readonly structwhen they are immutable -- withoutreadonly, the JIT generates defensive copies on everyinparameter access andreadonlyfield access, silently negating the performance benefit of using structs. - Return rented ArrayPool buffers in finally blocks -- forgetting to return starves the pool and causes fallback allocations that negate the benefit.
- Use
StringComparison.Ordinalfor internal comparisons -- omitting the comparison parameter defaults to culture-aware comparison, which is slower and produces surprising results for technical strings (file paths, identifiers). - Sealed classes help performance only when the JIT can see the concrete type -- if the object is accessed through an interface variable in a non-devirtualizable call site, sealing provides no benefit. Verify with
[DisassemblyDiagnoser]. - Do not re-teach language syntax -- reference [skill:dotnet-csharp-modern-patterns] for Span/Memory syntax details. This skill focuses on when and why to use these patterns for performance.
Knowledge Sources
Performance patterns in this skill are grounded in guidance from:
- Stephen Toub -- .NET Performance blog series (devblogs.microsoft.com/dotnet/author/toub). Authoritative source on Span<T>, ValueTask, ArrayPool, async internals, and runtime performance characteristics.
- Stephen Cleary -- Async best practices and concurrent collections guidance. Author of Concurrency in C# Cookbook.
- Nick Chapsas -- Modern .NET performance patterns and benchmarking methodology.
These sources inform the patterns and rationale presented above. This skill does not claim to represent or speak for any individual.
FAQ
What does the dotnet-performance-patterns skill do?
Optimizing .NET allocations/throughput. Span, ArrayPool, ref struct, sealed, stackalloc.
How do I install dotnet-performance-patterns?
Run npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-performance-patterns in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/dotnet-performance-patterns/ (personal) or .claude/skills/dotnet-performance-patterns/ (project).
Which AI agents can use dotnet-performance-patterns?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is dotnet-performance-patterns safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains dotnet-performance-patterns?
It is published by wshaddix in wshaddix/dotnet-skills. It has 117 recorded installs.
More from wshaddix/dotnet-skills
dotnet-xunit
wshaddix/dotnet-skills
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
dotnet-testing-strategy
wshaddix/dotnet-skills
Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.
modern-csharp-coding-standards
wshaddix/dotnet-skills
Write modern, high-performance C code using records, pattern matching, value objects, async/await, Span /Memory , and best-practice API design patterns. Emphasizes functional-style programming with C 12+ features. Use when writing new C code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.
csharp-wolverinefx
wshaddix/dotnet-skills
Build .NET applications with WolverineFX for messaging, HTTP services, and event sourcing. Use when implementing command handlers, message handlers, HTTP endpoints with WolverineFx.HTTP, transactional outbox patterns, event sourcing with Marten, CQRS architectures, cascading messages, batch message processing, or configuring transports like RabbitMQ, Azure Service Bus, or Amazon SQS.
testcontainers
wshaddix/dotnet-skills
Patterns for using Testcontainers in .NET integration tests to spin up real dependencies like databases and message queues. Use when writing integration tests that require real databases, testing with message brokers like RabbitMQ or Kafka, or isolating test dependencies with Docker containers.
microsoft-extensions-dependency-injection
wshaddix/dotnet-skills
Dependency injection patterns and best practices using Microsoft.Extensions.DependencyInjection for .NET applications. Use when configuring DI containers in .NET, choosing between service lifetimes (Singleton, Scoped, Transient), or implementing decorator patterns and service interception.
Popular backend & apis skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
entra-app-registration
microsoft/azure-skills
Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.
google-agents-cli-adk-code
google/agents-cli
This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).
embedded-captions
heygen-com/hyperframes
Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.
entra-agent-id
microsoft/azure-skills
Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).
neon
neondatabase/agent-skills
Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.