Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

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!

📅 Timeline - Performance w .NET
  • .NET Framework - podstawowe narzędzia (Stopwatch, GC.GetTotalMemory)
  • 2013 - 🔥 BenchmarkDotNet - profesjonalny benchmarking!
  • C# 7.2 (2017) - 🔥 Span<T>, Memory<T> - zero-copy!
  • .NET Core 2.1+ (2018+) - Performance improvements everywhere
  • Roslyn (2015+) - Code analyzers, live suggestions
Benchmarking z BenchmarkDotNet

Dlaczego BenchmarkDotNet?

// ❌ 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)

Roslyn analyzers - third-party

// Popular analyzers:

// 1. StyleCop.Analyzers - code style
// dotnet add package StyleCop.Analyzers

// 2. Roslynator - 500+ analyzers + refactorings
// dotnet add package Roslynator.Analyzers

// 3. SonarAnalyzer - code quality, bugs, vulnerabilities
// dotnet add package SonarAnalyzer.CSharp

// 4. Meziantou.Analyzer - best practices
// dotnet add package Meziantou.Analyzer

// Live suggestions w IDE:
public async Task<string> GetDataAsync()
{
    var response = await httpClient.GetAsync(url);
    //                                             ^ Suggestion: Add ConfigureAwait(false)
    
    return await response.Content.ReadAsStringAsync();
}

EditorConfig - enforce style

// .editorconfig - project-wide style
[*.cs]

# Indentation
indent_style = space
indent_size = 4

# File-scoped namespaces
csharp_style_namespace_declarations = file_scoped:warning

# var preferences
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion

# Expression-bodied members
csharp_style_expression_bodied_methods = when_on_single_line:suggestion

# Pattern matching
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion

# Null checking
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion

# Build enforces these rules! ✨

Performance profiling tools

// Narzędzia do performance profiling:

// 1. dotnet-counters - real-time metrics
// dotnet tool install -g dotnet-counters
// dotnet counters monitor --process-id [PID]

// 2. dotnet-trace - collect traces
// dotnet tool install -g dotnet-trace
// dotnet trace collect --process-id [PID]

// 3. Visual Studio Profiler
// Debug → Performance Profiler
// - CPU Usage
// - Memory Usage
// - .NET Async
// - Database

// 4. JetBrains dotMemory / dotTrace
// - Memory profiling
// - CPU profiling
// - Timeline analysis

// 5. PerfView - Microsoft tool
// - CPU sampling
// - GC analysis
// - EventSource tracing
Podsumowanie

  • BenchmarkDotNet - precyzyjne, powtarzalne benchmarki
  • Memory optimization - struct, ArrayPool, ObjectPool
  • Span<T> - zero-copy slicing, stackalloc, ZERO allocations
  • StringBuilder - używaj w pętlach, nie += operator
  • Async best practices:
    • ConfigureAwait(false) w bibliotekach
    • ValueTask<T> dla hot paths
    • NIGDY async void
    • Parallel.ForEachAsync dla concurrent
  • Code analyzers - Roslyn, StyleCop, EditorConfig
  • Profiling tools - dotnet-counters, Visual Studio, PerfView

Następny wpis: Podsumowanie kursu i przyszłość C#!