Jakie są wyjątki od wydajności w Javie?


496

Pytanie: Czy obsługa wyjątków w Javie jest naprawdę powolna?

Konwencjonalna wiedza, a także wiele wyników Google, mówi, że wyjątkowa logika nie powinna być używana do normalnego przepływu programów w Javie. Zazwyczaj podaje się dwa powody:

  1. jest naprawdę wolny - nawet o rząd wielkości wolniejszy niż zwykły kod (podane przyczyny są różne),

i

  1. jest bałagan, ponieważ ludzie oczekują, że w wyjątkowym kodzie będą obsługiwane tylko błędy.

To pytanie dotyczy nr 1.

Na przykład ta strona opisuje obsługę wyjątków Java jako „bardzo powolną” i wiąże powolność z tworzeniem ciągu komunikatu o wyjątku - „ten ciąg jest następnie wykorzystywany do tworzenia zgłaszanego obiektu wyjątku. To nie jest szybkie”. Artykuł „ Efektywna obsługa wyjątków w Javie” mówi, że „przyczyną tego jest aspekt tworzenia wyjątków obsługi wyjątków, co powoduje, że generowanie wyjątków jest z natury powolne”. Innym powodem jest to, że spowalnia to generowanie śladu stosu.

Moje testy (przy użyciu Java 1.6.0_07, Java HotSpot 10.0, w 32-bitowym systemie Linux) wskazują, że obsługa wyjątków nie jest wolniejsza niż zwykły kod. Próbowałem uruchomić metodę w pętli, która wykonuje jakiś kod. Na końcu metody używam wartości logicznej, aby wskazać, czy zwrócić, czy rzucić . W ten sposób faktyczne przetwarzanie jest takie samo. Próbowałem uruchamiać metody w różnych zamówieniach i uśredniać czas testu, sądząc, że mogło to być rozgrzewanie JVM. We wszystkich moich testach rzut był co najmniej tak szybki jak powrót, jeśli nie szybszy (do 3,1% szybciej). Jestem całkowicie otwarty na możliwość, że moje testy były niepoprawne, ale nie widziałem niczego na drodze do próbki kodu, porównań testów lub wyników w ciągu ostatniego roku lub dwóch, które pokazują, że obsługa wyjątków w Javie jest faktycznie powolny.

To, co prowadzi mnie na tę ścieżkę, to interfejs API, którego musiałem użyć, który wyrzucił wyjątki w ramach normalnej logiki sterowania. Chciałem je poprawić, ale teraz mogę tego nie robić. Czy zamiast tego będę musiał pochwalić ich myślenie naprzód?

W artykule Efektywna obsługa wyjątków Java w kompilacji just-in-time autorzy sugerują, że obecność samych procedur obsługi wyjątków, nawet jeśli nie zostaną zgłoszone żadne wyjątki, wystarcza, aby kompilator JIT nie zoptymalizował poprawnie kodu, spowalniając go w ten sposób . Nie przetestowałem jeszcze tej teorii.


8
Wiem, że nie pytałeś o 2), ale naprawdę powinieneś wiedzieć, że użycie wyjątku dla przepływu programu nie jest lepsze niż użycie GOTO. Niektórzy bronią gotos, niektórzy broniliby tego, o czym mówisz, ale jeśli zapytasz kogoś, kto wdrożył i utrzymywał albo przez pewien czas, powiedzą ci, że oboje są słabi, trudni do utrzymania w praktyce projektowej (i prawdopodobnie przeklinają nazwisko osoby, która uważała, że ​​są wystarczająco inteligentne, aby podjąć decyzję o ich użyciu).
Bill K,

80
Bill, twierdząc, że stosowanie wyjątków dla przepływu programu nie jest lepsze niż używanie GOTO, nie jest lepszy niż twierdzenie, że używanie warunków i pętli do przepływu programu nie jest lepsze niż używanie GOTO. To czerwony śledź. Wytłumacz się. Wyjątki mogą i są skutecznie wykorzystywane do przepływu programu w innych językach. Na przykład idiomatyczny kod Python regularnie korzysta z wyjątków. Mogę i utrzymałem kod, który korzysta z wyjątków w ten sposób (choć nie Java) i nie sądzę, żeby było w tym coś z natury nie tak.
mmalone

14
@mmalone przy użyciu wyjątków dla normalnego przepływu sterowania jest złym pomysłem w Javie, ponieważ wybór paradygmatu został dokonany w ten sposób . Przeczytaj Bloch EJ2 - wyraźnie stwierdza, że ​​zacytuj (poz. 57) exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow- podając pełne i wyczerpujące wyjaśnienie, dlaczego. I był facetem, który napisał lib Java. Dlatego to on definiuje kontrakt API klas. / zgadzam się z Billem K. w tej sprawie.

8
@ OndraŽižka Jeśli niektóre frameworki to robią (używają wyjątków w nietypowych warunkach), są wadliwe i łamane projektowo, co zrywa kontrakt klasy wyjątków języka. To, że niektórzy ludzie piszą kiepski kod, nie czyni go mniej kiepskim.

8
Nikt inny niż twórca stackoverflow.com nie myli się co do wyjątków. Złota zasada tworzenia oprogramowania nigdy nie czyni prostego złożonego i nieporęcznym. Pisze: „Prawdą jest, że to, co powinno być prostym 3-liniowym programem, często rozkwita do 48 linii, jeśli dobrze sprawdzasz błędy, ale takie jest życie…” To jest poszukiwanie czystości, a nie prostoty.
sf_jeff,

Odpowiedzi:


345

Zależy to od sposobu wdrożenia wyjątków. Najprostszym sposobem jest użycie setjmp i longjmp. Oznacza to, że wszystkie rejestry procesora są zapisywane na stosie (co zajmuje już trochę czasu) i być może trzeba utworzyć inne dane ... wszystko to dzieje się już w instrukcji try. Instrukcja throw musi rozwinąć stos i przywrócić wartości wszystkich rejestrów (i możliwe inne wartości w maszynie wirtualnej). Więc try i throw są równie powolne i to jest dość powolne, jednak jeśli nie zostanie zgłoszony żaden wyjątek, wyjście z bloku try nie zajmuje w większości przypadków żadnego czasu (ponieważ wszystko jest umieszczane na stosie, który czyści się automatycznie, jeśli metoda istnieje).

