Come funzionano le eccezioni (dietro le quinte) in c ++

Continuo a vedere le persone dire che le eccezioni sono lente, ma non vedo mai alcuna prova. Quindi, invece di chiedere se lo sono, chiederò come funzionano le eccezioni dietro la scena, così posso prendere una decisione su quando usarli e se sono lenti.

Da quello che so, le eccezioni sono la stessa cosa che fare un sacco di ritorno, ma controlla anche quando deve smettere di fare il ritorno. Come controlla quando fermarsi? Sto facendo un’ipotesi e dicendo che c’è un secondo stack che contiene il tipo di eccezione e la posizione dello stack quindi restituisce finché non arriva lì. Sto anche indovinando l’unica volta in cui lo stack è il touch è su un lancio e ogni tentativo / catch. AFAICT che implementa un comportamento simile con il codice di ritorno richiederebbe lo stesso tempo. Ma questo è tutto un tentativo, quindi voglio sapere.

Come funzionano davvero le eccezioni?

Invece di indovinare, ho deciso di guardare effettivamente il codice generato con un piccolo pezzo di codice C ++ e una vecchia installazione Linux.

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

L’ho compilato con g++ -m32 -W -Wall -O3 -save-temps -c , e ho guardato il file assembly generato.

  .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 è MyException::~MyException() , quindi il compilatore ha deciso che era necessaria una copia non in linea del distruttore.

 .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: 

Sorpresa! Non ci sono istruzioni aggiuntive sul normale percorso del codice. Il compilatore invece ha generato blocchi di codice di correzione extra fuori linea, referenziati tramite una tabella alla fine della funzione (che in realtà è inserita in una sezione separata dell’eseguibile). Tutto il lavoro viene svolto dietro le quinte dalla libreria standard, basata su queste tabelle ( _ZTI11MyException is typeinfo for MyException ).

OK, quella non era in realtà una sorpresa per me, già sapevo come faceva questo compilatore. Continuando con l’output di assemblaggio:

  .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 

Qui vediamo il codice per lanciare un’eccezione. Sebbene non ci sia un sovraccarico in più semplicemente perché potrebbe essere lanciata un’eccezione, c’è ovviamente un sacco di spese generali per lanciare e catturare un’eccezione. La maggior parte è nascosta all’interno di __cxa_throw , che deve:

  • Walk the stack con l’aiuto delle tabelle delle eccezioni fino a quando non trova un gestore per quell’eccezione.
  • Srotolare lo stack fino a quando non arriva a quel gestore.
  • In realtà chiama il gestore.

Confrontalo con il costo della semplice restituzione di un valore e vedi perché le eccezioni dovrebbero essere utilizzate solo per rendimenti eccezionali.

Per finire, il resto del file assembly:

  .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" 

I dati di typeinfo.

  .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 

Ancora più tabelle di gestione delle eccezioni e informazioni extra assortite.

Quindi, la conclusione, almeno per GCC su Linux: il costo è lo spazio extra (per i gestori e le tabelle) indipendentemente dal fatto che vengano lanciate eccezioni, oltre al costo aggiuntivo di analizzare le tabelle ed eseguire i gestori quando viene lanciata un’eccezione. Se si utilizzano le eccezioni anziché i codici di errore e un errore è raro, può essere più veloce , poiché non si ha più il sovraccarico di testare gli errori.

Se vuoi maggiori informazioni, in particolare cosa fanno tutte le funzioni __cxa_ , vedi le specifiche originali da cui provengono:

  • Itanium C ++ ABI

Le eccezioni erano lente ai vecchi tempi.
Nella maggior parte dei compilatori moderni questo non vale più.

Nota: solo perché abbiamo eccezioni non significa che non usiamo anche i codici di errore. Quando l’errore può essere gestito localmente, utilizzare i codici di errore. Quando gli errori richiedono più contesto per le eccezioni dell’uso della correzione: l’ho scritto molto più eloquentemente qui: quali sono i principi che guidano le vostre politiche di gestione delle eccezioni?

Il costo del codice di gestione delle eccezioni quando non vengono utilizzate eccezioni è praticamente pari a zero.

Quando viene lanciata un’eccezione, c’è del lavoro.
Ma devi confrontarlo con il costo di restituire i codici di errore e ricontrollarli fino al punto in cui l’errore può essere gestito. Entrambi richiedono più tempo per scrivere e mantenere.

C’è anche un trucco per i novizi:
Sebbene gli oggetti di eccezione dovrebbero essere piccoli, alcune persone ci mettono dentro molte cose. Quindi hai il costo di copiare l’object di eccezione. La soluzione è due volte:

  • Non inserire oggetti extra nella tua eccezione.
  • Cattura per riferimento const.

A mio parere, scommetterei che lo stesso codice con le eccezioni è o più efficiente o almeno comparabile come il codice senza eccezioni (ma ha tutto il codice extra per controllare i risultati dell’errore di funzione). Ricorda che non ricevi nulla gratis il compilatore sta generando il codice che dovresti aver scritto in primo luogo per controllare i codici di errore (e di solito il compilatore è molto più efficiente di un umano).

Esistono diversi modi per implementare le eccezioni, ma in genere si basano su un supporto sottostante dal sistema operativo. Su Windows questo è il meccanismo di gestione delle eccezioni strutturato.

C’è una discussione decente dei dettagli sul progetto del codice: come un compilatore C ++ implementa la gestione delle eccezioni

Il sovraccarico delle eccezioni si verifica perché il compilatore deve generare codice per tenere traccia di quali oggetti devono essere destrutturati in ogni frame dello stack (o più precisamente scope) se un’eccezione si propaga fuori da quell’ambito. Se una funzione non ha variabili locali nello stack che richiedono la chiamata di distruttori, allora non dovrebbe avere una penalizzazione delle prestazioni in termini di gestione delle eccezioni.

L’uso di un codice di ritorno può solo srotolare un singolo livello della pila alla volta, mentre un meccanismo di gestione delle eccezioni può saltare molto più indietro nello stack in una sola operazione se non c’è nulla da fare nei frame di stack intermedi.

Matt Pietrek ha scritto un eccellente articolo su Win32 Structured Exception Handling . Mentre questo articolo è stato originariamente scritto nel 1997, si applica ancora oggi (ma ovviamente si applica solo a Windows).

Questo articolo esamina il problema e in pratica rileva che nella pratica esiste un costo di runtime per le eccezioni, sebbene il costo sia piuttosto basso se l’eccezione non viene generata. Buon articolo, consigliato.

Un mio amico ha scritto un po ‘come Visual C ++ gestisce le eccezioni alcuni anni fa.

http://www.xyzw.de/c160.html

Tutte buone risposte

Inoltre, pensate a quanto è più facile eseguire il debug del codice che fa ‘if checks’ come gate nella parte superiore dei metodi invece di consentire al codice di generare eccezioni.

Il mio motto è che è facile scrivere codice che funzioni. La cosa più importante è scrivere il codice per la prossima persona che la guarda. In alcuni casi, sei tu tra 9 mesi e non vuoi maledire il tuo nome!