Deneme / yakalama blokları istisnalar atılmadığında performansa zarar verir mi?


274

Bir Microsoft çalışanıyla yapılan kod incelemesi sırasında bir try{}bloğun içindeki kodun büyük bir bölümüne rastladık . O ve bir BT temsilcisi bunun kodun performansı üzerinde etkileri olabileceğini öne sürdü. Aslında, kodun çoğunun try / catch bloklarının dışında olması ve sadece önemli bölümlerin kontrol edilmesi gerektiğini önerdiler. Microsoft çalışanı ekledi ve yaklaşan bir beyaz kağıdın yanlış deneme / yakalama bloklarına karşı uyarı verdiğini söyledi.

Etrafa baktım ve optimizasyonları etkileyebileceğini gördüm , ancak yalnızca kapsamlar arasında bir değişken paylaşıldığında geçerli görünüyor.

Kodun sürdürülebilirliği hakkında soru sormuyorum, hatta doğru istisnaları bile ele almıyorum (söz konusu kod şüphesiz yeniden faktoring gerektiriyor). Ayrıca akış kontrolü için istisnalar kullanmaya değinmiyorum, çoğu durumda bu açıkça yanlış. Bunlar önemli konular (bazıları daha önemli), ama burada odak değil.

İstisnalar zaman nasıl Try / catch bloğu performansını etkiliyor değil atılmış?


147
"Performansın doğruluğunu feda eden hiçbiri hak etmiyor."
Joel Coehoorn

16
Bununla birlikte, doğruluk her zaman performans için feda edilmeye gerek yoktur.
Dan Davies Brackett

19
Basit merak etmeye ne dersiniz?
Samantha Branham

63
@Joel: Belki de Kobi merakın cevabını bilmek istiyor. Performansın daha iyi veya daha kötü olup olmayacağını bilmek, mutlaka koduyla çılgınca bir şey yapacağını göstermez. Kendi adına bilgi peşinde koşmak iyi bir şey değil mi?
LukeH

6
İşte bu değişikliği yapıp yapmayacağınızı bilmek için iyi bir algoritma. İlk olarak, müşteri odaklı anlamlı performans hedefleri belirleyin. İkinci olarak, önce doğru ve net olmak için kodu yazın. Üçüncü olarak, hedeflerinize karşı test edin. Dördüncüsü, hedeflerinize ulaşırsanız, işi erken kesin ve plaja gidin. Beşinci olarak, hedeflerinize ulaşmazsanız, çok yavaş kodu bulmak için bir profil oluşturucu kullanın. Altıncı, bu kod gereksiz bir istisna işleyici nedeniyle çok yavaş olursa, yalnızca istisna işleyiciyi kaldırın. Değilse, gerçekten çok yavaş olan kodu düzeltin. Sonra üçüncü adıma geri dönün.
Eric Lippert

Yanıtlar:


203

Kontrol et.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

Çıktı:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

Milisaniye cinsinden:

449
416

Yeni kod:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

Yeni sonuçlar:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334

24
JIT derlemesinin öncekini etkilemediğinden emin olmak için bunları ters sırada deneyebilir misiniz?
JoshJordan

28
Bunun gibi programlar, istisna işlemenin etkisini test etmek için iyi bir aday gibi görünmüyor, normal deneme {} catch {} bloklarında neler olacağını çok fazla optimize edecek. Bu konuda öğle yemeğine çıkabilirim ...
LorenVS

30
Bu bir hata ayıklama derlemesi. JIT bunları optimize etmez.
Ben M

7
Bu hiç doğru değil, bir düşünün. Kaç kez kullandığınızı bir döngüde yakalamayı deneyin? Çoğu zaman bir denemede döngü kullanacaksınız. C
Athiwat Chunlakhan

9
Gerçekten mi? "İstisnalar atılmadığında try / catch blokları performansı nasıl etkiler?"
Ben M

105

Try / catch ile ve try / catch olmadan tüm istatistikleri gördükten sonra merak bakmaya zorladı arkasında iki durumda için oluşturulan ne olduğunu görmek için. İşte kod:

C #:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C #:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

Ben bir IL uzmanı değilim ama .locals init ([0] class [mscorlib]System.Exception ex)on yedi satır kadar denemek / yakalamak olmadan yöntemi için oldukça benzer şeyler sonra dördüncü satırda bir yerel istisna nesne oluşturulduğunu görebilirsiniz IL_0021: leave.s IL_002f. Bir istisna oluşursa kontrol çizgiye atlar, IL_0025: ldloc.0aksi takdirde etikete atlar IL_002d: leave.s IL_002fve işlev döner.