Sun i inni uznali, że jest to prawdopodobnie nieoptymalne i oczywiście maszyny wirtualne stają się coraz szybsze w miarę upływu czasu. Istnieje inny sposób implementacji wyjątków, który sprawia, że ​​próba sama w sobie jest błyskawiczna (w rzeczywistości nic się nie dzieje podczas próby w ogóle - wszystko, co musi się zdarzyć, jest już zrobione, gdy klasa jest ładowana przez maszynę wirtualną) i powoduje, że rzut nie jest tak wolny . Nie wiem, która JVM korzysta z tej nowej, lepszej techniki ...

... ale piszesz w Javie, więc Twój kod będzie później działał tylko na jednej maszynie JVM w jednym określonym systemie? Ponieważ jeśli może kiedykolwiek działać na innej platformie lub w dowolnej innej wersji JVM (być może dowolnego innego dostawcy), kto twierdzi, że korzysta także z szybkiej implementacji? Szybka jest bardziej skomplikowana niż wolna i nie jest łatwo możliwa we wszystkich systemach. Chcesz pozostać przenośny? Nie polegaj więc na tym, że wyjątki są szybkie.

To także robi dużą różnicę w tym, co robisz w bloku try. Jeśli otworzysz blok try i nigdy nie wywołasz żadnej metody z tego bloku try, blok try będzie bardzo szybki, ponieważ JIT może wtedy traktować rzut jak zwykłe goto. Nie musi on zapisywać stanu stosu, ani nie musi się rozwijać stosu, jeśli zostanie zgłoszony wyjątek (wystarczy przeskoczyć do programów obsługi przechwytywania). Jednak to nie jest to, co zwykle robisz. Zwykle otwierasz blok try, a następnie wywołujesz metodę, która może zgłosić wyjątek, prawda? I nawet jeśli użyjesz tylko bloku try w swojej metodzie, jaki to będzie sposób, który nie wywołuje żadnej innej metody? Czy to po prostu obliczy liczbę? Do czego więc potrzebujesz wyjątków? Istnieją znacznie bardziej eleganckie sposoby regulowania przebiegu programu. W zasadzie wszystko inne niż prosta matematyka,

Zobacz następujący kod testowy:

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

Wynik:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

Spowolnienie z bloku try jest zbyt małe, aby wykluczyć mylące czynniki, takie jak procesy w tle. Ale blok catch zabił wszystko i sprawił, że 66 razy wolniej!

Jak powiedziałem, wynik nie będzie taki zły, jeśli umieścisz try / catch i wyrzucisz wszystko w ramach tej samej metody (metoda3), ale jest to specjalna optymalizacja JIT, na której nie polegałbym. I nawet podczas korzystania z tej optymalizacji rzut jest nadal dość wolny. Więc nie wiem, co próbujesz tutaj zrobić, ale jest zdecydowanie lepszy sposób na zrobienie tego niż użycie try / catch / throw.


7
Świetna odpowiedź, ale chciałbym dodać, że o ile wiem, do pomiaru wydajności należy używać System.nanoTime (), a nie System.currentTimeMillis ().
Simon Forsberg

10
@ SimonAndréForsberg nanoTime()wymaga Java 1.5 i miałem tylko Java 1.4 w systemie, którego użyłem do napisania powyższego kodu. Nie odgrywa też dużej roli w praktyce. Jedyna różnica między nimi polega na tym, że jedna jest nanosekunda, a druga milisekunda i na nanoTimeto nie mają wpływu manipulacje zegarem (które są nieistotne, chyba że ty lub proces systemowy zmodyfikuje zegar systemowy dokładnie w momencie uruchomienia kodu testowego). Ogólnie jednak masz rację, nanoTimejest oczywiście lepszym wyborem.
Mecki

2
Naprawdę należy zauważyć, że twój test jest ekstremalnym przypadkiem. Pokazujesz bardzo mały wynik wydajności dla kodu z tryblokiem, ale nie throw. Twój throwtest rzuca wyjątki w 50% przypadków try. To wyraźnie sytuacja, w której awaria nie jest wyjątkowa . Obniżenie tego do zaledwie 10% znacznie obniża wydajność. Problem z tego rodzaju testem polega na tym, że zachęca on ludzi do całkowitego zaprzestania stosowania wyjątków. Korzystanie z wyjątków w celu wyjątkowej obsługi przypadków działa znacznie lepiej niż pokazuje test.
Nate

1
@Nate Po pierwsze, powiedziałem bardzo jasno, że wszystko to zależy od sposobu wdrożenia wyjątków. Właśnie testowałem JEDNĄ konkretną implementację, ale jest ich wiele i Oracle może wybrać zupełnie inną z każdą wersją. Po drugie, jeśli wyjątki są tylko wyjątkowe, czym zwykle są, oczywiście wpływ jest mniejszy, jest to tak oczywiste, że tak naprawdę nie sądzę, aby ktoś musiał wyraźnie to podkreślać, a zatem nie mogę w ogóle zrozumieć waszego punktu widzenia. I po trzecie, wyjątek nadużywa go źle, wszyscy się z tym zgadzają, więc używanie ich z dużą ostrożnością jest bardzo dobrą rzeczą.
Mecki

4
@Glide Rzut nie jest jak czysty return. Pozostawia metodę gdzieś w środku ciała, może nawet w środku operacji (która do tej pory zakończyła się tylko o 50%), a catchblok może mieć 20 ramek stosu w górę (metoda ma tryblok, wywołujący metodę1, który wywołuje metodę2, która wywołuje mehtod3, ..., aw metodzie20 w środku operacji zgłaszany jest wyjątek). Stos musi być odwijany o 20 klatek w górę, wszystkie niedokończone operacje muszą być cofnięte (operacje nie mogą być w połowie wykonane), a rejestry procesora muszą być w stanie czystym. To wszystko pochłania czas.
Mecki

255

Do Twojej wiadomości rozszerzyłem eksperyment, który przeprowadziła Mecki:

method1 took 1733 ms, result was 2
method2 took 1248 ms, result was 2
method3 took 83997 ms, result was 2
method4 took 1692 ms, result was 2
method5 took 60946 ms, result was 2
method6 took 25746 ms, result was 2

Pierwsze 3 są takie same jak Mecki (mój laptop jest oczywiście wolniejszy).

metoda4 jest identyczna z metodą3, z tą różnicą, że tworzy new Integer(1)raczej działanie niż działanie throw new Exception().

metoda5 jest podobna do metody3, z tą różnicą, że tworzy new Exception()bez rzucania.

