Welche Auswirkungen haben Ausnahmen auf die Leistung in Java?

496

Frage: Ist die Ausnahmebehandlung in Java tatsächlich langsam?

Konventionelle Erkenntnisse sowie viele Google-Ergebnisse besagen, dass außergewöhnliche Logik nicht für den normalen Programmfluss in Java verwendet werden sollte. Normalerweise werden zwei Gründe angegeben:

  1. es ist wirklich langsam - sogar eine Größenordnung langsamer als normaler Code (die angegebenen Gründe variieren),

und

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

Diese Frage handelt von # 1.

Als Beispiel beschreibt diese Seite die Behandlung von Java-Ausnahmen als "sehr langsam" und bezieht die Langsamkeit auf die Erstellung der Ausnahmemeldungszeichenfolge - "Diese Zeichenfolge wird dann zum Erstellen des ausgelösten Ausnahmeobjekts verwendet. Dies ist nicht schnell." In dem Artikel Effektive Ausnahmebehandlung in Java heißt es: "Der Grund dafür liegt im Aspekt der Objekterstellung bei der Ausnahmebehandlung, wodurch das Auslösen von Ausnahmen von Natur aus langsam wird." Ein weiterer Grund dafür ist, dass die Stack-Trace-Generierung sie verlangsamt.

Meine Tests (unter Verwendung von Java 1.6.0_07, Java HotSpot 10.0 unter 32-Bit-Linux) haben ergeben, dass die Ausnahmebehandlung nicht langsamer ist als normaler Code. Ich habe versucht, eine Methode in einer Schleife auszuführen, die Code ausführt. Am Ende der Methode verwende ich einen Booleschen Wert, um anzugeben, ob zurückgegeben oder geworfen werden soll . Auf diese Weise ist die tatsächliche Verarbeitung dieselbe. Ich habe versucht, die Methoden in verschiedenen Reihenfolgen auszuführen und meine Testzeiten zu mitteln, da ich dachte, es könnte das Aufwärmen der JVM gewesen sein. In all 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 Bezug auf das Codebeispiel, Testvergleiche oder Ergebnisse gesehen, die zeigen, dass die Ausnahmebehandlung in Java tatsächlich so ist langsam.

Was mich auf diesen Weg führte, war eine API, die ich verwenden musste und die Ausnahmen als Teil der normalen Steuerlogik auslöste. Ich wollte sie in ihrer Verwendung korrigieren, aber jetzt kann ich es möglicherweise nicht. Muss ich sie stattdessen für ihr vorausschauendes Denken loben?

In dem Artikel Effiziente Java-Ausnahmebehandlung bei der Just-in-Time-Kompilierung schlagen die Autoren vor, dass das Vorhandensein von Ausnahmebehandlungsroutinen allein, auch wenn keine Ausnahmen ausgelöst werden, ausreicht, um zu verhindern, dass der JIT-Compiler den Code ordnungsgemäß optimiert und ihn dadurch verlangsamt . Ich habe diese Theorie noch nicht getestet.

John Ellinwood
quelle
8
Ich weiß, dass Sie nicht nach 2) gefragt haben, aber Sie sollten wirklich erkennen, dass die Verwendung einer Ausnahme für den Programmfluss nicht besser ist als die Verwendung von GOTOs. Einige Leute verteidigen gotos, andere verteidigen, wovon Sie sprechen, aber wenn Sie jemanden fragen, der beides für einen bestimmten Zeitraum implementiert und gewartet hat, werden sie Ihnen sagen, dass beide schlecht sind, um Designpraktiken aufrechtzuerhalten (und wahrscheinlich fluchen werden) den Namen der Person, die dachte, sie sei klug genug, um die Entscheidung zu treffen, sie zu verwenden).
Bill K
80
Bill, der behauptet, dass die Verwendung von Ausnahmen für den Programmfluss nicht besser ist als die Verwendung von GOTOs, ist nicht besser als die Behauptung, dass die Verwendung von Bedingungen und Schleifen für den Programmfluss nicht besser ist als die Verwendung von GOTOs. Es ist ein roter Hering. Erklären Sie sich. Ausnahmen können und werden effektiv für den Programmfluss in anderen Sprachen verwendet. Beispielsweise verwendet der idiomatische Python-Code regelmäßig Ausnahmen. Ich kann und habe Code gepflegt, der auf diese Weise Ausnahmen verwendet (allerdings nicht Java), und ich glaube nicht, dass irgendetwas von Natur aus daran falsch ist.
Mmalone
14
@mmalone Die Verwendung von Ausnahmen für den normalen Kontrollfluss ist in Java eine schlechte Idee, da die Paradigmenauswahl auf diese Weise getroffen wurde . Lesen Sie Bloch EJ2 - er stellt klar fest, zitieren Sie (Punkt 57) exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow- und geben Sie eine vollständige und ausführliche Erklärung, warum. Und er war der Mann, der schrieb Java lib. Daher ist er derjenige, der den API-Vertrag der Klassen definiert. Ich stimme Bill K zu.
8
@ OndraŽižka Wenn ein Framework dies tut (Ausnahmen in nicht außergewöhnlichem Zustand verwenden), ist es fehlerhaft und vom Design her fehlerhaft, wodurch der Ausnahmeklassenvertrag der Sprache gebrochen wird. Nur weil manche Leute miesen Code schreiben, wird er nicht weniger mies.
8
Kein anderer als der Ersteller von stackoverflow.com ist in Bezug auf Ausnahmen falsch. Die goldene Regel der Softwareentwicklung ist niemals, das Einfache komplex und unhandlich zu machen. Er schreibt: "Es ist wahr, dass ein einfaches 3-Zeilen-Programm oft zu 48 Zeilen aufblüht, wenn Sie eine gute Fehlerprüfung durchführen, aber das ist das Leben, ..." Dies ist eine Suche nach Reinheit, nicht nach Einfachheit.
sf_jeff