Hiçbir istisna oluşmazsa, yalnızca istisna nesnelerini ve bir atlama talimatını tutmak için yerel değişkenler oluşturma yükü olduğunu güvenle varsayabilirim .


33
IL, C # ile aynı notasyonda bir try / catch bloğu içerir, bu yüzden bu, sahne arkasında bir try / catch'in ne kadar ek yük anlamına geldiğini gerçekten göstermez! Sadece IL daha fazla eklemez, derlenmiş montaj kodunda bir şey eklenmediği anlamına gelmez. IL, tüm .NET dillerinin ortak bir temsilidir. Makine kodu DEĞİLDİR!
awe

64

Hayır. Bir try / nihayet engellemenin önemsiz optimizasyonlarının programınız üzerinde gerçekten ölçülebilir bir etkisi varsa, muhtemelen ilk etapta .NET kullanmamalısınız.


10
Bu mükemmel bir nokta - listemizdeki diğer öğelerle karşılaştırıldığında, bu minik olmalı. Temel dil özelliklerinin doğru davranması ve güvenebileceğimiz şeyleri (sql, dizinler, algoritmalar) optimize etmesi için güvenmeliyiz.
Kobi

3
Sıkı döngüler düşünün dostum. Oyun sunucusundaki bir soket veri akışından nesneleri okuduğunuz ve serileştirdiğiniz döngü ve olabildiğince sıkmaya çalıştığınız örnek. Yani binaryformatter yerine nesne serileştirme için MessagePack ve sadece bayt dizileri, vb oluşturmak yerine ArrayPool <byte> kullanın ... Bu senaryolarda birden çok (belki de iç içe) etkisi sıkı döngü içinde yakalamak blokları deneyin. Bazı optimizasyonlar derleyici tarafından atlanacak ve istisna değişkeni Gen0 GC'ye geçecektir. Söylediğim tek şey, her şeyin bir etkisi olduğu "Bazı" senaryolar olduğu.
tcwicks

35

.NET istisna modelinin oldukça kapsamlı açıklaması.

Rico Mariani'nin Performans Tidbitleri: İstisna Maliyet: Ne zaman ve ne zaman

İlk maliyet türü, kodunuzda istisna işlemenin statik maliyetidir. Yönetilen istisnalar aslında burada nispeten iyi yapıyor, bu yüzden statik maliyet C ++ 'da söylenenden çok daha düşük olabilir. Bu neden? Statik maliyet gerçekten iki çeşit yerde gerçekleşir: Birincisi, bu yapılar için kodun bulunduğu try / nihayet / catch / throw'ın gerçek siteleri. İkincisi, yönetilmeyen kodda, bir istisna atılırsa tahrip edilmesi gereken tüm nesneleri takip etmekle ilişkili gizli maliyet vardır. Mevcut olması gereken önemli miktarda temizleme mantığı var ve sinsi kısım bile olmayan kodun

Dmitriy Zaslavskiy:

Chris Brumme'in notuna göre: Bazı optimizasyonların JIT tarafından yakalama varlığında yapılmamasıyla ilgili bir maliyet var


1
C ++ ile ilgili şey, standart kütüphanenin çok büyük bir kısmının istisnalar atacağıdır. Onlar hakkında isteğe bağlı bir şey yok. Nesnelerinizi bir tür istisna politikası ile tasarlamanız gerekir ve bunu yaptıktan sonra artık gizli bir maliyet yoktur.
David Thornley

Rico Mariani'nin iddiaları yerli C ++ için tamamen yanlış. "statik maliyet C ++ 'da söylenenden çok daha düşük olabilir" - Bu doğru değil. Bununla birlikte, makalenin yazıldığı 2003'te istisna mekanizması tasarımının ne olduğundan emin değilim. C ++ gerçekten hiç maliyeti yoktur istisnalar zaman değil olurlarsa olsunlar kaç deneme / yakalama blokları var ve nerede, atılmış.
BJovke

