EDYCJA 20.11.2009 :
Właśnie czytałem ten artykuł MSDN dotyczący poprawy wydajności kodu zarządzanego i ta część przypomniała mi o tym pytaniu:
Koszt wydajności rzucania wyjątku jest znaczny. Chociaż obsługa wyjątków strukturalnych jest zalecanym sposobem obsługi warunków błędów, upewnij się, że wyjątki są używane tylko w wyjątkowych okolicznościach, gdy wystąpią warunki błędu. Nie używaj wyjątków dla zwykłego przepływu sterowania.
Oczywiście dotyczy to tylko platformy .NET i jest również skierowane do osób tworzących aplikacje o wysokiej wydajności (takich jak ja); więc oczywiście nie jest to uniwersalna prawda. Mimo to jest nas wielu programistów .NET, więc uznałem, że warto to zauważyć.
EDYCJA :
OK, po pierwsze, wyjaśnijmy jedną rzecz: nie mam zamiaru z nikim walczyć o kwestię wydajności. Ogólnie rzecz biorąc, jestem skłonny zgodzić się z tymi, którzy uważają, że przedwczesna optymalizacja jest grzechem. Pozwólcie jednak, że przedstawię dwie kwestie:
Plakat prosi o obiektywne uzasadnienie konwencjonalnej opinii, że wyjątki powinny być używane oszczędnie. Możemy rozmawiać o czytelności i odpowiednim projekcie, ile tylko chcemy; ale są to sprawy subiektywne, z ludźmi gotowymi do kłótni po obu stronach. Myślę, że plakat jest tego świadomy. Faktem jest, że używanie wyjątków do sterowania przepływem programu jest często nieefektywnym sposobem wykonywania pewnych czynności. Nie, nie zawsze , ale często . Dlatego rozsądną radą jest oszczędne stosowanie wyjątków, podobnie jak oszczędne jedzenie czerwonego mięsa lub picie wina.
Istnieje różnica między optymalizacją bez powodu a pisaniem wydajnego kodu. Konsekwencją tego jest to, że istnieje różnica między pisaniem czegoś, co jest solidne, jeśli nie zoptymalizowane, a czymś, co jest po prostu nieefektywne. Czasami myślę, że kiedy ludzie kłócą się o takie rzeczy, jak obsługa wyjątków, tak naprawdę rozmawiają między sobą, ponieważ omawiają zasadniczo różne rzeczy.
Aby zilustrować mój punkt widzenia, rozważ następujące przykłady kodu w języku C #.
Przykład 1: Wykrywanie nieprawidłowych danych wejściowych użytkownika
To jest przykład tego, co nazwałbym nadużyciem wyjątków .
int value = -1;
string input = GetInput();
bool inputChecksOut = false;
while (!inputChecksOut) {
try {
value = int.Parse(input);
inputChecksOut = true;
} catch (FormatException) {
input = GetInput();
}
}
Ten kod jest dla mnie śmieszny. Oczywiście, że działa . Nikt się z tym nie spiera. Ale powinno to być coś takiego:
int value = -1;
string input = GetInput();
while (!int.TryParse(input, out value)) {
input = GetInput();
}
Przykład 2: Sprawdzanie istnienia pliku
Myślę, że ten scenariusz jest w rzeczywistości bardzo powszechny. Z pewnością wielu ludziom wydaje się to o wiele bardziej „akceptowalne”, ponieważ dotyczy operacji we / wy plików:
string text = null;
string path = GetInput();
bool inputChecksOut = false;
while (!inputChecksOut) {
try {
using (FileStream fs = new FileStream(path, FileMode.Open)) {
using (StreamReader sr = new StreamReader(fs)) {
text = sr.ReadToEnd();
}
}
inputChecksOut = true;
} catch (FileNotFoundException) {
path = GetInput();
}
}
Wydaje się to rozsądne, prawda? Próbujemy otworzyć plik; jeśli go tam nie ma, wychwytujemy ten wyjątek i próbujemy otworzyć inny plik ... Co w tym złego?
Nic takiego. Ale rozważ tę alternatywę, która nie rzuca żadnych wyjątków:
string text = null;
string path = GetInput();
while (!File.Exists(path)) path = GetInput();
using (FileStream fs = new FileStream(path, FileMode.Open)) {
using (StreamReader sr = new StreamReader(fs)) {
text = sr.ReadToEnd();
}
}
Oczywiście, gdyby wyniki tych dwóch podejść były faktycznie takie same, byłaby to naprawdę kwestia czysto doktrynalna. Więc spójrzmy. Dla pierwszego przykładu kodu utworzyłem listę 10000 losowych ciągów, z których żaden nie reprezentował prawidłowej liczby całkowitej, a następnie dodałem prawidłowy ciąg liczb całkowitych na samym końcu. Stosując oba powyższe podejścia, moje wyniki były następujące:
Korzystanie try
/ catch
blok: 25,455 sekund
Korzystanie int.TryParse
: 1,637 milisekund
W drugim przykładzie zrobiłem w zasadzie to samo: utworzyłem listę 10000 losowych ciągów, z których żaden nie był prawidłową ścieżką, a następnie dodałem prawidłową ścieżkę na samym końcu. Oto wyniki:
Korzystanie try
/ catch
blok: 29,989 sekund
Korzystanie File.Exists
: 22,820 milisekund
Wiele osób zareagowałoby na to, mówiąc: „No cóż, rzucanie i łapanie 10000 wyjątków jest skrajnie nierealne; to przesadza z wynikami”. Oczywiście, że tak. Różnica między wyrzuceniem jednego wyjątku a samodzielną obsługą złych danych wejściowych nie będzie zauważalna dla użytkownika. Faktem jest, że używanie wyjątków jest w tych dwóch przypadkach od 1000 do ponad 10 000 razy wolniejsze niż podejścia alternatywne, które są równie czytelne - jeśli nie bardziej.
Dlatego GetNine()
poniżej podałem przykład metody. Nie chodzi o to, że jest nieznośnie powolny lub niedopuszczalnie wolny; chodzi o to, że jest wolniejszy niż powinien ... bez powodu .
Ponownie, to tylko dwa przykłady. Od kursu nie będzie wtedy, gdy hit wydajność wykorzystania wyjątków nie jest to ciężkie (Pavel ma rację, po wszystkim, to jest uzależnione od wykonania) jest. Mówię tylko: spójrzmy prawdzie w oczy, chłopaki - w przypadkach takich jak ten powyżej, rzucanie i łapanie wyjątku jest analogiczne do GetNine()
; to po prostu nieefektywny sposób na zrobienie czegoś , co można z łatwością zrobić lepiej .
Prosisz o uzasadnienie, jakby to była jedna z tych sytuacji, w których wszyscy wskoczyli na modę, nie wiedząc dlaczego. Ale w rzeczywistości odpowiedź jest oczywista i myślę, że już ją znasz. Obsługa wyjątków ma horrendalną wydajność.
OK, może to dobrze dla twojego scenariusza biznesowego, ale relatywnie rzecz biorąc , rzucanie / wychwytywanie wyjątku wprowadza znacznie więcej narzutów niż jest to konieczne w wielu, wielu przypadkach. Wiesz to, ja to wiem: przez większość czasu , jeśli używasz wyjątków do sterowania przepływem programu, po prostu piszesz wolny kod.
Równie dobrze możesz zapytać: dlaczego ten kod jest zły?
private int GetNine() {
for (int i = 0; i < 10; i++) {
if (i == 9) return i;
}
}
Założę się, że jeśli sprofilujesz tę funkcję, okaże się, że działa ona dość szybko w przypadku typowej aplikacji biznesowej. Nie zmienia to faktu, że jest to okropnie nieefektywny sposób na osiągnięcie czegoś, co można zrobić o wiele lepiej.
To właśnie mają na myśli ludzie, gdy mówią o wyjątkowych „nadużyciach”.