Les exceptions en C ++ sont-elles vraiment lentes

98

Je regardais la gestion des erreurs systématiques en C ++ - Andrei Alexandrescu il prétend que les exceptions en C ++ sont très très lentes.

Est-ce toujours vrai pour C ++ 98?

Avinash
la source
42
Cela n'a aucun sens de demander si les "exceptions C ++ 98" sont plus rapides / plus lentes que les "exceptions C ++ 03" ou "C ++ 11 exceptions". Leurs performances dépendent de la manière dont le compilateur les implémente dans vos programmes, et le standard C ++ ne dit rien sur la manière dont ils doivent être implémentés; la seule exigence est que leur comportement doit suivre la norme (la règle du «comme si»).
In silico
Question connexe (mais pas vraiment en double): stackoverflow.com/questions/691168/…
Philipp
2
oui, c'est très lent, mais ils ne doivent pas être lancés pour une opération normale ou utilisés comme une branche
BЈовић
J'ai trouvé une question similaire .
PaperBirdMaster
Pour clarifier ce que BЈовић a dit, il ne faut pas avoir peur d'utiliser des exceptions. C'est lorsqu'une exception est levée que vous rencontrez des opérations (potentiellement) chronophages. Je suis également curieux de savoir pourquoi vous voulez savoir spécifiquement pour C ++ 89 ... cette dernière version est C ++ 11, et le temps qu'il faut pour que les exceptions s'exécutent est défini par l'implémentation, d'où mon temps `` potentiellement '' .
thecoshman

Réponses:

162

Le modèle principal utilisé aujourd'hui pour les exceptions (Itanium ABI, VC ++ 64 bits) est le modèle d'exceptions à coût zéro.

L'idée est qu'au lieu de perdre du temps en mettant en place une garde et en vérifiant explicitement la présence d'exceptions partout, le compilateur génère une table d'appoint qui mappe tout point pouvant lever une exception (Program Counter) à une liste de gestionnaires. Lorsqu'une exception est levée, cette liste est consultée pour choisir le bon gestionnaire (le cas échéant) et la pile est déroulée.

Par rapport à la if (error)stratégie typique :

  • le modèle à coût zéro, comme son nom l'indique, est gratuit lorsqu'aucune exception ne se produit
  • cela coûte environ 10x / 20x an iflorsqu'une exception se produit

Le coût, cependant, n'est pas anodin à mesurer:

  • La table d'appoint est généralement froide , et donc la récupérer de mémoire prend beaucoup de temps
  • Déterminer le bon gestionnaire implique RTTI: de nombreux descripteurs RTTI à récupérer, dispersés dans la mémoire et des opérations complexes à exécuter (essentiellement un dynamic_casttest pour chaque gestionnaire)

Donc, la plupart du temps, le cache manque, et donc pas trivial par rapport au code CPU pur.

Remarque: pour plus de détails, lisez le rapport TR18015, chapitre 5.4 Gestion des exceptions (pdf)

Donc, oui, les exceptions sont lentes sur le chemin exceptionnel , mais elles sont autrement plus rapides que les vérifications explicites ( ifstratégie) en général.

Remarque: Andrei Alexandrescu semble remettre en question ce "plus rapide". J'ai personnellement vu les choses évoluer dans les deux sens, certains programmes étant plus rapides avec des exceptions et d'autres plus rapides avec des succursales, il semble donc effectivement y avoir une perte d'optimisabilité dans certaines conditions.


Est-ce que ça importe ?

Je dirais que non. Un programme doit être écrit avec la lisibilité à l'esprit, pas la performance (du moins, pas comme premier critère). Des exceptions doivent être utilisées lorsque l'on s'attend à ce que l'appelant ne puisse pas ou ne souhaite pas gérer l'échec sur place et le transmettre dans la pile. Bonus: en C ++ 11, les exceptions peuvent être rassemblées entre les threads en utilisant la bibliothèque standard.

C'est subtil cependant, je prétends qu'il map::findne faut pas lancer mais je suis d'accord avec le map::findretour d'un checked_ptrqui jette si une tentative de déréférencement échoue parce que c'est nul: dans ce dernier cas, comme dans le cas de la classe qu'Alexandrescu a introduite, l'appelant choisit entre la vérification explicite et le recours à des exceptions. Donner du pouvoir à l'appelant sans lui donner plus de responsabilités est généralement un signe de bonne conception.

