Czy wyjątki w C ++ są naprawdę powolne

99

Oglądałem Systematic Error Handling w C ++ - Andrei Alexandrescu twierdzi, że wyjątki w C ++ są bardzo powolne.

Czy jest to nadal prawdą w przypadku C ++ 98?

Avinash
źródło
44
Nie ma sensu pytać, czy „Wyjątki C ++ 98” są szybsze / wolniejsze niż „Wyjątki C ++ 03” lub „Wyjątki C ++ 11”. Ich wydajność zależy od tego, jak kompilator zaimplementuje je w twoich programach, a standard C ++ nie mówi nic o tym, jak powinny być zaimplementowane; jedynym wymaganiem jest to, że ich zachowanie musi być zgodne ze standardem (zasada „jak gdyby”).
In silico
Powiązane (ale nie do końca zduplikowane) pytanie: stackoverflow.com/questions/691168/…
Philipp
2
tak, jest bardzo powolny, ale nie należy ich wyrzucać do normalnych operacji ani używać jako gałęzi
BЈовић
Znalazłem podobne pytanie .
PaperBirdMaster
Aby wyjaśnić, co powiedział BЈовић, nie należy się bać stosowania wyjątków. To wtedy, gdy zostanie zgłoszony wyjątek, napotykasz (potencjalnie) czasochłonne operacje. Jestem również ciekawy, dlaczego chcesz wiedzieć konkretnie dla C ++ 89 ... że najnowsza wersja to C ++ 11, a czas potrzebny do uruchomienia wyjątków jest zdefiniowany jako implementacja, stąd mój „potencjalnie” czasochłonny .
thecoshman

Odpowiedzi:

163

Głównym modelem używanym obecnie do wyjątków (Itanium ABI, VC ++ 64 bity) są wyjątki modelu Zero-Cost.

Pomysł polega na tym, że zamiast tracić czas, ustawiając ochronę i jawnie sprawdzając obecność wyjątków wszędzie, kompilator generuje tabelę boczną, która mapuje dowolny punkt, który może zgłosić wyjątek (licznik programu) do listy programów obsługi. Gdy zostanie zgłoszony wyjątek, ta lista jest konsultowana w celu wybrania odpowiedniego modułu obsługi (jeśli istnieje), a stos jest rozwijany.

W porównaniu z typową if (error)strategią:

  • model Zero-Cost, jak sama nazwa wskazuje, jest darmowy, gdy nie występują żadne wyjątki
  • kosztuje około 10x / 20x, ifgdy wystąpi wyjątek

Koszt nie jest jednak trywialny do zmierzenia:

  • Stolik boczny jest generalnie zimny , więc pobranie go z pamięci zajmuje dużo czasu
  • Określenie właściwej procedury obsługi obejmuje RTTI: wiele deskryptorów RTTI do pobrania, rozproszonych w pamięci i złożone operacje do uruchomienia (w zasadzie dynamic_casttest dla każdego modułu obsługi)

Tak więc głównie chybienia w pamięci podręcznej, a zatem nie są trywialne w porównaniu z czystym kodem procesora.

Uwaga: aby uzyskać więcej informacji, przeczytaj raport TR18015, rozdział 5.4 Obsługa wyjątków (pdf)

Tak więc tak, wyjątki są powolne na ścieżce wyjątkowej , ale poza tym są szybsze niż ifgeneralnie jawne kontrole ( strategia).

Uwaga: Andrei Alexandrescu wydaje się kwestionować to „szybciej”. Osobiście widziałem, jak rzeczy zmieniają się w obie strony, niektóre programy są szybsze z wyjątkami, a inne są szybsze z gałęziami, więc rzeczywiście wydaje się, że w pewnych warunkach następuje utrata optymalizacji.


Czy to ma znaczenie ?

Twierdzę, że tak nie jest. Program powinien być napisany z myślą o czytelności , a nie wydajności (przynajmniej nie jako pierwsze kryterium). Wyjątki mają być używane, gdy oczekuje się, że wywołujący nie może lub nie chce poradzić sobie z awarią na miejscu, i przekazuje go w górę stosu. Bonus: w C ++ 11 wyjątki mogą być kierowane między wątkami przy użyciu biblioteki standardowej.

