Kod Performansı Nasıl Ölçülür? (BenchmarkDotNet)
Bir problemi çözmenin genellikle birden fazla yöntemi vardır. Peki hangi yöntem daha iyi sorusuna nasıl cevap vereceğiz?
Öznel yargılar ve yorumların bizim meslekte pek yeri yoktur. Mesela, içime doğdu, ikinci kod daha hızlı çalışacak ya da yazı tura atalım hangisi gelirse artık onu devreye alır ne olacağına bakarız gibi yaklaşımlar sizlerin de takdir edeceği üzere pek anlamlı olmayacaktır.
Öncelikle davranış ve performans olarak kodumuzu ölçmeli, gelen sorulara da kanıtlarımız ile cevap vermeliyiz.
Bir kodun nasıl davranacağını ölçmek için bildiğiniz üzere birim(unit) testleri, entegrasyon(integration) testleri, uçtan uca(E2E) testler gibi kavramlar kullanılmaktadır böylece bu testlerin sonuçlarını kanıt olarak sunabiliriz.
Peki ya performans konusunda nasıl bir yol izleyeceğiz? Mesela bu metot ortalama şu kadar sürede çalışır, şu kadar bellek kullanır gibi bilgilere nasıl ulaşabiliriz?
Jmeter gibi yük testleri bize uç noktaları (endpoint) performans açısından test etme imkanı sunar ancak daha alt seviyede metotların performanslarına dair bir çıktı üretmez. Tam da bu noktada BenchmarkDotNet alt yapısı karşımıza çıkıyor. Araştırma yaparken denk geldiğim bir sorunun cevabında, BenchmarkDotNet(BDN) için micro benchmarking terimi kullanıldığını gördüm yani unit testlerde olduğu gibi olabildiğince küçük parçaların ölçümünü yapmak için kullanılabilecek bir yapısı var.
Örnek olarak bir metin birleştirme işlemi yapıyor olalım. Metin birleştirme işleminin birden çok alternatifi bulunuyor.
- String builder
- $ ile birleştirme (string interpolation)
- + operatörü ile birleştirme
- String format ile birleştirme
- String concat ile birleştirme
Bunlardan hangisini seçeceğiz?
Sonucunda ortaya çıkacak olan değerin doğru hesaplanıp hesaplanmadığını birim test ile ölçebileceğimizi konuştuk, şimdi de BDN ile hangisinin daha verimli olduğunu ölçebiliriz.
Bunun için var olan bir test projemize ya da bu iş için açacağımız yeni projeye aşağıdaki nuget paketini eklememiz yeterli.
https://www.nuget.org/packages/BenchmarkDotNet
Daha sonra bu proje içinde yeni bir sınıf oluşturarak aşağıdaki gibi kodlama yapabiliriz. Ben burada işi yapacak olan kodu da test içerisinde kodladım ancak gerçek dünyada bu sınıf içerisinde daha önce yazdığımız sınıflardaki metotları çağırmamız daha uygun olacaktır.
Testi yapacağımız sınıfın üst kısmına [MemoryDiagnoser] özelliğini ekliyoruz.
GlobalSetup kısmında test başlamadan önce yapılmasını istediğimiz işlemleri yazıyoruz. Bir de GlobalCleanup özelliği var orada ise test sonlandığında yapılmasını istediklerinizi kodlayabilirsiniz.
Test edeceğimiz kodun başına [Benchmark] özelliğini yazıyoruz.
Daha sonra program.cs içerisinde testi başlatacak kodu yazıyoruz.
Son olarak Visual Studio 2022 içinde bulunan Developer Console kısmına gelerek test projemizin konumuna gidiyoruz ve aşağıdaki komutu çalıştırıyoruz.
Böylece test başlıyor. Tamamlandığında ise aşağıdaki gibi bir çıktı ile karşılaşıyoruz.
Burada bizim için önemli olan Method, Mean, StdDev ve Allocated kolonlarıdır. Diğer kolonların detaylı açıklamalarına aşağıdaki bağlantı ile erişebilirsiniz.
Mean : Ortalama çalışma süresi
StdDev : Standart sapma (ne kadar küçük ise o kadar iyi diyebiliriz)
Allocated : Bellek kullanım durumu
Çok sayıda çıktı tipine sahip olan BDN ile eğer bilgisayarınızda R runtime kurulu ise ve sistem değişkenlerinde R_HOME değişkeni tanımlı ise png formatında plot çıktıları da alabiliyoruz.
Bu örnekte görüleceği üzere StringConcat kullanımı bizim için ideal çözüm olabilir. Tabi ki bu kararı vermeden önce problemi detaylı analiz etmek ve test içindeki senaryoları buna göre uyarlamak gerekir.
Özellikle eski kodları yenilerken, daha önce yazdığımız bir kodu refactor ederken hem birim test ile davranışını ölçmek hem de benchmarkdotnet ile yeni kodun eskiye kıyasla ne kadar iyileştiğini ortaya koymak değerli bir çıktı olacaktır.
Yazılım geliştiriciler olarak ortaya koyduğumuz çalışmaların yarattığı değeri aktarmakta zaman zaman zorlanıyoruz. Bu tarz yöntemler ile daha ölçülebilir şekilde ifade etmek elimizi güçlendirecektir.
BONUS
Asıl amacı bu olmasa da Rest vs Grpc olarak yazdığım iki farklı endpoint için bir test yaptım. Tabi ki allocated kısmındaki veri sadece istemci tarafını ölçüyor ancak ortalama cevap süresi bizim için önemli bir gösterge.
Aynı işi yapan Grpc alt yapısı özellikle tek bir kanalı kullandığında 10 kata kadar daha hızlı çalışıyor. Senaryomuza göre eğer mümkün ise iki servisimizi haberleştirirken Grpc kullanmak, bu tabloya bakıldığında daha mantılı görünüyor.
Aynı testi Jmeter ile de yaptım ve Plan dosyalarını repo içerisinde paylaştım. Jmeter ile Grpc testi yapabilmek için aşağıdaki eklentiyi dahil etmeniz gerekiyor.
https://github.com/zalopay-oss/jmeter-grpc-request
Yukarıdaki örneğe ait kodları aşağıdaki repoda bulabilirsiniz.
https://github.com/mehmetalierol/BenchmarkdotnetSample
Uygulama Elastic üzerine loglama ve APM ile takip alt yapılarını da içeriyor. Benchmark çalıştırdığınızda GRPC ve REST uygulamalarının Elastic APM üzerindeki yansımalarını da inceleyebilirsiniz.
Elastic ve ElasticAPM’i deneyimlemek isterseniz aşağıdaki videoda adım adım anlattım :)
Daha detaylı bilgi için: https://benchmarkdotnet.org/articles/guides/getting-started.html
https://github.com/dotnet/BenchmarkDotNet