Ciągle widzę, jak ludzie mówią, że wyjątki są powolne, ale nigdy nie widzę żadnego dowodu. Zamiast więc pytać, czy tak jest, zapytam, jak wyjątki działają za kulisami, abym mógł podejmować decyzje, kiedy ich używać i czy są powolne.
Z tego, co wiem, wyjątki są takie same, jak wykonywanie kilku zwrotów, z wyjątkiem tego, że po każdym powrocie sprawdza również, czy musi zrobić kolejny, czy zatrzymać. Jak sprawdza, kiedy przestać wracać? Wydaje mi się, że istnieje drugi stos, który przechowuje typ wyjątku i lokalizację stosu, a następnie wraca, dopóki tam nie dotrze. Domyślam się również, że jedyny przypadek, gdy ten drugi stack zostanie dotknięty, to rzut i każdy try / catch. AFAICT implementacja podobnego zachowania z kodami powrotu zajęłaby tyle samo czasu. Ale to tylko przypuszczenie, więc chcę wiedzieć, co naprawdę się dzieje.
Jak naprawdę działają wyjątki?
Odpowiedzi:
Zamiast zgadywać, zdecydowałem się przyjrzeć wygenerowanemu kodowi z małym fragmentem kodu C ++ i nieco starą instalacją Linuksa.
Skompilowałem go za pomocą
g++ -m32 -W -Wall -O3 -save-temps -c
i spojrzałem na wygenerowany plik zespołu._ZN11MyExceptionD1Ev
jestMyException::~MyException()
, więc kompilator zdecydował, że potrzebuje nieliniowej kopii destruktora.Niespodzianka! W normalnej ścieżce kodu nie ma żadnych dodatkowych instrukcji. Kompilator zamiast tego wygenerował dodatkowe bloki kodu korekcji poza linią, do których odwołuje się tabela na końcu funkcji (która w rzeczywistości jest umieszczona w oddzielnej sekcji pliku wykonywalnego). Cała praca jest wykonywana za kulisami przez bibliotekę standardową, opartą na tych tabelach (
_ZTI11MyException
jesttypeinfo for MyException
).OK, to nie było dla mnie niespodzianką, już wiedziałem, jak ten kompilator to zrobił. Kontynuując wyjście z montażu:
Tutaj widzimy kod do zgłaszania wyjątku. Chociaż nie było dodatkowego narzutu po prostu dlatego, że mógł zostać wyrzucony wyjątek, oczywiście rzucanie i przechwytywanie wyjątku wiąże się z dużym obciążeniem. Większość z nich jest ukryta w środku
__cxa_throw
, co musi:Porównaj to z kosztem zwykłego zwrotu wartości, a zobaczysz, dlaczego wyjątki powinny być używane tylko w przypadku wyjątkowych zwrotów.
Na koniec pozostała część pliku zespołu:
Dane typeinfo.
Jeszcze więcej tabel obsługi wyjątków i różne dodatkowe informacje.
Tak więc wniosek, przynajmniej dla GCC na Linuksie: kosztem jest dodatkowa przestrzeń (dla programów obsługi i tabel) niezależnie od tego, czy wyjątki są wyrzucane, czy nie, plus dodatkowy koszt parsowania tabel i wykonywania programów obsługi, gdy zgłaszany jest wyjątek. Jeśli używasz wyjątków zamiast kodów błędów, a błąd jest rzadki, może być szybszy , ponieważ nie masz już narzutu związanego z testowaniem błędów.
Jeśli chcesz uzyskać więcej informacji, w szczególności co robią wszystkie
__cxa_
funkcje, zobacz oryginalną specyfikację, z której pochodzą:źródło
Wyjątkiem jest powolny było prawdziwe w dawnych czasach.
W większości współczesnych kompilatorów nie jest to już prawdą.
Uwaga: tylko dlatego, że mamy wyjątki, nie oznacza, że nie używamy również kodów błędów. Jeśli błąd można obsłużyć lokalnie, użyj kodów błędów. Kiedy błędy wymagają więcej kontekstu do korekty, użyj wyjątków: napisałem to znacznie bardziej elokwentnie tutaj: Jakie zasady kierują twoją polityką obsługi wyjątków?
Koszt kodu obsługi wyjątków, gdy nie są używane żadne wyjątki, jest praktycznie zerowy.
Gdy zostanie zgłoszony wyjątek, część pracy jest wykonywana.
Ale musisz to porównać z kosztem zwracania kodów błędów i sprawdzania ich aż do momentu, w którym błąd można obsłużyć. Zarówno pisanie, jak i konserwacja bardziej czasochłonne.
Jest też jedna pułapka dla nowicjuszy:
chociaż obiekty Exception mają być małe, niektórzy ludzie wkładają do nich wiele rzeczy. Wtedy masz koszt kopiowania obiektu wyjątku. Rozwiązanie jest dwojakie:
Moim zdaniem założyłbym się, że ten sam kod z wyjątkami jest albo bardziej wydajny, albo przynajmniej tak samo porównywalny jak kod bez wyjątków (ale ma cały dodatkowy kod do sprawdzania wyników błędów funkcji). Pamiętaj, że nie dostajesz nic za darmo, kompilator generuje kod, który powinieneś był napisać w pierwszej kolejności, aby sprawdzić kody błędów (a zwykle kompilator jest znacznie wydajniejszy niż człowiek).
źródło
Istnieje wiele sposobów implementacji wyjątków, ale zazwyczaj będą one polegać na pewnym podstawowym wsparciu systemu operacyjnego. W systemie Windows jest to ustrukturyzowany mechanizm obsługi wyjątków.
Istnieje przyzwoita dyskusja na temat szczegółów projektu kodu: jak kompilator C ++ implementuje obsługę wyjątków
Narzut wyjątków występuje, ponieważ kompilator musi wygenerować kod, aby śledzić, które obiekty muszą zostać zniszczone w każdej ramce stosu (lub dokładniej w zakresie), jeśli wyjątek propaguje się poza ten zakres. Jeśli funkcja nie ma zmiennych lokalnych na stosie, które wymagają wywołania destruktorów, nie powinna mieć obniżonej wydajności w związku z obsługą wyjątków.
Użycie kodu powrotu może rozwinąć tylko jeden poziom stosu na raz, podczas gdy mechanizm obsługi wyjątków może przeskoczyć znacznie dalej w dół stosu w jednej operacji, jeśli nie ma nic do zrobienia w pośrednich ramkach stosu.
źródło
Matt Pietrek napisał doskonały artykuł o obsłudze wyjątków strukturalnych w Win32 . Chociaż ten artykuł został pierwotnie napisany w 1997 r., Nadal obowiązuje (ale oczywiście dotyczy tylko systemu Windows).
źródło
W tym artykule przeanalizowano ten problem i zasadniczo stwierdzono, że w praktyce wyjątki są kosztowne w czasie wykonywania, chociaż koszt jest dość niski, jeśli wyjątek nie zostanie zgłoszony. Dobry artykuł, polecam.
źródło
Mój przyjaciel napisał kilka lat temu, jak Visual C ++ radzi sobie z wyjątkami.
http://www.xyzw.de/c160.html
źródło
Wszystkie dobre odpowiedzi.
Pomyśl także o tym, o ile łatwiej jest debugować kod, który wykonuje „jeśli sprawdza” jako bramy na początku metod, zamiast zezwalać kodowi na zgłaszanie wyjątków.
Moją dewizą jest to, że łatwo jest napisać działający kod. Najważniejsze jest napisanie kodu dla następnej osoby, która go obejrzy. W niektórych przypadkach to Ty za 9 miesięcy i nie chcesz przeklinać swojego imienia!
źródło