Minecraft-Console-Client/.skills/dotnet-performance-profiling-and-optimization/references/critical-patterns.md

289 lines
7.2 KiB
Markdown
Raw Normal View History

2026-06-25 23:02:43 +02:00
# 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+
```csharp
public string GetData()
=> GetDataAsync().Result;
```
```csharp
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+
```csharp
ValueTask<int> vt = SomeMethodAsync();
int a = await vt;
int b = await vt;
```
```csharp
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+
```csharp
string sub = input.Substring(5, 10);
```
```csharp
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+
```csharp
byte[] buf = new byte[4096];
```
```csharp
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+
```csharp
for (int i = 0; i < 10_000; i++)
Span<byte> buf = stackalloc byte[1024];
```
```csharp
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+
```csharp
string s = string.Format("{0}.{1}", major, minor);
```
```csharp
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+
```csharp
bool found = text.IndexOf("Content-Type") >= 0;
```
```csharp
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+
```csharp
int val = int.Parse(str.Substring(5, 3));
```
```csharp
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+
```csharp
private static readonly Regex s_re =
new(@"\w+@\w+\.\w+", RegexOptions.Compiled);
```
```csharp
[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+
```csharp
var r = new Regex(@"^(\w+)+$");
```
```csharp
var r = new Regex(@"^\w+$", RegexOptions.NonBacktracking);
```
**Impact: Can hang process indefinitely on crafted input.**
### Use TryGetValue Instead of ContainsKey + Indexer
🔴 **DO** | .NET Core+
```csharp
if (dict.ContainsKey(key))
Use(dict[key]);
```
```csharp
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+
```csharp
bool found = items.Any(x => x.Name == target);
```
```csharp
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+
```csharp
foreach (Type t in types) { Validate(t); }
_types = types.ToArray();
```
```csharp
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+
```csharp
string json = JsonSerializer.Serialize(post);
```
```csharp
[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+
```csharp
JsonSerializer.Serialize(obj, new JsonSerializerOptions());
```
```csharp
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+
```csharp
using var client = new HttpClient();
await client.GetStringAsync(url);
```
```csharp
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)
```csharp
int pos = text.IndexOfAny("ABCDEF".ToCharArray());
```
✅ (.NET 8 and .NET 10 — `SearchValues<char>` is the same API on both)
```csharp
private static readonly SearchValues<char> s_hex = SearchValues.Create("ABCDEF");
int pos = text.AsSpan().IndexOfAny(s_hex);
```
✅ (.NET 10 only — multi-string `SearchValues<string>`)
```csharp
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.
```bash
# .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
```