15 Stimmen

Kosten für das Auslösen von C++0x-Ausnahmen

Welche Auswirkungen hat das Auslösen von Ausnahmen in C++0x auf die Leistung? Inwieweit ist dies compilerabhängig? Dies ist nicht dasselbe wie die Frage Wie hoch sind die Kosten für die Eintragung einer try Block, auch wenn keine Ausnahme ausgelöst wird .

Sollten wir erwarten, dass wir Ausnahmen mehr für die allgemeine logische Handhabung wie in Java verwenden?

22 Stimmen

Sie sollten keine Ausnahmen für die allgemeine Logikbehandlung in Java verwenden.

8 Stimmen

@Bill: s/in Java/EVER/ Sie werden nicht umsonst "Ausnahmen" genannt; sie sind außergewöhnlich.

0 Stimmen

@Pesto: Ich stimme zu, aber ich sprach mehr über die Tatsache, dass der OP zu denken schien, es sei gängige Praxis in Java speziell.

37voto

Steve Jessop Punkte 264569
#include <iostream>
#include <stdexcept>

struct SpaceWaster {
    SpaceWaster(int l, SpaceWaster *p) : level(l), prev(p) {}
    // we want the destructor to do something
    ~SpaceWaster() { prev = 0; }
    bool checkLevel() { return level == 0; }
    int level;
    SpaceWaster *prev;
};

void thrower(SpaceWaster *current) {
    if (current->checkLevel()) throw std::logic_error("some error message goes here\n");
    SpaceWaster next(current->level - 1, current);
    // typical exception-using code doesn't need error return values
    thrower(&next);
    return;
}

int returner(SpaceWaster *current) {
    if (current->checkLevel()) return -1;
    SpaceWaster next(current->level - 1, current);
    // typical exception-free code requires that return values be handled
    if (returner(&next) == -1) return -1;
    return 0;
}

int main() {
    const int repeats = 1001;
    int returns = 0;
    SpaceWaster first(1000, 0);

    for (int i = 0; i < repeats; ++i) {
        #ifdef THROW
            try {
                thrower(&first);
            } catch (std::exception &e) {
                ++returns;
            }
        #else
            returner(&first);
            ++returns;
        #endif
    }
    #ifdef THROW
        std::cout << returns << " exceptions\n";
    #else
        std::cout << returns << " returns\n";
    #endif
}

Mickey Mouse Benchmarking-Ergebnisse:

$ make throw -B && time ./throw
g++     throw.cpp   -o throw
1001 returns

real    0m0.547s
user    0m0.421s
sys     0m0.046s

$ make throw CPPFLAGS=-DTHROW -B && time ./throw
g++  -DTHROW   throw.cpp   -o throw
1001 exceptions

real    0m2.047s
user    0m1.905s
sys     0m0.030s

In diesem Fall dauert es also etwa 1,5 ms, wenn eine Ausnahme 1000 Stack-Ebenen nach oben geworfen wird, anstatt normal zurückzukehren. Das schließt den Eintritt in den try-Block ein, der auf einigen Systemen zur Ausführungszeit kostenlos ist, auf anderen Systemen jedes Mal Kosten verursacht, wenn man try eingibt, und auf anderen Systemen nur jedes Mal Kosten verursacht, wenn man die Funktion eingibt, die den try enthält. Für eine wahrscheinliche Anzahl von 100 Stack-Levels habe ich die Wiederholungen auf 10k erhöht, weil alles 10 Mal schneller war. Die Ausnahme kostete also 0,1ms.

Bei 10 000 Stapelstufen waren es 18,7s gegenüber 4,1s, also etwa 14ms zusätzliche Kosten für die Ausnahme. Für dieses Beispiel ergibt sich also ein ziemlich konsistenter Overhead von 1,5us pro Stackebene (wobei jede Ebene ein Objekt zerstört).

Offensichtlich spezifiziert C++0x nicht die Leistung für Ausnahmen (oder irgendetwas anderes, außer der Big-O-Komplexität für Algorithmen und Datenstrukturen). Ich glaube nicht, dass es Ausnahmen in einer Weise ändert, die viele Implementierungen ernsthaft beeinflussen wird, weder positiv noch negativ.

0 Stimmen

Das ist eine großartige Antwort, denn die Benchmarks und die Interpretation der Daten sagen viel mehr aus als "ich glaube bla bla bla". Wenn Sie diese mit Windows und einigen Versionen von Visual Studio wiederholen könnten, wäre dies die "de facto" Antwort auf die Ausnahmebehandlung