1
@BJovke C ++ "sıfır maliyet istisnası işleme" yalnızca istisnalar atıldığında çalışma zamanı maliyeti olmadığı anlamına gelir, ancak istisnalar üzerindeki tüm temizleme kodu çağrı yıkıcıları nedeniyle hala büyük bir kod boyutu maliyeti vardır. Ayrıca, normal kod yolunda istisnaya özel bir kod oluşturulmasa da, maliyet hala sıfır değildir, çünkü istisna olasılığı optimizasyonu hala kısıtlar (örneğin bir istisna durumunda kalması gereken şeyler) bir yerde -> değerler daha az agresif bir şekilde atılabilir -> daha az verimli kayıt tahsisi)
Daniel

24

Örnekte yapı Ben M'den farklıdır . İç fordöngü içinde iki durum arasında iyi bir karşılaştırma olmamasına neden olacak şekilde yukarı doğru genişletilecektir .

Aşağıdakiler, kontrol edilecek kodun tamamının (değişken bildirimi dahil) Try / Catch bloğunun içinde olduğu karşılaştırma için daha doğrudur:

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Ben M'den orijinal test kodunu çalıştırdığımda, hem Debug hem de Releas konfigürasyonunda bir fark fark ettim.

Bu sürümde, hata ayıklama sürümünde (aslında diğer sürümden daha fazla) bir fark fark ettim, ancak Sürüm sürümünde hiçbir fark yoktu.

Gerçekleştiriyorum :
Bu testte dayanarak, biz bu deneyin / Yakalama söyleyebiliriz yapar performansı üzerinde küçük bir etkiye sahiptir.

DÜZENLEME:
Döngü değerini 10000000'den 1000000000'e yükseltmeye çalıştım ve sürümde bazı farklılıklar elde etmek için Release'de tekrar koştum ve sonuç şuydu:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

Sonucun tutarsız olduğunu görüyorsunuz. Bazı durumlarda Try / Catch kullanan sürüm aslında daha hızlıdır!


1
Bunu da fark ettim, bazen dene / yakala ile daha hızlı. Ben'in cevabı üzerine yorum yaptım. Ancak, 24 seçmenin aksine, bu tür kıyaslamalardan hoşlanmıyorum, bunun iyi bir gösterge olduğunu düşünmüyorum. Kod bu durumda daha hızlı, ama her zaman olacak mı?
Kobi

5
Bu, makinenizin aynı anda çeşitli başka görevler yaptığını kanıtlamaz mı? Geçen zaman asla iyi bir ölçü değildir, geçen zamanı değil, işlemci zamanını kaydeden bir profilleyici kullanmanız gerekir.
Colin Desmond

2
@Kobi: Programınızın diğerinden daha hızlı çalıştığının bir kanıtı olarak yayınlayacaksanız bunun karşılaştırmanın en iyi yolu olmadığını kabul ediyorum, ancak bir geliştirici olarak size bir yöntemin diğerinden daha iyi performans gösteren bir gösterge verebilir . Bu durumda, farklılıkların (en azından Sürüm yapılandırması için) cahil olduğunu söyleyebiliriz.
korku

1
try/catchBurada zamanlama yapmıyorsunuz . 10M döngülerine karşı 12 kritik giriş bölümüne deneme / yakalama zamanlaması yapıyorsunuz . Döngünün gürültüsü, try / catch'in herhangi bir etkisini ortadan kaldıracaktır. bunun yerine try / catch'i sıkı döngünün içine koyarsanız ve / with olmadan karşılaştırırsanız, try / catch'in maliyeti ile sonuçlanırsınız. (şüphesiz, böyle bir kodlama genellikle iyi bir uygulama değildir, ancak bir yapının yükünü zamanlamak istiyorsanız, bunu böyle yaparsınız). Günümüzde BenchmarkDotNet, güvenilir yürütme zamanlamaları için kullanılabilecek bir araçtır.
Abel

15

A'nın gerçek etkisini try..catchsıkı bir döngüde test ettim ve normal bir durumda performans endişesi olamayacak kadar küçük.

Döngü çok az iş yaparsa (testimde bir yaptım x++), istisna işlemenin etkisini ölçebilirsiniz. İstisna işleme özelliğiyle döngü çalışması yaklaşık on kat daha uzun sürdü.

Döngü bazı gerçek işler yaparsa (testimde Int32.Parse yöntemini çağırdım), istisna işlemenin ölçülemeyecek kadar az etkisi vardır. Döngülerin sırasını değiştirerek çok daha büyük bir fark aldım ...


11

catch bloklarını deneyin performans üzerinde ihmal edilebilir bir etkiye sahiptir, ancak istisna Fırlatma oldukça büyük olabilir, bu muhtemelen iş arkadaşınızın karıştırıldığı yerdir.


