Yükleniyor...

C# Benchmarking Kullanımı (BenchmarkDotNet)

C#’ta BenchmarkDotNet ile benchmarking yapmayı öğrenin. Performans ölçümü ve kod optimizasyonu için pratik örnekler.

Performans ölçümü (benchmarking), iki veya daha fazla yaklaşımın hız, bellek tüketimi ve ölçeklenebilirlik açısından karşılaştırılmasıdır. .NET dünyasında bu iş için BenchmarkDotNet endüstri standardıdır. Derlemeyi optimize eder, ısınma (warmup) yapar, çoklu tekrarlar çalıştırır, istatistiksel analiz (ortalama, medyan, sapma) üretir ve raporlar. Bu makalede BenchmarkDotNet'in kurulumu, temel kullanımı ve gerçek hayata yönelik püf noktalarını göreceksiniz.


Kurulum

BenchmarkDotNet paketini test/benchmark projenize ekleyin:


dotnet new console -n BenchmarkOrnek
cd BenchmarkOrnek
dotnet add package BenchmarkDotNet

Programı Release modda çalıştırmanız önerilir; BDN bunu otomatik yönetir fakat proje yapılandırmanız güncel olmalıdır.


İlk Benchmark: Basit Karşılaştırma

Aşağıdaki örnek, iki dizge birleştirme yöntemini karşılaştırır. Her benchmark metodu [Benchmark] ile işaretlenir; çalıştırıcı BenchmarkRunner.Run<T>() ile çağrılır.


using System;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class StringBenchmarks
{
    private const int N = 1000;

    [Benchmark]
    public string PlusOperator()
    {
        string s = string.Empty;
        for (int i = 0; i < N; i++)
            s += "x";
        return s;
    }

    [Benchmark]
    public string StringBuilderAppend()
    {
        var sb = new StringBuilder();
        for (int i = 0; i < N; i++)
            sb.Append('x');
        return sb.ToString();
    }
}

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

Çalıştırıldığında BenchmarkDotNet.Artifacts altında detaylı raporlar (.md, .csv, .html) üretir.


Temel Öznitelikler (Attributes)


using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class SearchBench
{
    private int[] _data;

    [Params(1_000, 100_000)]
    public int Size;

    [GlobalSetup]
    public void Setup()
    {
        var rnd = new Random(42);
        _data = new int[Size];
        for (int i = 0; i < Size; i++) _data[i] = rnd.Next();
        Array.Sort(_data);
    }

    [Benchmark(Baseline = true)]
    public bool LinearSearch() => Array.Exists(_data, x => x == 42);

    [Benchmark]
    public bool BinarySearch() => Array.BinarySearch(_data, 42) >= 0;
}

Baseline = true seçeneği, sonuç tablosunda oran (Ratio) hesaplanmasına olanak verir.


Jobs, Runtime ve Ortam Ayarları

Job yapılandırmalarıyla hedef çalışma zamanı, JIT, platform, GC modu gibi ayarlar belirlenebilir.


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net472)]
[MemoryDiagnoser]
public class HashBench
{
    [Benchmark] public int DefaultHash() => "abcdef".GetHashCode();
}

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

Bu yapı tek seferde birden fazla hedef runtime üzerinde kıyaslama yapar (ör. .NET 8.0 vs .NET Framework 4.7.2).


Warmup, Iteration ve Outlier Yönetimi

BenchmarkDotNet, gereksiz gürültüyü azaltmak için otomatik ısınma ve çok sayıda ölçüm iterasyonu yapar. İsterseniz manuel ayarlayabilirsiniz:


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;

[Config(typeof(MyConfig))]
public class MathBench
{
    private class MyConfig : ManualConfig
    {
        public MyConfig()
        {
            AddColumn(BenchmarkDotNet.Columns.StatisticColumn.Mean,
                      BenchmarkDotNet.Columns.StatisticColumn.StdDev);
            AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default);
            AddJob(BenchmarkDotNet.Jobs.Job.Default
                .WithWarmupCount(3)
                .WithIterationCount(10));
        }
    }

    [Benchmark] public double Sqrt() => Math.Sqrt(12345.6789);
}