metoda6 jest podobna do metody3, z tą różnicą, że generuje wstępnie utworzony wyjątek (zmienną instancji), zamiast tworzyć nowy.

W Javie znaczną część kosztów zgłaszania wyjątku stanowi czas gromadzenia śladu stosu, który występuje, gdy tworzony jest obiekt wyjątku. Rzeczywisty koszt zgłoszenia wyjątku, choć duży, jest znacznie niższy niż koszt utworzenia wyjątku.


48
+1 Twoja odpowiedź rozwiązuje podstawowy problem - czas potrzebny na rozwinięcie i prześledzenie stosu, a następnie zgłoszenie błędu. Wybrałbym to jako ostateczną odpowiedź.
Inżynier,

8
miły. ~ 70% tworzy wyjątek, ~ 30% go rzuca. dobra informacja.
chaqke

1
@Basil - Powinieneś być w stanie to zrozumieć na podstawie powyższych liczb.
Hot Licks,

1
Może to być specyficzne dla implementacji. Jakiej wersji Java użyto do tych testów?
Thorbjørn Ravn Andersen

3
Możemy zauważyć, że w standardowym kodzie tworzenie i zgłaszanie wyjątków występuje w rzadkich przypadkach (mam na myśli w czasie wykonywania), jeśli tak nie jest, albo warunki wykonania są bardzo złe, albo sam projekt jest problemem; w obu przypadkach występy nie stanowią problemu ...
Jean-Baptiste Yunès

70

Aleksey Shipilëv przeprowadził bardzo dokładną analizę, w której porównał wyjątki Javy w różnych kombinacjach warunków:

  • Nowo utworzone wyjątki vs. wstępnie utworzone wyjątki
  • Włączone śledzenie stosu vs wyłączone
  • Żądanie śledzenia stosu nigdy nie było wymagane
  • Złapany na najwyższym poziomie vs powtórne rzucenie na każdym poziomie vs przykuty / owinięty na każdym poziomie
  • Różne poziomy głębokości stosu wywołań Java
  • Brak optymalizacji inliningu w porównaniu do ekstremalnej inlinizacji w porównaniu do ustawień domyślnych
  • Zdefiniowane przez użytkownika pola czytane a nie czytane

Porównuje je także do sprawdzania kodu błędu na różnych poziomach częstotliwości błędów.

Wnioski (cytowane dosłownie z jego postu) były następujące:

  1. Naprawdę wyjątkowe wyjątki są pięknie występujące. Jeśli użyjesz ich zgodnie z przeznaczeniem i przekażesz tylko naprawdę wyjątkowe przypadki spośród przeważnie dużej liczby nietypowych spraw obsługiwanych przez zwykły kod, wówczas użycie wyjątków jest wygraną wydajności.

  2. Koszty wydajności wyjątków mają dwa główne elementy: konstrukcję śledzenia stosu, gdy wystąpi wyjątek, oraz rozwijanie stosu podczas rzucania wyjątku.

  3. Koszty budowy śledzenia stosu są proporcjonalne do głębokości stosu w momencie wystąpienia wyjątku. To już jest złe, bo kto na Ziemi zna głębokość stosu, na której zostałaby wywołana ta metoda rzucania? Nawet jeśli wyłączysz generowanie śledzenia stosu i / lub buforujesz wyjątki, możesz tylko pozbyć się tej części kosztu wydajności.

  4. Koszty rozwijania stosu zależą od tego, jakie mamy szczęście, przybliżając moduł obsługi wyjątków do skompilowanego kodu. Staranne ustrukturyzowanie kodu w celu uniknięcia wyszukiwania głębokich procedur obsługi wyjątków prawdopodobnie pomaga nam osiągnąć więcej szczęścia.

  5. Jeśli wyeliminujemy oba efekty, koszt wydajności wyjątków jest kosztem lokalnego oddziału. Bez względu na to, jak pięknie to brzmi, nie oznacza to, że powinieneś używać wyjątków jako zwykłego przepływu sterowania, ponieważ w takim przypadku jesteś na łasce optymalizacji kompilatora! Powinieneś ich używać tylko w naprawdę wyjątkowych przypadkach, gdy częstotliwość wyjątków amortyzuje możliwy nieszczęśliwy koszt podniesienia faktycznego wyjątku.

  6. Optymistyczna reguła wydaje się mieć częstotliwość 10 ^ 4, ponieważ wyjątki są wystarczająco wyjątkowe. To oczywiście zależy od dużej wagi samych wyjątków, dokładnych działań podjętych w procedurach obsługi wyjątków itp.

Rezultatem jest to, że gdy wyjątek nie zostanie zgłoszony, nie ponosisz kosztów, więc gdy wyjątkowy stan jest wystarczająco rzadki, obsługa wyjątków jest szybsza niż używanie za ifkażdym razem. Pełny post jest bardzo wart przeczytania.


41

Moja odpowiedź jest niestety zbyt długa, aby opublikować tutaj. Pozwólcie, że streszczę tutaj i odsyłam do http://www.fuwjax.com/how-slow-are-java-exceptions/ celu uzyskania szczegółowych informacji.

Prawdziwe pytanie nie brzmi: „Jak wolno„ awarie są zgłaszane jako wyjątki ”w porównaniu z„ kodem, który nigdy nie zawodzi ”? jak może przyjąć zaakceptowana odpowiedź. Zamiast tego pytanie powinno brzmieć „Jak wolno„ awarie są zgłaszane jako wyjątki ”w porównaniu z awariami zgłaszanymi na inne sposoby?” Zasadniczo dwa inne sposoby zgłaszania awarii są albo z wartościami wartowników, albo z opakowaniami wyników.

Wartości Sentinel są próbą zwrócenia jednej klasy w przypadku sukcesu, a drugiej w przypadku niepowodzenia. Możesz myśleć o tym prawie jak o zwróceniu wyjątku zamiast rzucania go. Wymaga to współużytkowanej klasy nadrzędnej z obiektem sukcesu, a następnie wykonania sprawdzenia „instanceof” i kilku rzutów w celu uzyskania informacji o sukcesie lub niepowodzeniu.

Okazuje się, że przy zagrożeniu bezpieczeństwa typu wartości Sentinel są szybsze niż wyjątki, ale tylko około dwukrotnie. To może wydawać się dużo, ale to 2x pokrywa tylko koszt różnicy implementacji. W praktyce współczynnik ten jest znacznie niższy, ponieważ nasze metody, które mogą zawieść, są znacznie bardziej interesujące niż kilka operatorów arytmetycznych, jak w przykładowym kodzie w innym miejscu na tej stronie.