Antworten:

345

Es hängt davon ab, wie Ausnahmen implementiert werden. Der einfachste Weg ist die Verwendung von setjmp und longjmp. Das bedeutet, dass alle Register der CPU in den Stapel 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 Stapel abwickeln und die Werte aller Register (und mögliche andere Werte in der VM) wiederherstellen. Try and Throw sind also gleich langsam, und das ist ziemlich langsam. Wenn jedoch keine Ausnahme ausgelöst wird, dauert das Verlassen des Try-Blocks in den meisten Fällen überhaupt nicht (da alles auf den Stapel gelegt wird, der automatisch bereinigt wird, wenn die Methode vorhanden ist).

Sun und andere haben erkannt, dass dies möglicherweise nicht optimal ist und dass VMs mit der Zeit immer schneller werden. Es gibt eine andere Möglichkeit, Ausnahmen zu implementieren, die es ermöglicht, sich blitzschnell zu versuchen (im Allgemeinen passiert überhaupt nichts für Versuche - alles, was passieren muss, ist bereits erledigt, wenn die Klasse von der VM geladen wird) und den Wurf nicht ganz so langsam macht . 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 ausgeführt wird? Denn wenn es jemals auf einer anderen Plattform oder einer anderen JVM-Version (möglicherweise von einem anderen Anbieter) ausgeführt wird, wer sagt, dass sie auch die schnelle Implementierung verwenden? Der schnelle ist komplizierter als der langsame und nicht auf allen Systemen leicht möglich. Sie möchten tragbar bleiben? Dann verlassen Sie sich nicht darauf, dass Ausnahmen schnell sind.

Es macht auch einen großen Unterschied, was Sie innerhalb eines Try-Blocks tun. Wenn Sie einen try-Block öffnen und niemals eine Methode aus diesem try-Block heraus aufrufen, ist der try-Block ultraschnell, da die JIT einen Wurf dann tatsächlich wie ein einfaches goto behandeln kann. Es muss weder den Stapelstatus speichern noch den Stapel abwickeln, wenn eine Ausnahme ausgelöst wird (es muss nur zu den Catch-Handlern gesprungen werden). Dies ist jedoch nicht das, was Sie normalerweise tun. Normalerweise öffnen Sie einen try-Block und rufen dann eine Methode auf, die möglicherweise eine Ausnahme auslöst, oder? Und selbst wenn Sie nur den try-Block in Ihrer Methode verwenden, welche Art von Methode wird dies sein, die keine andere Methode aufruft? Wird es nur eine Zahl berechnen? Wofür brauchen Sie dann Ausnahmen? Es gibt viel elegantere Möglichkeiten, den Programmfluss zu regulieren. Für so ziemlich alles andere als einfache Mathematik,

Siehe 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 des Try-Blocks ist zu gering, um Störfaktoren wie Hintergrundprozesse auszuschließen. Aber der Fangblock hat alles getötet und 66-mal langsamer gemacht!

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

Mecki
quelle
7
Gute Antwort, aber ich möchte nur hinzufügen, dass System.nanoTime () meines Wissens zur Messung der Leistung verwendet werden sollte, nicht System.currentTimeMillis ().
Simon Forsberg
10
@ SimonAndréForsberg nanoTime()benötigt Java 1.5 und ich hatte nur Java 1.4 auf dem System verfügbar, das ich zum Schreiben des obigen Codes verwendet habe. Auch in der Praxis spielt es keine große Rolle. Der einzige Unterschied zwischen den beiden besteht darin, dass eine Nanosekunde die andere Millisekunde ist und nanoTimenicht durch Taktmanipulationen beeinflusst wird (die irrelevant sind, es sei denn, Sie oder der Systemprozess ändern die Systemuhr genau in dem Moment, in dem der Testcode ausgeführt wird). Im Allgemeinen haben Sie Recht, nanoTimeist natürlich die bessere Wahl.
Mecki
2
Es sollte wirklich beachtet werden, dass Ihr Test ein Extremfall ist. Sie zeigen einen sehr kleinen Leistungstreffer für Code mit einem tryBlock, aber nein throw. Ihr throwTest wirft Ausnahmen 50% der Zeit , es durch das geht try. Dies ist eindeutig eine Situation, in der der Fehler nicht außergewöhnlich ist . Wenn Sie dies auf nur 10% reduzieren, wird der Leistungseinbruch massiv verringert. Das Problem bei dieser Art von Test ist, dass die Benutzer dazu ermutigt werden, Ausnahmen überhaupt nicht mehr zu verwenden. Die Verwendung von Ausnahmen für die Ausnahmefallbehandlung ist weitaus besser als das, was Ihr Test zeigt.
Nate
1
@Nate Zunächst habe ich sehr deutlich gesagt, dass dies alles davon abhängt, wie Ausnahmen implementiert werden. Ich habe gerade EINE spezifische Implementierung getestet, aber es gibt viele, und Oracle kann mit jeder Version eine ganz andere auswählen. Zweitens, wenn Ausnahmen nur außergewöhnlich sind, was sie normalerweise sind, ist die Auswirkung natürlich geringer. Dies ist so offensichtlich, dass ich wirklich nicht denke, dass man explizit darauf hinweisen muss, und daher kann ich Ihren Punkt hier überhaupt nicht verstehen. Und drittens, Ausnahme überbeansprucht es schlecht, alle sind sich einig, also ist es eine sehr gute Sache, sie mit viel Sorgfalt zu verwenden.
Mecki
4
@Glide Ein Wurf ist nicht wie ein sauberer return. Es hinterlässt eine Methode irgendwo in der Mitte des Körpers, möglicherweise sogar mitten in einer Operation (die bisher nur zu 50% abgeschlossen wurde), und der catchBlock kann 20 tryStapelrahmen nach oben sein (eine Methode hat einen Block, der Methode1 aufruft). die Methode2 aufruft, die mehtod3, ... aufruft und in Methode20 mitten in einer Operation eine Ausnahme auslöst). Der Stapel muss 20 Frames nach oben abgewickelt werden, alle nicht abgeschlossenen Vorgänge müssen rückgängig gemacht werden (Vorgänge dürfen nicht zur Hälfte ausgeführt werden) und die CPU-Register müssen sich in einem sauberen Zustand befinden. Das alles kostet Zeit.
Mecki
255