Jest to jednak subtelne, twierdzę, że map::findnie powinno się rzucać, ale nie przeszkadza mi map::findzwracanie a, checked_ptrktóre rzuca, jeśli próba wyłuskiwania kończy się niepowodzeniem, ponieważ jest zerowa: w drugim przypadku, jak w przypadku klasy, którą wprowadził Alexandrescu, dzwoniący wybiera między jawną kontrolą a poleganiem na wyjątkach. Umocnienie rozmówcy bez obciążania go większą odpowiedzialnością jest zwykle oznaką dobrego projektu.

Matthieu M.
źródło
3
+1 Dodałbym tylko cztery rzeczy: (0) o obsłudze ponownego wrzucania dodanej w C ++ 11; (1) odniesienie do sprawozdania komisji w sprawie wydajności C ++; (2) kilka uwag o poprawności (jako przebijająca nawet czytelność); oraz (3) o spektaklu, uwagi na temat porównania go z przypadkiem niestosowania wyjątków (wszystko jest względne)
Cheers i hth. - Alf
2
@ Cheersandhth.-Alf: (0), (1) i (3) wykonane: dzięki. Jeśli chodzi o poprawność (2), mimo że przewyższa czytelność, nie jestem pewien, czy wyjątki prowadzą do bardziej poprawnego kodu niż inne strategie obsługi błędów (tak łatwo jest zapomnieć o wielu niewidocznych ścieżkach tworzenia wyjątków wykonywania).
Matthieu M.
2
Opis może być lokalnie poprawny, ale warto zauważyć, że obecność wyjątków ma globalny wpływ na założenia i optymalizacje, które może wykonać kompilator. Implikacje te mają ten problem, że nie mają „trywialnych kontrprzykładów”, ponieważ kompilator zawsze może przejrzeć mały program. Profilowanie na realistycznej, dużej bazie kodu z wyjątkami i bez nich może być dobrym pomysłem.
Kerrek SB,
4
> model Zero-Cost, jak sama nazwa wskazuje, jest darmowy, gdy nie występuje żaden wyjątek, co nie jest prawdą aż do najdrobniejszych poziomów szczegółowości. generowanie większej ilości kodu zawsze ma wpływ na wydajność, nawet jeśli jest mały i subtelny ... załadowanie pliku wykonywalnego może zająć systemowi operacyjnemu trochę więcej czasu, albo otrzymasz więcej błędów i-cache. a co z kodem rozwijania stosu? a co z eksperymentami, które możesz zrobić, aby zmierzyć efekty, zamiast próbować zrozumieć to za pomocą racjonalnego myślenia?
jheriko
2
@jheriko: Myślę, że właściwie odpowiedziałem już na większość twoich pytań. Nie powinno to mieć wpływu na czas ładowania (zimny kod nie powinien być ładowany), nie powinno to mieć wpływu na i-cache (zimny kod nie powinien dostać się do i-cache), ... aby odpowiedzieć na jedno brakujące pytanie: "jak mierzyć" => zastąpienie dowolnego wyrzuconego wyjątku wywołaniem funkcji abortumożliwi zmierzenie rozmiaru pliku binarnego i sprawdzenie, czy czas ładowania / i-cache zachowują się podobnie. Oczywiście lepiej nie uderzać w żaden z abort...
Matthieu M.
60

Gdy pytanie zostało wysłane, byłem w drodze do lekarza, czekając taksówką, więc miałem wtedy tylko czas na krótki komentarz. Ale po skomentowaniu, głosowaniu za i przeciw, lepiej dodam własną odpowiedź. Nawet jeśli odpowiedź Matthieu jest już całkiem dobra.


Czy wyjątki są szczególnie powolne w C ++ w porównaniu z innymi językami?

Ponownie roszczenie

„Oglądałem Systematic Error Handling w C ++ - Andrei Alexandrescu twierdzi, że wyjątki w C ++ są bardzo powolne.”