Z drugiej strony Owijarki Rezultatów wcale nie poświęcają bezpieczeństwa typu. Zawierają informacje o sukcesach i niepowodzeniach w jednej klasie. Dlatego zamiast „instanceof” udostępniają one „isSuccess ()” i funkcje pobierające zarówno obiekty sukcesu, jak i niepowodzenia. Jednak obiekty wynikowe są około dwa razy wolniejsze niż przy użyciu wyjątków. Okazuje się, że tworzenie nowego obiektu opakowującego za każdym razem jest znacznie droższe niż czasem rzucanie wyjątku.

Ponadto wyjątki stanowią podany język wskazujący, że metoda może zawieść. Nie ma innego sposobu, aby stwierdzić na podstawie samego interfejsu API, które metody będą zawsze (głównie) działały, a które powinny zgłaszać awarię.

Wyjątki są bezpieczniejsze niż wartowniki, szybsze niż obiekty wynikowe i mniej zaskakujące niż oba. Nie sugeruję, aby spróbować / catch zastąpić if / else, ale wyjątki to właściwy sposób zgłaszania awarii, nawet w logice biznesowej.

To powiedziawszy, chciałbym zauważyć, że dwa najczęstsze sposoby znacznego wpływu na wydajność, na które natknąłem się, to tworzenie niepotrzebnych obiektów i zagnieżdżonych pętli. Jeśli masz wybór między utworzeniem wyjątku lub nie utworzeniem wyjątku, nie twórz wyjątku. Jeśli masz wybór pomiędzy czasami tworzeniem wyjątku lub ciągłym tworzeniem innego obiektu, utwórz wyjątek.


5
Postanowiłem przetestować długoterminową wydajność trzech implementacji w porównaniu z implementacją kontroli, która sprawdza awarie bez zgłaszania. Wskaźnik awaryjności procesu wynosi około 4%. Iteracja testu wywołuje ten proces 10000 razy w stosunku do jednej ze strategii. Każda strategia jest testowana 1000 razy, a ostatnie 900 razy służy do generowania statystyk. Oto średnie czasy w nanosach: Kontrola 338 Wyjątek 429 Wynik 348 Sentinel 345
Fuwjax

2
Dla zabawy wyłączyłem fillInStackTrace w teście wyjątków. Oto czasy: Kontrola 347 Wyjątek 351 Wynik 364 Sentinel 355
Fuwjax

Fuwjax, chyba że coś mi umknie (i przyznaję, że przeczytałem tylko Twój wpis SO, a nie wpis na blogu), wygląda na to, że dwa powyższe komentarze są sprzeczne z Twoim postem. Zakładam, że niższe wartości są lepsze w twoim teście, prawda? W takim przypadku generowanie wyjątków z włączoną funkcją fillInStackTrace (co jest domyślnym i zwykłym zachowaniem) powoduje niższą wydajność niż w przypadku pozostałych dwóch opisanych technik. Czy coś przeoczyłem, czy też skomentowałeś, aby obalić swój post?
Felix GV

@Fuwjax - sposobem na uniknięcie wyboru „rock and hard place”, który tu prezentujesz, jest wstępne przydzielenie obiektu reprezentującego „sukces”. Zwykle można również wstępnie przydzielić obiekty do typowych przypadków awarii. Wtedy tylko w rzadkim przypadku przekazania dodatkowych szczegółów powstaje nowy obiekt. (Jest to ekwiwalent liczb całkowitych „kodów błędów” plus osobne wywołanie w celu uzyskania szczegółów ostatniego błędu - techniki, która istnieje od dziesięcioleci.)
ToolmakerSteve

@Fuwjax Zgłoszenie wyjątku nie tworzy obiektu na koncie? Nie jestem pewien, czy rozumiem to rozumowanie. Niezależnie od tego, czy zgłosisz wyjątek, czy zwrócisz wynikowy obiekt, tworzysz obiekty. W tym sensie obiekty wynikowe nie są wolniejsze niż zgłoszenie wyjątku.
Matthias

20

Rozszerzyłem odpowiedzi udzielone przez @Mecki i @incarnate , bez wypełniania stosu śledzenia Java.

Z Javą 7+ możemy korzystać Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace). Ale w przypadku Java6 zobacz moją odpowiedź na to pytanie

// This one will regularly throw one
public void method4(int i) throws NoStackTraceThrowable {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceThrowable();
    }
}

// This one will regularly throw one
public void method5(int i) throws NoStackTraceRuntimeException {
    value = ((value + i) / i) << 1;
    // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
    // an AND operation between two integers. The size of the number plays
    // no role. AND on 32 BIT always ANDs all 32 bits
    if ((i & 0x1) == 1) {
        throw new NoStackTraceRuntimeException();
    }
}

public static void main(String[] args) {
    int i;
    long l;
    Test t = new Test();

    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method4(i);
        } catch (NoStackTraceThrowable e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method4 took " + l + " ms, result was " + t.getValue() );


    l = System.currentTimeMillis();
    t.reset();
    for (i = 1; i < 100000000; i++) {
        try {
            t.method5(i);
        } catch (RuntimeException e) {
            // Do nothing here, as we will get here
        }
    }
    l = System.currentTimeMillis() - l;
    System.out.println( "method5 took " + l + " ms, result was " + t.getValue() );
}

Wyjście z Javą 1.6.0_45, na Core i7, 8 GB RAM:

method1 took 883 ms, result was 2
method2 took 882 ms, result was 2
method3 took 32270 ms, result was 2 // throws Exception
method4 took 8114 ms, result was 2 // throws NoStackTraceThrowable
method5 took 8086 ms, result was 2 // throws NoStackTraceRuntimeException

Tak więc nadal metody zwracające wartości są szybsze w porównaniu do metod zgłaszających wyjątki. IMHO, nie możemy zaprojektować przejrzystego interfejsu API, używając tylko typów zwrotów zarówno dla przepływów sukcesu, jak i błędów. Metody generujące wyjątki bez stacktrace są 4-5 razy szybsze niż normalne wyjątki.

Edycja: NoStackTraceThrowable.java Dzięki @Greg

