DEV Community

Cover image for C# exception performansı: .NET 10 ölçümü
Hüseyin Bozdemir
Hüseyin Bozdemir

Posted on Originally published at huseyinbozdemir.com on

C# exception performansı: .NET 10 ölçümü

"Exception pahalıdır, akış kontrolünde kullanma." Bu cümleyi ilk kez bir söyleşide duymuştum; arkasından gelen öneri de belliydi: hata durumunda exception fırlatma, sonuç döndür, yani Result pattern. Ama "ne kadar pahalı" sorusuna kimse sayı vermiyordu, ben de ölçtüm.

Kısa cevap: try/catch bloğunun kendisi neredeyse bedava. Pahalı olan exception fırlatmak: .NET 10'da tek bir throw ve catch, aynı durumu dönüş değeriyle bildirmekten yaklaşık 1.300 kat yavaş. Yine de mutlak değer mikrosaniye düzeyinde; hata seyrekse bunu hissetmezsin.

Ölçüm düzeneği

BenchmarkDotNet 0.15.8, .NET 10.0.1, Windows 11, AMD Ryzen 7 5700X. Ölçümün özü şu iki metot:

[Benchmark(Baseline = true)]
public int SonucDon() => SonucIle(Derinlik).Kod;

[Benchmark]
public int FirlatYakala()
{
    try { return Firlat(Derinlik); }
    catch (InvalidOperationException) { return -1; }
}
Enter fullscreen mode Exit fullscreen mode

Derinlik, hatanın kaç metot aşağıda oluştuğunu belirliyor (1 ve 10). JIT ara metotları birleştirip sonucu çarpıtmasın diye hepsi [MethodImpl(MethodImplOptions.NoInlining)] ile işaretli. Ölçüm kodunun tamamı ve ham sonuçlar GitHub'da.

try/catch performansı düşürür mü?

Hata hiç oluşmadığında:

Süre
try/catch yok 0,24 ns
try/catch var 0,91 ns

Fark bir nanosaniyenin altında. .NET'te try bloğu her çalıştığında ek bir iş yapılmaz; hangi kod aralığının hangi catch'e ait olduğu bir tabloda durur ve bu tabloya ancak exception fırlatıldığında bakılır. Kalan 0,7 ns, bir veritabanı sorgusunun ya da HTTP isteğinin yanında ölçülemeyecek kadar küçük.

Exception fırlatmanın maliyeti

Hata oluştuğunda tablo değişiyor:

Hatanın derinliği Sonuç döndürmek throw + catch Ayrılan bellek
1 metot 1,1 ns 1.491 ns 320 B
10 metot 11,2 ns 3.820 ns 1.336 B

Exception fırlatıldığında runtime bir nesne oluşturur, uygun catch'i bulmak için yığını yürür, stack trace bilgisini toplar ve aradaki her çerçeveyi geri sarar. Derinlik arttıkça bu iş büyüyor: 10 seviyede süre 2,5 katına, ayrılan bellek 4 katına çıkıyor.

Bu sayılar eskisinden iyi. .NET 9'da CoreCLR'ın exception işleme altyapısı yeniden yazıldı ve Microsoft'un mikro ölçümlerinde 2-4 kat hızlandı. Eski bir yazıda gördüğün rakamlar bugün için fazla karamsar olabilir.

Parse mı, TryParse mı?

Gündelik hayattaki karşılığı geçersiz girdi. "12a4" gibi bir metni sayıya çevirmeye çalışınca:

// 1.708 ns, 480 B
try { return int.Parse(girdi); }
catch (FormatException) { return -1; }

// 8 ns, bellek ayırmıyor
return int.TryParse(girdi, out var sayi) ? sayi : -1;
Enter fullscreen mode Exit fullscreen mode

Yaklaşık 200 kat fark. Kullanıcıdan, dosyadan ya da bir HTTP isteğinden gelen girdinin hatalı olması beklenen bir durum; orada TryParse hem hızlı hem de niyeti açık.