Zu Ihrer Information, ich habe das Experiment von Mecki 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 bei Mecki (mein Laptop ist offensichtlich langsamer).

Methode4 ist identisch mit Methode3, außer dass sie eine erstellt, new Integer(1)anstatt dies zu tun throw new Exception().

method5 ist wie method3, nur dass es das erstellt, new Exception()ohne es zu werfen.

method6 ist wie method3, außer dass eine vorab erstellte Ausnahme (eine Instanzvariable) ausgelöst wird, anstatt eine neue zu erstellen.

In Java ist ein Großteil der Kosten für das Auslösen einer Ausnahme die Zeit, die für das Sammeln der Stapelverfolgung aufgewendet wird, die beim Erstellen des Ausnahmeobjekts anfällt. Die tatsächlichen Kosten für das Auslösen der Ausnahme sind zwar hoch, aber erheblich geringer als die Kosten für das Erstellen der Ausnahme.

Hot Licks
quelle
48
+1 Ihre Antwort befasst sich mit dem Kernproblem - der Zeit, die zum Abwickeln und Verfolgen des Stapels benötigt wird, und zweitens dem Auslösen des Fehlers. Ich hätte dies als endgültige Antwort ausgewählt.
Ingenieur
8
nett. ~ 70% erstellen die Ausnahme, ~ 30% werfen sie aus. gute Infos.
Chaqke
1
@Basil - Das sollten Sie aus den obigen Zahlen herausfinden können.
Hot Licks
1
Dies kann implementierungsspezifisch sein. Welche Java-Version wurde für diese Benchmarks verwendet?
Thorbjørn Ravn Andersen
3
Wir können bemerken, dass im Standardcode das Erstellen und Auslösen von Ausnahmen in seltenen Fällen auftritt (zur Laufzeit meine ich). Wenn dies nicht der Fall ist, sind entweder die Laufzeitbedingungen sehr schlecht oder das Design selbst ist das Problem. In beiden Fällen sind Aufführungen kein
Problem
70

Aleksey Shipilëv hat eine sehr gründliche Analyse durchgeführt, in der er Java-Ausnahmen unter verschiedenen Kombinationen von Bedingungen bewertet :

  • Neu erstellte Ausnahmen im Vergleich zu vorab erstellten Ausnahmen
  • Stack-Trace aktiviert oder deaktiviert
  • Stapelverfolgung angefordert vs nie angefordert
  • Auf der obersten Ebene gefangen gegen auf jeder Ebene neu geworfen gegen auf jeder Ebene verkettet / gewickelt
  • Verschiedene Ebenen der Java-Aufrufstapeltiefe
  • Keine Inlining-Optimierungen im Vergleich zu extremem Inlining im Vergleich zu Standardeinstellungen
  • Benutzerdefinierte Felder lesen oder nicht lesen

Er vergleicht sie auch mit der Leistung der Überprüfung eines Fehlercodes auf verschiedenen Ebenen der Fehlerhäufigkeit.

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

  1. Wirklich außergewöhnliche Ausnahmen sind wunderschön 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 kommunizieren, die mit regulärem Code behandelt werden, ist die Verwendung von Ausnahmen der Leistungsgewinn.

  2. Die Leistungskosten von Ausnahmen bestehen aus zwei Hauptkomponenten: der Stapelverfolgungskonstruktion, wenn die Ausnahme instanziiert wird, und dem Abwickeln des Stapels während des Ausnahmewurfs.

  3. Die Kosten für die Stapelverfolgungskonstruktion sind proportional zur Stapeltiefe zum Zeitpunkt der Instanziierung der Ausnahme. Das ist schon schlecht, denn wer auf der Erde kennt die Stapeltiefe, bei der diese Wurfmethode aufgerufen werden würde? Selbst wenn Sie die Stack-Trace-Generierung deaktivieren und / oder die Ausnahmen zwischenspeichern, können Sie nur diesen Teil der Leistungskosten entfernen.

  4. Die Kosten für das Abwickeln des Stapels hängen davon ab, wie viel Glück wir haben, wenn wir den Ausnahmebehandler im kompilierten Code näher bringen. Eine sorgfältige Strukturierung des Codes, um eine gründliche Suche nach Ausnahmebehandlungsroutinen zu vermeiden, hilft uns wahrscheinlich dabei, mehr Glück zu haben.

  5. Sollten wir beide Effekte eliminieren, sind die Leistungskosten für Ausnahmen die der lokalen Niederlassung. Egal wie schön es klingt, das bedeutet nicht, dass Sie Ausnahmen als üblichen Kontrollfluss verwenden sollten, denn in diesem Fall sind Sie der Optimierung des Compilers ausgeliefert! Sie sollten sie nur in wirklich außergewöhnlichen Fällen verwenden, in denen die Ausnahmefrequenz die möglichen unglücklichen Kosten für die Auslösung der tatsächlichen Ausnahme amortisiert .

  6. Die optimistische Faustregel scheint 10 ^ -4 zu sein. Die Häufigkeit für Ausnahmen ist außergewöhnlich genug. Dies hängt natürlich von den Schwergewichten der Ausnahmen selbst, den genauen Aktionen der Ausnahmebehandlungsroutinen usw. ab.

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

