571 Stimmen

Welche Auswirkungen haben Ausnahmen auf die Leistung in Java?

Frage: Ist die Ausnahmebehandlung in Java wirklich langsam?

Konventionelle Weisheit und auch viele Google-Ergebnisse besagen, dass außergewöhnliche Logik nicht für den normalen Programmablauf in Java verwendet werden sollte. Dafür werden in der Regel zwei Gründe genannt,

  1. er ist wirklich langsam - sogar um eine Größenordnung langsamer als normaler Code (die angegebenen Gründe sind unterschiedlich),

y

  1. es ist chaotisch, weil die Leute erwarten, dass nur Fehler in außergewöhnlichem Code behandelt werden.

Diese Frage bezieht sich auf die Nummer 1.

Ein Beispiel, diese Seite beschreibt die Java-Ausnahmebehandlung als "sehr langsam" und bezieht die Langsamkeit auf die Erstellung der Ausnahmemeldungszeichenfolge - "diese Zeichenfolge wird dann bei der Erstellung des Ausnahmeobjekts verwendet, das ausgelöst wird. Das ist nicht schnell." Der Artikel Effektive Ausnahmebehandlung in Java sagt, dass "der Grund dafür in dem Aspekt der Objekterzeugung bei der Ausnahmebehandlung liegt, der das Auslösen von Ausnahmen von Natur aus langsam macht". Ein anderer Grund ist, dass die Stack-Trace-Generierung die Ursache für die Verlangsamung ist.

Meine Tests (mit Java 1.6.0_07, Java HotSpot 10.0, auf 32-Bit-Linux) zeigen, dass die Ausnahmebehandlung nicht langsamer ist als normaler Code. Ich habe versucht, eine Methode in einer Schleife auszuführen, die einen Code ausführt. Am Ende der Methode verwende ich einen Booleschen Wert, um anzugeben, ob return o werfen . Auf diese Weise ist die tatsächliche Verarbeitung die gleiche. Ich habe versucht, die Methoden in verschiedenen Reihenfolgen auszuführen und den Durchschnitt meiner Testzeiten zu ermitteln, weil ich dachte, dass die JVM sich aufwärmen könnte. In allen meinen Tests war der Wurf mindestens so schnell wie die Rückkehr, wenn nicht sogar schneller (bis zu 3,1 % schneller). Ich bin völlig offen für die Möglichkeit, dass meine Tests falsch waren, aber ich habe in den letzten ein oder zwei Jahren nichts in Form von Codebeispielen, Testvergleichen oder Ergebnissen gesehen, die zeigen, dass die Ausnahmebehandlung in Java tatsächlich langsam ist.

Was mich auf diesen Weg geführt hat, war eine API, die ich verwenden musste und die Ausnahmen als Teil der normalen Steuerungslogik auslöste. Ich wollte sie in ihrer Verwendung korrigieren, aber jetzt kann ich das vielleicht nicht mehr. Werde ich sie stattdessen für ihr vorausschauendes Denken loben müssen?

In dem Papier Effiziente Java-Ausnahmebehandlung bei der Just-in-Time-Kompilierung Die Autoren weisen darauf hin, dass das Vorhandensein von Ausnahmebehandlungsroutinen allein, auch wenn keine Ausnahmen ausgelöst werden, ausreicht, um den JIT-Compiler daran zu hindern, den Code ordnungsgemäß zu optimieren und ihn somit zu verlangsamen. Ich habe diese Theorie noch nicht getestet.

13 Stimmen

Ich weiß, dass Sie nicht nach 2) gefragt haben, aber Sie sollten wirklich erkennen, dass die Verwendung einer Ausnahme für den Programmablauf nicht besser ist als die Verwendung von GOTOs. Einige Leute verteidigen Gotos, andere würden das verteidigen, wovon Sie sprechen, aber wenn Sie jemanden fragen, der beides über einen längeren Zeitraum implementiert und gewartet hat, wird er Ihnen sagen, dass beides schlechte, schwer zu wartende Entwurfspraktiken sind (und er wird wahrscheinlich den Namen der Person verfluchen, die dachte, sie sei klug genug, um die Entscheidung zu treffen, sie zu verwenden).

98 Stimmen

Bill, die Behauptung, die Verwendung von Ausnahmen für den Programmablauf sei nicht besser als die Verwendung von GOTOs, ist nicht besser als die Behauptung, die Verwendung von Konditionalen und Schleifen für den Programmablauf sei nicht besser als die Verwendung von GOTOs. Das ist ein Ablenkungsmanöver. Erkläre dich selbst. Ausnahmen können und werden in anderen Sprachen effektiv für den Programmfluss verwendet. Idiomatischer Python-Code verwendet zum Beispiel regelmäßig Ausnahmen. Ich kann und habe Code gepflegt, der Ausnahmen auf diese Weise verwendet (allerdings nicht in Java), und ich glaube nicht, dass daran etwas grundsätzlich falsch ist.

