Cómo funcionan las excepciones (entre bastidores) en c++


Sigo viendo a la gente decir que las excepciones son lentas, pero nunca veo ninguna prueba. Entonces, en lugar de preguntar si lo son, 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 un montón de retorno, pero también comprueba cuando necesita dejar de hacer el retorno. ¿Cómo comprueba cuándo parar? Estoy tomando una conjetura y diciendo que hay una segunda pila que contiene el tipo de excepción y la ubicación de la pila luego devuelve hasta que llega allí. También estoy adivinando que la única vez que la pila es el toque es en un tiro y cada intento / captura. AFAICT implementar un comportamiento similar con el código de retorno tomaría la misma cantidad de tiempo. Pero todo esto es una suposición, así que quiero saberlo.

¿Cómo funcionan realmente las excepciones?

Author: Henrique Gouveia, 2008-11-21

7 answers

En lugar de adivinar, decidí mirar el código generado con una pequeña pieza de código C++ y una instalación de Linux algo antigua.

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 de código normal. En su lugar, el compilador generó código de reparación extra fuera de línea bloques, referenciados a través de 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 detrás de las escenas 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 montaje:

    .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. Mientras que no había gastos generales adicionales simplemente porque una excepción podría ser lanzado, obviamente hay una gran cantidad de gastos generales en realidad lanzar y la captura de una excepción. La mayor parte está escondida 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.
  • Desenrolla la pila hasta que llegue a ese controlador.
  • En realidad llame al controlador.

Compare eso con el costo de simplemente devolver un valor, y verá por qué las excepciones deben usarse solo para rendimientos excepcionales.

Para terminar, el resto del archivo ensamblador:

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

Aún 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 tablas) independientemente de si se lanzan o no excepciones, más el costo adicional de analizar las tablas y ejecutar los manejadores cuando se lanza una excepción. Si utiliza excepciones en lugar de error códigos, y un error es raro, puede ser más rápido, ya que ya no tiene la sobrecarga de probar errores.

En caso de que desee más información, en particular lo que hacen todas las funciones __cxa_, consulte la especificación original de la que provienen:

 99
Author: CesarB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-21 03:22:51

Que las excepciones sean lentas era cierto en los viejos tiempos.
En la mayoría de los compiladores modernos esto ya no es cierto.

Nota: Solo porque tengamos excepciones no significa que no utilicemos códigos de error también. Cuando el error se puede manejar 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 están utilizando excepciones es prácticamente cero.

Cuando se lanza una excepción se realiza algún trabajo.
Pero tiene que comparar esto con el costo de devolver los códigos de error y verificarlos hasta el punto donde se puede manejar el error. Tanto más tiempo para escribir y mantener.

También hay una gotcha para los novicios:
Aunque se supone que los objetos de excepción son pequeños, algunas personas ponen muchas cosas dentro de ellos. Entonces usted tiene el costo de copiar el objeto exception. La solución allí es doble:

  • No pongas cosas adicionales en tu excepción.
  • Capturas por referencia const.

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 excepciones (pero tiene todo el código adicional para verificar los resultados de error de la función). Recuerde que no está recibiendo nada gratis el compilador está generando el código que debería haber escrito en el primer lugar para comprobar los códigos de error (y por lo general el compilador es mucho más eficiente que un humano).

 13
Author: Martin York,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:17:38

Hay varias formas de implementar excepciones, pero normalmente se basan en algún soporte subyacente del sistema operativo. En Windows este es el mecanismo estructurado de manejo de excepciones.

Hay una discusión decente sobre los detalles del Proyecto de Código: Cómo un compilador de C++ implementa el manejo de excepciones

La sobrecarga de excepciones se produce porque el compilador tiene que generar código para realizar un seguimiento de qué objetos se deben destruir en cada marco de pila (o más alcance exacto) si una excepción se propaga fuera de ese alcance. Si una función no tiene variables locales en la pila que requieran que se llame a los destructores, entonces no debería tener una penalización de rendimiento en el manejo 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 atrás en la pila en una operación si no hay nada que hacer en los marcos de la pila intermedia.

 12
Author: Rob Walker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-21 02:13:07

Matt Pietrek escribió un excelente artículo sobre Win32 Structured Exception Handling . Si bien este artículo fue escrito originalmente en 1997, todavía se aplica hoy en día (pero, por supuesto, solo se aplica a Windows).

 6
Author: Greg Hewgill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-21 02:17:27

Este artículo examina el problema y básicamente encuentra que en la práctica hay un costo de tiempo de ejecución para las excepciones, aunque el costo es bastante bajo si la excepción no se lanza. Buen artículo, recomendado.

 5
Author: Alastair,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-21 02:23:26

Un amigo mío escribió un poco cómo Visual C++ maneja las excepciones hace algunos años.

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

 2
Author: Nils Pipenbrinck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-21 02:18:26

Todas buenas respuestas.

También, piense en lo mucho más fácil que es depurar código que hace 'if checks' como puertas en la parte superior de los métodos en lugar de permitir que el código lance excepciones.

Mi lema es que es fácil escribir código que funcione. Lo más importante es escribir el código para la siguiente persona que lo mire. En algunos casos, es usted en 9 meses, y usted no quiere estar maldiciendo su nombre!

 1
Author: Kieveli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-21 02:23:05