Doval
quelle
41

Meine Antwort ist leider einfach zu lang, um hier zu posten. Lassen Sie mich hier zusammenfassen und Sie auf http://www.fuwjax.com/how-slow-are-java-exceptions/ verweisen. um die wichtigsten Details zu erfahren.

Die eigentliche Frage lautet hier nicht: "Wie langsam werden" Fehler als Ausnahmen gemeldet "im Vergleich zu" Code, der niemals ausfällt "?" wie die akzeptierte Antwort Sie glauben lassen könnte. Stattdessen sollte die Frage lauten: "Wie langsam werden" Fehler als Ausnahmen gemeldet "im Vergleich zu Fehlern, die auf andere Weise gemeldet wurden?" Im Allgemeinen sind die beiden anderen Möglichkeiten, Fehler zu melden, entweder Sentinel-Werte oder Ergebnis-Wrapper.

Sentinel-Werte sind ein Versuch, eine Klasse im Erfolgsfall und eine andere im Fehlerfall zurückzugeben. Sie können sich fast vorstellen, eine Ausnahme zurückzugeben, anstatt eine auszulösen. Dies erfordert eine gemeinsam genutzte übergeordnete Klasse mit dem Erfolgsobjekt und führt dann eine "Instanz of" -Prüfung und einige Casts durch, um die Erfolgs- oder Fehlerinformationen abzurufen.

Es stellt sich heraus, dass Sentinel-Werte unter dem Risiko der Typensicherheit schneller als Ausnahmen sind, jedoch nur um den Faktor ungefähr 2x. Das mag viel erscheinen, aber 2x deckt nur die Kosten für den Implementierungsunterschied ab. In der Praxis ist der Faktor viel geringer, da unsere Methoden, die möglicherweise fehlschlagen, viel interessanter sind als einige arithmetische Operatoren, wie im Beispielcode an anderer Stelle auf dieser Seite.

Ergebnisverpackungen hingegen opfern die Typensicherheit überhaupt nicht. Sie fassen die Erfolgs- und Misserfolgsinformationen in einer einzigen Klasse zusammen. Anstelle von "instanceof" stellen sie also ein "isSuccess ()" und Getter für die Erfolgs- und Fehlerobjekte bereit. Ergebnisobjekte sind jedoch ungefähr 2x langsamer als die Verwendung von Ausnahmen. Es stellt sich heraus, dass das Erstellen eines neuen Wrapper-Objekts jedes Mal viel teurer ist, als manchmal eine Ausnahme auszulösen.

Darüber hinaus sind Ausnahmen die Sprache, mit der angegeben wird, dass eine Methode möglicherweise fehlschlägt. Es gibt keine andere Möglichkeit, anhand der API zu erkennen, welche Methoden immer (meistens) funktionieren und welche Fehler melden sollen.

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

Trotzdem möchte ich darauf hinweisen, dass die beiden häufigsten Möglichkeiten, die Leistung, auf die ich gestoßen bin, erheblich zu beeinträchtigen, darin bestehen, unnötige Objekte und verschachtelte Schleifen zu erstellen. Wenn Sie die Wahl haben, eine Ausnahme zu erstellen oder keine Ausnahme zu erstellen, erstellen Sie die Ausnahme nicht. Wenn Sie die Wahl haben, manchmal eine Ausnahme zu erstellen oder ständig ein anderes Objekt zu erstellen, erstellen Sie die Ausnahme.

Fuwjax
quelle
5
Ich habe mich entschlossen, die Langzeitleistung der drei Implementierungen im Vergleich zu einer Steuerungsimplementierung zu testen, die ohne Berichterstellung auf Fehler überprüft. Der Prozess hat eine Ausfallrate von ca. 4%. Eine Iteration eines Tests ruft den Prozess 10000 Mal für eine der Strategien auf. Jede Strategie wird 1000 Mal getestet und die letzten 900 Mal werden verwendet, um die Statistiken zu generieren. Hier sind die durchschnittlichen Zeiten in Nanos: Kontrolle 338 Ausnahme 429 Ergebnis 348 Sentinel 345
Fuwjax
2
Nur zum Spaß habe ich fillInStackTrace im Ausnahmetest deaktiviert. Hier sind die Zeiten jetzt: Kontrolle 347 Ausnahme 351 Ergebnis 364 Sentinel 355
Fuwjax
Fuwjax, es sei denn, ich vermisse etwas (und ich gebe zu, ich habe nur Ihren SO-Beitrag gelesen, nicht Ihren Blog-Beitrag), scheinen Ihre beiden obigen Kommentare Ihrem Beitrag zu widersprechen. Ich gehe davon aus, dass niedrigere Zahlen in Ihrem Benchmark besser sind, oder? In diesem Fall führt das Generieren von Ausnahmen mit aktiviertem fillInStackTrace (dies ist das Standard- und übliche Verhalten) zu einer langsameren Leistung als die beiden anderen von Ihnen beschriebenen Techniken. Vermisse ich etwas oder haben Sie tatsächlich einen Kommentar abgegeben, um Ihren Beitrag zu widerlegen?
Felix GV
@Fuwjax - Um die hier vorgestellte Auswahl "Rock and Hard Place" zu vermeiden, müssen Sie ein Objekt vorab zuweisen , das "Erfolg" darstellt. Normalerweise kann man Objekte auch für die häufigsten Fehlerfälle vorab zuweisen. Nur in dem seltenen Fall, dass zusätzliche Details zurückgegeben werden, wird ein neues Objekt erstellt. (Dies ist der OO - äquivalent von integer „Fehlercodes“, sowie ein separater Anruf die Details des letzten Fehlers zu erhalten -. Eine Technik , die seit Jahrzehnten bestehenden)
ToolmakerSteve
@Fuwjax Wenn Sie also eine Ausnahme auslösen, wird von Ihrem Konto kein Objekt erstellt? Ich bin mir nicht sicher, ob ich diese Argumentation verstehe. Unabhängig davon, ob Sie eine Ausnahme auslösen oder ein Ergebnisobjekt zurückgeben, erstellen Sie Objekte. In diesem Sinne sind Ergebnisobjekte nicht langsamer als das Auslösen einer Ausnahme.
Matthias
20

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