Matthieu M.
la source
3
+1 Je n'ajouterais que quatre choses: (0) à propos de la prise en charge de la relance ajoutée dans C ++ 11; (1) une référence au rapport de la commission sur l'efficacité C ++; (2) quelques remarques sur l'exactitude (comme surpassant même la lisibilité); et (3) à propos de la performance, des remarques sur sa mesure par rapport au cas de non-utilisation d'exceptions (tout est relatif)
Cheers et hth. - Alf
2
@ Cheersandhth.-Alf: (0), (1) et (3) terminé: merci. En ce qui concerne l'exactitude (2), bien qu'elle l'emporte sur la lisibilité, je ne suis pas sûr des exceptions menant à un code plus correct que d'autres stratégies de gestion des erreurs (il est si facile d'oublier les nombreux chemins invisibles des exceptions d'exécution créées).
Matthieu M.
2
La description peut être localement correcte, mais il peut être intéressant de noter que la présence d'exceptions a des implications globales sur les hypothèses et les optimisations que le compilateur peut faire. Ces implications souffrent du problème qu'elles n'ont "aucun contre-exemple trivial", puisque le compilateur peut toujours voir à travers un petit programme. Le profilage sur une base de code réaliste et volumineuse avec et sans exceptions peut être une bonne idée.
Kerrek SB
4
> le modèle à coût zéro, comme son nom l'indique, est gratuit lorsqu'aucune exception ne se produit, ce n'est en fait pas vrai jusqu'aux plus fins niveaux de détail. générer plus de code a toujours un impact sur les performances, même s'il est petit et subtil ... cela peut prendre un peu plus de temps pour que le système d'exploitation charge l'exécutable, ou vous obtiendrez plus d'erreurs i-cache. aussi, qu'en est-il du code de déroulement de la pile? aussi, qu'en est-il des expériences que vous pouvez faire pour mesurer les effets au lieu d'essayer de les comprendre avec une pensée rationnelle?
jheriko
2
@jheriko: Je crois que j'ai déjà répondu à la plupart de vos questions, en fait. Le temps de chargement ne doit pas être impacté (le code froid ne doit pas être chargé), l'i-cache ne doit pas être impacté (le code froid ne doit pas entrer dans l'i-cache), ... donc pour répondre à la seule question manquante: "how to measure" => remplacer toute exception lancée par un appel à abortvous permettra de mesurer l'empreinte de taille binaire et de vérifier que le load-time / i-cache se comporte de la même manière. Bien sûr, mieux vaut ne frapper aucun des abort...
Matthieu M.
60

Lorsque la question a été postée, j'étais en route pour le médecin, avec un taxi qui m'attendait, alors je n'ai eu le temps que pour un bref commentaire. Mais après avoir maintenant commenté et voté pour et contre, je ferais mieux d'ajouter ma propre réponse. Même si la réponse de Matthieu est déjà plutôt bonne.


Les exceptions sont-elles particulièrement lentes en C ++, par rapport aux autres langages?

Re la réclamation

«Je regardais la gestion des erreurs systématiques en C ++ - Andrei Alexandrescu affirme que les exceptions en C ++ sont très très lentes.»

Si c'est littéralement ce que prétend Andrei, alors pour une fois, il est très trompeur, sinon carrément faux. Pour une exception levée / levée est toujours lente par rapport aux autres opérations de base dans le langage, quel que soit le langage de programmation . Pas seulement en C ++ ou plus en C ++ que dans d'autres langages, comme l'indique la prétendue revendication.

En général, quelle que soit la langue, les deux fonctionnalités de base du langage qui sont des ordres de grandeur plus lentes que les autres, car elles se traduisent par des appels de routines qui gèrent des structures de données complexes, sont

  • lancement d'exceptions, et

  • allocation de mémoire dynamique.

Heureusement, en C ++, on peut souvent éviter les deux dans le code à temps critique.

Malheureusement, il n'y a rien de tel qu'un déjeuner gratuit , même si l'efficacité par défaut de C ++ est assez proche. :-) Pour l'efficacité gagnée en évitant le lancement d'exceptions et l'allocation de mémoire dynamique est généralement obtenue en codant à un niveau inférieur d'abstraction, en utilisant C ++ comme juste un «meilleur C». Et une abstraction inférieure signifie une plus grande «complexité».

Une plus grande complexité signifie plus de temps consacré à la maintenance et peu ou pas de bénéfice de la réutilisation du code, qui sont des coûts monétaires réels, même s'ils sont difficiles à estimer ou à mesurer. Par exemple, avec C ++, on peut, si on le souhaite, échanger une certaine efficacité du programmeur contre une efficacité d'exécution. Le fait de le faire est en grande partie une décision d'ingénierie et d'intuition, car en pratique, seul le gain, et non le coût, peut être facilement estimé et mesuré.


Existe-t-il des mesures objectives des performances de génération d'exceptions C ++?

Oui, le comité international de normalisation C ++ a publié un rapport technique sur les performances C ++, TR18015 .


Que signifie le fait que les exceptions sont «lentes»?

Cela signifie principalement qu'un throwpeut prendre un temps très long ™ par rapport à par exemple une intaffectation, en raison de la recherche de gestionnaire.

Comme TR18015 l'explique dans sa section 5.4 «Exceptions», il existe deux stratégies principales de mise en œuvre de la gestion des exceptions,

  • l'approche où chaque try-bloc configure dynamiquement la capture d'exceptions, de sorte qu'une recherche dans la chaîne dynamique des gestionnaires soit effectuée lorsqu'une exception est levée, et

  • l'approche où le compilateur génère des tables de recherche statiques qui sont utilisées pour déterminer le gestionnaire pour une exception levée.

La première approche très flexible et générale est presque forcée dans Windows 32 bits, tandis que dans le pays 64 bits et dans * nix-land, la deuxième approche beaucoup plus efficace est couramment utilisée.

Comme ce rapport l'indique également, pour chaque approche, il existe trois domaines principaux dans lesquels la gestion des exceptions a un impact sur l'efficacité:

  • try-blocs,

  • fonctions régulières (opportunités d'optimisation), et

  • throw-expressions.

Principalement, avec l'approche du gestionnaire dynamique (Windows 32 bits), la gestion des exceptions a un impact sur les tryblocs, principalement quelle que soit la langue (car cela est forcé par le schéma de gestion des exceptions structuré de Windows ), tandis que l'approche de la table statique a un coût à peu près nul pourtry - blocs. Discuter de cela prendrait beaucoup plus d'espace et de recherche que ce qui est pratique pour une réponse SO. Alors, consultez le rapport pour plus de détails.

Malheureusement, le rapport, de 2006, est déjà un peu daté de la fin de 2012, et pour autant que je sache, il n'y a rien de comparable qui soit plus récent.

Une autre perspective importante est que l'impact de l' utilisation d'exceptions sur les performances est très différent de l'efficacité isolée des fonctionnalités de langage de prise en charge, car, comme le note le rapport,

"Lors de l'examen de la gestion des exceptions, il doit être comparé à d'autres moyens de traiter les erreurs."

Par exemple:

  • Coûts de maintenance dus aux différents styles de programmation (exactitude)

  • ifVérification redondante des échecs du site d'appel par rapport à la centralisationtry

  • Problèmes de mise en cache (par exemple, un code plus court peut tenir dans le cache)

Le rapport a une liste différente d'aspects à prendre en compte, mais de toute façon, le seul moyen pratique d'obtenir des faits concrets sur l'efficacité de l'exécution est probablement d'implémenter le même programme en utilisant des exceptions et en n'utilisant pas d'exceptions, dans un délai déterminé sur le temps de développement, et avec les développeurs. familier avec chaque manière, puis MESURER .


Quel est un bon moyen d'éviter la surcharge des exceptions?

L'exactitude l'emporte presque toujours sur l'efficacité.

Sans exception, les événements suivants peuvent facilement se produire:

  1. Un certain code P est destiné à obtenir une ressource ou à calculer des informations.

  2. Le code appelant C aurait dû vérifier le succès / échec, mais ne le fait pas.

  3. Une ressource inexistante ou des informations non valides sont utilisées dans le code suivant C, provoquant un chaos général.

Le principal problème est le point (2), où avec le schéma de code de retour habituel, le code appelant C n'est pas obligé de vérifier.

Il existe deux approches principales qui forcent une telle vérification:

  • Où P lève directement une exception en cas d'échec.

  • Où P renvoie un objet que C doit inspecter avant d'utiliser sa valeur principale (sinon une exception ou une terminaison).

La deuxième approche était, AFAIK, d'abord décrite par Barton et Nackman dans leur livre * Scientific and Engineering C ++: An Introduction with Advanced Techniques and Examples , où ils ont introduit une classe appelée Fallowpour un résultat de fonction «possible». Une classe similaire appelée optionalest maintenant offerte par la bibliothèque Boost. Et vous pouvez facilement implémenter une Optionalclasse vous-même, en utilisant unstd::vector porteur de valeur pour le cas d'un résultat non POD.

Avec la première approche, le code appelant C n'a d'autre choix que d'utiliser des techniques de gestion des exceptions. Avec la seconde approche, cependant, le code appelant C peut décider lui-même s'il doit effectuer ifune vérification basée ou une gestion générale des exceptions. Ainsi, la deuxième approche permet de faire un compromis entre le programmeur et le temps d'exécution.


Quel est l'impact des différents standards C ++ sur les performances des exceptions?

"Je veux savoir si cela est toujours vrai pour C ++ 98"

C ++ 98 a été le premier standard C ++. Pour les exceptions, il a introduit une hiérarchie standard des classes d'exceptions (malheureusement plutôt imparfaite). Le principal impact sur les performances était la possibilité de spécifications d'exception (supprimées dans C ++ 11), qui n'ont cependant jamais été entièrement implémentées par le compilateur Windows C ++ principal Visual C ++: Visual C ++ accepte la syntaxe de spécification d'exception C ++ 98, mais ignore simplement spécifications d'exception.

C ++ 03 n'était qu'un corrigendum technique de C ++ 98. La seule nouveauté en C ++ 03 était l' initialisation de la valeur . Ce qui n'a rien à voir avec les exceptions.

Avec le standard C ++ 11, les spécifications d'exception générale ont été supprimées et remplacées par noexcept mot clé.

La norme C ++ 11 a également ajouté la prise en charge du stockage et de la relance des exceptions, ce qui est idéal pour propager les exceptions C ++ à travers les rappels en langage C. Cette prise en charge limite efficacement la manière dont l'exception actuelle peut être stockée. Cependant, pour autant que je sache, cela n'a pas d'impact sur les performances, sauf dans la mesure où dans le code plus récent, la gestion des exceptions peut plus facilement être utilisée des deux côtés d'un rappel en langage C.

Bravo et hth. - Alf
la source
6
"les exceptions sont toujours lentes par rapport aux autres opérations de base dans le langage, quel que soit le langage de programmation" ... sauf dans les langages conçus pour compiler l'utilisation des exceptions dans le contrôle de flux ordinaire.
Ben Voigt
4
"lancer une exception implique à la fois l'allocation et le déroulement de la pile". Ce n'est évidemment pas vrai en général et, encore une fois, OCaml est un contre-exemple. Dans les langages récupérés par garbage collector, il n'est pas nécessaire de dérouler la pile car il n'y a pas de destructeurs, donc vous n'avez que longjmple gestionnaire.
JD
2
@JonHarrop: vous ne savez probablement pas que Pyhon a une clause finally pour la gestion des exceptions. cela signifie qu'une implémentation Python a un déroulement de pile ou n'est pas Python. vous semblez ignorer complètement les sujets sur lesquels vous faites des réclamations (fantastiques). Désolé.
Acclamations et hth. - Alf
2
@ Cheersandhth.-Alf: "Pyhon a une clause finally pour la gestion des exceptions. Cela signifie qu'une implémentation Python a un déroulement de pile ou n'est pas Python". La try..finallyconstruction peut être implémentée sans déroulement de la pile. F #, C # et Java implémentent tous try..finallysans utiliser le déroulement de la pile. Vous venez longjmpau gestionnaire (comme je l'ai déjà expliqué).
JD
4
@JonHarrop: vous semblez comme posant un dilemme. mais cela n'a aucune pertinence je peux voir quoi que ce soit jusqu'ici discuté, et jusqu'à présent vous avez posté une longue séquence d' absurdités à consonance négative . Je devrais vous faire confiance pour être d'accord ou non avec une formulation vague, car en tant qu'antagoniste, vous choisissez ce que vous allez révéler que cela "signifie", et je ne vous fais certainement pas confiance après toutes ces absurdités dénuées de sens, le vote négatif, etc.
Acclamations et hth. - Alf
12

Cela dépend du compilateur.

GCC, par exemple, était connu pour avoir de très mauvaises performances lors de la gestion des exceptions, mais cela s'est considérablement amélioré au cours des dernières années.

Mais notez que la gestion des exceptions devrait - comme son nom l'indique - être l'exception plutôt que la règle dans la conception de votre logiciel. Lorsque vous avez une application qui génère tellement d'exceptions par seconde que cela a un impact sur les performances et que cela est toujours considéré comme un fonctionnement normal, vous devriez plutôt penser à faire les choses différemment.

Les exceptions sont un excellent moyen de rendre le code plus lisible en éliminant tout ce code de gestion des erreurs maladroit, mais dès qu'elles font partie du flux normal du programme, elles deviennent vraiment difficiles à suivre. N'oubliez pas que a throwest à peu près goto catchdéguisé.

Philipp
la source
-1 re la question telle qu'elle se présente maintenant, "est-ce toujours vrai pour C ++ 98", cela ne dépend certainement pas du compilateur. aussi, cette réponse throw new Exceptionest un Java-ism. on ne devrait en règle générale jamais lancer de pointeurs.
Acclamations et hth. - Alf
1
la norme 98 dicte-t-elle exactement comment les exceptions doivent être mises en œuvre?
thecoshman
6
C ++ 98 est une norme ISO, pas un compilateur. Il existe de nombreux compilateurs qui l'implémentent.
Philipp
3
@thecoshman: Non. Le standard C ++ ne dit rien sur la façon dont quoi que ce soit devrait être implémenté (à l'exception peut-être de la partie "Limites d'implémentation" du standard).
In silico
2
@Insilico alors je ne peux que tirer la conclusion logique que (de manière choquante) c'est l'implémentation définie (lecture, spécifique au compilateur) comment les exceptions fonctionnent.
thecoshman
12

Vous ne pouvez jamais revendiquer les performances à moins de convertir le code en assembly ou de le comparer.

Voici ce que vous voyez: (quick-bench)

Le code d'erreur n'est pas sensible au pourcentage d'occurrence. Les exceptions ont un peu de surcharge tant qu'elles ne sont jamais lancées. Une fois que vous les jetez, la misère commence. Dans cet exemple, il est lancé pour 0%, 1%, 10%, 50% et 90% des cas. Lorsque les exceptions sont levées 90% du temps, le code est 8 fois plus lent que le cas où les exceptions sont levées 10% du temps. Comme vous le voyez, les exceptions sont vraiment lentes. Ne les utilisez pas s'ils sont lancés fréquemment. Si votre application n'a pas d'exigence en temps réel, n'hésitez pas à les lancer si elles se produisent très rarement.

Vous voyez de nombreuses opinions contradictoires à leur sujet. Mais finalement, les exceptions sont-elles lentes? Je ne juge pas. Regardez simplement la référence.

Benchmark des performances des exceptions C ++

Arash
la source
4

Oui, mais cela n'a pas d'importance. Pourquoi?
Lis ça:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

Fondamentalement, cela dit que l'utilisation d'exceptions comme Alexandrescu décrites (ralentissement 50x parce qu'elles utilisent catchcomme else) est tout simplement erronée. Cela étant dit pour les personnes qui aiment le faire comme ça, je souhaite que C ++ 22 :) ajoute quelque chose comme:
(notez que ce devrait être un langage de base car il s'agit essentiellement d'un compilateur générant du code à partir d'un existant)

result = attempt<lexical_cast<int>>("12345");  //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...     
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
     int x = result.get(); // or result.result;
}
else 
{
     // even possible to see what is the exception that would have happened in original function
     switch (result.exception_type())
     //...

}

PS notez également que même si les exceptions sont aussi lentes ... ce n'est pas un problème si vous ne passez pas beaucoup de temps dans cette partie du code pendant l'exécution ... Par exemple si la division float est lente et que vous la faites 4x plus vite, peu importe si vous passez 0,3% de votre temps à faire la division FP ...

NoSenseEtAl
la source
0

Comme in silico a déclaré que son implémentation dépendait, mais en général, les exceptions sont considérées comme lentes pour toute implémentation et ne devraient pas être utilisées dans du code à haute performance.

EDIT: Je ne dis pas de ne pas les utiliser du tout, mais pour le code à haute performance, il est préférable de les éviter.

Chris McCabe
la source
9
Il s'agit au mieux d'une manière très simpliste d'examiner les performances d'exception. Par exemple, GCC utilise une implémentation «à coût nul» où vous n'encourez pas de perte de performances si aucune exception n'est levée. Et les exceptions sont destinées à des circonstances exceptionnelles (c'est-à-dire rares), donc même si elles sont lentes par une métrique, ce n'est toujours pas une raison suffisante pour ne pas les utiliser.
In silico
@insilico si vous regardez pourquoi j'ai dit, je n'ai pas dit de ne pas utiliser les exceptions point final. J'ai spécifié du code intensif en performances, c'est une évaluation précise, je travaille principalement avec gpgpus et je serais abattu si j'utilisais des exceptions.
Chris McCabe