public class NoStackTraceThrowable extends Throwable { 
    public NoStackTraceThrowable() { 
        super("my special throwable", null, false, false);
    }
}

ciekawe, dzięki. Oto deklaracja brakującej klasy:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
Greg

na początku pisałeś, With Java 7+, we can useale później pisałeś , Output with Java 1.6.0_45,więc jest to wynik Java 6 lub 7?
WBAR

1
@WBAR z Java 7, wystarczy użyć Throwablekonstruktora, który ma boolean writableStackTracearg. Ale nie jest to obecne w Javie 6 i niższych. Dlatego podałem niestandardową implementację dla Java 6 i niższych. Powyższy kod dotyczy Java 6 i niższych. Przeczytaj uważnie pierwszą linię drugiego akapitu.
manikanta

@manikanta „IMHO, nie możemy zaprojektować przejrzystego interfejsu API, używając tylko typów zwrotów zarówno dla przepływów sukcesu, jak i błędów.” - możemy, jeśli użyjemy opcji / wyników / być może w wielu językach.
Hejazzman

@Hejazzman Zgadzam się. Ale Optionallub podobny przyszedł nieco później do Java. Wcześniej używaliśmy również obiektów otoki z flagami powodzenia / błędów. Ale wydaje się to trochę hacków i nie wydaje mi się naturalne.
manikanta

8

Jakiś czas temu napisałem klasę, aby przetestować względną wydajność konwersji ciągów znaków na ints przy użyciu dwóch metod: (1) wywołaj Integer.parseInt () i złap wyjątek lub (2) dopasuj ciąg z wyrażeniem regularnym i wywołaj parseInt () tylko jeśli mecz się powiedzie. Użyłem wyrażenia regularnego w najbardziej efektywny sposób, w jaki mogłem (tj. Tworząc obiekty Pattern i Matcher przed interferencją w pętli) i nie wydrukowałem ani nie zapisałem śladów stosu z wyjątków.

W przypadku listy dziesięciu tysięcy ciągów, jeśli wszystkie były poprawnymi liczbami, podejście parseInt () było czterokrotnie szybsze niż podejście wyrażenia regularnego. Ale jeśli tylko 80% łańcuchów jest poprawnych, regex był dwa razy szybszy niż parseInt (). A jeśli 20% było poprawnych, co oznacza, że ​​wyjątek został zgłoszony i wyłapany w 80% przypadków, regex był około dwadzieścia razy szybszy niż parseInt ().

Byłem zaskoczony wynikiem, biorąc pod uwagę, że podejście wyrażenia regularnego przetwarza poprawne ciągi dwa razy: raz dla dopasowania i ponownie dla parseInt (). Ale rzucanie i łapanie wyjątków z nawiązką to zrekompensowało. Taka sytuacja prawdopodobnie nie zdarza się bardzo często w prawdziwym świecie, ale jeśli tak, zdecydowanie nie należy stosować techniki wychwytywania wyjątków. Ale jeśli weryfikujesz tylko dane wejściowe użytkownika lub coś w tym rodzaju, z całą pewnością skorzystaj z metody parseInt ().


z którego JVM korzystałeś? czy nadal jest tak wolny z Sun-JDK 6?
Benedikt Waldvogel

Wykopałem go i ponownie uruchomiłem pod JDK 1.6u10 przed przesłaniem tej odpowiedzi, i takie są wyniki, które opublikowałem.
Alan Moore,

To bardzo, bardzo przydatne! Dzięki. W moich zwykłych przypadkach użycia muszę analizować dane wejściowe użytkownika (używając czegoś podobnego Integer.ParseInt()) i spodziewam się, że w większości przypadków dane wejściowe użytkownika będą poprawne, więc w moim przypadku użycia wydaje się, że od czasu do czasu trafienie wyjątkowego trafienia jest właściwą drogą .
markvgti

8

Myślę, że pierwszy artykuł odnosi się do przemierzania stosu wywołań i tworzenia śladu stosu jako drogiej części, a podczas gdy drugi artykuł tego nie mówi, myślę, że jest to najdroższa część tworzenia obiektów. John Rose ma artykuł, w którym opisuje różne techniki przyspieszania wyjątków . (Wstępne przydzielanie i ponowne użycie wyjątku, wyjątki bez śladów stosu itp.)

Ale nadal - uważam, że należy to uznać za zło konieczne, w ostateczności. Powodem tego jest emulacja funkcji w innych językach, które nie są (jeszcze) dostępne w JVM. NIE powinieneś przyzwyczajać się do używania wyjątków dla przepływu kontrolnego. Zwłaszcza nie ze względu na wydajność! Jak sam wspominasz w punkcie 2, ryzykujesz w ten sposób maskowanie poważnych błędów w kodzie i trudniej będzie go utrzymać nowym programistom.

Znaki mikrodruku w Javie są zaskakująco trudne do poprawienia (jak mi powiedziano), zwłaszcza gdy wchodzisz na terytorium JIT, więc naprawdę wątpię, aby stosowanie wyjątków było szybsze niż „powrót” w prawdziwym życiu. Na przykład podejrzewam, że masz w swoim teście od 2 do 5 ramek stosu? Teraz wyobraź sobie, że Twój kod zostanie wywołany przez komponent JSF wdrożony przez JBoss. Teraz możesz mieć ślad stosu, który ma kilka stron.

Być może mógłbyś opublikować swój kod testowy?


7

Nie wiem, czy te tematy się odnoszą, ale kiedyś chciałem zastosować jedną sztuczkę polegającą na śledzeniu stosu bieżącego wątku: chciałem odkryć nazwę metody, która wyzwoliła tworzenie instancji wewnątrz instancji klasy (tak, pomysł jest szalony, Całkowicie się poddałem). Odkryłem więc, że wywoływanie Thread.currentThread().getStackTrace()jest bardzo wolne (dzięki natywnej dumpThreadsmetodzie, której używa wewnętrznie).

Zatem Java Throwablema odpowiednio natywną metodę fillInStackTrace. Myślę, że catchopisany wcześniej blok- zabójca w jakiś sposób wyzwala wykonanie tej metody.

Ale pozwól, że opowiem ci inną historię ...

W Scali niektóre funkcje funkcjonalne są kompilowane w JVM przy użyciu ControlThrowable, który rozszerza Throwablei zastępuje jego fillInStackTracew następujący sposób:

override def fillInStackTrace(): Throwable = this

Zaadaptowałem więc powyższy test (ilość cykli zmniejsza się o dziesięć, moja maszyna jest nieco wolniejsza :):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

