W Javie używanie rzucania / łapania jako części logiki, gdy tak naprawdę nie ma błędu, jest ogólnie złym pomysłem (częściowo), ponieważ rzucanie i wychwytywanie wyjątku jest kosztowne, a wykonywanie go wielokrotnie w pętli jest zwykle znacznie wolniejsze niż inne struktury kontrolne, które nie wymagają zgłaszania wyjątków.
Moje pytanie brzmi: czy koszty ponoszone są w samym rzucie / wyłapaniu, czy podczas tworzenia obiektu wyjątku (ponieważ otrzymuje on wiele informacji o środowisku wykonawczym, w tym stos wykonywania)?
Innymi słowy, jeśli to zrobię
Exception e = new Exception();
ale nie rzucaj, czy to większość kosztu rzucania, czy też obsługa rzutów i łap jest kosztowna?
Nie pytam, czy umieszczenie kodu w bloku try / catch zwiększa koszt wykonania tego kodu, pytam, czy złapanie wyjątku jest kosztowną częścią, czy utworzenie (wywołanie konstruktora) wyjątku jest kosztowną częścią .
Innym sposobem zadawania tego pytania jest to, że jeśli stworzyłem jeden przypadek wyjątku i rzucałem go w kółko, czy byłoby to znacznie szybsze niż tworzenie nowego wyjątku za każdym razem, gdy rzucam?
źródło
Odpowiedzi:
Tworzenie obiektu wyjątku nie jest droższe niż tworzenie innych zwykłych obiektów. Główny koszt jest ukryty w natywnej
fillInStackTrace
metodzie, która przechodzi przez stos wywołań i zbiera wszystkie wymagane informacje do zbudowania śledzenia stosu: klasy, nazwy metod, numery linii itp.Mit o wysokich kosztach wyjątków wynika z faktu, że większość
Throwable
konstruktorów domyślnie dzwonifillInStackTrace
. Istnieje jednak jeden konstruktor, który utworzyThrowable
ślad bez stosu. Umożliwia tworzenie rzutów, które są bardzo szybkie w tworzeniu. Innym sposobem na utworzenie lekkich wyjątków jest zastąpieniefillInStackTrace
.A co z rzuceniem wyjątku?
W rzeczywistości, to zależy od tego, gdzie rzucony wyjątek jest złapany .
Jeśli zostanie przechwycony tą samą metodą (lub ściślej mówiąc, w tym samym kontekście, ponieważ kontekst może obejmować kilka metod ze względu na wstawianie), to
throw
jest tak szybki i prosty jakgoto
(oczywiście po kompilacji JIT).Jeśli jednak
catch
blok znajduje się gdzieś głębiej na stosie, JVM musi rozwinąć ramki stosu, co może potrwać znacznie dłużej. Zajmuje to jeszcze więcej czasu, jeśli w grę wchodząsynchronized
bloki lub metody, ponieważ odwijanie oznacza zwolnienie monitorów należących do usuniętych ramek stosu.Mógłbym potwierdzić powyższe stwierdzenia odpowiednimi testami porównawczymi, ale na szczęście nie muszę tego robić, ponieważ wszystkie aspekty zostały już doskonale omówione w stanowisku inżyniera wydajności HotSpot Alexeya Shipileva: Wyjątkowa wydajność wyjątku Lil .
źródło
Pierwsza operacja w większości
Throwable
konstruktorów jest wypełnienie śladu stosu, czyli tam, gdzie jest większość kosztów.Istnieje jednak chroniony konstruktor z flagą, aby wyłączyć śledzenie stosu. Ten konstruktor jest również dostępny podczas rozszerzania
Exception
. Jeśli utworzysz niestandardowy typ wyjątku, możesz uniknąć tworzenia śladu stosu i uzyskać lepszą wydajność kosztem mniejszej ilości informacji.Jeśli utworzysz pojedynczy wyjątek dowolnego typu w zwykły sposób, możesz go ponownie rzucić wiele razy, bez narzutu związanego z wypełnianiem śladu stosu. Jednak ślad stosu będzie odzwierciedlał to, gdzie został zbudowany, a nie gdzie został rzucony w konkretnym przypadku.
Obecne wersje Java próbują zoptymalizować tworzenie śledzenia stosu. Wywoływany jest kod macierzysty w celu wypełnienia śladu stosu, który zapisuje ślad w lżejszej natywnej strukturze. Odpowiednie
StackTraceElement
obiekty Java są tworzone leniwie z tego rekordu tylko wtedygetStackTrace()
, gdyprintStackTrace()
wywoływane są metody, lub inne metody wymagające śledzenia.Jeśli wyeliminujesz generowanie śladu stosu, drugim głównym kosztem jest odwrócenie stosu między rzutem a złapaniem. Im mniej ramek pośrednich napotkanych przed wychwyceniem wyjątku, tym szybciej będzie to możliwe.
Zaprojektuj swój program tak, aby wyjątki były zgłaszane tylko w naprawdę wyjątkowych przypadkach, a optymalizacje takie jak te są trudne do uzasadnienia.
źródło
Tutaj jest dobry opis wyjątków.
http://shipilev.net/blog/2014/exceptional-performance/
Konkluzja jest taka, że konstrukcja śladu stosu i rozwijanie stosu są drogimi częściami. Poniższy kod wykorzystuje funkcję, w
1.7
której możemy włączać i wyłączać ślady stosu. Następnie możemy to wykorzystać, aby zobaczyć, jakie koszty mają różne scenariuszePoniżej przedstawiono terminy tworzenia samego obiektu. Dodałem
String
tutaj, abyś mógł zobaczyć, że bez zapisania stosu prawie nie ma różnicy w tworzeniuJavaException
obiektu iString
. Przy włączonym zapisywaniu stosu różnica jest dramatyczna, tzn. Co najmniej o jeden rząd wielkości wolniejsza.Poniżej pokazano, ile czasu zajęło mu powrót z rzutu na określonej głębokości milion razy.
Poniżej prawie na pewno rażące jest uproszczenie ...
Jeśli weźmiemy pod uwagę głębokość 16 przy zapisywaniu stosu, wówczas tworzenie obiektów zajmuje około ~ 40% czasu, rzeczywisty ślad stosu stanowi zdecydowaną większość tego. ~ 93% tworzenia wystąpienia obiektu JavaException wynika z pobrania śladu stosu. Oznacza to, że odwijanie stosu w tym przypadku zajmuje pozostałe 50% czasu.
Kiedy wyłączamy konta tworzenia obiektów śledzenia stosu dla znacznie mniejszej części, tj. 20%, a rozwijanie stosu stanowi teraz 80% czasu.
W obu przypadkach odwijanie stosu zajmuje dużą część całkowitego czasu.
Ramki stosu w tym przykładzie są małe w porównaniu do tego, co zwykle można znaleźć.
Możesz zajrzeć do kodu bajtowego za pomocą javap
tj. dotyczy metody 4 ...
źródło
Utworzenie śledzenia
Exception
zenull
stosem zajmuje tyle samo czasu, co razemthrow
itry-catch
blok. Jednak wypełnienie śladu stosu zajmuje średnio 5 razy dłużej .Stworzyłem następujący test porównawczy, aby zademonstrować wpływ na wydajność. Dodałem
-Djava.compiler=NONE
opcję do konfiguracji uruchamiania, aby wyłączyć optymalizację kompilatora. Aby zmierzyć wpływ budowy śladu stosu, rozszerzyłemException
klasę, aby skorzystać z konstruktora bez stosu:Kod testu porównawczego jest następujący:
Wynik:
Oznacza to, że utworzenie a
NoStackException
jest w przybliżeniu tak drogie, jak wielokrotne rzucanie nimException
. Pokazuje także, że utworzenieException
i wypełnienie śladu stosu trwa około 4x dłużej.źródło
Ta część pytania ...
Wygląda na pytanie, czy utworzenie wyjątku i buforowanie go gdzieś poprawia wydajność. Tak. Jest to to samo, co wyłączenie stosu zapisywanego podczas tworzenia obiektu, ponieważ zostało to już zrobione.
To są czasy, które mam, proszę przeczytać zastrzeżenie po tym ...
Oczywiście problemem jest to, że ślad stosu wskazuje teraz, gdzie utworzono instancję obiektu, a nie skąd został on rzucony.
źródło
Wykorzystując odpowiedź @ AustinD jako punkt wyjścia, wprowadziłem kilka poprawek. Kod na dole.
Oprócz dodawania przypadku wielokrotnego zgłaszania jednego wystąpienia wyjątku, wyłączyłem również optymalizację kompilatora, aby uzyskać dokładne wyniki wydajności. Dodałem
-Djava.compiler=NONE
do argumentów VM, zgodnie z tą odpowiedzią . (W Eclipse edytuj Uruchom konfigurację → Argumenty, aby ustawić ten argument maszyny wirtualnej)Wyniki:
Tak więc utworzenie wyjątku kosztuje około 5 razy więcej niż rzucanie + łapanie go. Zakładając, że kompilator nie optymalizuje dużej części kosztów.
Dla porównania, oto ten sam przebieg testowy bez wyłączania optymalizacji:
Kod:
źródło