İçeriğe geç
Hüseyin Bozdemir
Tüm yazılar

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

C# ve .NET 10'da exception maliyeti ölçümle: try/catch neredeyse bedava, throw yaklaşık 1.300 kat yavaş. Ne zaman Result ya da TryParse kullanmalı?

5 dk okumaGüncellendi:

'Exception pahalı mı? Sayıyla cevap.' başlığı; sağda büyük turuncu ×1.300 oranı, altında 'throw + catch, sonuç döndürmeye göre' ve üç çip: tek throw 1,5 µs, try/catch 0,9 ns, .NET 10.
İçindekiler
  1. Ölçüm düzeneği
  2. try/catch performansı düşürür mü?
  3. Exception fırlatmanın maliyeti
  4. Parse mı, TryParse mı?
  5. Exception yerine Result döndürmek
  6. Ne zaman önemli?
  7. Kısa kural
  8. Sık sorulanlar
  9. try/catch yazmak performansı düşürür mü?
  10. Exception yerine ne zaman Result pattern kullanmalı?
  11. .NET 9 ve 10’da exception maliyeti değişti mi?

“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; }
}

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;

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ı");

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.

Sık sorulanlar

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

Hayır. Hata oluşmadığında try/catch bloğunun bedeli bir nanosaniyenin altında (0,24 ns yerine 0,91 ns). .NET’te try bloğu her çalıştığında ek iş yapılmaz; hangi aralığın hangi catch’e ait olduğu bir tabloda durur ve o tabloya ancak exception fırlatıldığında bakılır.

Exception yerine ne zaman Result pattern kullanmalı?

Hata normal akışın parçasıysa: geçersiz girdi, bulunamayan kayıt, doğrulama hatası. Bunlar sık yaşanır ve her seferinde mikrosaniye artı bellek ayırma bedeli öder. Veritabanı bağlantısının kopması gibi seyrek ve beklenmeyen durumlarda exception doğru araç, maliyeti önemsiz.

.NET 9 ve 10’da exception maliyeti değişti mi?

Evet, .NET 9’da CoreCLR’ın exception işleme altyapısı yeniden yazıldı ve Microsoft’un mikro ölçümlerinde 2-4 kat hızlandı. .NET 10’daki ölçümümde tek bir throw+catch 1 metot derinlikte 1,5 µs, 10 metotta 3,8 µs; eski yazılardaki rakamlar bugün için fazla karamsar olabilir.