Ist es teuer, Try-Catch-Blöcke zu verwenden, auch wenn niemals eine Ausnahme ausgelöst wird?

189

Wir wissen, dass es teuer ist, Ausnahmen zu erwischen. Aber ist es auch teuer, einen Try-Catch-Block in Java zu verwenden, selbst wenn niemals eine Ausnahme ausgelöst wird?

Ich habe die Frage / Antwort zum Stapelüberlauf gefunden. Warum sind Versuchsblöcke teuer? , aber es ist für .NET .

jsedano
quelle
30
Diese Frage hat wirklich keinen Sinn. Try..catch hat einen ganz bestimmten Zweck. Wenn Sie es brauchen, brauchen Sie es. Was ist auf jeden Fall ein Versuch ohne Fang?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }ist legal und nützlich
A4L
4
Diese Kosten müssen gegen die Vorteile abgewogen werden. Es steht nicht alleine da. In jedem Fall ist teuer relativ, und bis Sie wissen, dass Sie es nicht können, ist es sinnvoll, die naheliegendste Methode zu verwenden, anstatt etwas nicht zu tun, da Sie dadurch im Laufe einer Stunde ein oder zwei Millisekunden sparen können Programmausführung.
Joel
4
Ich hoffe, dies führt nicht zu einer Situation vom Typ "Lasst uns Fehlercodes neu erfinden" ...
mikołak
6
@ SAFX: Mit Java7 können Sie den finallyBlock sogar mit einemtry-with-resources
a_horse_with_no_name

Antworten:

201

tryhat fast keine Kosten. Anstatt die Arbeit zur Einrichtung des tryzur Laufzeit zu erledigen, sind die Metadaten des Codes zur Kompilierungszeit so strukturiert, dass beim Auslösen einer Ausnahme jetzt eine relativ teure Operation ausgeführt wird, bei der der Stapel hochgefahren wird und geprüft wird, ob tryBlöcke vorhanden sind, die dies abfangen würden Ausnahme. tryKann aus der Sicht eines Laien genauso gut frei sein. Es wird tatsächlich die Ausnahme ausgelöst, die Sie kostet - aber wenn Sie nicht Hunderte oder Tausende von Ausnahmen auslösen, werden Sie die Kosten immer noch nicht bemerken.


tryhat einige kleinere Kosten damit verbunden. Java kann einige Optimierungen für Code in einem tryBlock nicht vornehmen, die es sonst tun würde. Beispielsweise ordnet Java Anweisungen in einer Methode häufig neu an, damit sie schneller ausgeführt wird. Java muss jedoch auch sicherstellen, dass beim Auslösen einer Ausnahme die Ausführung der Methode so beobachtet wird, als würden ihre im Quellcode geschriebenen Anweisungen ausgeführt um bis zu einer Zeile.

Denn in einem tryBlock kann eine Ausnahme ausgelöst werden (in jeder Zeile des try-Blocks! Einige Ausnahmen werden asynchron ausgelöst, z. B. durch Aufrufen stopeines Threads (der veraltet ist), und sogar darüber hinaus kann OutOfMemoryError fast überall auftreten) kann abgefangen werden und Code wird anschließend auf dieselbe Weise weiter ausgeführt. Es ist schwieriger, über Optimierungen nachzudenken, die vorgenommen werden können, sodass die Wahrscheinlichkeit geringer ist, dass sie auftreten. (Jemand müsste den Compiler programmieren, um dies zu tun, die Richtigkeit zu begründen und zu garantieren usw. Es wäre ein großer Schmerz für etwas, das "außergewöhnlich" sein soll.) Aber auch in der Praxis werden Sie solche Dinge nicht bemerken.