İstatistiklerde Mean, Median, StdDev, Min, Max gibi değerler yer alır; aykırı değerler filtrelenebilir.


Parametrik Karşılaştırmalar

[Params] ile farklı veri boyutlarını veya stratejileri tek tabloda görebilirsiniz.


using BenchmarkDotNet.Attributes;
using System.Linq;

[MemoryDiagnoser]
public class SumBench
{
    [Params(10, 1000, 100000)]
    public int N;

    private int[] _arr;

    [GlobalSetup]
    public void Setup() => _arr = Enumerable.Range(1, N).ToArray();

    [Benchmark(Baseline = true)]
    public long ForLoop()
    {
        long s = 0;
        for (int i = 0; i < _arr.Length; i++) s += _arr[i];
        return s;
    }

    [Benchmark]
    public long LinqSum() => _arr.Sum(x => (long)x);
}

Sonuçlar, N değerine göre değişimi görmenizi sağlar; mikro-optimizasyon mu yoksa algoritmik fark mı, kolayca anlaşılır.


İleri Teşhis: Disassembly ve Donanım Sayaçları


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;

[DisassemblyDiagnoser(printSource: true, maxDepth: 2)]
public class CryptoBench
{
    [Benchmark] public byte[] GuidNew() => Guid.NewGuid().ToByteArray();
}

Bu teşhisler, sıcak kod yollarındaki gereksiz tahsisleri ve talimat seviyesindeki farkları görmede çok faydalıdır.


Çıktıyı Dışa Aktarma (Exporters)

Sonuçlar Markdown/CSV/HTML olarak dışa alınabilir. CI/CD raporlaması için idealdir.


using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;

public class CsvConfig : ManualConfig
{
    public CsvConfig()
    {
        AddExporter(CsvExporter.Default);
        AddLogger(ConsoleLogger.Default);
    }
}

Sık Yapılan Hatalar ve En İyi Uygulamalar


Gerçek Hayat Örneği: JSON Serileştirme Karşılaştırması

Aşağıdaki örnekte farklı JSON kütüphaneleriyle (örnek amaçlı) serileştirme karşılaştırılır ve bellek tahsisi ölçülür. Hangi kütüphanenin sizin veri modeliniz için daha hızlı ve az tahsis yaptığını net biçimde görebilirsiniz.


using System;
using System.Collections.Generic;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
// Ek: Newtonsoft.Json kullanıyorsanız paketi ekleyip uncomment edebilirsiniz
// using Newtonsoft.Json;

[MemoryDiagnoser]
public class JsonBench
{
    private List<Kisi> _list;

    [Params(100, 10_000)]
    public int Count;

    [GlobalSetup]
    public void Setup()
    {
        _list = new List<Kisi>(Count);
        for (int i = 0; i < Count; i++)
            _list.Add(new Kisi { Ad = "Ada", Soyad = "Lovelace", Yas = 28, Aktif = (i % 2) == 0 });
    }

    [Benchmark(Baseline = true)]
    public string SystemTextJson() => JsonSerializer.Serialize(_list);

    // [Benchmark]
    // public string NewtonsoftJson() => JsonConvert.SerializeObject(_list);
}

public class Kisi
{
    public string Ad { get; set; }
    public string Soyad { get; set; }
    public int Yas { get; set; }
    public bool Aktif { get; set; }
}

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

Rapor, her yöntem için Mean, Error, StdDev, Allocated gibi metrikleri tek tabloda verir. Allocated değerinin düşük olması GC baskısını azaltır ve yoğun iş yüklerinde büyük fark yaratır.


CI/CD Entegrasyonu ve Tekrarlanabilirlik


TL;DR

  • BenchmarkDotNet, güvenilir ve tekrarlanabilir .NET performans ölçümleri için standarttır.
  • [Benchmark], [Params], [MemoryDiagnoser] temel taşlardır.
  • Release/x64 ve gerçekçi veriyle ölçüm yapın; sonucu mutlaka kullanın.
  • Jobs ile birden çok runtime ve GC ayarı karşılaştırın.
  • Raporları CSV/MD/HTML dışa aktararak CI/CD’de izleyin; baseline ile regresyonları yakalayın.

İlişkili Makaleler