Czy korzystanie z bloków try-catch jest drogie, nawet jeśli wyjątek nigdy nie jest zgłaszany?

189

Wiemy, że wychwytywanie wyjątków jest drogie. Ale czy korzystanie z bloku try-catch w Javie jest również kosztowne, nawet jeśli wyjątek nigdy nie jest zgłaszany?

Znalazłem pytanie / odpowiedź Przepełnienie stosu Dlaczego blokowanie prób jest drogie? , ale dotyczy .NET .

Jsedano
źródło
30
To pytanie naprawdę nie ma sensu. Try..catch ma bardzo konkretny cel. Jeśli potrzebujesz, potrzebujesz. W każdym razie, po co próbować bez haczyka?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }jest legalny i użyteczny
A4L
4
Koszt ten należy porównać z korzyściami. To nie jest samodzielne. W każdym razie koszt jest względny i dopóki nie dowiesz się, że nie możesz tego zrobić, warto zastosować najbardziej oczywistą metodę, a nie nic, ponieważ może to zaoszczędzić milisekundę lub dwie w ciągu godziny wykonanie programu.
Joel
4
Mam nadzieję, że nie prowadzi to do sytuacji typu „
wymyślmy na
6
@SAFX: w Javie 7 możesz nawet pozbyć się finallybloku używająctry-with-resources
a_horse_w_na_name

Odpowiedzi:

201

trynie ma prawie żadnych kosztów. Zamiast wykonywać konfigurację tryw czasie wykonywania, metadane kodu są ustrukturyzowane w czasie kompilacji, tak że kiedy zgłoszony jest wyjątek, wykonuje on stosunkowo kosztowną operację wchodzenia na stos i sprawdzania, czy tryistnieją jakieś bloki, które mogłyby to wychwycić wyjątek. Z punktu widzenia laika tryrównie dobrze może być bezpłatny. W rzeczywistości rzuca wyjątek, który cię kosztuje - ale dopóki nie wprowadzisz setek lub tysięcy wyjątków, nadal nie zauważysz kosztu.


trywiąże się z tym niewielkie koszty. Java nie może dokonać optymalizacji kodu w trybloku, który w przeciwnym razie by zrobił. Na przykład Java często zmienia kolejność instrukcji w metodzie, aby przyspieszyć jej działanie - ale Java musi także zagwarantować, że jeśli zostanie zgłoszony wyjątek, wykonanie metody jest obserwowane tak, jakby jej instrukcje zapisane w kodzie źródłowym były wykonywane w kolejności do jakiejś linii.

Ponieważ w trybloku może zostać zgłoszony wyjątek (w dowolnej linii w bloku try! Niektóre wyjątki są zgłaszane asynchronicznie, na przykład przez wywołanie stopwątku (który jest przestarzały), a nawet poza tym OutOfMemoryError może się zdarzyć prawie wszędzie), a jednak to może zostać przechwycony, a kod będzie kontynuowany po tej samej metodzie, trudniej jest uzasadnić optymalizacje, które można wprowadzić, więc jest mniej prawdopodobne, że tak się stanie. (Ktoś musiałby zaprogramować kompilator, aby to robił, uzasadniał i gwarantował poprawność, itp. Byłoby to wielkim problemem dla czegoś, co ma być „wyjątkowe”). Ale w praktyce nie zauważysz takich rzeczy.

Patashu
źródło
2
Niektóre wyjątki są zgłaszane asynchronicznie , nie są asynchroniczne, ale rzucane w bezpiecznych punktach. a ta część próby wiąże się z niewielkimi kosztami. Java nie może dokonać optymalizacji kodu w bloku try, który w innym przypadku wymagałby poważnego odwołania. W pewnym momencie kod prawdopodobnie znajduje się w bloku try / catch. Być może prawdą jest, że trudniej byłoby wstawić blok try / catch i zbudować odpowiednią siatkę dla wyniku, ale część z / rearanżacją jest niejednoznaczna.
bestsss
2
Czy try...finallyblok bez catchuniemożliwia również pewne optymalizacje?
dajood
5
@Patashu „Zgłaszanie wyjątku kosztuje cię” Technicznie, rzucanie wyjątku nie jest drogie; tworzenie instancji Exceptionobiektu zajmuje najwięcej czasu.
Austin D
72

Zmierzmy to, prawda?

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

Na moim komputerze wyświetla się coś takiego:

try     0.598 ns
no try  0.601 ns

Przynajmniej w tym trywialnym przykładzie instrukcja try nie miała mierzalnego wpływu na wydajność. Możesz zmierzyć bardziej złożone.

Ogólnie rzecz biorąc, zalecam nie przejmować się kosztem wydajności konstrukcji językowych, dopóki w kodzie nie ma dowodów na rzeczywisty problem z wydajnością. Lub, jak ujął to Donald Knuth : „przedwczesna optymalizacja jest źródłem wszelkiego zła”.

