Więc wiem, że try / catch dodaje trochę narzutów i dlatego nie jest dobrym sposobem kontrolowania przepływu procesu, ale skąd się bierze ten narzut i jaki jest jego rzeczywisty wpływ?
Odpowiedzi:
Nie jestem ekspertem w implementacji języków (więc traktuj to z przymrużeniem oka), ale myślę, że jednym z największych kosztów jest rozwijanie stosu i przechowywanie go w celu śledzenia stosu. Podejrzewam, że dzieje się tak tylko wtedy, gdy wyjątek jest rzucany (ale nie wiem), a jeśli tak, byłby to przyzwoity ukryty koszt za każdym razem, gdy rzucany jest wyjątek ... więc to nie tak, że skaczesz z jednego miejsca w kodzie do innego dużo się dzieje.
Nie wydaje mi się, aby to był problem, o ile używasz wyjątków dla WYJĄTKOWEGO zachowania (a więc nie jest to typowa, oczekiwana ścieżka przez program).
Trzy kwestie do omówienia:
Po pierwsze, faktyczne posiadanie bloków try-catch w kodzie jest niewielkie lub ŻADNE. Nie powinno to być brane pod uwagę, próbując uniknąć umieszczania ich w aplikacji. Uderzenie związane z wydajnością pojawia się tylko wtedy, gdy zostanie zgłoszony wyjątek.
Gdy wyjątek jest rzucany oprócz operacji rozwijania stosu itp., Które mają miejsce, o których wspominali inni, powinieneś być świadomy, że dzieje się cała masa rzeczy związanych ze środowiskiem uruchomieniowym / odbiciami, aby wypełnić elementy klasy wyjątku, takie jak ślad stosu obiekt i różne składowe typu itp.
Uważam, że jest to jeden z powodów, dla których ogólna rada, jeśli zamierzasz throw;
ponownie rzucić wyjątek, polega na tym, aby po prostu zamiast ponownie wyrzucić wyjątek lub utworzyć nowy, ponieważ w tych przypadkach wszystkie te informacje o stosie są ponownie gromadzone, podczas gdy w prostym Rzuć to wszystko jest zachowane.
throw new Exception("Wrapping layer’s error", ex);
Czy pytasz o obciążenie związane z używaniem try / catch / final, gdy wyjątki nie są generowane, czy też o obciążenie związane z używaniem wyjątków do kontrolowania przepływu procesu? To ostatnie jest nieco podobne do używania laski dynamitu do zapalania świeczki urodzinowej malucha, a związane z tym narzuty przypadają na następujące obszary:
Możesz spodziewać się dodatkowych błędów stron z powodu zgłoszonego wyjątku, który uzyskuje dostęp do kodu nierezydentnego i danych, których normalnie nie ma w zestawie roboczym aplikacji.
oba powyższe elementy zwykle uzyskują dostęp do „zimnego” kodu i danych, więc możliwe są twarde błędy strony, jeśli w ogóle masz niedobór pamięci:
Jeśli chodzi o rzeczywisty wpływ kosztów, może się to znacznie różnić w zależności od tego, co jeszcze dzieje się w Twoim kodzie w danym momencie. Jon Skeet ma tutaj dobre podsumowanie z kilkoma przydatnymi linkami. Zwykle zgadzam się z jego stwierdzeniem, że jeśli dojdziesz do punktu, w którym wyjątki znacząco szkodzą Twojemu występowi, masz problemy z wykorzystaniem wyjątków wykraczających poza samo wykonanie.
Z mojego doświadczenia wynika, że największym narzutem jest rzucenie wyjątku i obsługa go. Kiedyś pracowałem nad projektem, w którym kod podobny do poniższego był używany do sprawdzenia, czy ktoś ma prawo edytować jakiś obiekt. Ta metoda HasRight () była używana wszędzie w warstwie prezentacji i często była wywoływana dla setek obiektów.
bool HasRight(string rightName, DomainObject obj) {
try {
CheckRight(rightName, obj);
return true;
}
catch (Exception ex) {
return false;
}
}
void CheckRight(string rightName, DomainObject obj) {
if (!_user.Rights.Contains(rightName))
throw new Exception();
}
Gdy baza testowa została wypełniona danymi testowymi, doprowadziło to do bardzo widocznego spowolnienia podczas otwierania nowych formularzy itp.
Więc refaktoryzowałem to w następujący sposób, który - według późniejszych szybkich i brudnych pomiarów - jest o około 2 rzędy wielkości szybszy:
bool HasRight(string rightName, DomainObject obj) {
return _user.Rights.Contains(rightName);
}
void CheckRight(string rightName, DomainObject obj) {
if (!HasRight(rightName, obj))
throw new Exception();
}
Krótko mówiąc, użycie wyjątków w normalnym przebiegu procesu jest o około dwa rzędy wielkości wolniejsze niż użycie podobnego przebiegu procesu bez wyjątków.
W przeciwieństwie do powszechnie akceptowanych teorii, try
/ catch
może mieć znaczący wpływ na wydajność i to niezależnie od tego, czy wyjątek jest zgłaszany, czy nie!
Te pierwsze zostały omówione w kilku postach na blogach przez MVP firmy Microsoft na przestrzeni lat i wierzę, że można je łatwo znaleźć, ale StackOverflow tak bardzo dba o zawartość, więc podam linki do niektórych z nich jako uzupełnienie :
try
/ catch
/finally
( i część druga ), autorstwa Petera Ritchiego, bada optymalizacje, któretry
/catch
/finally
wyłączają (i przejdę dalej z cytatami ze standardu)Parse
vs TryParse
vsConvertTo
Ian Huff twierdzi bezczelnie, że „obsługa wyjątków jest bardzo powolny” i dowodzi tego punktu przez pitInt.Parse
iInt.TryParse
przed siebie ... Dla każdego, kto upiera się, żeTryParse
używatry
/catch
za kulisami, to powinna rzucić trochę światła!Jest też ta odpowiedź, która pokazuje różnicę między zdemontowanym kodem z użyciem i bez użycia try
/ catch
.
Wydaje się więc oczywiste, że nie jest to obciążenie, które jest rażąco obserwowalne w generacji kodu, a nawet wydaje się, że narzut być potwierdzony przez ludzi ceniących Microsoft! A jednak powtarzam internet ...
Tak, istnieją dziesiątki dodatkowych instrukcji MSIL dla jednej trywialnej linii kodu, a to nawet nie obejmuje wyłączonych optymalizacji, więc technicznie jest to mikro-optymalizacja.
Opublikowałem odpowiedź lata temu, która została usunięta, ponieważ dotyczyła produktywności programistów (optymalizacja makro).
Jest to niefortunne, ponieważ żadna oszczędność kilku nanosekund tu i ówdzie czasu procesora prawdopodobnie nadrobi wiele godzin ręcznej optymalizacji przez ludzi. Za co Twój szef płaci więcej: godzinę Twojego czasu czy godzinę z uruchomionym komputerem? W którym momencie wyciągamy wtyczkę i przyznajemy, że czas po prostu kupić szybszy komputer ?
Oczywiście powinniśmy optymalizować nasze priorytety , a nie tylko nasz kod! W swojej ostatniej odpowiedzi oparłem się na różnicach między dwoma fragmentami kodu.
Używanie try
/ catch
:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
Nie używam try
/ catch
:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
Rozważ z perspektywy programisty ds. Utrzymania ruchu, który jest bardziej narażony na marnowanie czasu, jeśli nie na profilowanie / optymalizację (omówione powyżej), co prawdopodobnie nie byłoby konieczne, gdyby nie problem try
/ catch
, a następnie przewijanie kod źródłowy ... Jeden z nich ma cztery dodatkowe wiersze standardowych śmieci!
Ponieważ coraz więcej pól jest wprowadzanych do klasy, wszystkie te standardowe śmieci gromadzą się (zarówno w kodzie źródłowym, jak i zdemontowanym) znacznie powyżej rozsądnych poziomów. Cztery dodatkowe linie na pole i zawsze są takie same ... Czy nie uczono nas, jak unikać powtarzania się? Przypuszczam, że moglibyśmy ukryć try
/ catch
za jakąś domową abstrakcją, ale ... wtedy równie dobrze moglibyśmy po prostu uniknąć wyjątków (tj. Używać Int.TryParse
).
Nie jest to nawet złożony przykład; Widziałem próby tworzenia instancji nowych klas w try
/ catch
. Weź pod uwagę, że cały kod wewnątrz konstruktora może zostać zdyskwalifikowany z pewnych optymalizacji, które w przeciwnym razie zostałyby automatycznie zastosowane przez kompilator. Czy jest lepszy sposób, aby rozwinąć teorię, że kompilator działa wolno , w przeciwieństwie do tego, że kompilator robi dokładnie to, co mu każą ?
Zakładając, że wspomniany konstruktor zgłosi wyjątek, aw rezultacie zostanie wywołany jakiś błąd, słaby programista musi go wyśledzić. To może nie być takie łatwe zadanie, ponieważ w przeciwieństwie do kodu spaghetti z koszmaru goto , try
/ catch
może powodować bałagan w trzech wymiarach , ponieważ może przesunąć się w górę stosu nie tylko do innych części tej samej metody, ale także innych klas i metod , a wszystko to będzie obserwowane przez programistę utrzymania ruchu, na własnej skórze ! Jednak mówi się nam, że „goto jest niebezpieczny”, heh!
Na koniec wspomnę, try
/ catch
ma swoją zaletę, która polega na tym, że został zaprojektowany do wyłączania optymalizacji ! Jest to pomoc w debugowaniu ! Do tego został zaprojektowany i do tego powinien służyć jako ...
Myślę, że to też jest pozytywny punkt. Można go użyć do wyłączenia optymalizacji, które w przeciwnym razie mogłyby sparaliżować bezpieczne, rozsądne algorytmy przekazywania wiadomości dla aplikacji wielowątkowych oraz do wychwytywania możliwych warunków wyścigu;) To jedyny scenariusz, jaki przychodzi mi do głowy, aby użyć try / catch. Nawet to ma alternatywy.
Co zrobić optymalizacje try
, catch
i finally
wyłączyć?
ZNANY JAKO
Jak są try
, catch
i finally
użyteczny jako debugowania słuchowych?
są barierami pisania. Wynika to ze standardu:
12.3.3.13 Instrukcje try-catch
Aby uzyskać zestawienie stmt formularza:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n
- Określony stan przypisania v na początku bloku try jest taki sam, jak określony stan przypisania v na początku stmt .
- Określony stan przypisania v na początku catch-block-i (dla dowolnego i ) jest taki sam, jak określony stan przypisania v na początku stmt .
- Określony stan przypisania v w punkcie końcowym stmt jest definitywnie przypisany wtedy (i tylko wtedy) v jest definitywnie przypisany w punkcie końcowym try-block i każdym catch-block-i (dla każdego i od 1 do n ).
Innymi słowy, na początku każdego try
stwierdzenia:
try
instrukcji muszą być zakończone, co wymaga blokady wątku na początku, co czyni go użytecznym do debugowania warunków wyścigu!try
instrukcjąPodobna historia dotyczy każdego catch
stwierdzenia; załóżmy, że w swojej try
instrukcji (lub konstruktorze lub funkcji, którą wywołuje itp.) przypisujesz do tej bezsensownej w inny sposób zmiennej (powiedzmy garbage=42;
), kompilator nie może wyeliminować tej instrukcji, bez względu na to, jak nieistotne jest to dla obserwowalnego zachowania programu . Zadanie musi zostać zakończone przed wprowadzeniem catch
bloku.
Za to, co jest warte, finally
opowiada podobnie poniżającą historię:
12.3.3.14 Instrukcje próbujące
Aby uzyskać instrukcję try stmt w postaci:
try try-block finally finally-block
• Określony stan przypisania v na początku bloku try jest taki sam, jak określony stan przypisania v na początku stmt .
• Określony stan przypisania v na początku końcowego bloku jest taki sam, jak określony stan przypisania v na początku stmt .
• Określony stan przypisania v w punkcie końcowym stmt jest definitywnie przypisany wtedy (i tylko wtedy) albo: o v jest ostatecznie przypisany w punkcie końcowym bloku try-block o vjest definitywnie przypisany w punkcie końcowym bloku końcowego Jeśli zostanie wykonany transfer przepływu sterowania (taki jak instrukcja goto ), który zaczyna się w bloku try-block i kończy poza blokiem try , wówczas v jest również uważane za ostatecznie przypisane do tego sterowanie przepływem, jeśli v jest ostatecznie przypisane w punkcie końcowym końcowego bloku . (Nie dzieje się tak tylko wtedy, gdy - jeśli v jest ostatecznie przypisane z innego powodu w tym transferze przepływu sterowania, to nadal jest uważane za ostatecznie przypisane).
12.3.3.15 Instrukcje try-catch-last
Określona analiza przyporządkowania dla try - catch - na końcu zestawienie postaci:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block
jest wykonywana tak, jakby instrukcja była instrukcją try - last zawierającą instrukcję try - catch :
try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block
Nie wspominając już o tym, że jest to wewnątrz często wywoływanej metody, może to wpływać na ogólne zachowanie aplikacji.
Na przykład uważam, że użycie Int32.Parse w większości przypadków jest złą praktyką, ponieważ rzuca wyjątki dla czegoś, co można łatwo złapać w inny sposób.
Podsumowując wszystko, co tutaj napisano:
1) Użyj bloków try..catch, aby wyłapać nieoczekiwane błędy - prawie bez spadku wydajności.
2) Nie używaj wyjątków dla wyjątkowych błędów, jeśli możesz tego uniknąć.
Napisałem o tym artykuł jakiś czas temu, ponieważ w tamtym czasie wiele osób o to pytało. Możesz go znaleźć i kod testowy pod adresem http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx .
W rezultacie istnieje niewielki narzut na blok try / catch, ale jest tak mały, że należy go zignorować. Jeśli jednak uruchamiasz bloki try / catch w pętlach, które są wykonywane miliony razy, możesz rozważyć przeniesienie bloku poza pętlę, jeśli to możliwe.
Kluczowy problem z wydajnością bloków try / catch polega na tym, że faktycznie przechwytujesz wyjątek. Może to spowodować zauważalne opóźnienie w aplikacji. Oczywiście, gdy coś pójdzie nie tak, większość programistów (i wielu użytkowników) uznaje przerwę za wyjątek, który wkrótce się wydarzy! Kluczowe jest tutaj, aby nie używać obsługi wyjątków do normalnych operacji. Jak sama nazwa wskazuje, są wyjątkowe i należy zrobić wszystko, co w ich mocy, aby nie zostały wyrzucone. Nie należy ich używać jako części oczekiwanego przepływu programu, który działa poprawnie.
W zeszłym roku zrobiłem wpis na blogu na ten temat. Sprawdź to. Podsumowując, blok próbny prawie nie kosztuje, jeśli nie wystąpi żaden wyjątek - a na moim laptopie wyjątek wynosił około 36 μs. To może być mniej niż się spodziewałeś, ale pamiętaj, że te wyniki są na płytkim stosie. Poza tym pierwsze wyjątki są naprawdę powolne.
try
/ catch
za dużo? Heh heh), ale wydaje się, że spierasz się ze specyfikacją językową i kilkoma MVP MS, którzy również pisali blogi na ten temat, dostarczając pomiarów Wręcz przeciwnie do twojej rady ... Jestem otwarty na sugestię, że moje badania są błędne, ale będę musiał przeczytać twój wpis na blogu, aby zobaczyć, co mówi.
try-catch
bloków i tryparse()
metod, ale koncepcja jest taka sama.
Znacznie łatwiej jest pisać, debugować i utrzymywać kod wolny od komunikatów o błędach kompilatora, komunikatów ostrzegawczych analizy kodu i rutynowych akceptowanych wyjątków (szczególnie wyjątków, które są zgłaszane w jednym miejscu i akceptowane w innym). Ponieważ jest łatwiejszy, kod będzie średnio lepiej napisany i mniej błędny.
Dla mnie ten programista i narzut jakości jest głównym argumentem przeciwko używaniu try-catch do przepływu procesów.
Obciążenie komputera związane z wyjątkami jest nieistotne w porównaniu i zwykle niewielkie, jeśli chodzi o zdolność aplikacji do spełniania wymagań wydajnościowych w świecie rzeczywistym.
Bardzo podoba mi się wpis na blogu Hafthora i aby dodać moje dwa centy do tej dyskusji, chciałbym powiedzieć, że zawsze było mi łatwo, aby WARSTWA DANYCH rzucała tylko jeden typ wyjątku (DataAccessException). W ten sposób mój POZIOM BIZNESOWY wie, jakiego wyjątku się spodziewać i go łapie. Następnie w zależności od dalszych reguł biznesowych (np. Jeśli mój obiekt biznesowy uczestniczy w przepływie pracy itp.), Mogę zgłosić nowy wyjątek (BusinessObjectException) lub kontynuować bez ponownego / wrzucania.
Powiedziałbym, że nie wahaj się i używaj try..catch, kiedy jest to konieczne, i używaj go mądrze!
Na przykład ta metoda uczestniczy w przepływie pracy ...
Komentarze?
public bool DeleteGallery(int id)
{
try
{
using (var transaction = new DbTransactionManager())
{
try
{
transaction.BeginTransaction();
_galleryRepository.DeleteGallery(id, transaction);
_galleryRepository.DeletePictures(id, transaction);
FileManager.DeleteAll(id);
transaction.Commit();
}
catch (DataAccessException ex)
{
Logger.Log(ex);
transaction.Rollback();
throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
}
}
}
catch (DbTransactionException ex)
{
Logger.Log(ex);
throw new BusinessObjectException("Cannot delete gallery.", ex);
}
return true;
}
W książce Programming Languages Pragmatics Michaela L. Scotta możemy przeczytać, że dzisiejsze kompilatory nie dodają żadnego narzutu w typowym przypadku, to znaczy, gdy nie występują żadne wyjątki. Dlatego każda praca jest wykonywana w czasie kompilacji. Ale gdy wyjątek zostanie zgłoszony w czasie wykonywania, kompilator musi przeprowadzić wyszukiwanie binarne, aby znaleźć poprawny wyjątek, a stanie się to dla każdego nowego rzutu, który wykonałeś.
Ale wyjątki są wyjątkami i ten koszt jest całkowicie akceptowalny. Jeśli spróbujesz wykonać obsługę wyjątków bez wyjątków i zamiast tego użyjesz zwrotnych kodów błędów, prawdopodobnie będziesz potrzebować instrukcji if dla każdego podprogramu, co spowoduje obciążenie w czasie rzeczywistym. Wiesz, że instrukcja if jest konwertowana na kilka instrukcji asemblera, które będą wykonywane za każdym razem, gdy wejdziesz do swoich podprogramów.
Przepraszam za mój angielski, mam nadzieję, że ci pomoże. Informacje te opierają się na cytowanej książce. Więcej informacji znajduje się w Rozdziale 8.5 Obsługa wyjątków.
Przeanalizujmy jeden z największych możliwych kosztów bloku try / catch, gdy jest używany tam, gdzie nie powinien być używany:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
A oto ten bez try / catch:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
Nie licząc nieznacznych odstępów, można zauważyć, że te dwa równoważne fragmenty kodu mają prawie dokładnie taką samą długość w bajtach. Ten ostatni zawiera wcięcie mniejsze o 4 bajty. Czy to źle?
Aby dodać zniewagę do obrażeń, uczeń decyduje się na zapętlenie, podczas gdy dane wejściowe mogą być analizowane jako int. Rozwiązaniem bez try / catch może być coś takiego:
while (int.TryParse(...))
{
...
}
Ale jak to wygląda, gdy używasz try / catch?
try {
for (;;)
{
x = int.Parse(...);
...
}
}
catch
{
...
}
Bloki Try / catch to magiczne sposoby na marnowanie wcięć, a wciąż nie wiemy nawet, dlaczego się nie udało! Wyobraź sobie, jak czuje się osoba przeprowadzająca debugowanie, gdy kod nadal jest wykonywany po przekroczeniu poważnej logicznej wady, zamiast zatrzymywania się z ładnym oczywistym błędem wyjątku. Bloki Try / catch to sprawdzanie poprawności danych / oczyszczanie danych przez leniwego człowieka.
Jednym z mniejszych kosztów jest to, że bloki try / catch rzeczywiście wyłączają niektóre optymalizacje: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Myślę, że to też jest pozytywny punkt. Można go użyć do wyłączenia optymalizacji, które w przeciwnym razie mogłyby sparaliżować bezpieczne, rozsądne algorytmy przekazywania wiadomości dla aplikacji wielowątkowych oraz do wychwytywania możliwych warunków wyścigu;) To jedyny scenariusz, jaki przychodzi mi do głowy, aby użyć try / catch. Nawet to ma alternatywy.
Int.Parse
na korzyść Int.TryParse
.