Wie funktionieren Ausnahmen (hinter den Kulissen) in c ++
-
08-07-2019 - |
Frage
Ich sehe ständig Leute sagen, dass Ausnahmen nur langsam, aber ich sehe nie einen Beweis. Anstatt also zu fragen, ob sie sind, werde ich fragen, wie Sie Ausnahmen hinter den Kulissen arbeiten, so kann ich ein Entscheidung treffen, wenn sie zu benutzen, und wenn sie langsam.
Von dem, was ich weiß, Ausnahmen sind das gleiche wie ein Bündel Rückkehr zu tun, aber es überprüft auch, wenn es die Rückkehr zu stoppen braucht tun. Wie überprüft es, wenn aufhören zu tun? Ich nehme eine Vermutung und sagen es einen zweiten Stapel ist, die die Art von Ausnahme und Stack-Position hält dann tut kehrt, bis er dort ankommt. Ich vermute auch das einzig Mal, dass Stack ist Berührung auf einem Wurf ist und jeder try / catch. AFAICT Umsetzung ein ähnliches Verhalten mit dem Rückkehrcode würde die gleiche Menge an Zeit in Anspruch nehmen. Aber das ist alles eine Vermutung, so will ich wissen.
Wie wirklich Ausnahmen arbeiten?
Lösung
Anstatt zu raten, entschied ich mich tatsächlich aussehen zu auf dem generierten Code mit einem kleinen Stück von C ++ Code und eine etwas alten Linux installieren.
class MyException
{
public:
MyException() { }
~MyException() { }
};
void my_throwing_function(bool throwit)
{
if (throwit)
throw MyException();
}
void another_function();
void log(unsigned count);
void my_catching_function()
{
log(0);
try
{
log(1);
another_function();
log(2);
}
catch (const MyException& e)
{
log(3);
}
log(4);
}
ich es mit g++ -m32 -W -Wall -O3 -save-temps -c
zusammengestellt, und schaute auf die generierte Assembly-Datei.
.file "foo.cpp"
.section .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
.align 2
.p2align 4,,15
.weak _ZN11MyExceptionD1Ev
.type _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
pushl %ebp
.LCFI0:
movl %esp, %ebp
.LCFI1:
popl %ebp
ret
.LFE7:
.size _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev
_ZN11MyExceptionD1Ev
ist MyException::~MyException()
, so dass der Compiler entschieden uns, es nicht-Inline-Kopie des destructor benötigt.
.globl __gxx_personality_v0
.globl _Unwind_Resume
.text
.align 2
.p2align 4,,15
.globl _Z20my_catching_functionv
.type _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
pushl %ebp
.LCFI2:
movl %esp, %ebp
.LCFI3:
pushl %ebx
.LCFI4:
subl $20, %esp
.LCFI5:
movl $0, (%esp)
.LEHB0:
call _Z3logj
.LEHE0:
movl $1, (%esp)
.LEHB1:
call _Z3logj
call _Z16another_functionv
movl $2, (%esp)
call _Z3logj
.LEHE1:
.L5:
movl $4, (%esp)
.LEHB2:
call _Z3logj
addl $20, %esp
popl %ebx
popl %ebp
ret
.L12:
subl $1, %edx
movl %eax, %ebx
je .L16
.L14:
movl %ebx, (%esp)
call _Unwind_Resume
.LEHE2:
.L16:
.L6:
movl %eax, (%esp)
call __cxa_begin_catch
movl $3, (%esp)
.LEHB3:
call _Z3logj
.LEHE3:
call __cxa_end_catch
.p2align 4,,3
jmp .L5
.L11:
.L8:
movl %eax, %ebx
.p2align 4,,6
call __cxa_end_catch
.p2align 4,,6
jmp .L14
.LFE9:
.size _Z20my_catching_functionv, .-_Z20my_catching_functionv
.section .gcc_except_table,"a",@progbits
.align 4
.LLSDA9:
.byte 0xff
.byte 0x0
.uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
.byte 0x1
.uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
.uleb128 .LEHB0-.LFB9
.uleb128 .LEHE0-.LEHB0
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB1-.LFB9
.uleb128 .LEHE1-.LEHB1
.uleb128 .L12-.LFB9
.uleb128 0x1
.uleb128 .LEHB2-.LFB9
.uleb128 .LEHE2-.LEHB2
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB3-.LFB9
.uleb128 .LEHE3-.LEHB3
.uleb128 .L11-.LFB9
.uleb128 0x0
.LLSDACSE9:
.byte 0x1
.byte 0x0
.align 4
.long _ZTI11MyException
.LLSDATT9:
Überraschung! Es fallen keine zusätzlichen Anweisungen an alle auf dem normalen Codepfad. Der Compiler erzeugte stattdessen zusätzliche out-of-line-Blöcke Fixup-Code über eine Tabelle am Ende der Funktion Bezug genommen (die auf einem separaten Abschnitt der ausführbaren setzen tatsächlich). Die ganze Arbeit hinter den Kulissen durch die Standard-Bibliothek durchgeführt, basierend auf diesen Tabellen (_ZTI11MyException
ist typeinfo for MyException
).
OK, das ist nicht wirklich eine Überraschung für mich war, wusste ich schon, wie diese Compiler es taten. Weiter mit dem Montage Ausgabe:
.text
.align 2
.p2align 4,,15
.globl _Z20my_throwing_functionb
.type _Z20my_throwing_functionb, @function
_Z20my_throwing_functionb:
.LFB8:
pushl %ebp
.LCFI6:
movl %esp, %ebp
.LCFI7:
subl $24, %esp
.LCFI8:
cmpb $0, 8(%ebp)
jne .L21
leave
ret
.L21:
movl $1, (%esp)
call __cxa_allocate_exception
movl $_ZN11MyExceptionD1Ev, 8(%esp)
movl $_ZTI11MyException, 4(%esp)
movl %eax, (%esp)
call __cxa_throw
.LFE8:
.size _Z20my_throwing_functionb, .-_Z20my_throwing_functionb
Hier sehen wir den Code eine Ausnahme zu werfen. Zwar gibt es keinen zusätzlichen Aufwand einfach war, weil eine Ausnahme könnte es geworfen wird, ist offensichtlich eine Menge Aufwand in tatsächlich zu werfen und eine Ausnahme zu kontrollieren. Das meiste davon ist in __cxa_throw
versteckt, die müssen:
- Gehen Sie den Stapel mit Hilfe der Ausnahmetabellen, bis es einen Handler für diese Ausnahme findet.
- Entspannen Sie den Stapel, bis es zu diesem Handler wird.
- Eigentlich den Handler aufrufen.
Vergleichen Sie das mit den Kosten nur einen Wert zurückgibt, und Sie sehen, warum Ausnahmen sollten nur für außergewöhnliche Erträge verwendet werden.
Zum Abschluss der Rest der Baugruppendatei:
.weak _ZTI11MyException
.section .rodata._ZTI11MyException,"aG",@progbits,_ZTI11MyException,comdat
.align 4
.type _ZTI11MyException, @object
.size _ZTI11MyException, 8
_ZTI11MyException:
.long _ZTVN10__cxxabiv117__class_type_infoE+8
.long _ZTS11MyException
.weak _ZTS11MyException
.section .rodata._ZTS11MyException,"aG",@progbits,_ZTS11MyException,comdat
.type _ZTS11MyException, @object
.size _ZTS11MyException, 14
_ZTS11MyException:
.string "11MyException"
Die Typeinfo-Daten.
.section .eh_frame,"a",@progbits
.Lframe1:
.long .LECIE1-.LSCIE1
.LSCIE1:
.long 0x0
.byte 0x1
.string "zPL"
.uleb128 0x1
.sleb128 -4
.byte 0x8
.uleb128 0x6
.byte 0x0
.long __gxx_personality_v0
.byte 0x0
.byte 0xc
.uleb128 0x4
.uleb128 0x4
.byte 0x88
.uleb128 0x1
.align 4
.LECIE1:
.LSFDE3:
.long .LEFDE3-.LASFDE3
.LASFDE3:
.long .LASFDE3-.Lframe1
.long .LFB9
.long .LFE9-.LFB9
.uleb128 0x4
.long .LLSDA9
.byte 0x4
.long .LCFI2-.LFB9
.byte 0xe
.uleb128 0x8
.byte 0x85
.uleb128 0x2
.byte 0x4
.long .LCFI3-.LCFI2
.byte 0xd
.uleb128 0x5
.byte 0x4
.long .LCFI5-.LCFI3
.byte 0x83
.uleb128 0x3
.align 4
.LEFDE3:
.LSFDE5:
.long .LEFDE5-.LASFDE5
.LASFDE5:
.long .LASFDE5-.Lframe1
.long .LFB8
.long .LFE8-.LFB8
.uleb128 0x4
.long 0x0
.byte 0x4
.long .LCFI6-.LFB8
.byte 0xe
.uleb128 0x8
.byte 0x85
.uleb128 0x2
.byte 0x4
.long .LCFI7-.LCFI6
.byte 0xd
.uleb128 0x5
.align 4
.LEFDE5:
.ident "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
.section .note.GNU-stack,"",@progbits
Noch mehr Ausnahmebehandlung Tabellen und verschiedene zusätzliche Informationen.
So die Schlussfolgerung, zumindest für GCC auf Linux: die Kosten sind extra Platz (für die Handler und Tabellen), ob oder nicht Ausnahmen ausgelöst werden, sowie die zusätzlichen Kosten für die Tabellen-Parsing und die Handler ausgeführt wird, wenn eine Ausnahme ist geworfen. Wenn Sie Ausnahmen anstelle von Fehlercodes zu verwenden, und ein Fehler ist selten, kann es sein, schneller , da Sie nicht den Aufwand für das Testen mehr für Fehler haben.
Falls Sie weitere Informationen wünschen, insbesondere, was die alle __cxa_
Funktionen haben, finden Sie in der ursprünglichen Spezifikation sie kamen:
Andere Tipps
Ausnahmen sind langsam wurde true in den alten Tagen.
In den meisten modernen Compiler hält dies nicht mehr wahr.
Hinweis: Nur weil wir Ausnahmen haben, bedeutet nicht, dass wir Fehlercodes nicht so gut verwenden. Wenn Fehler gehandhabt werden kann lokal verwenden Fehlercodes. Wenn Fehler mehr Kontext für die Korrektur Ausnahmen Verwendung erfordern: Ich schrieb es viel mehr beredt hier: Was sind die Prinzipien Ihre Ausnahmebehandlung Politik führen?
Die Kosten für die Ausnahmebehandlung Code, wenn keine Ausnahmen verwendet werden, ist praktisch gleich Null.
Wenn eine Ausnahme ausgelöst wird, gibt es eine Arbeit erledigt.
Aber Sie haben dies zum Vergleich mit den Kosten der Fehlercodes zurückkehrt und überprüft sie den ganzen Weg zurück bis zum Punkt, wo der Fehler gehandhabt werden kann. Sowohl mehr Zeit zu schreiben und zu pflegen raubend.
Auch gibt es ein gotcha für Anfänger:
Obwohl Exception-Objekte sollen einige Leute zu klein setzen viele Sachen in ihnen. Dann haben Sie die Kosten für das Kopieren des Ausnahmeobjekts. Die Lösung gibt es zwei Ziele verfolgt:
- Stellen Sie kein Extramaterial in Ihrer Ausnahme.
- Fang durch konstante Referenz.
Meiner Meinung nach würde ich darauf wetten, dass der gleiche Code mit Ausnahmen entweder effizienter oder zumindest so vergleichbar wie der Code ohne die Ausnahmen (aber hat alle zusätzlichen Code-Funktion Fehler Ergebnisse zu überprüfen). Denken Sie daran, Sie sind nicht kostenlos Compiler immer etwas, um den Code zu erzeugen Sie an erster Stelle geschrieben haben, sollten Fehlercodes (und in der Regel der Compiler sind viel effizienter als ein Mensch).
prüfenEs gibt eine Reihe von Möglichkeiten, wie Sie Ausnahmen implementieren könnte, aber in der Regel werden sie auf eine zugrunde liegende Unterstützung der OS verlassen. Unter Windows ist dies die strukturierte Ausnahmebehandlung.
Es gibt anständige Diskussion der Details auf Code Project: Wie ein C ++ Compiler implementiert Ausnahmebehandlung
Der Overhead von Ausnahmen tritt auf, weil die Compiler Code zu erzeugen hat, um zu verfolgen, welche Objekte in jedem Stapelrahmen zerstört werden muß (oder mehr Umfang genau), wenn eine Ausnahme von diesem Umfang ausbreitet. Wenn eine Funktion keine lokalen Variablen auf dem Stapel hat, die Destruktoren benötigen aufgerufen werden, dann sollte es keine Leistungseinbuße WRT Ausnahmebehandlung hat.
einen Return-Code verwendet, kann nur eine einzige Ebene des Stapels zu einer Zeit entspannen, während eine Ausnahmebehandlung viel weiter zurück in dem Stapel in einem Arbeitsgang springen kann, wenn es nichts für sie in dem Zwischenstapelrahmen zu tun.
Matt Pietrek schrieb einen ausgezeichneten Artikel über Win32 Structured Exception Handling . Während dieser Artikel ursprünglich im Jahr 1997 geschrieben wurde, ist es noch heute gilt (aber natürlich gilt nur für Windows).
dieser Artikel das Problem untersucht und findet im Grunde, dass in der Praxis ein Lauf- ist Zeit Kosten zu Ausnahmen, obwohl die Kosten relativ niedrig sind, wenn die Ausnahme nicht ausgelöst. Guter Artikel, empfohlen.
Ein Freund von mir hat ein bisschen wie Visual C ++ Exceptions vor einigen Jahren.
Alle guten Antworten.
Auch darüber nachdenken, wie viel einfacher es ist zu Debug-Code, der an der Spitze der Methoden ‚wenn Schecks‘, wie Gates tut stattdessen den Code erlauben Ausnahmen zu werfen.
Mein Motto ist, dass es einfach ist, Code zu schreiben, das funktioniert. Das Wichtigste ist, den Code für die nächste Person zu schreiben, die es betrachtet. In einigen Fällen ist es Ihnen in 9 Monaten, und Sie wollen nicht, Ihren Namen zu verfluchen!