5 Stimmen

Beachten Sie, dass einige Web-Frameworks Exceptions als bequeme Möglichkeit zur Umleitung verwenden - z.B. Wicket's RestartResponseException . Es passiert nur wenige Male pro Anfrage, normalerweise nicht, und ich kann mir kaum einen bequemeren Weg in einem Java-orientierten Komponenten-Framework vorstellen.

376voto

Mecki Punkte 113876

Es kommt darauf an, wie Ausnahmen implementiert werden. Der einfachste Weg ist die Verwendung von setjmp und longjmp. Das bedeutet, dass alle Register der CPU auf den Stack geschrieben werden (was bereits einige Zeit in Anspruch nimmt) und möglicherweise einige andere Daten erstellt werden müssen... all dies geschieht bereits in der try-Anweisung. Die throw-Anweisung muss den Stack abwickeln und die Werte aller Register (und eventuell anderer Werte in der VM) wiederherstellen. Try und Throw sind also gleich langsam, und das ist ziemlich langsam, aber wenn keine Ausnahme ausgelöst wird, nimmt das Verlassen des Try-Blocks in den meisten Fällen überhaupt keine Zeit in Anspruch (da alles auf den Stack gelegt wird, der automatisch aufgeräumt wird, wenn die Methode existiert).

Sun und andere haben erkannt, dass dies möglicherweise suboptimal ist und natürlich werden VMs mit der Zeit immer schneller. Es gibt einen anderen Weg, Ausnahmen zu implementieren, der try selbst blitzschnell macht (eigentlich passiert bei try überhaupt nichts - alles, was passieren muss, ist bereits erledigt, wenn die Klasse von der VM geladen wird) und throw ist nicht ganz so langsam. Ich weiß nicht, welche JVM diese neue, bessere Technik verwendet...

...aber schreiben Sie in Java, damit Ihr Code später nur auf einer JVM auf einem bestimmten System läuft? Denn wer sagt denn, dass er auch auf einer anderen Plattform oder einer anderen JVM-Version (möglicherweise von einem anderen Hersteller) läuft, wenn er auch die schnelle Implementierung verwendet? Die schnelle Implementierung ist komplizierter als die langsame und nicht ohne weiteres auf allen Systemen möglich. Sie wollen portabel bleiben? Dann verlassen Sie sich nicht darauf, dass Ausnahmen schnell sind.

Es macht auch einen großen Unterschied, was Sie innerhalb eines Versuchsblocks tun. Wenn Sie einen Try-Block öffnen und nie eine Methode aus diesem Try-Block heraus aufrufen, wird der Try-Block extrem schnell sein, da die JIT dann einen Throw wie ein einfaches goto behandeln kann. Es muss weder den Stack-Status speichern noch den Stack abwickeln, wenn eine Ausnahme geworfen wird (es muss nur zu den Catch-Handlern springen). Dies ist jedoch nicht das, was Sie normalerweise tun. Normalerweise öffnen Sie einen Try-Block und rufen dann eine Methode auf, die eine Ausnahme auslösen könnte, richtig? Und selbst wenn Sie nur den Try-Block innerhalb Ihrer Methode verwenden, was für eine Methode wird das sein, die keine andere Methode aufruft? Wird sie nur eine Zahl berechnen? Wozu brauchen Sie dann Ausnahmen? Es gibt viel elegantere Möglichkeiten, den Programmablauf zu regeln. Für so ziemlich alles andere als einfache Mathematik müssen Sie eine externe Methode aufrufen, und damit ist der Vorteil eines lokalen Try-Blocks bereits zunichte gemacht.

Siehe den folgenden Testcode:

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

Ergebnis:

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

Die Verlangsamung durch den Try-Block ist zu gering, um störende Faktoren wie Hintergrundprozesse auszuschließen. Aber der catch-Block hat alles abgewürgt und den Prozess 66 Mal langsamer gemacht!

Wie ich schon sagte, wird das Ergebnis nicht so schlecht sein, wenn Sie try/catch und throw alle innerhalb der gleichen Methode (method3) setzen, aber dies ist eine spezielle JIT-Optimierung, auf die ich mich nicht verlassen würde. Und selbst wenn man diese Optimierung verwendet, ist der Throw immer noch ziemlich langsam. Ich weiß also nicht, was Sie versuchen, hier zu tun, aber es gibt definitiv einen besseren Weg, es zu tun, als mit try/catch/throw.

291voto

Hot Licks Punkte 46043