Jeśli tak dosłownie twierdzi Andrei, to chociaż raz jest bardzo mylący, jeśli nie wręcz się myli. Dla podniesionych / wyrzuconych wyjątków jest zawsze powolna w porównaniu z innymi podstawowymi operacjami w języku, niezależnie od języka programowania . Nie tylko w C ++ lub bardziej w C ++ niż w innych językach, jak wskazuje rzekome twierdzenie.

Ogólnie rzecz biorąc, głównie niezależnie od języka, dwie podstawowe cechy języka, które są o rząd wielkości wolniejsze niż pozostałe, ponieważ przekładają się na wywołania procedur obsługujących złożone struktury danych, to

  • zgłaszanie wyjątków i

  • dynamiczna alokacja pamięci.

Na szczęście w C ++ można często uniknąć obu w przypadku kodu krytycznego czasowo.

Niestety nie ma czegoś takiego jak darmowy lunch , nawet jeśli domyślna wydajność C ++ jest dość bliska. :-) Ze względu na wydajność uzyskaną dzięki unikaniu rzucania wyjątków i dynamicznej alokacji pamięci, generalnie uzyskuje się kodowanie na niższym poziomie abstrakcji, używając C ++ jako „lepszego C”. A niższa abstrakcja oznacza większą „złożoność”.

Większa złożoność oznacza więcej czasu poświęconego na konserwację i niewielkie lub żadne korzyści z ponownego wykorzystania kodu, które są realnymi kosztami pieniężnymi, nawet jeśli są trudne do oszacowania lub zmierzenia. To znaczy, w przypadku C ++ można, jeśli jest to pożądane, zamienić wydajność programisty na wydajność wykonywania. To, czy to zrobić, jest w dużej mierze decyzją inżynieryjną i intuicyjną, ponieważ w praktyce można łatwo oszacować i zmierzyć tylko zysk, a nie koszt.


Czy istnieją obiektywne miary wydajności zgłaszania wyjątków w języku C ++?

Tak, międzynarodowy komitet normalizacyjny C ++ opublikował raport techniczny dotyczący wydajności C ++, TR18015 .


Co to znaczy, że wyjątki są „powolne”?

Przede wszystkim oznacza to, że throwmoże to zająć Very Long Time ™ w porównaniu np. Z intzadaniem, ze względu na poszukiwanie przewodnika.

Jak wyjaśnia TR18015 w sekcji 5.4 „Wyjątki”, istnieją dwie główne strategie wdrażania obsługi wyjątków,

  • podejście, w którym każdy try-block dynamicznie ustawia przechwytywanie wyjątków, tak że wyszukiwanie w górę dynamicznego łańcucha programów obsługi jest wykonywane, gdy zostanie zgłoszony wyjątek, oraz

  • podejście, w którym kompilator generuje statyczne tabele wyszukiwania, które są używane do określania programu obsługi dla zgłaszanego wyjątku.

Pierwsze bardzo elastyczne i ogólne podejście jest prawie wymuszone w 32-bitowym systemie Windows, podczas gdy w 64-bitowym środowisku i * nix-land powszechnie stosowane jest drugie, znacznie bardziej wydajne podejście.

Jak omówiono w tym raporcie, w przypadku każdego podejścia istnieją trzy główne obszary, w których obsługa wyjątków wpływa na wydajność:

  • try-Bloki,

  • zwykłe funkcje (możliwości optymalizacji) oraz

  • throw-wyrażenia.

Głównie przy dynamicznym podejściu obsługi (32-bitowy system Windows) obsługa wyjątków ma wpływ na trybloki, głównie niezależnie od języka (ponieważ jest to wymuszone przez schemat obsługi wyjątków strukturalnych systemu Windows ), podczas gdy metoda statycznej tabeli ma z grubsza zerowy koszt dla try- Bloki. Omówienie tego wymagałoby dużo więcej miejsca i badań, niż jest to praktyczne w przypadku odpowiedzi SO. Zobacz raport, aby uzyskać szczegółowe informacje.

Niestety raport z 2006 roku jest już trochę datowany na koniec 2012 roku iz tego co wiem, nie ma nic porównywalnego, co byłoby nowsze.

