BENCHMARKDOTNET NEDİR?

Veri Akademi8 dk okuma
Üç boyutlu performans benchmark çubuk grafiği, üç dikey çubuk ve kronometre simgesi, mor tonlarda

Bir kod parçasının ne kadar sürdüğünü merak edip etrafına bir Stopwatch sarmak yazılım geliştirmenin neredeyse refleksif hareketidir. Sonuca bakarsın, kararı verirsin, devam edersin. Sonra biri aynı kodu farklı makinede çalıştırır, süre tamamen değişir. Hatta aynı makinede ikinci ölçümde bile fark çıkar. Soğuk başlangıç, JIT derlemesi, çöp toplayıcının araya girmesi, işletim sistemi zamanlaması — hepsi sonuca karışır. Mikro ölçekli karşılaştırmalarda Stopwatch'a güvenmek, terazide elini de tutarken tartı yapmaya benzer.

BenchmarkDotNet tam olarak bu kaygan zemini disipline eden bir .NET kütüphanesidir. Karşılaştırılacak metotları işaretlersin, çalıştırırsın; kütüphane warmup turlarını, tekrarlı çalıştırmayı, istatistiksel analizi, izole process'te yürütmeyi ve raporlamayı senin yerine yapar. Çıktıda artık "iki saniye sürdü" değil; "metot A ortalama 12.4 ns, B ortalama 47.1 ns, sapma şu kadar" gibi karşılaştırılabilir sayılar görürsün.

1. Stopwatch Neden Tek Başına Yetmiyor

Manuel ölçümde birden fazla görünmez değişken sonucu kirletir. İlk çalıştırmada JIT henüz metodu derlememiştir, bu yüzden ilk tur diğerlerinden kat be kat yavaş olur. Çöp toplayıcı (GC) ölçüm sırasında devreye girerse aniden milisaniyeler eklenir. CPU başka bir thread için frekansı düşürmüş olabilir. Termal throttling, OS scheduler, hatta antivirüs taraması bile etkiler.

Tek bir Stopwatch.Start / Stop bunlardan hiçbirini ayıklamaz. İki yöntemi yan yana koyup "bu daha hızlıymış" demek için aslında onlarca tur çalıştırıp ortalamayı, sapmayı, aykırı değerleri çıkarmak gerekir. BenchmarkDotNet bu sürecin tamamını otomatize eder.

2. Kütüphanenin Kutusundan Çıkanlar

BenchmarkDotNet, .NET Foundation altında geliştirilen açık kaynak bir projedir. NuGet üzerinden eklenir ve genellikle ayrı bir Console Application projesinde tutulur. Asıl uygulamadan izole çalıştırılır çünkü kütüphane benchmark'ı kendi başlattığı bir alt-process içinde, optimize edilmiş Release modunda yürütür.

Tipik bir kurulum şöyle ilerler:

  • dotnet add package BenchmarkDotNet ile paket eklenir
  • Karşılaştırılacak metotları içeren bir sınıf yazılır
  • Her metoda [Benchmark] attribute'u koyulur
  • Program.cs'te BenchmarkRunner.Run<SinifAdi>() çağrısı yapılır
  • Proje Release modunda çalıştırılır

Bu noktadan sonra kütüphane sırasıyla pilot turu (kaç iterasyon gerektiğini tahmin etme), warmup (JIT'in metotu derlemesini ve cache'in ısınmasını bekleme), gerçek ölçüm ve istatistiksel analiz aşamalarını yürütür. Sonuç hem ekrana hem markdown/CSV/HTML olarak dosyaya yazılır. Projenin resmi belgelerinde tüm attribute'lar ve gelişmiş senaryolar örneklerle anlatılır.

Üç boyutlu üç satırlı sonuç tablosu modeli, kazanan satır vurgulu ve diğerleri yan, mor tonlarda

3. İlk Karşılaştırma: Benchmark Attribute'u

Klasik bir örnek olarak string birleştirme yöntemlerini ele alalım. Beş elemanlı bir diziyi += operatörü ile mi yoksa StringBuilder ile mi birleştirmek daha hızlıdır?

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class StringConcat
{
    private readonly string[] _items = { "a", "b", "c", "d", "e" };

    [Benchmark]
    public string PlusOperator()
    {
        string result = "";
        foreach (var item in _items)
            result += item;
        return result;
    }

    [Benchmark]
    public string WithBuilder()
    {
        var sb = new System.Text.StringBuilder();
        foreach (var item in _items)
            sb.Append(item);
        return sb.ToString();
    }
}

