È costoso utilizzare i blocchi try-catch anche se non viene mai generata un'eccezione?


189

Sappiamo che è costoso catturare eccezioni. Ma è anche costoso utilizzare un blocco try-catch in Java anche se non viene mai generata un'eccezione?

Ho trovato la domanda / risposta Stack Overflow Perché i blocchi try sono costosi? , ma è per .NET .


30
Non ha davvero senso questa domanda. Try..catch ha uno scopo molto specifico. Se ne hai bisogno, ne hai bisogno. In ogni caso, a che serve provare senza pescare?
JohnFx

49
try { /* do stuff */ } finally { /* make sure to release resources */ }è legale e utile
A4L

4
Tale costo deve essere valutato rispetto ai benefici. Non è solo. In ogni caso, costoso è relativo e fino a quando non sai di non poterlo fare, ha senso utilizzare il metodo più ovvio piuttosto che non fare qualcosa perché potrebbe farti risparmiare un millisecondo o due nel corso di un'ora di esecuzione del programma.
Gioele

4
Spero che questo non porti a una situazione di tipo "reinventiamo-codici di errore" ...
mikołak,

6
@SAFX: con Java7 puoi persino sbarazzarti del finallyblocco usando atry-with-resources
a_horse_with_no_name

Risposte:


201

trynon ha quasi nessuna spesa. Invece di fare il lavoro di impostazione tryin fase di esecuzione, i metadati del codice sono strutturati in fase di compilazione in modo tale che quando viene generata un'eccezione, ora esegue un'operazione relativamente costosa di camminare nello stack e vedere se tryesistono blocchi che catturerebbero questo eccezione. Dal punto di vista di un laico, trypuò anche essere libero. In realtà sta generando l'eccezione che ti costa, ma a meno che tu non stia generando centinaia o migliaia di eccezioni, non noterai ancora il costo.


tryha alcuni costi minori associati. Java non può fare alcune ottimizzazioni sul codice in un tryblocco che altrimenti farebbe. Ad esempio, Java riorganizza spesso le istruzioni in un metodo per renderlo più veloce, ma Java deve anche garantire che se viene generata un'eccezione, l'esecuzione del metodo viene osservata come se le sue istruzioni, come scritte nel codice sorgente, fossero eseguite in ordine fino a qualche riga.

Perché in un tryblocco può essere generata un'eccezione (in qualsiasi riga del blocco try! Alcune eccezioni vengono generate in modo asincrono, ad esempio chiamando stopun thread (che è deprecato) e persino oltre che OutOfMemoryError può accadere quasi ovunque) e tuttavia può essere catturato e il codice continua a essere eseguito successivamente nello stesso metodo, è più difficile ragionare sulle ottimizzazioni che possono essere fatte, quindi hanno meno probabilità di accadere. (Qualcuno dovrebbe programmare il compilatore per eseguirli, ragionare e garantire la correttezza, ecc. Sarebbe un grande dolore per qualcosa che dovrebbe essere "eccezionale") Ma, di nuovo, in pratica non noterai cose come questa.


2
Alcune eccezioni vengono generate in modo asincrono , non sono asincrone ma vengono gettate in punti sicuri. e questa parte di prova ha alcuni costi minori associati. Java non può fare alcune ottimizzazioni sul codice in un blocco try che altrimenti farebbe bisogno di un riferimento serio. Ad un certo punto è molto probabile che il codice sia all'interno del blocco try / catch. Potrebbe essere vero che il blocco try / catch sarebbe più difficile da allineare e costruire un reticolo adeguato per il risultato, ma la parte con il riarrangiamento è ambigua.
bestsss

2
Un try...finallyblocco senza catchimpedisce anche alcune ottimizzazioni?
dajood,

5
@Patashu "In realtà è lanciare l'eccezione che ti costa" Tecnicamente, lanciare l'eccezione non è costoso; l'istanza Exceptiondell'oggetto è ciò che richiede la maggior parte del tempo.
Austin D

72

Misuriamolo, vero?

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);
        }
    }
}

Sul mio computer, questo stampa qualcosa di simile:

try     0.598 ns
no try  0.601 ns

Almeno in questo banale esempio, la dichiarazione try non ha avuto alcun impatto misurabile sulle prestazioni. Sentiti libero di misurare quelli più complessi.