Inną ważną perspektywą jest to, że wpływ stosowania wyjątków na wydajność różni się znacznie od izolowanej wydajności funkcji języka pomocniczego, ponieważ, jak zauważono w raporcie,

„Rozważając obsługę wyjątków, należy porównać to z alternatywnymi sposobami radzenia sobie z błędami”.

Na przykład:

  • Koszty utrzymania wynikające z różnych stylów programowania (poprawność)

  • Nadmiarowe ifsprawdzanie awarii w miejscu wywołania a scentralizowanetry

  • Problemy z buforowaniem (np. Krótszy kod może zmieścić się w pamięci podręcznej)

Raport zawiera inną listę aspektów do rozważenia, ale i tak jedynym praktycznym sposobem uzyskania twardych faktów na temat wydajności wykonania jest prawdopodobnie wdrożenie tego samego programu z wykorzystaniem wyjątków i bez wyjątków, w ramach ustalonego limitu czasu programowania oraz z programistami zaznajomiony z każdym sposobem, a następnie POMIAR .


Jaki jest dobry sposób na uniknięcie kosztów związanych z wyjątkami?

Prawidłowość prawie zawsze przeważa nad wydajnością.

Bez wyjątków łatwo może się zdarzyć:

  1. Część kodu P jest przeznaczona do uzyskiwania zasobów lub obliczania pewnych informacji.

  2. Kod wywołujący C powinien był sprawdzić powodzenie / niepowodzenie, ale tak nie jest.

  3. Nieistniejący zasób lub nieprawidłowe informacje są używane w kodzie po C, powodując ogólny chaos.

Głównym problemem jest punkt (2), w którym przy zwykłym schemacie kodu powrotnego kod wywołujący C nie jest zmuszony do sprawdzenia.

Istnieją dwa główne podejścia, które wymuszają takie sprawdzanie:

  • Gdzie P bezpośrednio zgłasza wyjątek, gdy się nie powiedzie.

  • Gdzie P zwraca obiekt, który C musi sprawdzić przed użyciem jego głównej wartości (w przeciwnym razie wyjątek lub zakończenie).

Drugim podejściem było, AFAIK, po raz pierwszy opisane przez Bartona i Nackmana w ich książce * Naukowy i inżynieryjny C ++: Wprowadzenie z zaawansowanymi technikami i przykładami , gdzie wprowadzili klasę Fallowwymagającą „możliwego” wyniku funkcji. Podobna klasa o nazwie optionaljest teraz oferowana w bibliotece Boost. I możesz łatwo zaimplementować Optionalklasę samodzielnie, używając std::vectorjako nośnika wartości dla przypadku wyniku innego niż POD.

W pierwszym podejściu kod wywołujący C nie ma innego wyjścia, jak tylko użyć technik obsługi wyjątków. Jednak w przypadku drugiego podejścia kod wywołujący C może sam zdecydować, czy wykonać ifsprawdzanie w oparciu o, czy ogólną obsługę wyjątków. Zatem drugie podejście wspiera kompromis między programistą a wydajnością czasu wykonania.


Jaki jest wpływ różnych standardów języka C ++ na wydajność wyjątków?

„Chcę wiedzieć, czy nadal dotyczy to języka C ++ 98”

C ++ 98 był pierwszym standardem C ++. Dla wyjątków wprowadził standardową hierarchię klas wyjątków (niestety raczej niedoskonałą). Główny wpływ na wydajność miała możliwość specyfikacji wyjątków (usuniętych w C ++ 11), które jednak nigdy nie zostały w pełni zaimplementowane przez główny kompilator Windows C ++ Visual C ++: Visual C ++ akceptuje składnię specyfikacji wyjątku C ++ 98, ale po prostu ignoruje specyfikacje wyjątków.

C ++ 03 był tylko technicznym sprostowaniem C ++ 98. Jedyną nowością w C ++ 03 była inicjalizacja wartości . Co nie ma nic wspólnego z wyjątkami.

Wraz ze standardem C ++ 11 zostały usunięte ogólne specyfikacje wyjątków i zastąpione noexceptsłowem kluczowym.