class Program
{
    static void Main() => BenchmarkRunner.Run<StringConcat>();
}

Bu kadarı yeter. dotnet run -c Release komutuyla çalıştırıldığında kütüphane iki metodu kıyaslar ve sonucu tablo halinde verir. Beş elemanlı küçük girdide aslında += operatörü StringBuilder'dan daha hızlı çıkar — çoğu insanın beklediğinin tersi. Çünkü kurulum maliyeti küçük girdide ağır basar. İşte BenchmarkDotNet'in kıymeti tam burada görünür: önyargıları sayıyla yıkar.

4. Aynı Metodu Farklı Parametrelerle Sınamak

Yukarıdaki örnek beş elemanlık bir diziyi ölçüyor. Peki dizi 10, 100, 1000 elemanlı olsa karşılaştırma değişir mi? Burada [Params] attribute'u devreye girer. Bir alana yerleştirilen bu attribute, BenchmarkDotNet'in her parametre değeri için ayrı bir ölçüm turu yürütmesini sağlar.

public class StringConcat
{
    [Params(5, 100, 1000)]
    public int N;

    private string[] _items;

    [GlobalSetup]
    public void Setup()
    {
        _items = new string[N];
        for (int i = 0; i < N; i++) _items[i] = "x";
    }

    [Benchmark]
    public string PlusOperator() { /* ... */ }

    [Benchmark]
    public string WithBuilder() { /* ... */ }
}

Sonuçta her metot her N değeri için ayrı satır olarak raporlanır. Böylece StringBuilder'ın hangi eşikten sonra üstün geldiğini somut görürsün. [GlobalSetup] her tur başına bir kez çalışır; her ölçüm iterasyonundan önce çalışmasını istersen [IterationSetup] kullanılır.

5. Bellek Tahsisinin Ölçülmesi

Hız tek başına yetmez. Bir metot hızlı olabilir ama her çağrıda yığına 50 KB ayırıyorsa yüksek yüklü serviste GC baskısı yaratır. BenchmarkDotNet için [MemoryDiagnoser] attribute'unu sınıfa eklediğinde rapor tablosuna üç sütun daha gelir:

  • Gen0 / Gen1 / Gen2: Her bin operasyon için gerçekleşen GC çağrı sayısı (kuşak bazlı)
  • Allocated: Operasyon başına yığında ayrılan bayt miktarı

İki yöntem aynı hızda görünüyor ama biri 0 bayt ayırıyor, diğeri her çağrıda 240 bayt ayırıyorsa tercih netleşir. Bu görünmez maliyeti yakalamak BenchmarkDotNet'in en değerli kazanımlarından biridir; çünkü .NET geliştiricilerin sıkça karşılaştığı performans sorunlarının önemli bölümü hız değil allocation kaynaklıdır.

Üç boyutlu yığılı bellek blokları, etrafında GC geri dönüşüm oku, heap allocation ölçümü mor tonlarda

6. Çıktı Tablosunu Doğru Okumak

Konsola düşen tablo ilk bakışta yoğun görünür. Önemli sütunlar şunlardır:

  • Mean: Tüm geçerli iterasyonların aritmetik ortalaması — birincil hız göstergesi
  • Error: Ortalamanın güven aralığı (%99.9). Mean ± Error
  • StdDev: Standart sapma. Yüksek değer ölçümde dalgalanma olduğunu söyler
  • Median: Aykırı değerlerden etkilenmemiş orta nokta. Mean ile arasında ciddi fark varsa ölçümde gürültü vardır
  • Ratio: [Baseline = true] ile işaretlediğin metoda oran. "2.31x" yorum yapmayı kolaylaştırır

İki metot karşılaştırılıyorsa sadece Mean'e bakmak yanıltıcıdır. StdDev birinde çok yüksekse o yöntem ortalamada hızlı görünse bile gerçek dünyada öngörülemez davranır. Allocated sıfır olan yöntem uzun vadede çoğu zaman daha sağlıklıdır.

7. BenchmarkDotNet Hangi Durumlarda Yetmez

Kütüphane mikro-benchmark için tasarlanmıştır: nanosaniye-mikrosaniye aralığında metot karşılaştırması. End-to-end senaryo testi, HTTP servisi yük testi veya veritabanı sorgu zamanlaması için doğru araç değildir. Bu tip ölçümlerde k6, JMeter, Gatling gibi yük testi araçları veya dotnet-counters, PerfView gibi profil araçları daha uygundur.