meriton
źródło
4
podczas gdy try / no try najprawdopodobniej będzie taki sam w większości JVM, znak mikrobench jest strasznie wadliwy.
bestsss
2
całkiem sporo poziomów: masz na myśli, że wyniki są obliczane poniżej 1 ns? Skompilowany kod całkowicie usunie zarówno try / catch ORAZ pętlę (sumowanie liczby od 1 do n jest trywialną arytmetyczną sumą postępu). Nawet jeśli kod zawiera try / wreszcie kompilator może to udowodnić, nie ma tam nic do wrzucenia. Kod abstrakcyjny ma tylko 2 witryny wywołujące i będzie klonowany i wstawiany. Jest więcej przypadków, po prostu wyszukaj kilka artykułów na temat mikrobenchmarka, a jeśli zdecydujesz się napisać mikrobenchmark, zawsze sprawdzaj wygenerowany zespół.
bestsss
3
Podane czasy dotyczą iteracji pętli. Ponieważ pomiar zostanie wykorzystany tylko wtedy, gdy jego całkowity czas, który upłynął> 0,1 sekundy (lub 2 miliardy iteracji, co nie miało miejsca w tym przypadku), uważam twoje twierdzenie, że pętla została usunięta w całości, trudno w to uwierzyć - ponieważ jeśli pętla została usunięta, co trwało 0,1 sekundy?
meriton
... i rzeczywiście, zgodnie z tym -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, zarówno pętla, jak i jej dodatek są obecne w generowanym kodzie natywnym. I nie, metody abstrakcyjne nie są wbudowane, ponieważ ich program wywołujący nie jest kompilowany w czasie (prawdopodobnie dlatego, że nie jest wywoływany wystarczająco często).
meriton
Jak napisać poprawny mikro-test porównawczy w Javie: stackoverflow.com/questions/504103/…
Vadzim
47

try/ catchmoże mieć pewien wpływ na wydajność. Wynika to z faktu, że uniemożliwia ona JVM optymalizację. Joshua Bloch w „Skutecznej Javie” powiedział:

• Umieszczenie kodu w bloku try-catch hamuje pewne optymalizacje, które w przeciwnym razie mogłyby być wykonane przez współczesne implementacje JVM.

Evgeniy Dorofeev
źródło
25
„uniemożliwia JVM optymalizację” ...? Czy mógłbyś w ogóle opracować?
The Kraken
5
@ Kodu Kraken wewnątrz bloków try (zwykle? Zawsze?) Nie można ponownie zamówić z kodem poza blokami try, jako jeden przykład.
Patashu
3
Pamiętaj, że pytanie brzmiało: „jest drogi”, a nie „czy ma jakikolwiek wpływ na wydajność”.
mikołak
3
dodałem fragment Effective Java i taka jest oczywiście Biblia; chyba że istnieje odniesienie, fragment nic nie mówi. Praktycznie każdy kod w Javie znajduje się w ramach try / wreszcie na pewnym poziomie.
bestsss
29

Tak, jak powiedzieli inni, tryblok hamuje niektóre optymalizacje {}otaczających go postaci. W szczególności optymalizator musi założyć, że w dowolnym punkcie bloku może wystąpić wyjątek, więc nie ma pewności, że instrukcje zostaną wykonane.

Na przykład:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

Bez tego trywartość obliczona do przypisania xmoże zostać zapisana jako „wspólne podwyrażenie” i ponownie użyta do przypisania y. Ale z powodu trybraku pewności, że pierwsze wyrażenie zostało kiedykolwiek ocenione, więc wyrażenie musi zostać ponownie obliczone. Zwykle nie jest to wielka sprawa w kodzie „liniowym”, ale może być znacząca w pętli.

Należy jednak zauważyć, że dotyczy to TYLKO kodu JITCed. javac robi tylko optymistyczną optymalizację, a interpretator kodu bajtowego ma zerowy koszt wejścia / wyjścia z trybloku. (Nie ma generowanych kodów bajtowych do oznaczenia granic bloków).

A dla bestsss:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

Wynik:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

wyjście javap:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

Brak „GOTO”.