Standard C ++ 11 dodał również obsługę przechowywania i ponownego wrzucania wyjątków, co jest świetne do propagowania wyjątków C ++ w wywołaniach zwrotnych języka C. Ta obsługa skutecznie ogranicza sposób przechowywania bieżącego wyjątku. Jednak, o ile wiem, nie ma to wpływu na wydajność, z wyjątkiem tego, że w nowszym kodzie obsługa wyjątków może być łatwiej używana po obu stronach wywołania zwrotnego języka C.

Pozdrawiam i hth. - Alf
źródło
7
„wyjątki są zawsze powolne w porównaniu z innymi podstawowymi operacjami w języku, niezależnie od języka programowania” ... z wyjątkiem języków zaprojektowanych do kompilowania użycia wyjątków do zwykłej kontroli przepływu.
Ben Voigt
5
„Zgłoszenie wyjątku obejmuje zarówno alokację, jak i rozwijanie stosu”. Oczywiście nie jest to prawdą w ogóle i ponownie, OCaml jest kontrprzykładem. W językach ze śmieciami nie ma potrzeby rozwijania stosu, ponieważ nie ma destruktorów, więc wystarczy longjmpprzejść do programu obsługi.
JD
2
@JonHarrop: prawdopodobnie nie zdajesz sobie sprawy, że Pyhon ma klauzulę końcową dotyczącą obsługi wyjątków. oznacza to, że implementacja Pythona albo ma rozwijanie stosu, albo nie jest Pythonem. wydajesz się być całkowicie nieświadomy tematów, o których twierdzisz (fantazjowanie). Przepraszam.
Pozdrawiam i hth. - Alf
2
@ Cheersandhth.-Alf: "Pyhon ma klauzulę last do obsługi wyjątków. Oznacza to, że implementacja Pythona albo ma rozwijanie stosu, albo nie jest Pythonem". try..finallyKonstrukt może być realizowany bez odwracania stosu. F #, C # i Java są implementowane try..finallybez używania rozwijania stosu. Ty tylko longjmpdo przewodnika (jak już wyjaśniłem).
JD
4
@JonHarrop: ty brzmieć jak stwarzające dylemat. ale nie ma to żadnego związku z czymkolwiek do tej pory omawianym, a jak dotąd opublikowałeś długą sekwencję negatywnie brzmiących bzdur . musiałbym ci zaufać, aby zgodzić się lub nie z jakimś niejasnym sformułowaniem, ponieważ jako antagonista wybierasz to, co ujawnisz, że to „oznacza”, a ja na pewno nie ufam ci po tych wszystkich bezsensownych bzdurach, odrzucaniu głosów itp.
Pozdrawiam i hth. - Alf
14

Nigdy nie możesz twierdzić, że chodzi o wydajność, chyba że przekonwertujesz kod na zestaw lub przetestujesz go.

Oto, co widzisz: (ławeczka do ćwiczeń)

Kod błędu nie jest wrażliwy na procent wystąpienia. Wyjątki mają trochę nad głową, o ile nigdy nie są wyrzucane. Gdy je rzucisz, zaczyna się nieszczęście. W tym przykładzie jest on rzucany w 0%, 1%, 10%, 50% i 90% przypadków. Gdy wyjątki są generowane w 90% przypadków, kod jest 8 razy wolniejszy niż w przypadku, gdy wyjątki są generowane w 10% przypadków. Jak widać, wyjątki są naprawdę powolne. Nie używaj ich, jeśli są często rzucane. Jeśli Twoja aplikacja nie wymaga czasu rzeczywistego, możesz je wyrzucić, jeśli występują bardzo rzadko.

Widzisz na ich temat wiele sprzecznych opinii. Ale wreszcie, czy wyjątki są powolne? Nie oceniam. Po prostu obserwuj benchmark.

Test porównawczy wydajności wyjątków C ++

Wysypka
źródło
13

To zależy od kompilatora.

Na przykład GCC był znany z bardzo słabej wydajności podczas obsługi wyjątków, ale w ciągu ostatnich kilku lat sytuacja uległa znacznej poprawie.