Mit Java 7+ können wir 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, 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

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 Fehlerflüsse 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);
    }
}
Manikanta
quelle
interessant, danke. Hier ist die fehlende Klassenerklärung:public class NoStackTraceThrowable extends Throwable { public NoStackTraceThrowable() { super("my special throwable", null, false, false); } }
Greg
am Anfang Du hast geschrieben, With Java 7+, we can useaber später hast du geschrieben, Output with Java 1.6.0_45,also ist dies Java 6 oder 7 Ergebnis?
WBAR
1
@WBAR von Java 7, wir müssen nur den ThrowableKonstruktor verwenden, der boolean writableStackTracearg hat. Dies ist jedoch in Java 6 und darunter nicht vorhanden. Aus diesem Grund habe ich eine benutzerdefinierte Implementierung für Java 6 und niedriger angegeben. Der obige Code ist also für Java 6 und niedriger. Bitte lesen Sie die erste Zeile des zweiten Absatzes sorgfältig durch.
Manikanta
@manikanta "IMHO, wir können keine klare API entwerfen, die nur Rückgabetypen für Erfolg und Fehlerfluss verwendet." - können wir, wenn wir Optionals / Results / Vielleicht so viele Sprachen verwenden.
Hejazzman
@ Hejazzman Ich stimme zu. Aber Optionaloder ähnliches kam etwas spät zu Java. Davor haben wir auch Wrapper-Objekte mit Erfolgs- / Fehlerflags verwendet. Aber es scheint ein bisschen Hacks zu sein und fühlt sich für mich nicht natürlich an.
Manikanta
8

Vor einiger Zeit habe ich eine Klasse geschrieben, um die relative Leistung der Konvertierung von Zeichenfolgen in Ints mit zwei Ansätzen zu testen: (1) Integer.parseInt () aufrufen und die Ausnahme abfangen oder (2) die Zeichenfolge mit einem regulären Ausdruck abgleichen und parseInt () aufrufen Nur wenn das Match erfolgreich ist. Ich habe den regulären Ausdruck so effizient wie möglich verwendet (dh das Erstellen der Pattern- und Matcher-Objekte vor dem Intering der Schleife) und die Stacktraces aus den Ausnahmen nicht gedruckt oder gespeichert.

Bei einer Liste von zehntausend Zeichenfolgen war der parseInt () -Ansatz viermal so schnell wie der Regex-Ansatz, wenn alle gültigen Zahlen waren. Wenn jedoch nur 80% der Zeichenfolgen gültig waren, war der reguläre Ausdruck doppelt so schnell wie parseInt (). Und wenn 20% gültig waren, was bedeutet, dass die Ausnahme in 80% der Fälle ausgelöst und abgefangen wurde, war der reguläre Ausdruck ungefähr zwanzigmal so schnell wie parseInt ().

Ich war von dem Ergebnis überrascht, da der Regex-Ansatz gültige Zeichenfolgen zweimal verarbeitet: einmal für das Match und erneut für parseInt (). Aber Ausnahmen zu werfen und zu fangen machte das mehr als wett. Diese Art von Situation tritt in der realen Welt wahrscheinlich nicht sehr häufig auf, aber wenn dies der Fall ist, sollten Sie auf keinen Fall die Technik zum Ausfangen von Ausnahmen verwenden. Wenn Sie jedoch nur Benutzereingaben oder ähnliches validieren, verwenden Sie auf jeden Fall den parseInt () -Ansatz.

Alan Moore
quelle
Welche JVM haben Sie verwendet? ist es mit sun-jdk 6 noch so langsam?
Benedikt Waldvogel
Ich habe es ausgegraben und unter JDK 1.6u10 erneut ausgeführt, bevor ich diese Antwort gesendet habe. Dies sind die Ergebnisse, die ich veröffentlicht habe.
Alan Moore
Das ist sehr, sehr nützlich! Vielen Dank. Für meine üblichen Anwendungsfälle muss ich Benutzereingaben analysieren (mit so etwas wie Integer.ParseInt()), und ich gehe davon aus, dass die Benutzereingaben meistens korrekt sind. Für meinen Anwendungsfall scheint es also der richtige Weg zu sein, gelegentlich einen Ausnahmetreffer zu verwenden .
Markvgti
8

Ich denke, der erste Artikel bezieht sich auf das Durchlaufen des Aufrufstapels und das Erstellen eines Stack-Trace als den teuren Teil, und während der zweite Artikel dies nicht sagt, denke ich, dass dies der teuerste Teil der Objekterstellung ist. John Rose hat einen Artikel, in dem er verschiedene Techniken beschreibt, um Ausnahmen zu beschleunigen . (Vorabzuweisung und Wiederverwendung einer Ausnahme, Ausnahmen ohne Stapelspuren usw.)

