Benchmarking¶
Benchmarking, kodun performansını ölçülebilir şekilde analiz eder; yanlış ölçüm yöntemleri yanıltıcı sonuçlara ve hatalı optimizasyona yol açar.
1. Stopwatch ile Güvenilmez Ölçüm¶
❌ Yanlış Kullanım: Manuel Stopwatch ile tek seferlik ölçüm yapmak.
var sw = Stopwatch.StartNew();
var result = SortArray(data);
sw.Stop();
Console.WriteLine($"Süre: {sw.ElapsedMilliseconds}ms");
// JIT warm-up dahil, GC etkisi belirsiz, tek ölçüm istatistiksel anlamsız
✅ İdeal Kullanım: BenchmarkDotNet ile güvenilir ölçüm yapın.
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class SortBenchmarks
{
private int[] _data;
[GlobalSetup]
public void Setup()
{
_data = Enumerable.Range(0, 10000).OrderByDescending(x => x).ToArray();
}
[Benchmark(Baseline = true)]
public int[] ArraySort()
{
var copy = _data.ToArray();
Array.Sort(copy);
return copy;
}
[Benchmark]
public int[] LinqOrderBy()
{
return _data.OrderBy(x => x).ToArray();
}
[Benchmark]
public int[] SpanSort()
{
var copy = _data.ToArray();
copy.AsSpan().Sort();
return copy;
}
}
2. Memory Allocation’ı Ölçmemek¶
❌ Yanlış Kullanım: Sadece süre ölçüp bellek tüketimini görmezden gelmek.
[Benchmark]
public string ConcatStrings()
{
var result = "";
for (int i = 0; i < 1000; i++)
result += i.ToString(); // Her iterasyonda allocation, GC baskısı
return result;
}
✅ İdeal Kullanım: MemoryDiagnoser ile allocation’ı da ölçün.
[MemoryDiagnoser]
public class StringBenchmarks
{
[Benchmark(Baseline = true)]
public string Concatenation()
{
var result = "";
for (int i = 0; i < 1000; i++)
result += i.ToString();
return result;
}
[Benchmark]
public string StringBuilder()
{
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i);
return sb.ToString();
}
[Benchmark]
public string StringCreate()
{
return string.Join("", Enumerable.Range(0, 1000));
}
}
// MemoryDiagnoser allocation miktarını gösterir:
// Concatenation: 2,000,000 B allocated
// StringBuilder: 12,000 B allocated
3. Parametre Varyasyonlarını Test Etmemek¶
❌ Yanlış Kullanım: Tek veri boyutuyla benchmark yapmak.
[Benchmark]
public void SearchList()
{
var list = Enumerable.Range(0, 1000).ToList();
list.Contains(999);
}
✅ İdeal Kullanım: Farklı veri boyutlarını parametrize edin.
[MemoryDiagnoser]
public class SearchBenchmarks
{
[Params(100, 1_000, 10_000, 100_000)]
public int Size { get; set; }
private List<int> _list;
private HashSet<int> _set;
[GlobalSetup]
public void Setup()
{
_list = Enumerable.Range(0, Size).ToList();
_set = new HashSet<int>(_list);
}
[Benchmark(Baseline = true)]
public bool ListContains() => _list.Contains(Size - 1);
[Benchmark]
public bool HashSetContains() => _set.Contains(Size - 1);
}
4. Micro-Benchmark Sonuçlarını Yanlış Yorumlamak¶
❌ Yanlış Kullanım: Nanosaniye farkı olan optimizasyonu production’a uygulamak.
// Benchmark: MethodA 5ns, MethodB 8ns
// "MethodA %37 daha hızlı!" - ama saniyede milyonlarca çağrı yoksa fark edilmez
✅ İdeal Kullanım: Hot path’i belirleyip anlamlı optimizasyona odaklanın.
// Önce profiler ile hot path'i belirleyin
// Sonra o noktayı benchmark'layın
[MemoryDiagnoser]
public class HotPathBenchmark
{
[Benchmark]
public async Task<List<OrderDto>> GetOrders_WithEagerLoading()
{
// Gerçek senaryo: N+1 query sorunu
return await _context.Orders
.Include(o => o.Items)
.Select(o => new OrderDto(o.Id, o.TotalPrice))
.ToListAsync();
}
[Benchmark]
public async Task<List<OrderDto>> GetOrders_WithProjection()
{
// Optimize: Sadece gerekli kolonlar
return await _context.Orders
.Select(o => new OrderDto(o.Id, o.TotalPrice))
.ToListAsync();
}
}
5. Benchmark Ortamını Kontrol Etmemek¶
❌ Yanlış Kullanım: Debug modda benchmark çalıştırmak.
dotnet run # Debug build, JIT optimizasyonları kapalı
✅ İdeal Kullanım: Release modda ve kontrollü ortamda çalıştırın.
// Program.cs
BenchmarkRunner.Run<MyBenchmarks>();
// Terminal
// dotnet run -c Release
// Veya belirli benchmark'ları filtreleyin
BenchmarkRunner.Run<MyBenchmarks>(
DefaultConfig.Instance
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
.AddDiagnoser(MemoryDiagnoser.Default));