Należy jednak pamiętać, że obsługa wyjątków powinna - jak sama nazwa wskazuje - być raczej wyjątkiem niż regułą w projekcie oprogramowania. Jeśli masz aplikację, która generuje tak wiele wyjątków na sekundę, że wpływa to na wydajność, a jest to nadal uważane za normalne działanie, powinieneś raczej pomyśleć o zrobieniu rzeczy inaczej.

Wyjątki to świetny sposób, aby uczynić kod bardziej czytelnym, usuwając z drogi cały ten niezgrabny kod obsługi błędów, ale gdy tylko staną się częścią normalnego przepływu programu, stają się naprawdę trudne do naśladowania. Pamiętaj, że a throwjest prawie goto catchw przebraniu.

Philipp
źródło
-1 pytanie w obecnym kształcie: „czy to nadal prawda dla C ++ 98”, które z pewnością nie zależy od kompilatora. również ta odpowiedź throw new Exceptionto Java-ism. z zasady nigdy nie należy rzucać wskazówkami.
Pozdrawiam i hth. - Alf
1
czy norma 98 dokładnie dyktuje, w jaki sposób mają być realizowane wyjątki?
thecoshman
6
C ++ 98 to standard ISO, a nie kompilator. Jest wiele kompilatorów, które ją implementują.
Philipp
3
@thecoshman: Nie. Standard C ++ nie mówi nic o tym, jak cokolwiek powinno być zaimplementowane (z możliwym wyjątkiem części standardu „Limity implementacji”).
In silico
2
@Insilico mogę tylko wyciągnąć logiczny wniosek, że (szokująco) zdefiniowano implementację (odczyt, specyficzny dla kompilatora) sposób działania wyjątków.
thecoshman
4

Tak, ale to nie ma znaczenia. Czemu?
Przeczytaj to:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

Zasadniczo mówi to, że używanie wyjątków, takich jak opisane przez Alexandrescu (spowolnienie 50x, ponieważ używają catchjako else), jest po prostu błędne. Biorąc to pod uwagę, dla ppl, którzy lubią to robić w ten sposób, chciałbym C ++ 22 :) dodałby coś takiego:
(zauważ, że musiałby to być język podstawowy, ponieważ jest to w zasadzie kompilator generujący kod z istniejącego)

result = attempt<lexical_cast<int>>("12345");  //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...     
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
     int x = result.get(); // or result.result;
}
else 
{
     // even possible to see what is the exception that would have happened in original function
     switch (result.exception_type())
     //...

}

PS również zauważ, że nawet jeśli wyjątki są tak wolne ... to nie jest problem, jeśli nie spędzasz dużo czasu w tej części kodu podczas wykonywania ... Na przykład, jeśli dzielenie float jest wolne i zrobisz to 4x szybciej to nie ma znaczenia, jeśli poświęcasz 0,3% swojego czasu na podział PR ...

NoSenseEtAl
źródło
0

Tak jak in silico powiedział, że jego implementacja jest zależna, ale generalnie wyjątki są uważane za powolne dla każdej implementacji i nie powinny być używane w kodzie wymagającym dużej wydajności.

EDYCJA: Nie mówię, że w ogóle ich nie używaj, ale w przypadku kodu wymagającego dużej wydajności najlepiej ich unikać.

Chris McCabe
źródło
9
W najlepszym przypadku jest to bardzo uproszczony sposób spojrzenia na wydajność wyjątków. Na przykład GCC używa implementacji „zerowego kosztu”, w której nie ma wpływu na wydajność, jeśli nie są zgłaszane żadne wyjątki. Wyjątki są przeznaczone dla wyjątkowych (tj. Rzadkich) okoliczności, więc nawet jeśli są powolne według jakiejś metryki, to wciąż nie jest wystarczający powód, aby ich nie używać.
In silico
@insilico, jeśli spojrzysz na to, dlaczego powiedziałem, nie powiedziałem, aby nie używać wyjątków kropka. Podałem kod wymagający dużej wydajności, to jest dokładna ocena, głównie pracuję z gpgpus i zostałbym nakręcony, gdybym użył wyjątków.
Chris McCabe,