Aber dennoch - ich denke, dies sollte nur als notwendiges Übel betrachtet werden, als letzter Ausweg. Johns Grund dafür ist, Funktionen in anderen Sprachen zu emulieren, die (noch) nicht in der JVM verfügbar sind. Sie sollten sich NICHT angewöhnen, Ausnahmen für den Kontrollfluss zu verwenden. Vor allem nicht aus Leistungsgründen! Wie Sie selbst in # 2 erwähnen, riskieren Sie, schwerwiegende Fehler in Ihrem Code auf diese Weise zu maskieren, und es wird für neue Programmierer schwieriger sein, diese zu warten.

Mikrobenchmarks in Java sind überraschend schwer zu korrigieren (wie mir gesagt wurde), insbesondere wenn Sie in das JIT-Gebiet gelangen. Daher bezweifle ich wirklich, dass die Verwendung von Ausnahmen im wirklichen Leben schneller ist als die "Rückkehr". Ich vermute zum Beispiel, dass Sie in Ihrem Test zwischen 2 und 5 Stapelrahmen haben? Stellen Sie sich nun vor, Ihr Code wird von einer von JBoss bereitgestellten JSF-Komponente aufgerufen. Jetzt haben Sie möglicherweise eine Stapelverfolgung, die mehrere Seiten lang ist.

Vielleicht könnten Sie Ihren Testcode posten?

Lars Westergren
quelle
7

Ich weiß nicht, ob diese Themen zusammenhängen, aber ich wollte einmal einen Trick implementieren, der sich auf den Stack-Trace des aktuellen Threads stützt: Ich wollte den Namen der Methode herausfinden, die die Instanziierung innerhalb der instanziierten Klasse ausgelöst hat (ja, die Idee ist verrückt, Ich habe es total aufgegeben). So entdeckte ich , dass Berufung Thread.currentThread().getStackTrace()ist extrem langsam (wegen nativen dumpThreadsMethode , die es intern verwendet).

Java Throwablehat dementsprechend eine native Methode fillInStackTrace. Ich denke, dass der zuvor catchbeschriebene Killerblock irgendwie die Ausführung dieser Methode auslöst.

Aber lassen Sie mich Ihnen eine andere Geschichte erzählen ...

In Scala werden einige Funktionsmerkmale in JVM mithilfe von kompiliert ControlThrowable, wodurch es auf folgende Weise erweitert Throwableund überschrieben fillInStackTracewird:

override def fillInStackTrace(): Throwable = this

Also habe ich den obigen Test angepasst (die Anzahl der Zyklen wird um zehn verringert, meine Maschine ist etwas langsamer :):

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)

}

Die Ergebnisse sind also:

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

Sie sehen, der einzige Unterschied zwischen method3und method4besteht darin, dass sie verschiedene Arten von Ausnahmen auslösen. Ja, method4ist immer noch langsamer als method1und method2, aber der Unterschied ist weitaus akzeptabler.

verkörpern
quelle
6

Ich habe einige Leistungstests mit JVM 1.5 durchgeführt und die Verwendung von Ausnahmen war mindestens zweimal langsamer. Durchschnittlich: Ausführungszeit bei einer trivial kleinen Methode mit Ausnahmen mehr als verdreifacht (3x). Eine trivial kleine Schleife, die die Ausnahme abfangen musste, führte zu einer zweifachen Erhöhung der Selbstzeit.

Ich habe ähnliche Zahlen im Produktionscode sowie in Mikro-Benchmarks gesehen.

Ausnahmen sollten definitiv NICHT für etwas verwendet werden, das häufig aufgerufen wird. Das Werfen von Tausenden von Ausnahmen pro Sekunde würde einen riesigen Flaschenhals verursachen.

Verwenden Sie beispielsweise "Integer.ParseInt (...)", um alle fehlerhaften Werte in einer sehr großen Textdatei zu finden - eine sehr schlechte Idee. (Ich habe gesehen, dass diese Dienstprogrammmethode die Leistung von Produktionscode beeinträchtigt.)

Verwenden einer Ausnahme zum Melden eines fehlerhaften Werts in einem Benutzer-GUI-Formular, vom Leistungsstandpunkt aus wahrscheinlich nicht so schlecht.

Unabhängig davon, ob es sich um eine gute Entwurfspraxis handelt oder nicht, würde ich die Regel befolgen: Wenn der Fehler normal / erwartet ist, verwenden Sie einen Rückgabewert. Wenn es abnormal ist, verwenden Sie eine Ausnahme. Beispiel: Beim Lesen von Benutzereingaben sind fehlerhafte Werte normal. Verwenden Sie einen Fehlercode. Wenn Sie einen Wert an eine interne Dienstprogrammfunktion übergeben, sollten fehlerhafte Werte durch Aufrufen von Code gefiltert werden. Verwenden Sie eine Ausnahme.

James Schek
quelle
Lassen Sie mich einige Dinge vorschlagen, die gut zu tun sind: Wenn Sie eine Zahl in einem Formular benötigen, anstatt Integer.valueOf (String) zu verwenden, sollten Sie stattdessen einen Matcher für reguläre Ausdrücke verwenden. Sie können das Muster vorkompilieren und wiederverwenden, sodass Matcher billig sind. Auf einem GUI-Formular ist es jedoch wahrscheinlich klarer, ein isValid / validate / checkField zu haben oder was Sie haben. Mit Java 8 haben wir auch optionale Monaden. Erwägen Sie daher, diese zu verwenden. (Die Antwort ist 9 Jahre alt, aber immer noch !: p)
Haakon Løtveit
4