Tak więc wyniki są następujące:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

Widać, jedyną różnicą pomiędzy method3i method4jest to, że oni rzucać różne rodzaje wyjątków. Tak, method4nadal jest wolniejszy niż method1i method2, ale różnica jest o wiele bardziej akceptowalna.


6

Zrobiłem kilka testów wydajności z JVM 1.5 i stosowanie wyjątków było co najmniej 2x wolniejsze. Średnio: czas wykonania na trywialnie małej metodzie ponad trzykrotnie (3x) z wyjątkami. W trywialnie małej pętli, która musiała wychwycić wyjątek, zaobserwowano dwukrotny wzrost czasu pracy.

Widziałem podobne liczby w kodzie produkcyjnym, a także w testach mikro.

Wyjątki powinny zdecydowanie NIEstosować do czegokolwiek, co często się nazywa. Zgłaszanie tysięcy wyjątków na sekundę spowodowałoby ogromną szyjkę butelki.

Na przykład użycie „Integer.ParseInt (...)” do znalezienia wszystkich złych wartości w bardzo dużym pliku tekstowym - bardzo zły pomysł. (Widziałem, jak ta metoda narzędzia zabija wydajność kodu produkcyjnego)

Używanie wyjątku do zgłaszania złej wartości w formularzu GUI użytkownika, prawdopodobnie nie jest tak źle z punktu widzenia wydajności.

Niezależnie od tego, czy jest to dobra praktyka projektowa, wybrałbym zasadę: jeśli błąd jest normalny / oczekiwany, użyj wartości zwracanej. Jeśli jest nienormalny, użyj wyjątku. Na przykład: odczyt danych wejściowych użytkownika, nieprawidłowe wartości są normalne - użyj kodu błędu. Przekazując wartość do wewnętrznej funkcji narzędzia, niepoprawne wartości powinny być filtrowane przez wywołanie kodu - użyj wyjątku.


Pozwól, że zasugeruję kilka rzeczy, które SĄ DOBRE: Możesz wstępnie skompilować i ponownie użyć wzorca, dzięki czemu dopasowywanie jest tanie. Jednak w formularzu GUI posiadanie isValid / validate / checkField lub tego, co masz, jest prawdopodobnie wyraźniejsze. Ponadto w Javie 8 mamy opcjonalne monady, więc rozważ ich użycie. (odpowiedź ma 9 lat, ale nadal!: p)
Haakon Løtveit

4

Wyjątkowa wydajność w Javie i C # pozostawia wiele do życzenia.

Jako programiści zmusza nas to do przestrzegania zasady „wyjątki powinny być powodowane rzadko”, po prostu ze względów praktycznych.

Jednak jako informatycy powinniśmy zbuntować się przeciwko temu problematycznemu stanowi. Osoba tworząca funkcję często nie ma pojęcia, jak często będzie ona wywoływana ani czy prawdopodobieństwo sukcesu lub niepowodzenia jest bardziej prawdopodobne. Tylko rozmówca ma te informacje. Próba uniknięcia wyjątków prowadzi do niejasności interfejsów API, w których w niektórych przypadkach mamy tylko czyste, ale powolne wersje wyjątków, aw innych przypadkach mamy szybkie, ale niezgrabne błędy wartości zwrotnej, aw jeszcze innych przypadkach dochodzimy do obu . Implementator biblioteki może być zmuszony napisać i utrzymywać dwie wersje interfejsów API, a osoba dzwoniąca musi zdecydować, która z dwóch wersji ma być używana w każdej sytuacji.

To rodzaj bałaganu. Gdyby wyjątki miały lepszą wydajność, moglibyśmy uniknąć tych nieporadnych idiomów i zastosować wyjątki, ponieważ miały one być użyte ... jako usystematyzowana funkcja zwracania błędów.

Naprawdę chciałbym zobaczyć mechanizmy wyjątków wdrażane przy użyciu technik bliższych wartościom zwracanym, abyśmy mogli mieć wydajność bliższą zwracanym wartościom, ponieważ do tego wracamy w kodzie wrażliwym na wydajność.

Oto przykładowy kod, który porównuje wydajność wyjątku do wydajności zwracającej błąd.

TestIt klasy publicznej {

int value;


public int getValue() {
    return value;
}

public void reset() {
    value = 0;
}

public boolean baseline_null(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        return shouldfail;
    } else {
        return baseline_null(shouldfail,recurse_depth-1);
    }
}

public boolean retval_error(boolean shouldfail, int recurse_depth) {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            return false;
        } else {
            return true;
        }
    } else {
        boolean nested_error = retval_error(shouldfail,recurse_depth-1);
        if (nested_error) {
            return true;
        } else {
            return false;
        }
    }
}

public void exception_error(boolean shouldfail, int recurse_depth) throws Exception {
    if (recurse_depth <= 0) {
        if (shouldfail) {
            throw new Exception();
        }
    } else {
        exception_error(shouldfail,recurse_depth-1);
    }

}

public static void main(String[] args) {
    int i;
    long l;
    TestIt t = new TestIt();
    int failures;

    int ITERATION_COUNT = 100000000;


    // (0) baseline null workload
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                t.baseline_null(shoulderror,recurse_depth);
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("baseline: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }


    // (1) retval_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                if (!t.retval_error(shoulderror,recurse_depth)) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("retval_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);
        }
    }

    // (2) exception_error
    for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
        for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
            int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

            failures = 0;
            long start_time = System.currentTimeMillis();
            t.reset();              
            for (i = 1; i < ITERATION_COUNT; i++) {
                boolean shoulderror = (i % EXCEPTION_MOD) == 0;
                try {
                    t.exception_error(shoulderror,recurse_depth);
                } catch (Exception e) {
                    failures++;
                }
            }
            long elapsed_time = System.currentTimeMillis() - start_time;
            System.out.format("exception_error: recurse_depth %s, exception_freqeuncy %s (%s), time elapsed %s ms\n",
                    recurse_depth, exception_freq, failures,elapsed_time);              
        }
    }
}

}

A oto wyniki:

baseline: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 683 ms
baseline: recurse_depth 2, exception_freqeuncy 0.25 (0), time elapsed 790 ms
baseline: recurse_depth 2, exception_freqeuncy 0.5 (0), time elapsed 768 ms
baseline: recurse_depth 2, exception_freqeuncy 0.75 (0), time elapsed 749 ms
baseline: recurse_depth 2, exception_freqeuncy 1.0 (0), time elapsed 731 ms
baseline: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 923 ms
baseline: recurse_depth 5, exception_freqeuncy 0.25 (0), time elapsed 971 ms
baseline: recurse_depth 5, exception_freqeuncy 0.5 (0), time elapsed 982 ms
baseline: recurse_depth 5, exception_freqeuncy 0.75 (0), time elapsed 947 ms
baseline: recurse_depth 5, exception_freqeuncy 1.0 (0), time elapsed 937 ms
baseline: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1154 ms
baseline: recurse_depth 8, exception_freqeuncy 0.25 (0), time elapsed 1149 ms
baseline: recurse_depth 8, exception_freqeuncy 0.5 (0), time elapsed 1133 ms
baseline: recurse_depth 8, exception_freqeuncy 0.75 (0), time elapsed 1117 ms
baseline: recurse_depth 8, exception_freqeuncy 1.0 (0), time elapsed 1116 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 742 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 743 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 734 ms
retval_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 723 ms
retval_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 728 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 920 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 1121   ms
retval_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 1037 ms
retval_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 1141   ms
retval_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 1130 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1218 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 1334  ms
retval_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 1478 ms
retval_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 1637 ms
retval_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 1655 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.0 (0), time elapsed 726 ms
exception_error: recurse_depth 2, exception_freqeuncy 0.25 (24999999), time elapsed 17487   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.5 (49999999), time elapsed 33763   ms
exception_error: recurse_depth 2, exception_freqeuncy 0.75 (99999999), time elapsed 67367   ms
exception_error: recurse_depth 2, exception_freqeuncy 1.0 (99999999), time elapsed 66990 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.0 (0), time elapsed 924 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.25 (24999999), time elapsed 23775  ms
exception_error: recurse_depth 5, exception_freqeuncy 0.5 (49999999), time elapsed 46326 ms
exception_error: recurse_depth 5, exception_freqeuncy 0.75 (99999999), time elapsed 91707 ms
exception_error: recurse_depth 5, exception_freqeuncy 1.0 (99999999), time elapsed 91580 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.0 (0), time elapsed 1144 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.25 (24999999), time elapsed 30440 ms
exception_error: recurse_depth 8, exception_freqeuncy 0.5 (49999999), time elapsed 59116   ms
exception_error: recurse_depth 8, exception_freqeuncy 0.75 (99999999), time elapsed 116678 ms
exception_error: recurse_depth 8, exception_freqeuncy 1.0 (99999999), time elapsed 116477 ms

Sprawdzanie i propagowanie wartości zwracanych dodaje pewien koszt w porównaniu do wywołania zerowego linii bazowej, a koszt ten jest proporcjonalny do głębokości wywołania. Przy głębokości łańcucha połączeń wynoszącej 8 wersja sprawdzająca wartość zwracaną po błędzie była około 27% wolniejsza niż wersja podstawowa, która nie sprawdzała zwracanych wartości.

Dla porównania, działanie wyjątku nie jest funkcją głębokości wywołania, ale częstotliwości wyjątku. Jednak degradacja wraz ze wzrostem częstotliwości wyjątków jest znacznie bardziej dramatyczna. Przy tylko 25% częstotliwości błędów kod działał 24-TIMES wolniej. Przy częstotliwości błędu 100% wersja wyjątku jest prawie 100-TIMES wolniejsza.

To sugeruje mi, że być może dokonujemy niewłaściwych kompromisów w naszych implementacjach wyjątków. Wyjątki mogą być szybsze, albo poprzez unikanie kosztownych spacerów łodygami, albo przez bezpośrednie przekształcenie ich w sprawdzanie wartości zwracanej przez kompilator. Dopóki tego nie zrobią, będziemy ich unikać, gdy chcemy, aby nasz kod działał szybko.


3

HotSpot jest w stanie usunąć kod wyjątku dla wyjątków generowanych przez system, pod warunkiem, że wszystkie są wstawiane. Jednak jawnie utworzony wyjątek i te, które w przeciwnym razie nie zostały usunięte, spędzają dużo czasu na tworzeniu śladu stosu. Zastąp, fillInStackTraceaby zobaczyć, jak może to wpłynąć na wydajność.


2

Nawet jeśli zgłaszanie wyjątku nie jest powolne, nadal złym pomysłem jest zgłaszanie wyjątków dla normalnego przebiegu programu. Używany w ten sposób jest analogiczny do GOTO ...

Wydaje mi się, że to jednak nie odpowiada na pytanie. Wyobrażam sobie, że „konwencjonalna” mądrość powoływania wyjątków na powolne była prawdziwa we wcześniejszych wersjach Java (<1.4). Utworzenie wyjątku wymaga, aby maszyna wirtualna utworzyła cały ślad stosu. Od tego czasu wiele się zmieniło w maszynie wirtualnej, aby przyspieszyć i prawdopodobnie jest to jeden z obszarów, który został ulepszony.


1
Dobrze byłoby zdefiniować „normalny przebieg programu”. Wiele napisano o używaniu sprawdzonych wyjątków jako awarii procesu biznesowego i niesprawdzonego wyjątku dla niepowodzeń niemożliwych do odzyskania, więc w pewnym sensie błąd w logice biznesowej nadal można uznać za normalny przepływ.
Spencer Kormos,

2
@Spencer K: Wyjątek, jak sama nazwa wskazuje, oznacza, że ​​wykryto wyjątkową sytuację (plik zniknął, sieć nagle się zamknęła, ...). Oznacza to, że sytuacja była NIESPODZIEWANA. Jeśli OCZEKUJE się, że sytuacja się wydarzy, nie użyłbym dla tego wyjątku.
Mecki,

2
@ Mecki: racja. Niedawno rozmawiałem z kimś na ten temat ... Pisali platformę sprawdzania poprawności i zgłaszali wyjątek w przypadku niepowodzenia sprawdzania poprawności. Myślę, że to zły pomysł, ponieważ byłoby to dość powszechne. Wolę, aby metoda zwróciła ValidationResult.
user38051,

2
Pod względem kontroli przepływu wyjątek jest analogiczny do breaklub return, nie goto.
Hot Licks

