BenchmarkDotNet Kütüphanesi ile .Net Kodunu Benchmark Etmek

BenchmarkDotNet, .NET uygulamalarımızda kodlarımızın performansını ölçebileceğimiz açık kaynak, lightweight  bir kütüphanedir.

Bu kütüphane ile aynı işi yapan farklı kod parçalarını, metotları birbirine karşı kıyaslayabiliriz.

Neden benchmarking yapmalıyız?

  • Projelerimizde kullanacağımız teknolojilere, alacağımız sonuçlarla karar verebiliriz. 
  • Kodlarda refactoring yapacağımız noktaları kolay kestirebiliriz.
  • Performansın düşük olduğu kodlarda iyileştirmeler yapabiliriz. 
  • Alacağımız ölçüm metriklerine göre bellek yönetimini yapabiliriz. 

Bu yazıda farklı senaryolarla kütüphane kullanımını anlatacağım.

Paketin, projeye dahil edilmesi

.NET / .NET Core Console Application projesi oluşturduktan sonra BenchmarkDotNet paketini Nuget Package Manager ile veya NuGet Package Manager Console ile aşağıdaki kodu kullanarak indirebiliriz.

Install-Package BenchmarkDotNet

İlgili Nuget paketi: NuGet Gallery | BenchmarkDotNet

Örneğimde, farklı iki ORM aracı ile aynı sayıdaki ürün listesini getiren metotları karşılaştırıp, kütüphanenin özelliklerine değineceğiz. Dapper ve Entity Framework Core, karşılaştırılacak araçlardır.  

Benchmark işlemi için bir sınıf oluşturduk. Sınıf içerisinde bütün ürünleri hem Dapper hem de Entity Framework Core ile listeleyecek metotların üzerinde  Benchmark attribute işaretlendi. 

Ayrıca listeleme metotlarının bulunduğu servislerin instance işlemleri Setup metodu ile tamamlanmış olup GlobalSetup attribute ile işaretlendi. GlobalSetup ile belirtilen metotta benchmark işleminden önce yapılacak işlemler, tanımlamalar belirtilir. 

    public class DapperEFCore
    { 
        private IProductService _productServiceDapper;
        private IProductService _productServiceEFCore;
        
        private DapperProductDal dapper=new DapperProductDal();
        private EfProductDal efCore = new EfProductDal();
        
        [GlobalSetup]
        public void Setup()
        {
            _productServiceEFCore = new ProductManager(efCore);
            _productServiceDapper = new ProductManager(dapper);           
        }  
 
        [Benchmark(Description = "Benchmark for Dapper ORM")]
        public List<Product> DapperGetAllProducts() => _productServiceDapper.GetAll().Data;
 
        [Benchmark(Description = "Benchmark for Entitiy Framework Core")]
        public List<Product> EFCoreGetAllProducts() => _productServiceEFCore.GetAll().Data;
    }

Dapper ve EF Core ORM'lerinin kullanıldığı GetAll metotları aşağıdaki gibi belirtilmiştir.

//EF Core - Set metodu

public List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null)
{
    using (var dbContext = new TContext())
    {
        return filter == null ?
            dbContext.Set<TEntity>().ToList() :
            dbContext.Set<TEntity>().Where(filter).ToList();
    }
}
 
//Dapper - Dapper.Contrib - GetAll metodu

public List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null)
{
    using (var connection = DbConnect.Connection)
    {
        return filter == null ?
          connection.GetAll<TEntity>().ToList() :
          connection.GetAll<TEntity>().Where(filter.Compile()).ToList();
    }
}
Diğer aşama ise Benchmark çalıştırmayı sağlayacak metot için BenchmarkRunner sınıfındaki Run metodunu kullanıp, benchmark sınıfımızı vermemiz gerekir. 
public class Program
{
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<DapperEFCore>();
        Console.ReadLine();
    }
}
Gerekli tanımlamalar yapıldıktan sonra konsol uygulamamızı çalıştırarak testimizi yapabiliriz. Dikkat edilecek önemli bir konu, uygulamamızı Release olarak derlememiz gerekir. Debug'da çalıştırırsak aşağıdaki gibi hata alacağız.



Release olarak çalıştırdığımızdan emin olduktan sonra benchmark işlemininin özetini aşağıdaki görselde görebiliyoruz. (Summary alanı)


Sonucu kısaca yorumlayacak olursak; 356.0 micro saniye > 641.6 micro saniye, yani Dapper ORM ile yapılan sorgu, EF Core ORM'e göre daha hızlı sonuç getirmiştir. 

