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

6.8 KiB
Raw Blame History

Memory & String Patterns

Use ReadOnlySpan<byte> for Constant Byte Data

🟡 DO assign constant byte arrays to ReadOnlySpan<byte> | .NET 5+

byte[] data = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F };

ReadOnlySpan<byte> data = [0x48, 0x65, 0x6C, 0x6C, 0x6F];
ReadOnlySpan<int> primes = [2, 3, 5, 7, 11, 13];

Impact: ~100x faster access than static byte[] field, zero allocation.

Use stackalloc for Small Temporary Buffers

🟡 DO use stackalloc for small, fixed-size temporary buffers | .NET Core+

char[] buffer = new char[64];
guid.TryFormat(buffer, out int written);

Span<char> buffer = stackalloc char[64];
guid.TryFormat(buffer, out int written);

Impact: Zero heap allocation, no GC pressure, instant alloc/dealloc.

Use Span.TryWrite for Allocation-Free Interpolation

🟡 DO use MemoryExtensions.TryWrite to format into Span<char> buffers | .NET 6+

string formatted = $"Date: {dt:R}";
destination.Write(formatted);

Span<char> buffer = stackalloc char[64];
buffer.TryWrite($"Date: {dt:R}", out int charsWritten);

Impact: Zero heap allocation for formatting operations.

Use Span.Split() for Zero-Allocation Splitting

🟡 DO use MemoryExtensions.Split for allocation-free string splitting | .NET 10 (or .NET 9) only — NOT available on .NET 8

(allocates string[] — the only built-in option on .NET 8)

string[] parts = input.Split(',');

.NET 10

foreach (Range range in input.AsSpan().Split(','))
{
    ReadOnlySpan<char> segment = input.AsSpan(range);
}

.NET 8 fallback — manual IndexOf loop on the span (no allocation)

ReadOnlySpan<char> remaining = input.AsSpan();
while (!remaining.IsEmpty)
{
    int idx = remaining.IndexOf(',');
    ReadOnlySpan<char> segment = idx < 0 ? remaining : remaining[..idx];
    // ... use segment ...
    remaining = idx < 0 ? default : remaining[(idx + 1)..];
}

Impact: 208 bytes → 0 bytes per split, 2x faster on .NET 10. The manual .NET 8 loop is also zero-allocation but more verbose.

Use UTF8 String Literals (u8 suffix)

🟡 DO use the u8 suffix for compile-time UTF8 ReadOnlySpan<byte> | .NET 7+

byte[] header = Encoding.UTF8.GetBytes("Content-Type");

ReadOnlySpan<byte> header = "Content-Type"u8;

Impact: 17ns → 0.006ns — eliminates runtime transcoding entirely.

Use ReadOnlySpan<char> Pattern Matching with switch

🟡 DO use switch on ReadOnlySpan<char> for allocation-free string matching | C# 11+

switch (attr.Value.Trim()) { case "preserve": /* ... */ break; }

switch (attr.Value.AsSpan().Trim())
{
    case "preserve": return Preserve;
    case "default": return Default;
}

Impact: Eliminates string allocation from Trim() in switch-based dispatch.

Use params ReadOnlySpan<T> to Eliminate Array Allocations

🟡 DO add params ReadOnlySpan<T> overloads to library methods | .NET 10 (or .NET 9) only — requires C# 13

(the only option on .NET 8 — accept the array allocation, or add explicit 1/2/3-argument overloads)

public static void Log(params string[] messages) { /* ... */ }
Log("Starting", "Processing", "Done");

.NET 10

public static void Log(params ReadOnlySpan<string> messages) { /* ... */ }
Log("Starting", "Processing", "Done");

.NET 8 fallback — keep params string[] and add fixed-arity overloads for the hot common cases

public static void Log(string m) { /* ... */ }
public static void Log(string m1, string m2) { /* ... */ }
public static void Log(string m1, string m2, string m3) { /* ... */ }
public static void Log(params string[] messages) { /* fallback for 4+ args */ }

Impact: Eliminates params array allocation on .NET 10. On .NET 8 fixed-arity overloads cover the hot 13 argument cases.

Avoid Chained String-Returning Operations

🟡 AVOID chains of 3+ string-returning method calls that each allocate intermediates | .NET Core+

Pattern 1: Chained .Replace() calls

string result = input.Replace("a", "b").Replace("c", "d").Replace("e", "f");

var sb = new StringBuilder(input.Length);
// single pass replacing all patterns

Pattern 2: Chained Regex.Replace() calls

public static string Underscore(this string input) =>
    Regex3.Replace(Regex2.Replace(Regex1.Replace(input, "$1_$2"), "$1_$2"), "_").ToLower();

return string.Create(totalLength, state, (span, s) => { /* write directly */ });

Pattern 3: += string concatenation in loops

string result = "";
foreach (var part in parts)
    result += separator + part;

var sb = new StringBuilder();
foreach (var part in parts)
    sb.Append(separator).Append(part);
return sb.ToString();

Impact: Eliminates N-1 intermediate string allocations per chain. For += in loops, eliminates O(n²) total allocation.

Cache char.ToString() for Known Character Sets

🟡 DO cache char.ToString() results when the set of characters is small and known | .NET Core+

return symbol.ToString();

foreach (var prefix in UnitPrefixes)
    input = input.Replace(prefix.Value.Name, prefix.Key.ToString());

private static readonly FrozenDictionary<char, string> s_charStrings =
    new Dictionary<char, string>
    {
        ['k'] = "k", ['M'] = "M", ['G'] = "G",
    }.ToFrozenDictionary();

return s_charStrings[symbol];

Impact: Eliminates one string allocation per char.ToString() call. Significant when called in loops or on hot paths.

Detection

Scan recipes for memory and string anti-patterns. Run these and report exact counts.

# .ToLower()/.ToUpper() without culture parameter (allocates + culture-sensitive)
grep -rn --include='*.cs' -E '\.(ToLower|ToUpper)\(\)' --exclude-dir=bin --exclude-dir=obj . | wc -l

# Chained .Replace( calls (3+ on one line — intermediate string allocations)
grep -rn --include='*.cs' '\.Replace(.*\.Replace(.*\.Replace(' --exclude-dir=bin --exclude-dir=obj . | wc -l

# params in method signatures (array allocation per call)
grep -rn --include='*.cs' 'params ' --exclude-dir=bin --exclude-dir=obj . | wc -l

# LINQ on strings — .All/.Any on IEnumerable<char> (replace with foreach loop)
grep -rn --include='*.cs' -E '\.(All|Any)\(char\.' --exclude-dir=bin --exclude-dir=obj . | wc -l

Patterns Requiring Manual Review

  • Boxing via string.Format: Can't determine argument types from grep — needs type analysis
  • += string concatenation in loops: += matches all types (int, list, event, string) — needs type context to confirm string
  • char.ToString(): Requires knowing the variable type is char — not reliably greppable