3
Istnieje mnóstwo paradygmatów programowania. Nie może być jednego „normalnego przepływu”, cokolwiek przez to rozumiesz. Zasadniczo mechanizm wyjątków jest po prostu sposobem na szybkie opuszczenie bieżącej ramki i rozwinięcie stosu do pewnego momentu. Słowo „wyjątek” nic nie wskazuje na jego „nieoczekiwany” charakter. Szybki przykład: bardzo naturalne jest „wyrzucanie” 404 z aplikacji internetowych, gdy pewne okoliczności występują po drodze routingu. Dlaczego ta logika nie miałaby być implementowana z wyjątkami? Jaki jest anty-wzór?
wcielono

2

Po prostu porównajmy powiedzmy Integer.parseInt z następującą metodą, która zwraca wartość domyślną w przypadku danych niemożliwych do przeanalizowania zamiast zgłaszania wyjątku:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

Tak długo, jak obie metody będą stosowane do „poprawnych” danych, obie będą działać w przybliżeniu z tą samą szybkością (nawet jeśli Integer.parseInt radzi sobie z bardziej złożonymi danymi). Ale gdy tylko spróbujesz parsować nieprawidłowe dane (np. Parsować „abc” 1.000.000 razy), różnica w wydajności powinna być niezbędna.


2

Świetny post na temat wydajności wyjątków to:

https://shipilev.net/blog/2014/exceptional-performance/

Tworzenie instancji a ponowne wykorzystywanie istniejących, ze śledzeniem stosu i bez, itp .:

Benchmark                            Mode   Samples         Mean   Mean error  Units

dynamicException                     avgt        25     1901.196       14.572  ns/op
dynamicException_NoStack             avgt        25       67.029        0.212  ns/op
dynamicException_NoStack_UsedData    avgt        25       68.952        0.441  ns/op
dynamicException_NoStack_UsedStack   avgt        25      137.329        1.039  ns/op
dynamicException_UsedData            avgt        25     1900.770        9.359  ns/op
dynamicException_UsedStack           avgt        25    20033.658      118.600  ns/op

plain                                avgt        25        1.259        0.002  ns/op
staticException                      avgt        25        1.510        0.001  ns/op
staticException_NoStack              avgt        25        1.514        0.003  ns/op
staticException_NoStack_UsedData     avgt        25        4.185        0.015  ns/op
staticException_NoStack_UsedStack    avgt        25       19.110        0.051  ns/op
staticException_UsedData             avgt        25        4.159        0.007  ns/op
staticException_UsedStack            avgt        25       25.144        0.186  ns/op

W zależności od głębokości śledzenia stosu:

Benchmark        Mode   Samples         Mean   Mean error  Units

exception_0000   avgt        25     1959.068       30.783  ns/op
exception_0001   avgt        25     1945.958       12.104  ns/op
exception_0002   avgt        25     2063.575       47.708  ns/op
exception_0004   avgt        25     2211.882       29.417  ns/op
exception_0008   avgt        25     2472.729       57.336  ns/op
exception_0016   avgt        25     2950.847       29.863  ns/op
exception_0032   avgt        25     4416.548       50.340  ns/op
exception_0064   avgt        25     6845.140       40.114  ns/op
exception_0128   avgt        25    11774.758       54.299  ns/op
exception_0256   avgt        25    21617.526      101.379  ns/op
exception_0512   avgt        25    42780.434      144.594  ns/op
exception_1024   avgt        25    82839.358      291.434  ns/op

Aby uzyskać więcej informacji (w tym asembler x64 z JIT) przeczytaj oryginalny post na blogu.

Oznacza to, że Hibernacja / Wiosna / etc-EE-shit są powolne z powodu wyjątków (xD) i przepisywanie kontroli aplikacji odpływa od wyjątków (zamień ją na continure/ breaki zwracanie booleanflag jak w C z wywołania metody) poprawiają wydajność aplikacji 10x-100x , w zależności od tego, jak często je rzucasz))


0

Zmieniłem odpowiedź @Mecki powyżej, aby metoda1 zwracała wartość logiczną i sprawdzała metodę wywołującą, ponieważ nie można po prostu zastąpić wyjątku niczym. Po dwóch cyklach metoda1 była nadal najszybsza lub tak szybka jak metoda2.

Oto migawka kodu:

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

i wyniki:

Uruchom 1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

Uruchom 2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2

0

Wyjątkiem jest przeznaczona do obsługi nieoczekiwanych warunków w czasie wykonywania tylko.

Użycie wyjątku zamiast prostej walidacji, które można wykonać w czasie kompilacji, opóźni walidację do czasu wykonania. To z kolei zmniejszy wydajność programu.

Zgłoszenie wyjątku zamiast prostego sprawdzania poprawności if..else sprawi, że kod będzie trudniejszy do napisania i utrzymania.


-3

Moja opinia o prędkości wyjątku a programowym sprawdzaniu danych.

Wiele klas miało konwerter wartości na ciąg (skaner / parser), szanowane i dobrze znane biblioteki;)

zwykle ma formę

class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}

nazwa wyjątku jest tylko przykładem, zwykle nie jest zaznaczona (środowisko wykonawcze), więc deklaracja wyrzuca jest tylko moim obrazem

czasami istnieją druga forma:

public static Example Parse(String input, Example defaultValue)

nigdy nie rzucając

Kiedy drugi nie jest dostępny (lub programista czyta za mało dokumentów i używa tylko pierwszego), napisz taki kod z wyrażeniem regularnym. Wyrażenia regularne są fajne, poprawne politycznie itp .:

Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
  Example v = Example.Parse(src);
}

z tym kodem programiści nie kosztują wyjątków. ALE MA porównywalny WYSOKIE koszty wyrażeń regularnych ZAWSZE w porównaniu z niewielkimi kosztami wyjątków.

Używam prawie zawsze w takim kontekście

try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}

bez analizowania stacktrace itp., wierzę, że po twoich wykładach dość szybko.

Nie bój się Wyjątków


-5

Dlaczego wyjątki powinny być wolniejsze niż normalne zwroty?

Dopóki nie wydrukujesz stosu śledzenia na terminalu, nie zapiszesz go w pliku lub czymś podobnym, catch-block nie wykona więcej pracy niż inne bloki kodu. Nie mogę więc sobie wyobrazić, dlaczego „rzucanie nową my_cool_error ()” powinno być tak wolne.

Dobre pytanie i czekam na dalsze informacje na ten temat!


17
Wyjątek musi przechwytywać informacje o śladzie stosu, nawet jeśli tak naprawdę się nie wykorzystuje.
Jon Skeet,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.