Legends kısmında başlıkların anlamlarını görebiliriz. Metot başlıkları, Benchmark atrributedeki Description ile atanmıştı. 


Farklı bir senaryo olarak; ProductId değeri 20'den büyük olan ürünleri filtrelemek için parametreli metot kullanacağız ve id değerini parametre olarak geçeceğiz. Arguments attributesi, metoda parametre olarak gönderilecek değeri yazmamızı sağlar. 
[Benchmark(Description = "Benchmark for Dapper ORM")]
[Arguments(20)]
public List<Product> DapperGetAllProducts(int productId) => _productServiceDapper.GetAll(x => x.ProductId > productId).Data;
 
[Benchmark(Description = "Benchmark for Entitiy Framework Core")]
[Arguments(20)]
public List<Product> EFCoreGetAllProducts(int productId) => _productServiceEFCore.GetAll(x => x.ProductId > productId).Data;
 
productId verdikten sonra aldığımız sonuç aşağıdaki gibidir.


Params attributesine verdiğimiz değerlerle farklı durumları da test edebiliriz. ProductId'si 1,10,20,50 den büyük olan ürünlerin listelerini getirecek işlemleri yapabiliriz. 
[Params(1, 10, 25, 50)]
public int productId;               
 
[Benchmark(Description = "Benchmark for Dapper ORM")]       
public List<Product> DapperGetAllProducts() => _productServiceDapper.GetAll(x => x.ProductId > productId).Data;
 
[Benchmark(Description = "Benchmark for Entitiy Framework Core")]
public List<Product> EFCoreGetAllProducts() => _productServiceEFCore.GetAll(x => x.ProductId > productId).Data;
Dört durum ve iki ORM aracı için toplam 8 (4x2) benchmark yapılacaktır. Benchmark başlarken bulunan adet bilgisini aşağıdaki görselden görebilirsiniz.


Sonuç listesini aşağıdaki görselde inceleyebilirsiniz. Senaryolar, farklı renklendirmelerle belirtilmiştir.


Bellek yönetimi konusunda ölçüm yapabilmek adına aşağıdaki paketi de projemize dahil etmemiz gerekiyor.

        Install-Package BenchmarkDotNet.Diagnostics.Windows

Paketi ekledikten sonra MemoryDiagnoser ekleyeceğiz. BenchmarkRunner çalışmasını sağlayacak yapılandırma tanımlamalarımızı yapmak için ManualConfig sınıfından miras alacak DapperEFCoreConfig sınıfı oluşturduk. 

Oluşturulduktan sonra ilgili benchmark sınıfımıza, Config attributesi üzerinden tanımlandı. Bu tanımlama ile bu benchmarkın, oluşturduğumuz config ayarları ile çalışacağını belirttik.
public class DapperEFCoreConfig : ManualConfig
{
    public DapperEFCoreConfig()
    {
        AddDiagnoser(MemoryDiagnoser.Default);
        Add(DefaultConfig.Instance);
    }
}
 
[Config(typeof(DapperEFCoreConfig))]
public class DapperEFCore
{
 
    //piece of code
AddDiagnoser metoduna verdiğimiz MemoryDiagnoser.Default, yeni bir MemoryDiagnoser sınıfı oluşturmak demektir. Aşağıdaki görselde konuyu özetleyen bilgi mevcuttur.


Config oluşturmanın farklı bir versiyonu olarak Run metodunun overload metodunda kullanımı aşağıdaki gibidir. Config attribute kullanmadan da seçenek bulunuyor.

public class Program
{
    public static void Main(string[] args)
    {
        var config = new ManualConfig();
        config.AddDiagnoser(MemoryDiagnoser.Default);
        config.Add(DefaultConfig.Instance);
 
        BenchmarkRunner.Run<DapperEFCore>(config);
    }
}

Bellek kullanımını Allocated başlığında görebileceksiniz. Ekran görüntüsünden, Dapper ile 40 KB kullanım olduğunu yorumlayabilirsiniz. 


Konsol uygulamamızdaki sonuçlar, bin - Release - net5.0 - BenchmarkDotNet.Artifacts klasörüne de kaydediliyor. txt uzantılı tarihe göre isimlendirilen dosyalar oluşturuluyor. result klasöründe ise farklı dosya uzantılarında sonuçlar kaydediliyor.


Özetle, BenchmarkDotNet paketiyle kodlarımızda performans karşılaştırması yapıp, optimizasyon sürecimize yön verebiliriz. 

Keyifli ve faydalı olmasını umarak, çalışmalarınızda kolaylıklar diliyorum...

Yararlanılan kaynaklar

Yorumlar