Performance to kluczowy aspekt profesjonalnego kodu! W 2015 roku optymalizacja była często "czarną magią". W 2026 roku masz BenchmarkDotNet do precyzyjnych pomiarów, Span<T> dla zero-allocation code, i Code Analyzers które podpowiadają best practices!
// ❌ Naiwny benchmarking - NIEPRECYZYJNY
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
MyMethod();
}
sw.Stop();
Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");
// Problemy:
// - JIT compilation (pierwszy run wolniejszy)
// - GC może włączyć się losowo
// - CPU może throttle
// - No warmup, no statistical analysis
// - Wyniki NIEPOWTARZALNE!
BenchmarkDotNet - profesjonalny benchmarking
🎉 BenchmarkDotNet
Precyzyjne, powtarzalne benchmarki! Warmup, statistics, memory allocations, wiele runtimes! ✨
// Install: dotnet add package BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser] // Pokaż memory allocations
public class StringBenchmarks
{
private const int N = 10000;
[Benchmark]
public string StringConcatenation()
{
string result = "";
for (int i = 0; i < N; i++)
{
result += "a"; // String concatenation
}
return result;
}
[Benchmark]
public string StringBuilderAppend()
{
var sb = new StringBuilder();
for (int i = 0; i < N; i++)
{
sb.Append("a"); // StringBuilder
}
return sb.ToString();
}
}
// Run benchmarks
class Program
{
static void Main()
{
BenchmarkRunner.Run<StringBenchmarks>();
}
}
// dotnet run -c Release
BenchmarkDotNet - wyniki
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|-------------------- |------------:|-----------:|-----------:|--------:|--------:|----------:|
| StringConcatenation | 1,234.5 ms | 12.34 ms | 11.56 ms | 150000 | 50000 | 500 MB |
| StringBuilderAppend| 2.3 ms | 0.05 ms | 0.04 ms | 1000 | 500 | 2 MB |
// StringBuilder 500x szybsze! 250x mniej alokacji! ✨
BenchmarkDotNet - attributes
// Params - testuj różne wartości
[MemoryDiagnoser]
public class ArrayBenchmarks
{
[Params(10, 100, 1000, 10000)]
public int N;
[Benchmark]
public int[] CreateArray()
{
return new int[N];
}
[Benchmark]
public List<int> CreateList()
{
return new List<int>(N);
}
}
// Arguments - pass arguments do benchmark
[Benchmark]
[Arguments(100)]
[Arguments(1000)]
public void ProcessData(int count)
{
for (int i = 0; i < count; i++)
{
// Process
}
}
// Baseline - porównaj inne do baseline
[Benchmark(Baseline = true)]
public void OldImplementation() { }
[Benchmark]
public void NewImplementation() { }
// Wyniki pokażą: "1.5x faster than baseline"
Memory Allocation Optimization
Problem - heap allocations
// ❌ Dużo alokacji na heap
public void ProcessData()
{
for (int i = 0; i < 1000000; i++)
{
var user = new User { Id = i }; // 1 million allocations!
ProcessUser(user);
}
// GC musi posprzątać 1 million obiektów! 😱
}
// Problem:
// - Alokacje na heap = GC pressure
// - GC pause = application freeze
// - Performance degradation
Struct zamiast class - stack allocation
// ✅ Struct - stack allocation (jeśli małe, <16 bytes idealnie)
public struct Point
{
public int X;
public int Y;
}
public void ProcessPoints()
{
for (int i = 0; i < 1000000; i++)
{
var point = new Point { X = i, Y = i }; // Stack allocation - ZERO GC! ✨
ProcessPoint(point);
}
}
// Zasady struct:
// ✅ Małe (idealnie <16 bytes)
// ✅ Immutable
// ✅ Value semantics (kopiowanie, nie reference)
// ❌ NIE dla dużych obiektów (kopiowanie kosztowne)
ArrayPool - reuse arrays
// ❌ Alokuj nowy array za każdym razem
public void ProcessData()
{
var buffer = new byte[4096]; // Alokacja!
ReadData(buffer);
ProcessBuffer(buffer);
// buffer jest GC'owany
}
// ✅ ArrayPool - reuse arrays, zero allocations!
using System.Buffers;
public void ProcessDataOptimized()
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096); // Pożycz z pool
try
{
ReadData(buffer);
ProcessBuffer(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer); // Zwróć do pool
}
}
// Zalety:
// ✅ Zero allocations (reuse)
// ✅ Zero GC pressure
// ✅ Używane w ASP.NET Core, Kestrel
Object pooling
// Object pooling - reuse expensive objects
using Microsoft.Extensions.ObjectPool;
// Definiuj policy
public class StringBuilderPooledObjectPolicy : IPooledObjectPolicy<StringBuilder>
{
public StringBuilder Create() => new StringBuilder();
public bool Return(StringBuilder obj)
{
obj.Clear(); // Reset state
return true;
}
}
// Użycie
var pool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());
public string BuildString()
{
var sb = pool.Get(); // Pożycz z pool
try
{
sb.Append("Hello ");
sb.Append("World");
return sb.ToString();
}
finally
{
pool.Return(sb); // Zwróć do pool
}
}
Span<T> w praktyce
Problem - substring allocations
// ❌ Substring tworzy NOWY string (alokacja!)
string data = "Hello World from C#";
string hello = data.Substring(0, 5); // Alokacja!
string world = data.Substring(6, 5); // Alokacja!
string csharp = data.Substring(17, 2); // Alokacja!
// 3 nowe stringi = 3 alokacje na heap!
Span<T> - zero-copy slicing!
🎉 C# 7.2 - Span<T>
Zero-copy view do pamięci! Slice bez alokacji! Stack-only type! ✨
// ✅ Span<T> - zero allocations!
string data = "Hello World from C#";
ReadOnlySpan<char> span = data.AsSpan();
ReadOnlySpan<char> hello = span.Slice(0, 5); // ZERO allocation! ✨
ReadOnlySpan<char> world = span.Slice(6, 5); // ZERO allocation!
ReadOnlySpan<char> csharp = span.Slice(17, 2); // ZERO allocation!
// Span = view do oryginalnego stringa, nie kopiuje!
// Porównanie
if (hello.SequenceEqual("Hello"))
{
Console.WriteLine("Match!");
}
Span<T> - parsing bez allocations
// ❌ Tradycyjne parsing - dużo alokacji
string csv = "123,456,789,101112";
string[] parts = csv.Split(','); // Alokuje array!
int[] numbers = new int[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
numbers[i] = int.Parse(parts[i]); // Każdy element osobno
}
// ✅ Span<T> parsing - ZERO alokacji!
ReadOnlySpan<char> csv = "123,456,789,101112".AsSpan();
Span<int> numbers = stackalloc int[4]; // Stack allocation!
int index = 0;
while (!csv.IsEmpty)
{
int commaIndex = csv.IndexOf(',');
ReadOnlySpan<char> part = commaIndex == -1
? csv
: csv.Slice(0, commaIndex);
numbers[index++] = int.Parse(part);
csv = commaIndex == -1
? ReadOnlySpan<char>.Empty
: csv.Slice(commaIndex + 1);
}
// ZERO heap allocations! ✨
Span<T> - stackalloc
// stackalloc - alokuj na stack (nie heap!)
Span<int> numbers = stackalloc int[100]; // Stack allocation!
for (int i = 0; i < 100; i++)
{
numbers[i] = i * 2;
}
// ProcessNumbers(numbers); // Pass jako Span<int>
// Zalety:
// ✅ ZERO heap allocations
// ✅ ZERO GC pressure
// ✅ Automatycznie cleaned up (stack)
// Ograniczenia:
// ❌ Max rozsądny rozmiar ~1KB (stack overflow risk)
// ❌ Nie może escape method (stack-only)
Memory<T> - heap-safe Span
// Problem: Span<T> nie może być w async methods
public async Task ProcessDataAsync()
{
Span<int> numbers = stackalloc int[100]; // ❌ BŁĄD - Span w async!
await Task.Delay(100);
}
// ✅ Memory<T> - heap-safe, async-safe
public async Task ProcessDataAsync()
{
Memory<int> memory = new int[100]; // OK w async
await Task.Delay(100);
Span<int> span = memory.Span; // Convert do Span gdy potrzebne
ProcessSpan(span);
}
// Memory<T> = heap-allocated, ale ma Span API
StringBuilder vs String Interpolation
String concatenation - kiedy co?
// Benchmark: String concatenation
[MemoryDiagnoser]
public class StringConcatBenchmarks
{
[Benchmark]
public string PlusOperator()
{
string result = "";
for (int i = 0; i < 100; i++)
{
result += "a"; // ❌ WORST - każda += tworzy NOWY string
}
return result;
}
[Benchmark]
public string StringInterpolation()
{
string result = "";
for (int i = 0; i < 100; i++)
{
result = $"{result}a"; // ❌ Też źle - równie wolne
}
return result;
}
[Benchmark]
public string StringConcat()
{
var parts = new string[100];
for (int i = 0; i < 100; i++)
{
parts[i] = "a";
}
return string.Concat(parts); // ✅ Lepsze - jedna alokacja
}
[Benchmark]
public string StringBuilderMethod()
{
var sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
sb.Append("a"); // ✅ BEST - internal buffer
}
return sb.ToString();
}
}
// Wyniki:
// PlusOperator: 5000 ns, 5000 B allocated ❌
// StringInterpolation: 5100 ns, 5000 B allocated ❌
// StringConcat: 500 ns, 200 B allocated ✅
// StringBuilder: 300 ns, 100 B allocated ✅✅
Kiedy używać czego?
// ✅ String interpolation - małe, jednorazowe
string message = $"Hello {name}, you are {age} years old";
// OK - jedna operacja, czytelne
// ✅ StringBuilder - pętla, wiele appendów
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
sb.Append(", ");
}
string result = sb.ToString();
// ✅ string.Concat - znana liczba stringów
string result = string.Concat(firstName, " ", lastName);
// ✅ string.Join - kolekcja
string csv = string.Join(",", numbers);
// ❌ NIE - += w pętli
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i; // ❌ 1000 alokacji!
}
// Zasada: jeśli >2-3 operacje w pętli → StringBuilder
Modern string interpolation (C# 10+)
// C# 10 - interpolated string handler (performance!)
StringBuilder sb = new StringBuilder();
// ✅ String interpolation bezpośrednio do StringBuilder
sb.Append($"Hello {name}"); // Optimized - nie tworzy intermediate string!
// ❌ Przed C# 10:
sb.Append($"Hello {name}");
// 1. Tworzy string "Hello John"
// 2. Append tego stringa do sb
// = 1 niepotrzebna alokacja
// ✅ C# 10+:
sb.Append($"Hello {name}");
// Bezpośrednio append do sb - ZERO intermediate string! ✨
Async Best Practices
ConfigureAwait(false) - biblioteki
// Library code - ZAWSZE ConfigureAwait(false)
public async Task<string> GetDataAsync()
{
// ❌ Bez ConfigureAwait
var response = await httpClient.GetAsync(url);
// Kontynuuje na ORIGINAL context (np. UI thread) - niepotrzebne!
// ✅ Z ConfigureAwait(false)
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
// Kontynuuje na THREAD POOL thread - szybsze!
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return content;
}
// Zasada:
// ✅ Library code: ZAWSZE ConfigureAwait(false)
// ❌ UI code (WPF, WinForms): NIE ConfigureAwait (potrzebny UI thread)
// ❌ ASP.NET Core: ConfigureAwait niepotrzebny (no SynchronizationContext)
ValueTask<T> - hot path optimization
// Problem: Task<T> alokuje nawet gdy synchronous
public async Task<User> GetUserAsync(int id)
{
if (_cache.TryGetValue(id, out var user))
{
return user; // Synchronous path - ale alokuje Task<User>!
}
return await _database.GetUserAsync(id);
}
// ✅ ValueTask<T> - zero allocation dla sync path!
public async ValueTask<User> GetUserAsync(int id)
{
if (_cache.TryGetValue(id, out var user))
{
return user; // ZERO allocation! ✨
}
return await _database.GetUserAsync(id);
}
// Kiedy ValueTask:
// ✅ Często synchronous completion (cache, pool)
// ✅ Hot path (wywołane bardzo często)
// ❌ Nie dla wszystkich async methods (overhead dla async paths)
Async void - NIGDY (except event handlers)
// ❌ async void - DANGEROUS
public async void ProcessDataAsync() // ❌ NIE!
{
await Task.Delay(100);
throw new Exception(); // UNOBSERVED - crash aplikacji!
}
// ✅ async Task
public async Task ProcessDataAsync() // ✅ OK
{
await Task.Delay(100);
throw new Exception(); // Exception propaguje do callera
}
// Wyjątek: event handlers
button.Click += async (s, e) => // ✅ OK - event handler
{
await LoadDataAsync();
};
// Zasada: NIGDY async void, ZAWSZE async Task (except event handlers)
Parallel.ForEachAsync - concurrent processing
// ❌ Sekwencyjne async - WOLNE
foreach (var url in urls)
{
await ProcessUrlAsync(url); // Jeden po drugim! 😱
}
// ✅ Task.WhenAll - wszystkie równolegle
var tasks = urls.Select(url => ProcessUrlAsync(url));
await Task.WhenAll(tasks); // Wszystkie na raz! ✨
// ✅ Parallel.ForEachAsync (.NET 6+) - z kontrolą parallelism
await Parallel.ForEachAsync(urls, new ParallelOptions
{
MaxDegreeOfParallelism = 10 // Max 10 równolegle
},
async (url, ct) =>
{
await ProcessUrlAsync(url, ct);
});
Code Analyzers i Roslyn
Built-in analyzers
// .NET SDK ma built-in analyzers
// Enable w .csproj
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
// Przykłady warnings:
// CA1806: Do not ignore method results
var user = GetUser(1);
user.ToString(); // ⚠️ Result ignored
// CA1031: Do not catch general exception types
try { }
catch (Exception) { } // ⚠️ Too general
// CA1822: Mark members as static
public string GetConstant() => "CONSTANT"; // ⚠️ Can be static
// CA2007: Consider calling ConfigureAwait
await httpClient.GetAsync(url); // ⚠️ Missing ConfigureAwait (libraries)