1 Stimmen

Ich habe MSVC nicht zur Hand (dies wurde auf Windows XP mit g++ und cygwin ausgeführt). Ich würde auch sein sehr Ich bin vorsichtig mit der Behauptung, dass dies ein guter Benchmark ist, da man bei einfachen Dingen nie weiß, ob nicht ein schlauer Compiler herausfindet, wie er die Rekursion in dem einen oder anderen Fall wegoptimieren kann. Das ist mir einmal bei einem Konformitätstest passiert, der prüfen sollte, ob das System Stack für N Ebenen der Rekursion hat. Das Anlegen von Structs ist ein recht zuverlässiger Weg, um dies zu verhindern, aber ich kann nichts versprechen, und die Ergebnisse sollten auf jeder Plattform sehr sorgfältig geprüft werden.

0 Stimmen

Ein Test mit Windows/VS würde nicht ausreichen. Die Ausnahme-Implementierung ist völlig unterschiedlich zwischen win32 und win64 (sowie für Windows auf x86 vs Itanium)

14voto

Brian Neal Punkte 30564

Die Leistung von Ausnahmen ist stark compilerabhängig. Sie müssen ein Profil Ihrer Anwendung erstellen, um festzustellen, ob dies ein Problem darstellt. Im Allgemeinen sollte dies nicht der Fall sein.

Sie sollten Ausnahmen wirklich für "außergewöhnliche Bedingungen" verwenden, nicht für die allgemeine logische Behandlung. Ausnahmen sind ideal für die Trennung von normalen Pfaden durch Ihren Code und Fehlerpfaden.

2 Stimmen

+1 auch von mir. Die Verwendung von Ausnahmen für normale Logik kann zu "Ausnahmespaghetti" führen, bei denen es anderthalb Stunden Arbeit ist, herauszufinden, wo das Ding gefangen ist. Besonders bei polymorphem Code, wo man vielleicht nicht einmal weiß, welche Implementierung von ListenerRegistry der Aufrufer ist, wenn man sie nicht ausführt und sieht, welchen Stacktrace man in der aktuellen Konfiguration erhält. Wenn Ausnahmen nur für Situationen verwendet werden, die erwartungsgemäß nicht wiederherstellbar sind, dann werden die meisten Code-Ebenen nicht einmal daran denken, zu versuchen, sich von ihnen zu erholen :-)

11voto

Martin York Punkte 245363

Ich bin der Meinung, dass die falsche Frage gestellt wurde.
Was die Kosten der Ausnahmen sind, ist nicht von Nutzen, nützlicher sind die Kosten der Ausnahmen im Vergleich zur Alternative. Sie müssen also messen, wie viel Ausnahmen kosten, und dies mit der Rückgabe von Fehlercodes vergleichen >>>AND<<< die Fehlercodes auf jeder Ebene der Stapelabwicklung überprüfen.

Beachten Sie auch, dass Sie Ausnahmen nicht verwenden sollten, wenn Sie die Kontrolle über alles haben. Innerhalb einer Klasse ist die Rückgabe eines Fehlercodes wahrscheinlich eine bessere Technik. Ausnahmen sollten verwendet werden, um die Kontrolle zur Laufzeit zu übertragen, wenn Sie nicht bestimmen können, wie (oder in welchem Kontext) Ihr Objekt zur Laufzeit verwendet werden wird.

Grundsätzlich sollte es dazu dienen, die Kontrolle auf eine höhere Kontextebene zu übertragen, wo ein Objekt mit genügend Kontext weiß, wie die Ausnahmesituation zu behandeln ist.

In Anbetracht dieser einfachen Verwendung sehen wir, dass Ausnahmen verwendet werden, um die Kontrolle über mehrere Ebenen im Stapelrahmen zu übertragen. Betrachten Sie nun den zusätzlichen Code, den Sie schreiben müssen, um einen Fehlercode auf demselben Aufrufstapel nach oben zu übertragen. Bedenken Sie die zusätzliche Komplexität, die entsteht, wenn Fehlercodes aus mehreren verschiedenen Richtungen kommen können, und versuchen Sie, all die verschiedenen Arten von Fehlercodes zu koordinieren.

Anhand dieses Beispiels können Sie sehen, wie Ausnahmen den Codefluss erheblich vereinfachen können, und Sie können die erhöhte Komplexität des Codeflusses erkennen. Die Frage ist dann, ob Ausnahmen teurer sind als die komplexen Fehlerbedingungstests, die bei jedem Stack-Frame durchgeführt werden müssen.