Zu Ihrer Information: Ich habe das Experiment, das Mecki durchgeführt hat, erweitert:

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

Die ersten 3 sind die gleichen wie die von Mecki (mein Laptop ist offensichtlich langsamer).

Methode4 ist identisch mit Methode3, mit der Ausnahme, dass sie eine new Integer(1) statt zu tun throw new Exception() .

Methode5 entspricht Methode3 mit dem Unterschied, dass sie die new Exception() ohne sie zu werfen.

Methode6 ist wie Methode3, mit dem Unterschied, dass sie eine zuvor erstellte Ausnahme (eine Instanzvariable) auslöst, anstatt eine neue zu erstellen.

In Java besteht ein Großteil der Kosten für das Auslösen einer Ausnahme in der Zeit, die für das Sammeln des Stack Trace aufgewendet wird, der bei der Erstellung des Ausnahmeobjekts entsteht. Die tatsächlichen Kosten für das Auslösen der Ausnahme sind zwar hoch, aber deutlich geringer als die Kosten für die Erstellung der Ausnahme.

92voto

Doval Punkte 2376

Aleksey Shipilëv hat eine sehr gründliche Analyse in dem er Java-Ausnahmen unter verschiedenen Kombinationen von Bedingungen vergleicht:

  • Neu erstellte Ausnahmen im Vergleich zu bereits erstellten Ausnahmen
  • Stack-Trace aktiviert vs. deaktiviert
  • Stack-Trace angefordert vs. nie angefordert
  • Auf der obersten Ebene gefangen vs. auf jeder Ebene zurückgeworfen vs. auf jeder Ebene verkettet/umhüllt
  • Verschiedene Stufen der Java-Aufrufstapeltiefe
  • Keine Inlining-Optimierungen vs. extremes Inlining vs. Standardeinstellungen
  • Benutzerdefinierte Felder lesen vs. nicht lesen

Er vergleicht sie auch mit der Leistung der Überprüfung eines Fehlercodes bei verschiedenen Fehlerhäufigkeiten.

Die Schlussfolgerungen (wörtlich zitiert aus seinem Beitrag) lauteten:

  1. Wirklich außergewöhnliche Ausnahmen sind wunderbar performant. Wenn Sie sie wie vorgesehen verwenden und nur die wirklich außergewöhnlichen Fälle unter der überwältigend großen Anzahl von nicht außergewöhnlichen Fällen, die von regulärem Code behandelt werden, kommunizieren, dann ist die Verwendung von Ausnahmen ein Leistungsgewinn.

  2. Die Leistungskosten von Ausnahmen haben zwei Hauptkomponenten: Stack-Trace-Konstruktion wenn Exception instanziiert wird und Stapelabwicklung beim Auslösen von Ausnahmen.

  3. Die Kosten für den Bau der Schornsteinspur sind proportional zur Schornsteintiefe zum Zeitpunkt der Instanziierung der Ausnahme. Das ist schon schlecht, denn wer kennt schon die Stapeltiefe, bei der diese Wurfmethode aufgerufen wird? Selbst wenn Sie die Stack-Trace-Erzeugung abschalten und/oder die Ausnahmen zwischenspeichern, können Sie nur diesen Teil der Leistungskosten loswerden.

  4. Die Kosten für die Stapelabwicklung hängen davon ab, wie gut es uns gelingt, den Exception-Handler im kompilierten Code näher zu bringen. Die sorgfältige Strukturierung des Codes zur Vermeidung tiefer Ausnahmehandler-Suche hilft uns wahrscheinlich, mehr Glück zu haben.

  5. Wenn wir beide Effekte eliminieren, sind die Leistungskosten der Ausnahmen die des lokalen Zweigs. So schön das auch klingt, das bedeutet nicht, dass Sie Ausnahmen als üblichen Kontrollfluss verwenden sollten, denn in diesem Fall Sie sind der Gnade des optimierenden Compilers ausgeliefert! Sie sollten sie nur in wirklichen Ausnahmefällen verwenden, wenn die Ausnahmehäufigkeit amortisiert die möglichen unglücklichen Kosten für das Auslösen der eigentlichen Ausnahme.

  6. Die optimistische Faustregel scheint zu sein 10^-4 Die Häufigkeit der Ausnahmen ist außergewöhnlich genug. Das hängt natürlich von der Schwere der Ausnahmen selbst ab, von den genauen Aktionen, die in den Ausnahmebehandlungsprogrammen durchgeführt werden, usw.

Das Ergebnis ist, dass, wenn eine Ausnahme nicht ausgelöst wird, keine Kosten anfallen. Wenn also die Ausnahmebedingung ausreichend selten ist, ist die Ausnahmebehandlung schneller als die Verwendung einer if jedes Mal. Der vollständige Beitrag ist sehr lesenswert.