Die Ausnahmeleistung in Java und C # lässt zu wünschen übrig.

Als Programmierer zwingt uns dies, die Regel "Ausnahmen sollten selten verursacht werden" einzuhalten, einfach aus praktischen Leistungsgründen.

Als Informatiker sollten wir uns jedoch gegen diesen problematischen Zustand auflehnen. Die Person, die eine Funktion erstellt, hat häufig keine Ahnung, wie oft sie aufgerufen wird oder ob Erfolg oder Misserfolg wahrscheinlicher sind. Nur der Anrufer hat diese Informationen. Der Versuch, Ausnahmen zu vermeiden, führt zu unklaren API-Idomen, bei denen wir in einigen Fällen nur saubere, aber langsame Ausnahmeversionen haben, in anderen Fällen schnelle, aber klobige Rückgabewertfehler, und in anderen Fällen haben wir beide . Der Bibliotheksimplementierer muss möglicherweise zwei Versionen von APIs schreiben und verwalten, und der Aufrufer muss entscheiden, welche von zwei Versionen in jeder Situation verwendet werden soll.

Das ist eine Art Chaos. Wenn Ausnahmen eine bessere Leistung hätten, könnten wir diese klobigen Redewendungen vermeiden und Ausnahmen verwenden, wie sie verwendet werden sollten ... als strukturierte Fehlerrückgabefunktion.

Ich würde mir wirklich wünschen, dass Ausnahmemechanismen mithilfe von Techniken implementiert werden, die näher an den Rückgabewerten liegen, sodass die Leistung näher an den Rückgabewerten liegt. Dies ist der Punkt, auf den wir in leistungsempfindlichem Code zurückgreifen.

Hier ist ein Codebeispiel, das die Ausnahmeleistung mit der Fehlerrückgabewertleistung vergleicht.

öffentliche Klasse TestIt {

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

}}

Und hier sind die Ergebnisse:

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

Das Überprüfen und Weitergeben von Rückgabewerten führt zu einigen Kosten im Vergleich zum Basis-Null-Aufruf, und diese Kosten sind proportional zur Anruftiefe. Bei einer Aufrufkettentiefe von 8 war die Version zur Überprüfung des Fehlerrückgabewerts etwa 27% langsamer als die Basisversion, bei der die Rückgabewerte nicht überprüft wurden.

Die Ausnahmeleistung ist im Vergleich nicht eine Funktion der Anruftiefe, sondern der Ausnahmefrequenz. Die Verschlechterung mit zunehmender Ausnahmefrequenz ist jedoch viel dramatischer. Bei einer Fehlerhäufigkeit von nur 25% lief der Code 24-mal langsamer. Bei einer Fehlerhäufigkeit von 100% ist die Ausnahmeversion fast 100-mal langsamer.

Dies deutet darauf hin, dass wir bei unseren Ausnahmeimplementierungen möglicherweise die falschen Kompromisse eingehen. Ausnahmen können schneller sein, entweder indem kostspielige Stalk-Walks vermieden werden oder indem sie direkt in eine vom Compiler unterstützte Rückgabewertprüfung umgewandelt werden. Bis sie dies tun, vermeiden wir sie, wenn unser Code schnell ausgeführt werden soll.

David Jeske
quelle
3

HotSpot ist durchaus in der Lage, Ausnahmecode für vom System generierte Ausnahmen zu entfernen, solange alles inline ist. Explizit erstellte Ausnahmen und solche, die ansonsten nicht entfernt wurden, verbringen jedoch viel Zeit mit der Erstellung des Stack-Trace. Überschreiben Sie, um fillInStackTracezu sehen, wie sich dies auf die Leistung auswirken kann.

Tom Hawtin - Tackline
quelle
2

Auch wenn das Auslösen einer Ausnahme nicht langsam ist, ist es immer noch eine schlechte Idee, Ausnahmen für den normalen Programmfluss auszulösen. Auf diese Weise wird es analog zu einem GOTO ...

Ich denke, das beantwortet die Frage allerdings nicht wirklich. Ich würde mir vorstellen, dass die "konventionelle" Weisheit, Ausnahmen langsam zu werfen, in früheren Java-Versionen (<1.4) zutraf. Zum Erstellen einer Ausnahme muss die VM den gesamten Stack-Trace erstellen. Seitdem hat sich in der VM viel geändert, um die Dinge zu beschleunigen, und dies ist wahrscheinlich ein Bereich, der verbessert wurde.