Die Antwort hängt wie immer davon ab (machen Sie beide Profile und verwenden Sie die Quicklist, wenn Sie das brauchen).

Aber wenn Geschwindigkeit nicht der einzige Preis ist.
Die Wartungsfreundlichkeit ist ein Kostenfaktor, der gemessen werden kann. Mit dieser Metrik der Kosten Ausnahmen immer gewinnen, da sie letztlich die constrol Fluss des Codes zu nur die Aufgabe, die getan werden muss, nicht die Aufgabe und Fehlerkontrolle zu machen.

0 Stimmen

"Mein Numpty-Benchmark versucht, dies zu simulieren (siehe den rekursiven Aufruf der Funktion "returner"). Ich habe nicht überprüft, ob der Compiler dies optimiert. Meine Schätzung von 13us für eine Ausnahme, die 3 Ebenen höher abgefangen wird, könnte also eine Überschätzung sein. Meine "throw"-Schleife ist mit Sicherheit proportional viel langsamer als die "return error"-Schleife. Natürlich ist das, bevor eine echte Arbeit hinzugefügt wird. Ich würde alle Vorschläge zu schätzen wissen, wie meine "returner" Schleife genauer reflektieren "check return code and bail out" Fehlerbehandlung.

2 Stimmen

Insbesondere könnte der Begriff "Rückkehrer" in der Erfolg Fall, je nachdem, welchen Weg der Compiler gewählt hat, um die Verzweigung auszugeben. Oft hört man nicht viel von den "Ausnahmen! Igitt! Glazial langsam!" darüber, dass sie zusätzliche Verzweigungen (im Vergleich zur ausnahmebasierten Fehlerbehandlung) in den Fall eines normalen, erfolgreichen Betriebs einführen.

7voto

Earlz Punkte 59611

Ich habe einmal eine x86-Emulationsbibliothek erstellt und Ausnahmen für Interrupts und dergleichen verwendet. Schlechte Idee. Selbst wenn ich keine Ausnahmen auslöste, wirkte sich das stark auf meine Hauptschleife aus. Meine Hauptschleife sah etwa so aus

try{
    CheckInterrupts();
    *(uint32_t*)&op_cache=ReadDword(cCS,eip);
    (this->*Opcodes[op_cache[0]])();
    //operate on the this class with the opcode functions in this class
    eip=(uint16_t)eip+1;

}
//eventually, handle these and do CpuInts...
catch(CpuInt_excp err){
    err.code&=0x00FF;
    switch(err.code){

Der Overhead, der durch die Aufnahme dieses Codes in einen Try-Block entsteht, macht die Ausnahmefunktionen zu den 2 der 5 größten Nutzer von CPU-Zeit.

Für mich ist das teuer.

0 Stimmen

Ja, das ist sie wirklich. Als ich ein Profil erstellte, führte ich sehr einfachen Code aus, der nie eine Ausnahme auslöste (außer am Ende), aber try und catch erfordern immer noch, dass der Stack abgewickelt wird. Und zu clafiy ich meine die beiden Ausnahmefunktionen (es gibt zwei mit gcc) war einer der Top-5-Funktionen nach Menge der verbrauchten CPU-Zeit sortiert aufgeführt

0 Stimmen

Denken Sie daran, dass Sie für jeden Stack-Frame alle lokalen Objektdestruktoren aufrufen und den lokalen Speicher löschen müssen. Jeder Frame wird eine nicht zu vernachlässigende Menge an Speicher verbrauchen. Deshalb sollten sie für "außergewöhnliche" Bedingungen reserviert werden.

6voto

jalf Punkte 235501

Es gibt keinen Grund, warum Ausnahmen in C++0x entweder schneller oder langsamer sein sollten als in C++03. Und das bedeutet, dass ihre Leistung völlig implementierungsabhängig ist. Windows verwendet völlig unterschiedliche Datenstrukturen zur Implementierung der Ausnahmebehandlung auf 32-Bit- und 64-Bit-Systemen sowie auf Itanium- und x86-Systemen. Auch bei Linux ist es nicht garantiert, dass es bei einer einzigen Implementierung bleibt. Es kommt darauf an. Es gibt mehrere gängige Methoden zur Implementierung der Ausnahmebehandlung, die alle ihre Vor- und Nachteile haben.

Es hängt also nicht von der Sprache ab (c++03 vs. 0x), sondern von Compiler, Laufzeitbibliothek, Betriebssystem und CPU-Architektur.

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