Aynı şekilde async metotlar ölçülebilir ama dikkat gerekir; await edilen network çağrısının süresi BenchmarkDotNet'in kontrolündeki determinizmin dışındadır. Bu durumda kütüphane sana metodun overhead'ini söyler, sistemin tamamının davranışını değil. Pratik senaryolarla benchmarking ve .NET performans optimizasyonunu derinlemesine işleyen .NET test ve performans eğitimi bu sınırları örnek vakalarla ayırır.

Doğru aracı doğru ölçek için kullanmak, performans çalışmasının en az kod yazmak kadar önemli kısmıdır. BenchmarkDotNet "şu metot şundan ne kadar hızlı" sorusunun cevabını verir; "bu sistem dakikada kaç istek kaldırır" sorusunun değil.

Sıkça Sorulan Sorular

BenchmarkDotNet ile basit bir Stopwatch ölçümü arasındaki temel fark nedir?

Stopwatch tek bir çalışmanın geçen süresini verir; warmup, JIT derleme süresi, GC kesintisi, OS scheduler etkisi hepsi bu sayıya karışmıştır. BenchmarkDotNet metodu yüzlerce kez izole bir process'te çalıştırır, istatistiksel analiz yapar ve ortalama, sapma, güven aralığı ile beraber raporlar. Karşılaştırma yapılabilir bir sayı üretir.

Hangi build modunda çalıştırmalıyım?

Mutlaka Release modunda. <code>dotnet run -c Release</code> komutuyla çalıştırılmalı. Debug modunda kütüphane uyarı verir ve raporun başına "results may be misleading" notu ekler. Çünkü Debug'da JIT optimizasyonları kapatılmış olur, ölçülen kod gerçek üretim kodu davranışını yansıtmaz.

İlk çalıştırmadaki sonuçlar neden ikincisinden farklı çıkıyor?

JIT compiler metodu ilk çağrıda derler, sonraki çağrılarda derlenmiş kodu çalıştırır. Ayrıca CPU cache, branch predictor ve TLB ısınır. BenchmarkDotNet bu sebeple ölçüm öncesinde warmup turu yapar ve sayıları o turdan sonra toplamaya başlar. Manuel Stopwatch'ta bu adım yoktur.

MemoryDiagnoser raporundaki Gen0 sütununu nasıl yorumlamalıyım?

Gen0 değeri 1000 operasyon başına gerçekleşen 0. kuşak çöp toplama sayısıdır. Yüksek değer yığında çok kısa ömürlü nesne ayrıldığını gösterir. Performans kritik kodda Allocated sütununun sıfıra yakın olması ve Gen kolonlarının boş kalması idealdir. Allocated 240 B olup ratio yüksekse alternatif yöntem genelde daha sağlıklıdır.

BenchmarkDotNet'i xUnit veya NUnit gibi test framework'ü içinde çalıştırabilir miyim?

Teknik olarak mümkün ama önerilmez. Kütüphane benchmark'ı ayrı bir Console Application içinde, izole alt-process açarak yürütür. Test runner içinden çağırmak ölçümün doğruluğunu zedeler ve test süresini saniyelerden dakikalara çıkarır. Benchmark projesini ayrı tutmak hem CI/CD'de hem bakımda daha temizdir.

Bir HTTP servisinin yük performansını ölçmek için BenchmarkDotNet uygun mu?

Hayır. BenchmarkDotNet mikro-benchmark içindir: tek metodun nanosaniye-mikrosaniye aralığındaki davranışı. HTTP servisin saniyede kaç istek kaldırdığını ölçmek istiyorsan k6, JMeter, Gatling veya wrk gibi yük testi araçları doğru tercihtir. İki araç birbirinin alternatifi değil, farklı katmanın aracıdır.

Params attribute ile kaç farklı değer tanımlamak mantıklıdır?

Genellikle 3-5 değer yeterlidir. Her ek değer toplam ölçüm süresini çarpan halinde uzatır. Eğer eğri davranışı görmek istiyorsan logaritmik artan değerler (10, 100, 1000, 10000) genelde yeterince fikir verir. Ondan fazlasını koymak özel bir araştırma değilse ölçüm süresini gereksiz uzatır.