son - tipo de excepciones en c++
¿Cómo funcionan las excepciones(detrás de escena) en c++ (7)
En lugar de adivinar, decidí mirar el código generado con una pequeña porción de código C ++ y una instalación Linux algo vieja.
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);
}
Lo compilé con g++ -m32 -W -Wall -O3 -save-temps -c
, y miré el archivo ensamblado generado.
.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
es MyException::~MyException()
, por lo que el compilador decidió que necesitaba una copia no en línea del destructor.
.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! No hay instrucciones adicionales en absoluto en la ruta del código normal. En su lugar, el compilador generó bloques adicionales de código de reparación fuera de línea, a los que se hace referencia mediante una tabla al final de la función (que en realidad se coloca en una sección separada del ejecutable). Todo el trabajo se realiza tras bambalinas por la biblioteca estándar, basado en estas tablas ( _ZTI11MyException
es typeinfo for MyException
).
OK, eso no fue realmente una sorpresa para mí, ya sabía cómo lo hizo este compilador. Continuando con la salida de ensamblaje:
.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
Aquí vemos el código para lanzar una excepción. Si bien no hubo sobrecarga adicional simplemente porque se pudo lanzar una excepción, obviamente hay mucho sobrecarga en lanzar y atrapar una excepción. La mayor parte está oculta dentro de __cxa_throw
, que debe:
- Recorre la pila con la ayuda de las tablas de excepciones hasta que encuentre un controlador para esa excepción.
- Desenrolle la pila hasta que llegue a ese controlador.
- En realidad, llama al controlador.
Compare eso con el costo de simplemente devolver un valor, y verá por qué las excepciones deben usarse solo para retornos excepcionales.
Para finalizar, el resto del archivo de ensamblaje:
.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"
Los datos de 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
Incluso más tablas de manejo de excepciones e información adicional variada.
Entonces, la conclusión, al menos para GCC en Linux: el costo es espacio extra (para los manejadores y las tablas) si se lanzan o no excepciones, más el costo adicional de analizar las tablas y ejecutar los manejadores cuando se produce una excepción. Si usa excepciones en lugar de códigos de error, y un error es raro, puede ser más rápido , ya que ya no tiene la sobrecarga de las pruebas de errores.
En caso de que desee obtener más información, en particular sobre qué hacen todas las funciones de __cxa_
, consulte la especificación original de la que provienen:
Sigo viendo que la gente dice que las excepciones son lentas, pero nunca veo ninguna prueba. Entonces, en lugar de preguntar si lo están, me preguntaré cómo funcionan las excepciones detrás de la escena, para poder tomar decisiones sobre cuándo usarlas y si son lentas.
Por lo que sé, las excepciones son lo mismo que hacer muchas vueltas, pero también verifica cuándo debe dejar de hacer la devolución. ¿Cómo se verifica cuándo detenerse? Adivino y digo que hay una segunda pila que contiene el tipo de excepción y la ubicación de la pila devuelve hasta que llega. También estoy adivinando que la única vez que la pila es el touch está en un lanzamiento y cada try / catch. AFAICT implementar un comportamiento similar con el código de retorno tomaría la misma cantidad de tiempo. Pero esto es todo una suposición, entonces quiero saber.
¿Cómo funcionan realmente las excepciones?
Existen varias maneras de implementar excepciones, pero generalmente se basarán en algún soporte subyacente del sistema operativo. En Windows, este es el mecanismo estructurado de manejo de excepciones.
Hay una discusión decente de los detalles en Code Project: cómo un compilador de C ++ implementa el manejo de excepciones
La sobrecarga de las excepciones se produce porque el compilador tiene que generar código para realizar un seguimiento de qué objetos deben destruirse en cada marco de pila (o más precisamente alcance) si una excepción se propaga fuera de ese alcance. Si una función no tiene variables locales en la pila que requieran el llamado de destructores, entonces no debería tener una penalización por el rendimiento de la gestión de excepciones wrt.
El uso de un código de retorno solo puede desenrollar un solo nivel de la pila a la vez, mientras que un mecanismo de manejo de excepciones puede saltar mucho más hacia atrás en una operación si no hay nada que hacer en los cuadros intermedios de la pila.
Las excepciones que eran lentas eran verdad en los viejos tiempos.
En la mayoría de los compiladores modernos esto ya no es cierto.
Nota: el hecho de que tengamos excepciones no significa que no usemos códigos de error también. Cuando el error puede ser manejado localmente use códigos de error. Cuando los errores requieren más contexto para la corrección, use excepciones: lo escribí mucho más elocuentemente aquí: ¿Cuáles son los principios que guían su política de manejo de excepciones?
El costo del código de manejo de excepciones cuando no se usan excepciones es prácticamente cero.
Cuando se lanza una excepción, se realiza un trabajo.
Pero tiene que comparar esto con el costo de devolver códigos de error y verificarlos todo el camino de regreso al punto donde se puede manejar el error. Tanto más tiempo para escribir y mantener.
También hay un gotcha para principiantes:
Aunque se supone que los objetos de excepción son pequeños, algunas personas colocan muchas cosas dentro de ellos. Entonces tiene el costo de copiar el objeto de excepción. La solución allí es doble:
- No pongas cosas extra en tu excepción.
- Catch by const referencia.
En mi opinión, apostaría a que el mismo código con excepciones es más eficiente o al menos tan comparable como el código sin las excepciones (pero tiene todo el código adicional para verificar los resultados del error de la función). Recuerde que no obtiene nada gratis, el compilador genera el código que debería haber escrito en primer lugar para verificar los códigos de error (y generalmente el compilador es mucho más eficiente que un humano).
Matt Pietrek escribió un excelente artículo sobre el Manejo de excepciones estructuradas de Win32 . Si bien este artículo se escribió originalmente en 1997, todavía se aplica hoy (pero, por supuesto, solo se aplica a Windows).
Todas las buenas respuestas.
Además, piense en lo fácil que es depurar el código que hace ''si comprueba'' como puertas en la parte superior de los métodos en lugar de permitir que el código arroje excepciones.
Mi lema es que es fácil escribir código que funcione. Lo más importante es escribir el código para la próxima persona que lo mire. En algunos casos, eres tú en 9 meses y no quieres maldecir tu nombre.
Un amigo mío escribió un poco cómo Visual C ++ maneja las excepciones hace algunos años.
Este artículo examina el problema y básicamente encuentra que en la práctica existe un costo de tiempo de ejecución para las excepciones, aunque el costo es bastante bajo si no se lanza la excepción. Buen artículo, recomendado.