Patashu
quelle
2
Einige Ausnahmen werden asynchron ausgelöst , sie sind nicht asynchron , sondern werden an sicheren Punkten ausgelöst. und dieser Teilversuch hat einige geringfügige Kosten, die damit verbunden sind. Java kann einige Optimierungen für Code in einem try-Block nicht vornehmen, für die es sonst eine ernsthafte Referenz benötigt. Irgendwann befindet sich der Code sehr wahrscheinlich im Try / Catch-Block. Es mag wahr sein, dass es schwieriger ist, einen Try / Catch-Block zu inlinieren und ein geeignetes Gitter für das Ergebnis zu erstellen, aber der Teil mit der Neuanordnung ist nicht eindeutig.
Bests
2
Verhindert ein try...finallyBlock ohne catchauch einige Optimierungen?
Tag
5
@Patashu „Es wirft tatsächlich die Ausnahme , dass die Kosten Sie“ Technisch gesehen , werfen die Ausnahme nicht teuer ist; Das Instanziieren des ExceptionObjekts dauert die meiste Zeit.
Austin D
72

Lassen Sie es uns messen, sollen wir?

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

Auf meinem Computer wird Folgendes gedruckt:

try     0.598 ns
no try  0.601 ns

Zumindest in diesem trivialen Beispiel hatte die try-Anweisung keinen messbaren Einfluss auf die Leistung. Fühlen Sie sich frei, komplexere zu messen.

Im Allgemeinen empfehle ich, sich keine Gedanken über die Leistungskosten von Sprachkonstrukten zu machen, bis Sie Hinweise auf ein tatsächliches Leistungsproblem in Ihrem Code haben. Oder wie Donald Knuth es ausdrückte : "Vorzeitige Optimierung ist die Wurzel allen Übels".

Meriton
quelle
4
Während try / no try bei den meisten JVMs sehr wahrscheinlich gleich ist, ist das Mikrobenchmark schrecklich fehlerhaft.
Bests
2
ziemlich viele Ebenen: Sie meinen, die Ergebnisse werden in weniger als 1 ns berechnet? Kompilierter Code entfernt sowohl den Versuch / Fang als auch die Schleife insgesamt (die Summierung der Zahl von 1 bis n ist eine triviale arithmetische Fortschrittssumme). Selbst wenn der Code try / finally enthält, kann der Compiler beweisen, dass dort nichts zu werfen ist. Der abstrakte Code hat nur 2 Aufrufstellen und wird geklont und inline. Es gibt weitere Fälle. Schlagen Sie einfach einige Artikel über Microbenchmark nach, und wenn Sie sich entscheiden, ein Microbenchmark zu schreiben, überprüfen Sie immer die generierte Baugruppe.
Bests
3
Die angegebenen Zeiten beziehen sich auf die Iteration der Schleife. Da eine Messung nur verwendet wird, wenn sie eine verstrichene Gesamtzeit von> 0,1 Sekunden hat (oder 2 Milliarden Iterationen, was hier nicht der Fall war), finde ich Ihre Behauptung, dass die Schleife in ihrer Gesamtheit entfernt wurde, kaum zu glauben - denn wenn Die Schleife wurde entfernt. Was dauerte 0,1 Sekunden, um ausgeführt zu werden?
Meriton
... und tatsächlich sind -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssemblysowohl die Schleife als auch die Addition darin im generierten nativen Code vorhanden. Und nein, die abstrakten Methoden sind nicht inline, weil ihr Aufrufer nicht nur rechtzeitig kompiliert wird (vermutlich, weil er nicht oft genug aufgerufen wird).
Meriton
Wie schreibe ich einen korrekten Mikro-Benchmark in Java: stackoverflow.com/questions/504103/…
Vadzim
46

tryIch catchkann einen Einfluss auf die Leistung haben. Dies liegt daran, dass JVM dadurch keine Optimierungen vornehmen kann. Joshua Bloch sagte in "Effective Java" Folgendes:

• Das Platzieren von Code in einem Try-Catch-Block verhindert bestimmte Optimierungen, die moderne JVM-Implementierungen andernfalls ausführen könnten.

