Czy bloki try / catch mają negatywny wpływ na wydajność, gdy nie są zgłaszane wyjątki?

274

Podczas przeglądu kodu z pracownikiem Microsoft natrafiliśmy na dużą część kodu wewnątrz try{}bloku. Ona i przedstawiciel IT zasugerowali, że może to mieć wpływ na wydajność kodu. W rzeczywistości zasugerowali, że większość kodu powinna znajdować się poza blokami try / catch i że należy sprawdzać tylko ważne sekcje. Pracownik Microsoft dodał i powiedział, że nadchodząca biała księga ostrzega przed nieprawidłowymi blokami try / catch.

Rozejrzałem się i odkryłem, że może to wpływać na optymalizacje , ale wydaje się, że ma to zastosowanie tylko wtedy, gdy zmienna jest współużytkowana między zakresami.

Nie pytam o łatwość konserwacji kodu, ani nawet o obsługę odpowiednich wyjątków (kod, o którym mowa, wymaga ponownej faktoryzacji, bez wątpienia). Nie odnoszę się również do używania wyjątków do kontroli przepływu, co w większości przypadków jest oczywiście błędne. Są to ważne kwestie (niektóre są ważniejsze), ale nie w tym miejscu.

Jak bloki try / catch wpływają na wydajność, gdy wyjątki nie są zgłaszane?

Kobi
źródło
147
„Ten, kto poświęci poprawność za działanie, nie zasługuje na żadne”.
Joel Coehoorn,
16
powiedziawszy, poprawność nie zawsze musi być poświęcana dla wydajności.
Dan Davies Brackett,
19
A może zwykła ciekawość?
Samantha Branham,
63
@Jel: Być może Kobi chce poznać odpowiedź z ciekawości. Wiedza, czy wydajność będzie lepsza czy gorsza, niekoniecznie oznacza, że ​​zrobi coś szalonego ze swoim kodem. Czy pogoń za wiedzą nie jest dobra?
Łukasza
6
Oto dobry algorytm pozwalający wiedzieć, czy wprowadzić tę zmianę, czy nie. Po pierwsze, ustal znaczące cele w zakresie wydajności oparte na klientach. Po drugie, najpierw napisz kod, który będzie zarówno poprawny, jak i czysty. Po trzecie, sprawdź to pod kątem swoich celów. Po czwarte, jeśli osiągniesz swoje cele, wcześniej przerwij pracę i idź na plażę. Po piąte, jeśli nie osiągasz swoich celów, użyj profilera, aby znaleźć zbyt wolny kod. Po szóste, jeśli ten kod okazuje się zbyt wolny z powodu niepotrzebnego modułu obsługi wyjątków, dopiero wtedy należy usunąć moduł obsługi wyjątków. Jeśli nie, napraw zbyt wolny kod. Następnie wróć do kroku trzeciego.
Eric Lippert,

Odpowiedzi:

203

Sprawdź to.

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

Wynik:

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

W milisekundach:

449
416

Nowy 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();
}

Nowe wyniki:

   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
Ben M.
źródło
24
Czy możesz wypróbować je również w odwrotnej kolejności, aby mieć pewność, że kompilacja JIT nie wpłynęła na tę pierwszą?
JoshJordan,
28
Takie programy nie wydają się być dobrymi kandydatami do testowania wpływu obsługi wyjątków, zbyt duża część tego, co dzieje się w normalnych blokach try {} catch {}, zostanie zoptymalizowana. Mogę wyjść na lunch ...
LorenVS,
30
To jest wersja do debugowania. JIT ich nie optymalizuje.
Ben M.
7
To wcale nie jest prawda, pomyśl o tym. Ile razy używasz spróbuj złapać w pętli? Przez większość czasu będziesz używać pętli w try.c
Athiwat Chunlakhan
9
Naprawdę? „Jak bloki try / catch wpływają na wydajność, gdy wyjątki nie są zgłaszane?”
Ben M
105

Po zobaczeniu wszystkich statystyk dla try / catch i bez try / catch, ciekawość zmusiła mnie do obejrzenia się za siebie, aby zobaczyć, co jest generowane dla obu przypadków. Oto kod:

DO#:

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

DO#:

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

Nie jestem ekspertem od IL, ale widzimy, że lokalny obiekt wyjątku jest tworzony w czwartej linii, .locals init ([0] class [mscorlib]System.Exception ex)po czym rzeczy są takie same jak w przypadku metody bez próbowania / przechwytywania do linii siedemnastej IL_0021: leave.s IL_002f. Jeśli wystąpi wyjątek, sterowanie przeskakuje do linii, w IL_0025: ldloc.0przeciwnym razie przeskakujemy do etykiety IL_002d: leave.s IL_002fi funkcja wraca.