In generale, ti consiglio di non preoccuparti del costo prestazionale dei costrutti linguistici fino a quando non avrai prove di un reale problema prestazionale nel tuo codice. O come diceva Donald Knuth : "l'ottimizzazione prematura è la radice di tutti i mali".


4
mentre è molto probabile che try / no try sia lo stesso sulla maggior parte delle JVM, il microbenchmark è terribilmente imperfetto.
bestsss

2
parecchi livelli: vuoi dire che i risultati sono calcolati in meno di 1 ns? Il codice compilato rimuoverà del tutto il ciclo try / catch AND (il numero di somma da 1 a n è una banale somma di progressione aritmetica). Anche se il codice contiene try / finalmente il compilatore può provare, non c'è nulla da lanciare lì. Il codice astratto ha solo 2 siti di chiamata e sarà clonato e integrato. Ci sono più casi, basta cercare alcuni articoli sul microbenchmark e decidi di scrivere un microbenchmark sempre controllare l'assemblaggio generato.
bestsss

3
I tempi riportati sono per iterazione del loop. Poiché una misurazione verrà utilizzata solo se ha un tempo totale trascorso> 0,1 secondi (o 2 miliardi di iterazioni, che non era il caso qui) trovo che la tua affermazione che il loop sia stato rimosso nella sua interezza sia difficile da credere - perché se il ciclo è stato rimosso, che cosa ha richiesto 0,1 secondi per essere eseguito?
Meriton,

... e in effetti, secondo -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblyil ciclo nativo generato, sono presenti sia il ciclo sia l'aggiunta in esso. E no, i metodi astratti non sono integrati, perché il loro chiamante non è compilato solo in tempo (presumibilmente, perché non è invocato abbastanza volte).
Meriton,

Come posso scrivere un micro-benchmark corretto in Java: stackoverflow.com/questions/504103/…
Vadzim

47

try/ catchpotrebbe avere un impatto sulle prestazioni. Questo perché impedisce a JVM di eseguire alcune ottimizzazioni. Joshua Bloch, in "Effective Java", ha dichiarato quanto segue:

• L'inserimento di codice all'interno di un blocco try-catch inibisce alcune ottimizzazioni che potrebbero altrimenti eseguire le moderne implementazioni JVM.


25
"impedisce a JVM di fare alcune ottimizzazioni" ...? Potresti elaborare del tutto?
Il Kraken

5
@Il codice Kraken all'interno dei blocchi di prova (di solito? Sempre?) Non può essere riordinato con il codice al di fuori dei blocchi di prova, ad esempio.
Patashu,

3
Si noti che la domanda era se "è costoso", non se "ha un impatto sulle prestazioni".
mikołak,

3
aggiunto un estratto da Effective Java , e questa è la bibbia di Java, ovviamente; a meno che non ci sia un riferimento, il brano non dice nulla. Praticamente qualsiasi codice in Java è all'interno di try / finalmente ad un certo livello.
bestsss

29

Sì, come hanno detto gli altri, un tryblocco inibisce alcune ottimizzazioni tra i {}personaggi che lo circondano. In particolare, l'ottimizzatore deve presumere che un'eccezione possa verificarsi in qualsiasi punto all'interno del blocco, quindi non vi è alcuna garanzia che le istruzioni vengano eseguite.

Per esempio:

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

Senza il try, il valore calcolato da assegnare a xpotrebbe essere salvato come "sottoespressione comune" e riutilizzato per assegnare a y. Ma a causa del trynon vi è alcuna garanzia che la prima espressione sia mai stata valutata, quindi l'espressione deve essere ricalcolata. Questo di solito non è un grosso problema nel codice "straight-line", ma può essere significativo in un ciclo.

Va notato, tuttavia, che ciò si applica SOLO al codice JITC. javac ha solo una quantità di ottimizzazione e ha un costo zero per l'interprete bytecode per entrare / uscire da un tryblocco. (Non ci sono bytecode generati per contrassegnare i confini del blocco.)

E per i migliori:

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

Produzione:

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

uscita 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
}

Nessun "GOTO".


Non ci sono bytecode generati per contrassegnare i confini del blocco, questo non è necessariamente - richiede GOTO per lasciare il blocco, altrimenti cadrà nel catch/finallyframe.
bestsss

@bestsss - Anche se viene generato un GOTO (che non è un dato) il suo costo è minuscolo, ed è lungi dall'essere un "marcatore" per un limite di blocco - GOTO può essere generato per molti costrutti.
Hot Licks,