Hot Licks
źródło
Nie ma generowanych kodów bajtowych do oznaczenia granic bloków, co niekoniecznie - wymaga GOTO opuszczenia bloku, w przeciwnym razie wpadnie do catch/finallyramki.
bestsss
@bestsss - Nawet jeśli generowane jest GOTO (co nie jest dane), jego koszt jest niewielki, i daleko mu do bycia „znacznikiem” granicy bloku - GOTO można wygenerować dla wielu konstrukcji.
Hot Licks
Nigdy nie wspominałem o kosztach, jednak nie wygenerowano żadnych kodów bajtów, to fałszywe stwierdzenie. To wszystko. W rzeczywistości w kodzie bajtowym nie ma bloków, ramki nie są równe blokom.
bestsss
Nie będzie GOTO, jeśli próba wpadnie bezpośrednio w końcu, i istnieją inne scenariusze, w których nie będzie GOTO. Chodzi o to, że nie ma nic w kolejności bajtów „enter try” / „exit try”.
Hot Licks
Nie będzie GOTO, jeśli próba wpadnie bezpośrednio w końcu - False! nie ma finallyw kodzie bajtowym, to jest try/catch(Throwable any){...; throw any;}i ma instrukcję catch z ramką i Throwable, które MUSZĄ BYĆ zdefiniowane (non null) i tak dalej. Dlaczego próbujesz spierać się na ten temat, możesz sprawdzić przynajmniej jakiś kod bajtowy? Obecne wytyczne dotyczące impl. w końcu kopiuje bloki i omija sekcję goto (poprzedni impl), ale kody bajtowe należy skopiować w zależności od liczby punktów wyjścia.
bestsss
8

Aby zrozumieć, dlaczego nie można przeprowadzić optymalizacji, warto zrozumieć podstawowe mechanizmy. Najbardziej zwięzły przykład, jaki udało mi się znaleźć, został zaimplementowany w makrach C pod adresem : http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

Kompilatory często mają trudności z określeniem, czy skok można zlokalizować na X, Y i Z, więc pomijają optymalizacje, których nie mogą zagwarantować bezpieczeństwa, ale sama implementacja jest raczej lekka.

technozaur
źródło
4
Te makra C znalezione dla try / catch nie są równoważne z Javą ani implementacją C #, które wysyłają 0 instrukcji w czasie wykonywania.
Patashu,
Implementacja Java jest zbyt obszerna, aby ją w całości uwzględnić, jest to implementacja uproszczona w celu zrozumienia podstawowej idei dotyczącej sposobu implementacji wyjątków. Mówienie, że emituje 0 instrukcji w czasie wykonywania, jest mylące. Na przykład prosty wyjątek klasycast rozszerza wyjątek środowiska wykonawczego, który rozszerza wyjątek, który rozszerza możliwość rzucania, co obejmuje: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/ ... ... To tak, jakby powiedzieć, że przypadek przełączania w C jest za darmo, jeśli kiedykolwiek zostanie użyta tylko 1 obudowa, nadal istnieje niewielki narzut związany z uruchomieniem.
technozaur
1
@Patashu Wszystkie te wstępnie skompilowane bity nadal muszą zostać załadowane podczas uruchamiania, niezależnie od tego, czy są kiedykolwiek używane. Nie ma sposobu, aby wiedzieć, czy wystąpi wyjątek braku pamięci w czasie wykonywania w czasie kompilacji - dlatego są one nazywane wyjątkami w czasie wykonywania - w przeciwnym razie byłyby to ostrzeżenia / błędy kompilatora, więc nie, nie wszystko zoptymalizuje , cały kod do ich obsługi jest zawarty w skompilowanym kodzie i ma koszt uruchomienia.
technozaur
2
Nie mogę mówić o C. W języku C # i Javie try jest implementowane przez dodanie metadanych, a nie kodu. Po wprowadzeniu bloku try nic nie jest wskazywane - po zgłoszeniu wyjątku stos jest rozwijany, a metadane sprawdzane pod kątem procedur obsługi tego typu wyjątku (kosztowne).
Patashu,
1
Tak, faktycznie zaimplementowałem interpreter Java i kompilator statycznego kodu bajtowego i pracowałem nad kolejnym JITC (dla IBM iSeries) i mogę powiedzieć, że nie ma nic, co „oznaczałoby” wejście / wyjście z zakresu try w kodach bajtów, ale raczej zakresy są określone w osobnej tabeli. Tłumacz nie robi nic specjalnego dla zakresu prób (dopóki nie zostanie zgłoszony wyjątek). JITC (lub statyczny kompilator kodu bajtowego) musi być świadomy granic, aby powstrzymać optymalizacje, jak wcześniej wspomniano.
Hot Licks
8

Kolejny mikrobenchmark ( źródło ).

Stworzyłem test, w którym mierzę wersję kodu try-catch i no-try-catch na podstawie wartości procentowej wyjątku. 10% procent oznacza, że ​​10% przypadków testowych miało podział na zero przypadków. W jednej sytuacji jest obsługiwany przez blok try-catch, w drugiej przez operator warunkowy. Oto moja tabela wyników:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Procent | Wynik (try / if, ns)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

Co oznacza, że ​​nie ma znaczącej różnicy między tymi przypadkami.

Andrey Chaschev
źródło
3

Odkryłem, że łapanie NullPointException jest dość drogie. W przypadku operacji 1,2 tys. Czas wynosił 200 ms i 12 ms, kiedy wymieniałem go w ten sam sposób, if(object==null)co było dla mnie dość poprawą.

Mateusz Kafłowski
źródło