Mogę spokojnie założyć, że jeśli nie wystąpią wyjątki, to narzutami tworzenia lokalnych zmiennych są tylko obiekty wyjątków i instrukcja skoku.

TheVillageIdiot
źródło
33
Cóż, IL zawiera blok try / catch w tej samej notacji co w C #, więc tak naprawdę nie pokazuje, ile narzutów oznacza próba / catch za kulisami! Tyle, że IL nie dodaje dużo więcej, nie znaczy tego samego, ponieważ nie jest dodawana do skompilowanego kodu asemblera. IL to tylko wspólna reprezentacja wszystkich języków .NET. To NIE jest kod maszynowy!
awe
64

Nie. Jeśli trywialne optymalizacje, które wyklucza blok try / wreszcie, rzeczywiście mają wymierny wpływ na twój program, prawdopodobnie nie powinieneś używać .NET.

John Kugelman
źródło
10
To doskonały punkt - w porównaniu z innymi pozycjami na naszej liście, ten powinien być niewielki. Powinniśmy ufać podstawowym funkcjom języka, aby zachowywać się poprawnie i optymalizować to, co możemy kontrolować (sql, indeksy, algorytmy).
Kobi
3
Pomyśl o koleżko z ciasnych pętli. Przykładowa pętla, w której czytasz i serializujesz obiekty ze strumienia danych gniazda na serwerze gry i próbujesz wycisnąć jak najwięcej. Więc przesyłaj MessagePack do serializacji obiektów zamiast binarnego formatowania i używaj ArrayPool <bajt> zamiast po prostu tworzyć tablice bajtów itp. W tych scenariuszach jaki jest wpływ wielu (być może zagnieżdżonych) prób złapania bloków w ciasnej pętli. Niektóre optymalizacje zostaną pominięte przez kompilator, a zmienna wyjątku idzie do Gen0 GC. Mówię tylko, że istnieją scenariusze „niektóre”, w których wszystko ma wpływ.
tcwicks
35

Całkiem wyczerpujące wyjaśnienie modelu wyjątku .NET.

Ciekawostki o wydajności Rico Mariani: Koszt wyjątku: kiedy rzucać, a kiedy nie

Pierwszy rodzaj kosztu to statyczny koszt obsługi wyjątków w kodzie. Zarządzane wyjątki faktycznie mają się tutaj stosunkowo dobrze, co oznacza, że ​​koszt statyczny może być znacznie niższy niż, powiedzmy, w C ++. Dlaczego to? Cóż, koszt statyczny jest ponoszony w dwóch rodzajach miejsc: po pierwsze, w rzeczywistych stronach try / wreszcie / catch / throw, gdzie jest kod dla tych konstrukcji. Po drugie, w niezmienionym kodzie istnieje ukryty koszt związany ze śledzeniem wszystkich obiektów, które należy zniszczyć w przypadku zgłoszenia wyjątku. Musi być obecna znaczna logika czyszczenia, a podstępną częścią jest to, że nawet kod, który nie wykonuje

Dmitrij Zasławski:

Zgodnie z notatką Chrisa Brumme'a: Istnieje również koszt związany z faktem, że JIT nie przeprowadza pewnej optymalizacji w obecności haczyka

arul
źródło
1
Rzeczą w C ++ jest to, że bardzo duża część standardowej biblioteki zgłasza wyjątki. Nie ma w nich nic opcjonalnego. Musisz zaprojektować swoje obiekty z jakąś zasadą wyjątków, a gdy to zrobisz, nie będzie już żadnych kosztów ukrytych.
David Thornley,
Twierdzenia Rico Mariani są całkowicie błędne dla natywnego C ++. „koszt statyczny może być znacznie niższy niż powiedzmy w C ++” - To po prostu nieprawda. Chociaż nie jestem pewien, jaki był projekt mechanizmu wyjątku w 2003 r., Kiedy powstał ten artykuł. C ++ naprawdę nie ma żadnych kosztów, gdy wyjątki nie są zgłaszane, bez względu na to, ile masz bloków try / catch i gdzie one są.
BJovke
1
@BJovke C ++ „zerowa obsługa wyjątków kosztowych” oznacza tylko, że nie ma żadnych kosztów w czasie wykonywania, gdy wyjątki nie są zgłaszane, ale nadal istnieje znaczny koszt rozmiaru kodu z powodu wszystkich kodów czyszczących wywołujących niszczyciele wyjątków. Ponadto, chociaż na normalnej ścieżce kodu nie jest generowany żaden kod specyficzny dla wyjątku, koszt nadal nie jest tak naprawdę zerowy, ponieważ możliwość wyjątków nadal ogranicza optymalizator (np. Rzeczy potrzebne w przypadku wyjątku muszą pozostać gdzieś -> wartości mogą być odrzucane mniej agresywnie -> mniej wydajny przydział rejestrów)
Daniel
24