Exception yerine Result döndürmek

Söyleşide önerilen yol buydu. En sade hali:

public readonly record struct Sonuc<T>(T? Deger, string? Hata)
{
    public bool Basarili => Hata is null;
    public static Sonuc<T> Ok(T deger) => new(deger, null);
    public static Sonuc<T> Fail(string hata) => new(default, hata);
}

public Sonuc<Siparis> Getir(int id) =>
    _depo.Bul(id) is { } siparis
        ? Sonuc<Siparis>.Ok(siparis)
        : Sonuc<Siparis>.Fail("Sipariş bulunamadı");
Enter fullscreen mode Exit fullscreen mode

Bedeli de var: çağıran taraf sonucu kontrol etmek zorunda. Kontrolü unutursa hata sessizce kaybolur; exception'ın iyi yanı tam olarak bunu unutmana izin vermemesi.

Ne zaman önemli?

Mutlak sayıya bak: 1,5 µs. Saniyede 1.000 exception fırlatan bir servis bunun için saniyede yaklaşık 1,5 ms CPU harcar. Veritabanı bağlantısının kopması gibi seyrek ve beklenmeyen durumlarda exception doğru araç, maliyeti önemsiz.

Hesap, hata normal akışın parçası olduğunda değişir. Kayıt bulunamadığında exception fırlatıp middleware'de 404'e çeviren yapılar gördüm. Okunaklı görünüyor; ama "bulunamadı" sık yaşanıyorsa her istek bu bedeli öder, ayrılan bellek de GC'ye iş çıkarır. Microsoft'un exception önerileri de aynı yönde: sık yaşanan durumları exception'a bırakmadan ele al.

Kendi servisinde bakmak için dotnet-counters monitor -n <süreç-adı> System.Runtime çalıştır ve exception sayacını izle. Saniyede yüzlerce exception görüyorsan hangilerinin aslında beklenen bir sonuç olduğuna bak.

Kısa kural

  • try/catch yazmanın bir bedeli yok; seyrek hatada exception fırlatmak da hissedilmez.
  • Exception fırlatmak, sonuç döndürmekten yüzlerce kat yavaş ve bellek ayırıyor; sık yaşanan durumlarda bu fark birikir.
  • Beklenen sonuçlar (geçersiz girdi, bulunamadı, doğrulama hatası) dönüş değeriyle, beklenmeyenler exception'la.

Bu yazı ilk olarak huseyinbozdemir.com'da yayımlandı.

Top comments (2)

Collapse
 
alewx profile image
alex •

exception fırlatma ve result döndürme arasında latency farkı ölçerken, cold start ve stacktrace maliyetini nasıl izledin? high‑throughput async senaryolarda da aynı sonuçları gözlemledin mi

Collapse
 
huseyinbozdemir profile image
Hüseyin Bozdemir •

Cold start'ı ayrıca izlemedim; BenchmarkDotNet ısınma (warmup) turlarını ölçüme katmadığı için JIT ve ilk çağrı maliyeti rakamların dışında kalıyor. Stack trace maliyeti ise ayrı değil, throw+catch sayısının içinde: 1 seviyede 1,5 µs, 10 seviyede 3,8 µs farkının büyük kısmı zaten yığını yürüyüp çerçeveleri toplamaktan geliyor; derinlikle bellek ayırmanın 320 B'den 1.336 B'ye çıkması da bundan.

Async ve yüksek trafik senaryosunu ölçmedim, yazının kapsamı senkron mikro ölçümdü. Beklentim async'te farkın artması: await sınırlarını geçen exception her seferinde yeniden fırlatılıyor ve ExceptionDispatchInfo ile stack trace birleştiriliyor. Ama bunu rakamla söyleyemem; ölçüm kodu GitHub'da, async bir varyant ekleyip sonucu buraya yazarım.