web-dev-qa-db-fra.com

Comment fonctionnent les exceptions (en arrière-plan) en c ++

Je continue de voir des gens dire que les exceptions sont lentes, mais je n'en vois aucune preuve. Donc, au lieu de demander si elles le sont, je vais demander comment fonctionnent les exceptions en arrière-plan, afin que je puisse décider quand les utiliser et si elles sont lentes.

D'après ce que je sais, les exceptions sont la même chose que de faire un tas de retours, mais il vérifie également quand il doit arrêter de faire le retour. Comment vérifie-t-il quand s'arrêter? Je fais une supposition et je dis qu'il y a une deuxième pile qui contient le type d'exception et l'emplacement de la pile revient ensuite jusqu'à ce qu'il y arrive. Je suppose également que le seul moment où la pile est en contact est sur un lancer et à chaque essai/capture. AFAICT implémentant un comportement similaire avec un code retour prendrait le même temps. Mais ce n'est qu'une supposition, donc je veux savoir.

Comment fonctionnent vraiment les exceptions?

105
user34537

Au lieu de deviner, j'ai décidé de regarder le code généré avec un petit morceau de code C++ et une installation Linux un peu ancienne.

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

Je l'ai compilé avec g++ -m32 -W -Wall -O3 -save-temps -c Et j'ai regardé le fichier d'assemblage généré.

    .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 Est MyException::~MyException(), donc le compilateur a décidé qu'il avait besoin d'une copie non en ligne du destructeur.

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

Surprise! Il n'y a aucune instruction supplémentaire sur le chemin de code normal. Le compilateur a plutôt généré des blocs de code de correction hors ligne supplémentaires, référencés via une table à la fin de la fonction (qui est en fait placée sur une section distincte de l'exécutable). Tout le travail est effectué en arrière-plan par la bibliothèque standard, basée sur ces tableaux (_ZTI11MyException Est typeinfo for MyException).

OK, ce n'était pas vraiment une surprise pour moi, je savais déjà comment ce compilateur faisait. Poursuivant avec la sortie de l'assemblage:

    .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

Ici, nous voyons le code pour lever une exception. Bien qu'il n'y ait pas eu de frais généraux supplémentaires simplement parce qu'une exception pourrait être levée, il y a évidemment beaucoup de frais généraux pour lancer et intercepter une exception. La majeure partie est cachée dans __cxa_throw, Qui doit:

  • Parcourez la pile à l'aide des tables d'exceptions jusqu'à ce qu'elle trouve un gestionnaire pour cette exception.
  • Déroulez la pile jusqu'à ce qu'elle atteigne ce gestionnaire.
  • Appelez en fait le gestionnaire.

Comparez cela avec le coût du simple retour d'une valeur, et vous voyez pourquoi les exceptions ne devraient être utilisées que pour des retours exceptionnels.

Pour terminer, le reste du fichier d'assemblage:

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

Les données 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

Encore plus de tables de gestion des exceptions et des informations supplémentaires assorties.

Donc, la conclusion, au moins pour GCC sur Linux: le coût est un espace supplémentaire (pour les gestionnaires et les tables), que des exceptions soient levées ou non, plus le coût supplémentaire de l'analyse des tables et de l'exécution des gestionnaires lorsqu'une exception est levée. Si vous utilisez des exceptions au lieu de codes d'erreur et qu'une erreur est rare, elle peut être plus rapide , car vous n'avez plus la charge de tester les erreurs .

Dans le cas où vous souhaitez plus d'informations, en particulier ce que font toutes les fonctions __cxa_, Consultez les spécifications d'origine dont elles proviennent:

104
CesarB

Les exceptions étant lentes était vrai dans l'ancien temps.
Dans la plupart des compilateurs modernes, cela n'est plus vrai.

Remarque: ce n'est pas parce que nous avons des exceptions que nous n'utilisons pas également les codes d'erreur. Lorsque l'erreur peut être gérée localement, utilisez des codes d'erreur. Lorsque les erreurs nécessitent plus de contexte pour la correction, utilisez les exceptions: je l'ai écrit de manière beaucoup plus éloquente ici: Quels sont les principes guidant votre politique de gestion des exceptions?

Le coût du code de gestion des exceptions lorsqu'aucune exception n'est utilisée est pratiquement nul.

Lorsqu'une exception est levée, un travail est effectué.
Mais vous devez comparer cela au coût de retour des codes d'erreur et de les vérifier jusqu'au point où l'erreur peut être gérée. Les deux prennent plus de temps à écrire et à maintenir.

Il y a aussi un gotcha pour les novices:
Bien que les objets Exception soient censés être petits, certaines personnes y mettent beaucoup de choses. Ensuite, vous avez le coût de la copie de l'objet d'exception. La solution est double:

  • Ne mettez pas de choses supplémentaires dans votre exception.
  • Capture par référence const.

À mon avis, je parierais que le même code avec des exceptions est soit plus efficace ou au moins aussi comparable que le code sans les exceptions (mais a tout le code supplémentaire pour vérifier les résultats des erreurs de fonction). N'oubliez pas que vous n'obtenez rien gratuitement le compilateur génère le code que vous auriez dû écrire en premier lieu pour vérifier les codes d'erreur (et généralement le compilateur est beaucoup plus efficace qu'un humain).

13
Martin York

Il existe plusieurs façons d'implémenter des exceptions, mais elles reposent généralement sur une prise en charge sous-jacente du système d'exploitation. Sous Windows, il s'agit du mécanisme de gestion des exceptions structuré.

Il y a une discussion décente sur les détails du projet de code: Comment un compilateur C++ implémente la gestion des exceptions

La surcharge des exceptions se produit parce que le compilateur doit générer du code pour garder une trace des objets qui doivent être détruits dans chaque trame de pile (ou plus précisément la portée) si une exception se propage hors de cette portée. Si une fonction n'a pas de variables locales sur la pile qui nécessitent d'appeler des destructeurs, elle ne doit pas avoir de gestion des exceptions par pénalité de performance.

L'utilisation d'un code retour ne peut dérouler qu'un seul niveau de la pile à la fois, tandis qu'un mécanisme de gestion des exceptions peut remonter beaucoup plus bas dans la pile en une seule opération s'il n'y a rien à faire dans les cadres de pile intermédiaires.

12
Rob Walker

Matt Pietrek a écrit un excellent article sur Win32 Structured Exception Handling . Bien que cet article ait été écrit à l'origine en 1997, il s'applique toujours aujourd'hui (mais bien sûr ne s'applique qu'à Windows).

6
Greg Hewgill

Cet article examine le problème et constate essentiellement qu'en pratique il y a un coût d'exécution pour les exceptions, bien que le coût soit assez faible si l'exception n'est pas levée. Bon article, recommandé.

5
Alastair

Il y a quelques années, un de mes amis a expliqué comment Visual C++ gère les exceptions.

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

2
Nils Pipenbrinck

Toutes les bonnes réponses.

Pensez également à la façon dont il est plus facile de déboguer du code qui fait "si vérifie" en tant que portes en haut des méthodes au lieu de permettre au code de lever des exceptions.

Ma devise est qu'il est facile d'écrire du code qui fonctionne. La chose la plus importante est d'écrire le code pour la prochaine personne qui le regardera. Dans certains cas, c'est vous dans 9 mois, et vous ne voulez pas maudire votre nom!

1
Kieveli