Struktura ta różni się od przykładu Ben M . Zostanie on rozciągnięty nad głową w wewnętrznej forpętli, co spowoduje, że nie będzie to dobre porównanie między dwoma przypadkami.

Poniższe informacje są bardziej dokładne do porównania, gdy cały kod do sprawdzenia (w tym deklaracja zmiennej) znajduje się w bloku Try / Catch:

        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();
        }

Kiedy uruchomiłem oryginalny kod testowy z Ben M , zauważyłem różnicę zarówno w konfiguracji Debugowania, jak i Releas.

W tej wersji zauważyłem różnicę w wersji debugowania (właściwie więcej niż w innej wersji), ale nie było różnicy w wersji Release.

Conclution :
W oparciu o te badania, myślę, że możemy powiedzieć, że try / catch nie mają niewielki wpływ na wydajność.

EDYCJA:
Próbowałem zwiększyć wartość pętli z 10000000 do 1000000000, i uruchomiłem ponownie w wersji, aby uzyskać pewne różnice w wersji, a wynik był następujący:

   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

Widzisz, że wynik jest niekonsekwentny. W niektórych przypadkach wersja wykorzystująca Try / Catch jest w rzeczywistości szybsza!

groza
źródło
1
Zauważyłem to również, czasem jest to szybsze z try / catch. Skomentowałem to w odpowiedzi Bena. Jednak w przeciwieństwie do 24 głosujących, nie lubię tego rodzaju testów porównawczych, nie sądzę, że to dobra wskazówka. W tym przypadku kod jest szybszy, ale czy zawsze tak będzie?
Kobi
5
Czy to nie dowodzi, że twoja maszyna wykonywała wiele innych zadań jednocześnie? Upływający czas nigdy nie jest dobrym miernikiem, musisz użyć profilera, który rejestruje czas procesora, a nie czas, który upłynął.
Colin Desmond,
2
@Kobi: Zgadzam się, że nie jest to najlepszy sposób na przeprowadzenie testu porównawczego, jeśli zamierzasz opublikować go jako dowód, że Twój program działa szybciej niż inny, czy coś takiego, ale może dać ci jako programista wskazówkę, że jedna metoda działa lepiej niż inna . W tym przypadku myślę, że możemy powiedzieć, że różnice (przynajmniej w konfiguracji wydania) są nie do pomylenia.
awe
1
Nie mierzysz czasu try/catch. Mierzysz czas 12 prób / złap wchodzenie w sekcję krytyczną przeciwko 10M pętlom. Szum pętli wyeliminuje wszelkie wpływy, jakie ma try / catch. jeśli zamiast tego umieścisz try / catch w ciasnej pętli i porównasz z / bez, skończyłbyś z kosztem try / catch. (bez wątpienia takie kodowanie nie jest ogólnie dobrą praktyką, ale jeśli chcesz zmierzyć czas narzutu konstrukcji, tak to robisz). Obecnie BenchmarkDotNet to narzędzie zapewniające rzetelne terminy realizacji.
Abel
15

Przetestowałem rzeczywisty wpływ try..catchciasnej pętli i sam w sobie jest zbyt mały, aby stanowić zagrożenie dla wydajności w każdej normalnej sytuacji.

Jeśli pętla działa bardzo mało (w moim teście zrobiłem to x++), możesz zmierzyć wpływ obsługi wyjątków. Uruchomienie pętli z obsługą wyjątków trwało około dziesięć razy dłużej.

Jeśli pętla rzeczywiście działa (w moim teście wywołałem metodę Int32.Parse), obsługa wyjątków ma zbyt mały wpływ, aby można ją było zmierzyć. Dostałem znacznie większą różnicę, zmieniając kolejność pętli ...

Guffa
źródło
11

spróbuj złapać bloki mają znikomy wpływ na wydajność, ale wyjątek Rzucanie może być dość spore, prawdopodobnie w tym miejscu twój współpracownik był zdezorientowany.