Evgeniy Dorofeev
quelle
24
"Es verhindert, dass JVM einige Optimierungen vornimmt" ...? Könnten Sie das überhaupt näher erläutern?
Der Kraken
5
@ Der Kraken-Code innerhalb von Try-Blöcken (normalerweise? Immer?) Kann beispielsweise nicht mit Code außerhalb von Try-Blöcken nachbestellt werden.
Patashu
3
Beachten Sie, dass die Frage lautete, ob "es teuer ist" und nicht, ob "es einen Einfluss auf die Leistung hat".
Mikołak
3
fügte einen Auszug aus Effective Java hinzu , und das ist natürlich die Bibel von Java; Sofern es keine Referenz gibt, sagt der Auszug nichts. Praktisch jeder Code in Java befindet sich in try / finally auf einer bestimmten Ebene.
Bests
29

Ja, wie die anderen gesagt haben, verhindert ein tryBlock einige Optimierungen in den ihn {}umgebenden Zeichen. Insbesondere muss der Optimierer davon ausgehen, dass an jedem Punkt innerhalb des Blocks eine Ausnahme auftreten kann, sodass nicht garantiert werden kann, dass Anweisungen ausgeführt werden.

Beispielsweise:

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

Ohne das könnte tryder Wert, dem die Zuweisung berechnet wurde, xals "allgemeiner Unterausdruck" gespeichert und zur Zuweisung wiederverwendet werden y. Aufgrund der tryTatsache, dass es keine Garantie dafür gibt, dass der erste Ausdruck jemals ausgewertet wurde, muss der Ausdruck neu berechnet werden. Dies ist normalerweise keine große Sache im "geradlinigen" Code, kann aber in einer Schleife von Bedeutung sein.

Es ist jedoch zu beachten, dass dies NUR für JITCed-Code gilt. javac führt nur eine geringfügige Optimierung durch, und der Bytecode-Interpreter hat keine Kosten für das Betreten / Verlassen eines tryBlocks. (Es werden keine Bytecodes generiert, um die Blockgrenzen zu markieren.)

Und für Bests:

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

Ausgabe:

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

Javap-Ausgabe:

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
}

Kein "GOTO".

Hot Licks
quelle
Es werden keine Bytecodes generiert, um die Blockgrenzen zu markieren. Dies ist nicht unbedingt erforderlich. GOTO muss den Block verlassen, da er sonst in den catch/finallyFrame fällt .
Bests
@bestsss - Selbst wenn ein GOTO generiert wird (was nicht selbstverständlich ist), sind die Kosten dafür winzig und es ist weit davon entfernt, ein "Marker" für eine Blockgrenze zu sein - GOTO kann für viele Konstrukte generiert werden.
Hot Licks
Ich habe die Kosten nie erwähnt, es werden jedoch keine Bytecodes generiert, was eine falsche Aussage ist. Das ist alles. Tatsächlich gibt es keine Blöcke im Bytecode, Frames sind nicht gleich Blöcke.
Bests
Es wird kein GOTO geben, wenn der Versuch direkt in das endgültige fällt, und es gibt andere Szenarien, in denen es kein GOTO gibt. Der Punkt ist, dass es nichts in der Reihenfolge der Bytecodes "enter try" / "exit try" gibt.
Hot Licks
Es wird kein GOTO geben, wenn der Versuch direkt ins Endliche fällt - Falsch! Es gibt keinen finallyBytecode, es ist try/catch(Throwable any){...; throw any;}Und es hat eine catch-Anweisung mit einem Frame und Throwable, die definiert werden muss (nicht null) und so weiter. Warum versuchen Sie, über das Thema zu streiten, können Sie zumindest einen Bytecode überprüfen? Die aktuelle Richtlinie für impl. von schließlich werden die Blöcke kopiert und der Abschnitt goto vermieden (vorheriges impl), aber die Bytecodes müssen kopiert werden, abhängig davon, wie viele Austrittspunkte es gibt.
Bests
8

