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ą:
ifgdy wystąpi wyjątekKoszt nie jest jednak trywialny do zmierzenia:
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.
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...
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.
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.
Tak, międzynarodowy komitet normalizacyjny C ++ opublikował raport techniczny dotyczący wydajności C ++, TR18015 .
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 .
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ę 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.
„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.
longjmpprzejść do programu obsługi.
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).
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.
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.
throw new Exceptionto 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ą 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 ...
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ć.