RHicke
źródło
8

Wpływ try / catch na wydajność.

Ale to nie jest ogromny wpływ. złożoność try / catch to na ogół O (1), podobnie jak proste przypisanie, z wyjątkiem sytuacji, gdy są one umieszczone w pętli. Musisz więc mądrze z nich korzystać.

Oto odniesienie do wydajności try / catch (nie wyjaśnia to jednak złożoności, ale jest implikowane). Zobacz sekcję Rzuć mniej wyjątków

Izaak
źródło
3
Złożoność to O (1), to nie znaczy zbyt wiele. Na przykład, jeśli wyposażysz sekcję kodu, która jest bardzo często wywoływana, w try-catch (lub wspominasz o pętli), O (1) może w końcu dodać się do mierzalnej liczby.
Csaba Toth,
6

Teoretycznie blok try / catch nie będzie miał wpływu na zachowanie kodu, chyba że rzeczywiście wystąpi wyjątek. Istnieją jednak pewne rzadkie okoliczności, w których istnienie bloku try / catch może mieć znaczący wpływ, a niektóre rzadkie, ale prawie niezrozumiałe, w których efekt może być zauważalny. Powodem tego jest podany kod, taki jak:

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

kompilator może być w stanie zoptymalizować instrukcję1 w oparciu o fakt, że instrukcja2 ma gwarancję wykonania przed instrukcją3. Jeśli kompilator rozpozna, że ​​rzecz 1 nie ma skutków ubocznych, a rzecz 2 faktycznie nie używa x, może bezpiecznie całkowicie pominąć rzecz 1. Jeśli [jak w tym przypadku] rzecz1 była kosztowna, mogłaby to być znaczna optymalizacja, chociaż przypadki, w których rzecz1 jest droga, to także te, które kompilator najmniej by zoptymalizował. Załóżmy, że kod został zmieniony:

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

Teraz istnieje sekwencja zdarzeń, w których instrukcja3 mogłaby zostać wykonana bez wykonania instrukcji2. Nawet jeśli nic w kodzie dla thing2nie może wyrzucić wyjątku, możliwe jest, że inny wątek użyje znaku Interlocked.CompareExchangepowiadomienia, który qzostał wyczyszczony i ustawiony na Thread.ResetAbort, a następnie wykona Thread.Abort()instrukcję przed, do której zapisała swoją wartość x. Następnie catchwykonałby Thread.ResetAbort()[przez delegata q], umożliwiając kontynuowanie wykonywania z instrukcją3. Taka sekwencja zdarzeń byłaby oczywiście wyjątkowo nieprawdopodobna, ale do wygenerowania kodu, który działa zgodnie ze specyfikacją, wymagany jest kompilator, nawet jeśli wystąpią takie nieprawdopodobne zdarzenia.

Ogólnie rzecz biorąc, kompilator znacznie częściej dostrzega możliwości pominięcia prostych bitów kodu niż złożone, a zatem rzadkie byłoby, gdyby próba / złapanie mogło wpłynąć na wydajność znacznie, gdyby nigdy nie zgłoszono wyjątków. Mimo to istnieją sytuacje, w których istnienie bloku try / catch może uniemożliwić optymalizację, która - gdyby nie try / catch - pozwoliłaby na szybsze działanie kodu.

supercat
źródło
5

Chociaż „ Zapobieganie jest lepsze niż postępowanie ”, w perspektywie wydajności i wydajności moglibyśmy wybrać try-catch zamiast wstępnej odmiany. Rozważ poniższy kod:

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

Oto wynik:

With Checking: 20367
With Exception: 13998
Ted Oddman
źródło
4

Zobacz dyskusję na temat implementacji try / catch, aby dowiedzieć się, jak działają bloki try / catch i jak niektóre implementacje mają wysoki narzut, a niektóre zerowy narzut, gdy nie występują wyjątki. W szczególności myślę, że 32-bitowa implementacja systemu Windows ma duży narzut, a 64-bitowa implementacja nie.

Ira Baxter
źródło
Opisałem dwa różne podejścia do wdrażania wyjątków. Podejścia te dotyczą zarówno C ++ i C #, jak i kodu zarządzanego / niezarządzanego. Których MS wybrał dla swojego C # Nie wiem dokładnie, ale architektura obsługująca wyjątki aplikacji na poziomie maszynowym dostarczona przez MS używa szybszego schematu. Byłbym trochę zaskoczony, gdyby implementacja C # dla 64 bitów jej nie wykorzystała.
Ira Baxter