C# / .NET/06advanced12 min

Wydajność i czysty kod w .NET

Czysty kod i wydajność nie stoją w sprzeczności — czytelny kod łatwiej zoptymalizować, a świadome wybory wydajnościowe nie muszą go zaciemniać. Ta lekcja zbiera praktyki, których pilnujemy w serwisach .NET ProfessNet.

Mierz, zanim optymalizujesz

Nie zgaduj — profiluj. Mikrooptymalizacje w kodzie, który nie jest wąskim gardłem, to strata czasu i czytelności.

Standard ProfessNet: optymalizujemy na podstawie pomiaru (profiler, BenchmarkDotNet), nie przeczuć. Czytelność > sprytna sztuczka, dopóki pomiar nie wskaże inaczej.

Unikaj zbędnych alokacji

W gorących ścieżkach uważamy na alokacje na stercie — to one obciążają GC.

// źle — string concat w pętli alokuje za każdym razem
var report = "";
foreach (var p in probes)
    report += p.Host + "\n";

// dobrze — StringBuilder
var sb = new StringBuilder();
foreach (var p in probes)
    sb.Append(p.Host).Append('\n');
var report = sb.ToString();

Dla parsowania bez kopiowania używamy Span<T> / ReadOnlySpan<char>:

ReadOnlySpan<char> span = line.AsSpan();
var forest = span[..span.IndexOf(',')];   // brak alokacji nowego stringa

Async streaming — IAsyncEnumerable

Dużych zbiorów nie wczytujemy całych do pamięci. Strumieniujemy je.

public async IAsyncEnumerable<Probe> StreamProbesAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var probe in _repository.QueryAsync(ct))
    {
        yield return probe;
    }
}

LINQ świadomie

LINQ jest czytelny, ale iteruje kolekcję — nie materializuj wielokrotnie i nie wołaj .Count() po IEnumerable, jeśli masz .Count na liście.

// źle — podwójna iteracja (Where + Any)
if (probes.Where(p => p.IsActive).Any()) { }

// dobrze — jedno przejście
if (probes.Any(p => p.IsActive)) { }

// materializuj raz, jeśli używasz wyniku wielokrotnie
var active = probes.Where(p => p.IsActive).ToList();

Czysty kod — krótkie metody, jasne nazwy

// źle — metoda robi za dużo, magiczne liczby
public void P(Probe x)
{
    if (x.S == 0 && x.T > 1440) { /* ... */ }
}

// dobrze — wyrażająca intencję
private const int StaleAfterMinutes = 1440;

public void MarkStaleIfNeeded(Probe probe)
{
    if (probe.IsOffline && probe.MinutesSinceLastSeen > StaleAfterMinutes)
    {
        probe.MarkStale();
    }
}
PraktykaZysk
StringBuilder w pętlimniej alokacji
Span<T> przy parsowaniubrak kopii stringów
IAsyncEnumerablestały narzut pamięci
jedno przejście LINQmniej iteracji
nazwane stałebrak magicznych liczb

Wskazówka: sealed na klasach, które nie są dziedziczone, daje JIT-owi okazję do optymalizacji wywołań wirtualnych — i dokumentuje intencję.


Mierz, zanim zoptymalizujesz; tnij alokacje w gorących ścieżkach; strumieniuj duże dane; pisz krótkie, nazwane metody. Wydajny .NET to suma świadomych, drobnych decyzji — nie heroicznych przepisywań na końcu projektu.