user38051
quelle
1
Es wäre gut, "normalen Programmablauf" zu definieren. Es wurde viel darüber geschrieben, geprüfte Ausnahmen als Geschäftsprozessfehler und als ungeprüfte Ausnahme für nicht behebbare Fehler zu verwenden. In gewissem Sinne könnte ein Fehler in der Geschäftslogik immer noch als normaler Ablauf angesehen werden.
Spencer Kormos
2
@Spencer K: Eine Ausnahme bedeutet, wie der Name schon sagt, dass eine Ausnahmesituation entdeckt wurde (eine Datei ist verschwunden, ein Netzwerk wurde plötzlich geschlossen, ...). Dies impliziert, dass die Situation UNERWARTET war. Wenn ERWARTET wird, dass die Situation eintreten wird, würde ich keine Ausnahme dafür verwenden.
Mecki
2
@Mecki: richtig. Ich hatte kürzlich eine Diskussion mit jemandem darüber ... Sie haben ein Validierungsframework geschrieben und eine Ausnahme für den Fall eines Validierungsfehlers ausgelöst. Ich denke, das ist eine schlechte Idee, da dies durchaus üblich wäre. Ich möchte lieber sehen, dass die Methode ein ValidationResult zurückgibt.
user38051
2
In Bezug auf den Kontrollfluss ist eine Ausnahme analog zu a breakoder returnnicht a goto.
Hot Licks
3
Es gibt Unmengen von Programmierparadigmen. Es kann keinen einzigen „normalen Fluss“ geben, was auch immer Sie damit meinen. Grundsätzlich ist der Ausnahmemechanismus nur eine Möglichkeit, den aktuellen Frame schnell zu verlassen und den Stapel bis zu einem bestimmten Punkt abzuwickeln. Das Wort "Ausnahme" impliziert nichts über seine "unerwartete" Natur. Ein kurzes Beispiel: Es ist sehr natürlich, 404s aus Webanwendungen zu „werfen“, wenn bestimmte Umstände auf dem Routing-Weg auftreten. Warum sollte diese Logik nicht mit Ausnahmen implementiert werden? Was ist das Anti-Muster?
Inkarnation
2

Vergleichen Sie einfach Integer.parseInt mit der folgenden Methode, die bei nicht analysierbaren Daten nur einen Standardwert zurückgibt, anstatt eine Ausnahme auszulösen:

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

Solange Sie beide Methoden auf "gültige" Daten anwenden, arbeiten beide ungefähr mit derselben Geschwindigkeit (obwohl Integer.parseInt komplexere Daten verarbeiten kann). Sobald Sie jedoch versuchen, ungültige Daten zu analysieren (z. B. "abc" 1.000.000 Mal zu analysieren), sollte der Leistungsunterschied von wesentlicher Bedeutung sein.

Inflamer
quelle
2

Ein großartiger Beitrag zur Ausnahmeleistung lautet:

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

Instanziieren oder Wiederverwenden vorhanden, mit und ohne Stapelverfolgung usw.:

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

Abhängig von der Tiefe der Stapelverfolgung:

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

Weitere Details (einschließlich x64 Assembler von JIT) finden Sie im Original-Blogbeitrag.

Das bedeutet, dass Hibernate / Spring / etc-EE-shit aufgrund von Ausnahmen (xD) langsam ist und das Umschreiben des App-Steuerungsflusses von Ausnahmen weg (durch continure/ ersetzen breakund booleanFlags wie in C vom Methodenaufruf zurückgeben) die Leistung Ihrer Anwendung 10x-100x verbessert , je nachdem wie oft du sie wirfst))

Gavenkoa
quelle
0

Ich habe die Antwort von @Mecki oben so geändert, dass Methode1 einen Booleschen Wert und eine Überprüfung der aufrufenden Methode zurückgibt, da Sie eine Ausnahme nicht einfach durch nichts ersetzen können. Nach zwei Läufen war Methode1 entweder immer noch die schnellste oder so schnell wie Methode2.

Hier ist eine Momentaufnahme des Codes:

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

und Ergebnisse:

Führen Sie 1 aus

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

Führen Sie 2 aus

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

Eine Ausnahme ist nur für die Behandlung unerwarteter Bedingungen zur Laufzeit vorgesehen .

Die Verwendung einer Ausnahme anstelle einer einfachen Validierung, die in der Kompilierungszeit durchgeführt werden kann, verzögert die Validierung bis zur Ausführungszeit. Dies wird wiederum die Effizienz des Programms verringern.

Das Auslösen einer Ausnahme anstelle einer einfachen if..else- Validierung macht das Schreiben und Verwalten des Codes ebenfalls komplex.

Gopinath
quelle
-3

Meine Meinung zur Ausnahmegeschwindigkeit im Vergleich zur programmgesteuerten Überprüfung von Daten.

Viele Klassen hatten einen String-zu-Wert-Konverter (Scanner / Parser), angesehene und bekannte Bibliotheken;)

hat normalerweise Form

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

Der Name der Ausnahme ist nur ein Beispiel, normalerweise ist er nicht aktiviert (Laufzeit), daher ist die Auslösedeklaration nur mein Bild

manchmal existieren zweite Form:

public static Example Parse(String input, Example defaultValue)

niemals werfen

Wenn der zweite Ins nicht verfügbar ist (oder der Programmierer zu wenig Dokumente liest und nur den ersten verwendet), schreiben Sie diesen Code mit regulären Ausdrücken. Reguläre Äußerungen sind cool, politisch korrekt usw.:

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

Mit diesem Code haben Programmierer keine Kosten für Ausnahmen. ABER HAT IMMER vergleichsweise sehr hohe Kosten für reguläre Ausdrücke im Vergleich zu kleinen Kosten für Ausnahmen manchmal.

Ich benutze fast immer in einem solchen Kontext

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

Ohne Stacktrace usw. zu analysieren, glaube ich nach Vorträgen von Yours ziemlich schnell.

Hab keine Angst vor Ausnahmen

Jacek Cz
quelle
-5

Warum sollten Ausnahmen langsamer sein als normale Renditen?

Solange Sie den Stacktrace nicht auf dem Terminal drucken, in einer Datei oder ähnlichem speichern, erledigt der catch-Block nicht mehr Arbeit als andere Codeblöcke. Ich kann mir also nicht vorstellen, warum "throw new my_cool_error ()" so langsam sein sollte.

Gute Frage und ich freue mich auf weitere Informationen zu diesem Thema!

qualbeen
quelle
17
Die Ausnahme muss die Informationen zum Stack-Trace erfassen, auch wenn dieser nicht verwendet wird.
Jon Skeet