44voto

Fuwjax Punkte 2197

Meine Antwort ist leider zu lang, um sie hier zu veröffentlichen. Ich fasse sie daher hier zusammen und verweise Sie auf http://www.fuwjax.com/how-slow-are-java-exceptions/ für die wichtigsten Details.

Die eigentliche Frage ist hier nicht "Wie langsam sind 'Fehler, die als Ausnahmen gemeldet werden' im Vergleich zu 'Code, der nie fehlschlägt'", wie die gängige Antwort vermuten lässt. Stattdessen sollte die Frage lauten: "Wie langsam sind 'als Ausnahmen gemeldete Fehler' im Vergleich zu Fehlern, die auf andere Weise gemeldet werden?" Im Allgemeinen sind die beiden anderen Arten der Fehlermeldung entweder mit Sentinel-Werten oder mit Ergebnis-Wrappern.

Sentinel-Werte sind ein Versuch, im Erfolgsfall eine Klasse und im Misserfolgsfall eine andere zurückzugeben. Man kann sich das fast so vorstellen, als würde man eine Ausnahme zurückgeben, anstatt eine zu werfen. Dies erfordert eine gemeinsame übergeordnete Klasse mit dem Erfolgsobjekt und dann eine "instanceof"-Prüfung und ein paar Casts, um die Erfolgs- oder Fehlerinformationen zu erhalten.

Es stellt sich heraus, dass auf die Gefahr hin, dass die Typsicherheit beeinträchtigt wird, Sentinel-Werte schneller sind als Ausnahmen, aber nur um einen Faktor von etwa 2x. Das mag viel erscheinen, aber der Faktor 2x deckt nur die Kosten für den Unterschied bei der Implementierung. In der Praxis ist der Faktor viel geringer, da unsere Methoden, die fehlschlagen könnten, viel interessanter sind als ein paar arithmetische Operatoren wie im Beispielcode an anderer Stelle auf dieser Seite.

Bei Result Wrappers hingegen wird die Typsicherheit in keiner Weise geopfert. Sie verpacken die Informationen über Erfolg und Misserfolg in einer einzigen Klasse. Anstelle von "instanceof" bieten sie also ein "isSuccess()" und Getter für die Erfolgs- und Misserfolgsobjekte. Allerdings sind die Ergebnisobjekte ungefähr 2x Langsamer als mit Ausnahmen. Es hat sich herausgestellt, dass es viel teurer ist, jedes Mal ein neues Wrapper-Objekt zu erstellen, als manchmal eine Ausnahme auszulösen.

Darüber hinaus sind Ausnahmen die von der Sprache gelieferte Art und Weise, um anzuzeigen, dass eine Methode fehlschlagen könnte. Es gibt keine andere Möglichkeit, nur aus der API zu erkennen, welche Methoden immer (meistens) funktionieren und welche einen Fehler melden müssen.

Ausnahmen sind sicherer als Sentinels, schneller als Ergebnisobjekte und weniger überraschend als beide. Ich schlage nicht vor, dass try/catch if/else ersetzen, aber Ausnahmen sind der richtige Weg, um Fehler zu melden, auch in der Geschäftslogik.

Dennoch möchte ich darauf hinweisen, dass die beiden häufigsten Arten, die Leistung erheblich zu beeinträchtigen, auf die ich gestoßen bin, die Erstellung unnötiger Objekte und verschachtelter Schleifen sind. Wenn Sie die Wahl haben, eine Ausnahme zu erzeugen oder keine Ausnahme zu erzeugen, sollten Sie die Ausnahme nicht erzeugen. Wenn Sie die Wahl haben, entweder manchmal eine Ausnahme zu erzeugen oder ständig ein anderes Objekt zu erzeugen, dann erzeugen Sie die Ausnahme.

23voto

manikanta Punkte 7272

Ich habe die Antworten erweitert, die von @Mecki y @inkarnate , ohne Stacktrace-Füllung für Java.

Mit Java 7+ können wir Folgendes verwenden Throwable(String message, Throwable cause, boolean enableSuppression,boolean writableStackTrace) . Aber für Java6, siehe meine Antwort auf diese Frage

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

Ausgabe mit Java 1.6.0_45, auf Core i7, 8GB 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

Daher sind Methoden, die Werte zurückgeben, immer noch schneller als Methoden, die Ausnahmen auslösen. IMHO können wir keine klare API entwerfen, die nur Rückgabetypen für Erfolgs- und Fehlerabläufe verwendet. Methoden, die Ausnahmen ohne Stacktrace auslösen, sind 4-5 mal schneller als normale Ausnahmen.

Bearbeiten: NoStackTraceThrowable.java Danke @Greg

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X