8

Deneme / yakalama performansı etkiler.

Ama bu büyük bir etki değil. try / catch karmaşıklığı genellikle bir döngüye yerleştirilmeleri dışında basit bir atama gibi O (1) şeklindedir. Bu yüzden onları akıllıca kullanmalısınız.

İşte try / catch performansıyla ilgili bir referans (bunun karmaşıklığını açıklamıyor, ancak ima ediliyor). Daha Az İstisna Atma bölümüne bir göz atın


3
Karmaşıklık O (1) 'dir. Örneğin, try-catch ile çok sık çağrılan bir kod bölümünü donatırsanız (veya bir döngüden bahsederseniz), O (1) ler sonunda ölçülebilir bir sayı ekleyebilir.
Csaba Toth

6

Teoride, bir istisna olmadıkça bir try / catch bloğunun kod davranışı üzerinde hiçbir etkisi olmayacaktır. Bununla birlikte, bir dene / yakala bloğunun varlığının büyük bir etkiye sahip olabileceği bazı nadir durumlar ve etkinin farkedilebildiği nadir ama çok zor olan bazı durumlar vardır. Bunun nedeni verilen kodun şudur:

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

derleyici, deyim2'nin deyim3'ten önce çalışacağının garanti edilmesine dayanarak bildirimi1 optimize edebilir. Derleyici, thing1'in hiçbir yan etkisi olmadığını fark ederse ve thing2 aslında x kullanmazsa, thing1'i güvenli bir şekilde atlayabilir. Eğer [bu durumda olduğu gibi] thing1 pahalıysa, bu büyük bir optimizasyon olabilir, ancak thing1'in pahalı olduğu durumlar da derleyicinin optimize etme olasılığı en düşük olanlardır. Kodun değiştirildiğini varsayalım:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

Şimdi, deyim3 yürütülmeden deyim3'ün çalışabileceği bir olaylar dizisi vardır. Koddaki hiçbir şey thing2bir istisna atamasa bile, başka bir iş parçacığının , temizlenmiş ve ayarlanmış bir Interlocked.CompareExchangebildirimi kullanmak ve ardından bir deyim değerini yazmadan önce gerçekleştirmesi mümkün olabilir . Daha sonra yürütülür [temsilci ile icra statement3 devam sağlayan]. Böyle bir olay dizisi elbette istisnai olarak imkansız olacaktır, ancak böyle bir olası olay meydana geldiğinde bile spesifikasyona göre çalışan kod üretmek için bir derleyici gerekmektedir.qThread.ResetAbortThread.Abort()xcatchThread.ResetAbort()q

Genel olarak, derleyicinin, basit kod parçalarını bırakma fırsatlarını karmaşık olanlardan daha çok fark etmesi daha olasıdır ve bu nedenle, bir istisna asla atılmazsa, bir deneme / yakalamanın performansı etkileyebileceği nadirdir. Yine de, bir dene / yakala bloğunun varlığının, denemeye / yakalamaya yönelik olarak kodun daha hızlı çalışmasına izin verebilecek optimizasyonları engelleyebileceği bazı durumlar vardır.


5

" Önleme idare etmekten daha iyi " olsa da , performans ve verimlilik perspektifinde, ön varyasyon üzerinde denemeyi seçebiliriz. Aşağıdaki kodu göz önünde bulundurun:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

İşte sonuç:

With Checking: 20367
With Exception: 13998

4

Deneme / yakalama bloklarının nasıl çalıştığı ve bazı uygulamaların yüksek ek yüke sahip olduğu ve bazı istisnalar olmadığında bazı ek yüklerin sıfır yüke sahip olduğu hakkında bir tartışma için try / catch uygulamasının tartışmasına bakın . Özellikle, Windows 32 bit uygulamasının yüksek yükü olduğunu ve 64 bit uygulamanın olmadığını düşünüyorum.


Açıkladığım, istisnaları uygulamaya yönelik iki farklı yaklaşım. Yaklaşımlar, yönetilen / yönetilmeyen kodun yanı sıra C ++ ve C # için de geçerlidir. Hangi MS onların C # için seçtim tam olarak bilmiyorum, ama MS tarafından sağlanan makine düzeyinde uygulamaların istisna işleme mimarisi daha hızlı düzeni kullanır. 64 bit için C # uygulaması bunu kullanmazsa biraz şaşırırdım.
Ira Baxter
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.