Minecraft-Console-Client/.skills/dotnet-performance-profiling-and-optimization/references/critical-patterns.md
2026-06-25 23:02:43 +02:00

7.2 KiB

Critical .NET Performance Anti-Patterns

17 patterns that cause deadlocks, order-of-magnitude regressions, or excessive allocations.

Async / Tasks

Never Block on Async (Sync-over-Async)

🔴 AVOID | .NET Core+

public string GetData()
    => GetDataAsync().Result;

public async Task<string> GetDataAsync()
    => await GetDataInternalAsync();

Impact: Deadlocks or thread pool starvation; wastes threads, destroys scalability.

Never Await a ValueTask Multiple Times

🔴 AVOID | .NET Core 2.1+

ValueTask<int> vt = SomeMethodAsync();
int a = await vt;
int b = await vt;

int result = await SomeMethodAsync();

Impact: Undefined behavior — silent data corruption or exceptions.

Memory / Allocation

Use Span<T> / AsSpan Instead of Substring for Slicing

🔴 DO | .NET Core 2.1+

string sub = input.Substring(5, 10);

ReadOnlySpan<char> sub = input.AsSpan(5, 10);

Impact: Eliminates per-slice allocations; 2-4x faster via vectorization.

Use ArrayPool<T> for Temporary Buffers

🔴 DO | .NET Core+

byte[] buf = new byte[4096];

byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
Process(buf);
ArrayPool<byte>.Shared.Return(buf);

Impact: Dramatically reduces GC pressure for buffer-heavy workloads.

Avoid stackalloc in Loops

🔴 AVOID | .NET 5+

for (int i = 0; i < 10_000; i++)
    Span<byte> buf = stackalloc byte[1024];

Span<byte> buf = stackalloc byte[1024];
for (int i = 0; i < 10_000; i++) { Process(buf); }

Impact: StackOverflowException — unrecoverable, no catch possible.

Avoid Boxing Value Types

🔴 AVOID | .NET 6+

string s = string.Format("{0}.{1}", major, minor);

string s = $"{major}.{minor}";

Impact: When replacing string.Format with C# 10+ interpolation, typical improvements are ~40% faster with significantly less allocation. Actual gains vary by call site.

Strings

Use StringComparison.Ordinal for Non-Linguistic Comparisons

🔴 DO | .NET Core+

bool found = text.IndexOf("Content-Type") >= 0;

bool found = text.Contains("Content-Type", StringComparison.Ordinal);

Impact: 2-3x faster; OrdinalIgnoreCase hash codes ~3.3x faster.

Use AsSpan Instead of Substring

🔴 DO | .NET Core 2.1+

int val = int.Parse(str.Substring(5, 3));

int val = int.Parse(str.AsSpan(5, 3));

Impact: Eliminates one string allocation per parse operation.

Regular Expressions

Use Source-Generated Regex [GeneratedRegex]

🔴 ALWAYS use [GeneratedRegex] for all static regex patterns | .NET 7+

private static readonly Regex s_re =
    new(@"\w+@\w+\.\w+", RegexOptions.Compiled);

[GeneratedRegex(@"\w+@\w+\.\w+")]
private static partial Regex EmailRegex();

Impact: Always beneficial or neutral for static patterns — near-zero startup, better throughput, and required for AOT/trimming scenarios.

Avoid Nested Quantifiers (Catastrophic Backtracking)

🔴 AVOID | .NET Core+

var r = new Regex(@"^(\w+)+$");

var r = new Regex(@"^\w+$", RegexOptions.NonBacktracking);

Impact: Can hang process indefinitely on crafted input.

Use TryGetValue Instead of ContainsKey + Indexer

🔴 DO | .NET Core+

if (dict.ContainsKey(key))
    Use(dict[key]);

if (dict.TryGetValue(key, out var value))
    Use(value);

Impact: ~2x faster (50% reduction in lookup time).

Avoid LINQ in Hot Paths

🔴 AVOID | .NET Core+

bool found = items.Any(x => x.Name == target);

bool found = false;
foreach (var item in items)
    if (item.Name == target) { found = true; break; }

Impact: Eliminates 1-3 allocations per call; measurable in tight loops.

Don't Iterate IEnumerable Multiple Times

🔴 AVOID | .NET Core+

foreach (Type t in types) { Validate(t); }
_types = types.ToArray();

Type[] arr = types.ToArray();
foreach (Type t in arr) { Validate(t); }
_types = arr;

Impact: Halves enumeration cost; prevents bugs from re-executing deferred queries.

JSON Serialization

Use System.Text.Json Source Generator

🔴 DO | .NET 6+

string json = JsonSerializer.Serialize(post);

[JsonSerializable(typeof(BlogPost))]
internal partial class AppJsonCtx : JsonSerializerContext { }
string json = JsonSerializer.Serialize(post, AppJsonCtx.Default.BlogPost);

Impact: 37-44% faster; enables trimming and Native AOT.

Cache JsonSerializerOptions

🔴 DO | .NET 5+

JsonSerializer.Serialize(obj, new JsonSerializerOptions());

private static readonly JsonSerializerOptions s_opts = new();
JsonSerializer.Serialize(obj, s_opts);

Impact: Up to 592x slower without caching (.NET 6); always cache or use defaults.

Networking

Reuse HttpClient Instances

🔴 DO | .NET Core 2.1+

using var client = new HttpClient();
await client.GetStringAsync(url);

private static readonly HttpClient s_http = new(new SocketsHttpHandler
{ PooledConnectionLifetime = TimeSpan.FromMinutes(5) });
await s_http.GetStringAsync(url);

Impact: Prevents socket exhaustion; 6-12x faster concurrent HTTPS.

General

Use SearchValues<T> for Repeated Set Searches

🔴 DO | .NET 8+ (works on both targets; .NET 10 adds multi-string overloads)

int pos = text.IndexOfAny("ABCDEF".ToCharArray());

(.NET 8 and .NET 10 — SearchValues<char> is the same API on both)

private static readonly SearchValues<char> s_hex = SearchValues.Create("ABCDEF");
int pos = text.AsSpan().IndexOfAny(s_hex);

(.NET 10 only — multi-string SearchValues<string>)

private static readonly SearchValues<string> s_keywords =
    SearchValues.Create(["error", "warning", "fatal"], StringComparison.OrdinalIgnoreCase);
int pos = log.AsSpan().IndexOfAny(s_keywords);  // SearchValues<string> overload is .NET 9+ BCL

On net8.0 use a SearchValues<char> with the first letter of each keyword and then fall back to string.IndexOf(StringComparison.Ordinal).

Impact: 2-10x faster for chars (both targets); 10-30x faster for multi-string on .NET 10.

Detection

Scan recipes for critical anti-patterns. Run these and report exact counts of issues found in each case.

# .IndexOf(string) without StringComparison (culture-aware, 2-3x slower)
grep -rn --include='*.cs' -E '\.IndexOf\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l

# .Substring( calls (allocates new string — consider AsSpan)
grep -rn --include='*.cs' '\.Substring(' --exclude-dir=bin --exclude-dir=obj . | wc -l

# .StartsWith/.EndsWith without StringComparison (culture-aware, 2-3x slower)
grep -rn --include='*.cs' -E '\.(StartsWith|EndsWith)\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l

# .Contains(string) without StringComparison — NOTE: will also match collection .Contains() calls; filter to string receivers
grep -rn --include='*.cs' -E '\.Contains\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l