Um zu verstehen, warum die Optimierungen nicht durchgeführt werden können, ist es hilfreich, die zugrunde liegenden Mechanismen zu verstehen. Das prägnanteste Beispiel, das ich finden konnte, wurde in C-Makros unter folgender Adresse implementiert: 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)

Compiler haben häufig Schwierigkeiten zu bestimmen, ob ein Sprung auf X, Y und Z lokalisiert werden kann, sodass sie Optimierungen überspringen, die sie nicht als sicher garantieren können, aber die Implementierung selbst ist eher leicht.

Technosaurus
quelle
4
Diese C-Makros, die Sie für try / catch gefunden haben, entsprechen nicht der Java- oder der C # -Implementierung, die 0 Laufzeitanweisungen ausgibt.
Patashu
Die Java-Implementierung ist zu umfangreich, um sie vollständig aufzunehmen. Dies ist eine vereinfachte Implementierung, um die Grundidee zu verstehen, wie Ausnahmen implementiert werden können. Zu sagen, dass 0 Laufzeitanweisungen ausgegeben werden, ist irreführend. Zum Beispiel erweitert eine einfache classcastexception die Laufzeitausnahme, die die Ausnahme erweitert, die throwable erweitert. Dazu gehören: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ... Das heißt, ein Switch-Case in C ist kostenlos, wenn nur 1 Fall jemals verwendet wird, gibt es immer noch einen kleinen Startaufwand.
Technosaurus
1
@Patashu Alle diese vorkompilierten Bits müssen beim Start noch geladen werden, unabhängig davon, ob sie jemals verwendet werden oder nicht. Es gibt keine Möglichkeit zu wissen, ob während der Laufzeit zur Kompilierungszeit eine Ausnahme wegen Speichermangel auftritt. Aus diesem Grund werden sie als Laufzeitausnahmen bezeichnet. Andernfalls handelt es sich um Compiler-Warnungen / -Fehler. Nein, es wird nicht alles optimiert Der gesamte Code für die Verarbeitung ist im kompilierten Code enthalten und hat Startkosten.
Technosaurus
2
Ich kann nicht über C sprechen. In C # und Java wird try durch Hinzufügen von Metadaten und nicht von Code implementiert. Wenn ein try-Block eingegeben wird, wird nichts ausgeführt, um dies anzuzeigen. Wenn eine Ausnahme ausgelöst wird, wird der Stapel abgewickelt und die Metadaten auf Handler dieses Ausnahmetyps überprüft (teuer).
Patashu
1
Ja, ich habe tatsächlich einen Java-Interpreter und einen statischen Bytecode-Compiler implementiert und an einem nachfolgenden JITC (für IBM iSeries) gearbeitet, und ich kann Ihnen sagen, dass nichts den Ein- / Ausgang des try-Bereichs in den Bytecodes "markiert", sondern vielmehr Die Bereiche sind in einer separaten Tabelle aufgeführt. Der Interpreter macht nichts Besonderes für einen Versuchsbereich (bis eine Ausnahme ausgelöst wird). Ein JITC (oder statischer Bytecode-Compiler) muss die Grenzen kennen, um Optimierungen wie zuvor angegeben zu unterdrücken.
Hot Licks
8

Noch ein Mikrobenchmark ( Quelle ).

Ich habe einen Test erstellt, in dem ich die Codeversion try-catch und no-try-catch anhand eines Ausnahmeprozentsatzes messe. 10% Prozent bedeutet, dass 10% der Testfälle durch Null geteilt wurden. In einer Situation wird es von einem Try-Catch-Block behandelt, in der anderen von einem bedingten Operator. Hier ist meine Ergebnistabelle:

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

Was besagt, dass es keinen signifikanten Unterschied zwischen diesen Fällen gibt.

Andrey Chaschev
quelle
3

Ich fand es ziemlich teuer, NullPointException zu fangen. Für 1,2-km-Operationen betrug die Zeit 200 ms und 12 ms, als ich sie auf die gleiche Weise handhabte, if(object==null)was für mich eine ziemlich gute Verbesserung war.

Mateusz Kaflowski
quelle