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?
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?
Odpowiedzi:
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ą:if
gdy wystąpi wyjątekKoszt nie jest jednak trywialny do zmierzenia:
dynamic_cast
test 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ż
if
generalnie 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::find
nie powinno się rzucać, ale nie przeszkadza mimap::find
zwracanie a,checked_ptr
któ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.źródło
abort
umożliwi zmierzenie rozmiaru pliku binarnego i sprawdzenie, czy czas ładowania / i-cache zachowują się podobnie. Oczywiście lepiej nie uderzać w żaden zabort
...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
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
throw
może to zająć Very Long Time ™ w porównaniu np. Zint
zadaniem, 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, orazpodejś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
try
bloki, 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 dlatry
- 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,
Na przykład:
Koszty utrzymania wynikające z różnych stylów programowania (poprawność)
Nadmiarowe
if
sprawdzanie 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ć:
Część kodu P jest przeznaczona do uzyskiwania zasobów lub obliczania pewnych informacji.
Kod wywołujący C powinien był sprawdzić powodzenie / niepowodzenie, ale tak nie jest.
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ę
Fallow
wymagającą „możliwego” wyniku funkcji. Podobna klasa o nazwieoptional
jest teraz oferowana w bibliotece Boost. I możesz łatwo zaimplementowaćOptional
klasę samodzielnie, używającstd::vector
jako 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ć
if
sprawdzanie 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?
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
noexcept
sł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.
źródło
longjmp
przejść do programu obsługi.try..finally
Konstrukt może być realizowany bez odwracania stosu. F #, C # i Java są implementowanetry..finally
bez używania rozwijania stosu. Ty tylkolongjmp
do przewodnika (jak już wyjaśniłem).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.
źródło
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
throw
jest prawiegoto catch
w przebraniu.źródło
throw new Exception
to Java-ism. z zasady nigdy nie należy rzucać wskazówkami.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ą
catch
jakoelse
), 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 ...
źródło
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ć.
źródło