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();
}
}
| Praktyka | Zysk |
|---|---|
StringBuilder w pętli | mniej alokacji |
Span<T> przy parsowaniu | brak kopii stringów |
IAsyncEnumerable | stały narzut pamięci |
| jedno przejście LINQ | mniej iteracji |
| nazwane stałe | brak magicznych liczb |
Wskazówka:
sealedna 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.