Non ho mai menzionato il costo, tuttavia non ci sono bytecode generati è una dichiarazione falsa. È tutto. In realtà, non ci sono blocchi in bytecode, i frame non equivalgono a blocchi.
bestsss

Non ci sarà un GOTO se il tentativo cade direttamente nel finale e ci sono altri scenari in cui non ci sarà GOTO. Il punto è che non c'è nulla nell'ordine di bytecode "enter try" / "exit try".
Hot Licks

Non ci sarà un GOTO se il tentativo cade direttamente nel finalmente - Falso! non esiste un finallybytecode, è try/catch(Throwable any){...; throw any;}E ha un'istruzione catch w / a frame e Throwable che DEVE ESSERE definita (non nulla) e così via. Perché provi a discutere dell'argomento, puoi controllare almeno alcuni bytecode? L'attuale linea guida per impl. of infine sta copiando i blocchi ed evitando la sezione goto (impl precedente) ma i bytecode devono essere copiati a seconda di quanti punti di uscita ci sono.
bestsss

8

Per capire perché non è possibile eseguire le ottimizzazioni, è utile comprendere i meccanismi sottostanti. L'esempio più conciso che ho trovato è stato implementato nelle macro C su: 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)

I compilatori hanno spesso difficoltà a determinare se un salto può essere localizzato in X, Y e Z, quindi saltano le ottimizzazioni che non possono garantire di essere sicure, ma l'implementazione stessa è piuttosto leggera.


4
Queste macro C che hai trovato per try / catch non equivalgono a Java o all'implementazione C #, che emettono 0 istruzioni di runtime.
Patashu,

L'implementazione java è troppo estesa per essere inclusa integralmente, questa è un'implementazione semplificata allo scopo di comprendere l'idea di base su come implementare le eccezioni. Dire che emette 0 istruzioni di runtime è fuorviante. Ad esempio, una semplice classcastexception estende la runtimeexception che estende l'eccezione che si estende gettabile che coinvolge: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ... È come dire che un caso in C è gratuito se viene mai usato solo 1 caso, c'è ancora un piccolo overhead di avvio.
technosaurus,

1
@Patashu Tutti questi bit precompilati devono ancora essere caricati all'avvio, indipendentemente dal fatto che vengano mai utilizzati. Non c'è modo di sapere se ci sarà un'eccezione di memoria esaurita durante il runtime al momento della compilazione - ecco perché vengono chiamati eccezioni del runtime - altrimenti sarebbero avvisi / errori del compilatore, quindi no non ottimizza tutto , tutto il codice per gestirli è incluso nel codice compilato e ha un costo di avvio.
technosaurus,

2
Non posso parlare di C. In C # e Java, provare è implementato aggiungendo metadati, non codice. Quando viene inserito un blocco try, non viene eseguito nulla per indicarlo: quando viene generata un'eccezione, lo stack non viene svolto e i metadati controllano i gestori di quel tipo di eccezione (costoso).
Patashu,

1
Sì, in realtà ho implementato un interprete Java e un compilatore bytecode statico e ho lavorato su un JITC successivo (per IBM iSeries) e posso dire che non c'è nulla che "contrassegni" l'ingresso / uscita dell'intervallo di prova nei bytecode, ma piuttosto gli intervalli sono identificati in una tabella separata. L'interprete non fa nulla di speciale per un intervallo di prova (fino a quando non viene sollevata un'eccezione). Un JITC (o compilatore statico bytecode) deve essere consapevole dei limiti per sopprimere le ottimizzazioni come precedentemente indicato.
Hot Licks

8

Ennesimo microbenchmark ( fonte ).

Ho creato un test in cui misuro la versione del codice try-catch e no-try-catch in base a una percentuale di eccezione. Una percentuale del 10% indica che il 10% dei casi di test era diviso per zero casi. In una situazione è gestito da un blocco try-catch, nell'altra da un operatore condizionale. Ecco la mia tabella dei risultati:

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

Il che afferma che non esiste alcuna differenza significativa tra nessuno di questi casi.


3

Ho trovato NullPointException piuttosto costoso. Per le operazioni da 1,2k il tempo era di 200 ms e 12 ms quando l'ho gestito allo stesso modo con il if(object==null)quale è stato piuttosto un miglioramento per me.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.