Quelle est la réelle surcharge de try / catch en C #?


94

Donc, je sais que try / catch ajoute une surcharge et n'est donc pas un bon moyen de contrôler le flux de processus, mais d'où vient cette surcharge et quel est son impact réel?

Réponses:


52

Je ne suis pas un expert des implémentations de langage (alors prenez cela avec un grain de sel), mais je pense que l'un des plus gros coûts est de dérouler la pile et de la stocker pour la trace de la pile. Je soupçonne que cela ne se produit que lorsque l'exception est levée (mais je ne sais pas), et si tel est le cas, ce serait un coût caché de taille décente chaque fois qu'une exception est lancée ... donc ce n'est pas comme si vous sautiez d'un seul endroit dans le code à un autre, il se passe beaucoup de choses.

Je ne pense pas que ce soit un problème tant que vous utilisez des exceptions pour un comportement EXCEPTIONNEL (donc pas votre chemin typique et attendu à travers le programme).


33
Plus précisément: essayer est bon marché, attraper est bon marché, lancer coûte cher. Si vous évitez d'essayer et d'attraper, le lancer coûte toujours cher.
Programmeur Windows

4
Hmmm - le balisage ne fonctionne pas dans les commentaires. Pour réessayer - les exceptions sont pour des erreurs, pas pour des «comportements exceptionnels» ou des conditions: blogs.msdn.com/kcwalina/archive/2008/07/17/…
HTTP 410

2
@Windows programmeur Stats / source s'il vous plaît?
Kapé

100

Trois points à souligner ici:

  • Premièrement, il y a peu ou pas de pénalité de performances à avoir des blocs try-catch dans votre code. Cela ne devrait pas être une considération lorsque vous essayez d'éviter de les avoir dans votre application. Le hit de performance n'entre en jeu que lorsqu'une exception est levée.

  • Lorsqu'une exception est levée en plus des opérations de déroulement de la pile, etc. qui ont lieu et que d'autres ont mentionnées, vous devez être conscient que tout un tas de choses liées à l'exécution / réflexion se produisent afin de peupler les membres de la classe d'exception tels que la trace de pile objet et les différents membres de type etc.

  • Je pense que c'est l'une des raisons pour lesquelles le conseil général, si vous voulez renvoyer l'exception, est de simplement throw;plutôt que de la renvoyer ou d'en construire une nouvelle, car dans ces cas, toutes ces informations de pile sont rassemblées alors que dans le simple jeter tout est préservé.


41
Aussi: Lorsque vous relancez une exception en tant que "throw ex", vous perdez la trace de pile d'origine et la remplacez par la trace de pile ACTUELLE; rarement ce qui est voulu. Si vous «lancez» simplement, la trace de pile d'origine dans l'exception est conservée.
Eddie

@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki

21

Êtes-vous en train de poser des questions sur la surcharge de l'utilisation de try / catch / finally lorsque les exceptions ne sont pas levées, ou la surcharge de l'utilisation d'exceptions pour contrôler le flux de processus? Ce dernier s'apparente un peu à l'utilisation d'un bâton de dynamite pour allumer la bougie d'anniversaire d'un tout-petit, et les frais généraux associés se situent dans les domaines suivants:

  • Vous pouvez vous attendre à des échecs de cache supplémentaires en raison de l'exception levée accédant aux données résidentes qui ne se trouvent normalement pas dans le cache.
  • Vous pouvez vous attendre à des erreurs de page supplémentaires en raison de l'exception levée qui accède au code non résident et aux données qui ne figurent normalement pas dans le jeu de travail de votre application.

    • par exemple, lever l'exception exigera que le CLR trouve l'emplacement des blocs finally et catch en fonction de l'adresse IP actuelle et de l'adresse IP de retour de chaque trame jusqu'à ce que l'exception soit gérée plus le bloc de filtre.
    • coût de construction supplémentaire et résolution de nom afin de créer les cadres à des fins de diagnostic, y compris la lecture de métadonnées, etc.
    • les deux éléments ci-dessus accèdent généralement au code et aux données "froids", donc des erreurs de page matérielles sont probables si vous avez une pression de mémoire du tout:

      • le CLR essaie de placer le code et les données rarement utilisés loin des données utilisées fréquemment pour améliorer la localité, donc cela fonctionne contre vous car vous forcez le froid à être chaud.
      • le coût des défauts de page matériels, le cas échéant, éclipsera tout le reste.
  • Les situations de capture typiques sont souvent profondes, donc les effets ci-dessus auraient tendance à être amplifiés (augmentant la probabilité de défauts de page).

Quant à l'impact réel du coût, cela peut varier beaucoup en fonction de ce qui se passe dans votre code à ce moment-là. Jon Skeet a un bon résumé ici , avec quelques liens utiles. J'ai tendance à être d'accord avec sa déclaration selon laquelle si vous arrivez au point où les exceptions nuisent considérablement à votre performance, vous rencontrez des problèmes en termes d'utilisation d'exceptions au-delà de la simple performance.


6

D'après mon expérience, la plus grosse surcharge est de lancer une exception et de la gérer. J'ai déjà travaillé sur un projet dans lequel un code similaire au suivant était utilisé pour vérifier si quelqu'un avait le droit de modifier un objet. Cette méthode HasRight () était utilisée partout dans la couche de présentation et était souvent appelée pour des centaines d'objets.

bool HasRight(string rightName, DomainObject obj) {
  try {
    CheckRight(rightName, obj);
    return true;
  }
  catch (Exception ex) {
    return false;
  }
}

void CheckRight(string rightName, DomainObject obj) {
  if (!_user.Rights.Contains(rightName))
    throw new Exception();
}

Lorsque la base de données de test s'est remplie de données de test, cela a conduit à un ralentissement très visible lors de l'ouverture de nouveaux formulaires, etc.

Je l'ai donc refactoré comme suit, qui - selon des mesures rapides et sales ultérieures - est environ 2 ordres de grandeur plus rapide:

bool HasRight(string rightName, DomainObject obj) {
  return _user.Rights.Contains(rightName);
}

void CheckRight(string rightName, DomainObject obj) {
  if (!HasRight(rightName, obj))
    throw new Exception();
}

Donc, en bref, l'utilisation d'exceptions dans un flux de processus normal est environ deux ordres de grandeur plus lente que l'utilisation d'un flux de processus similaire sans exceptions.


1
Pourquoi voudriez-vous lever une exception ici? Vous pourriez gérer le cas de ne pas avoir les droits sur place.
ThunderGr

@ThunderGr c'est en fait ce que j'ai changé, en le rendant deux ordres de grandeur plus rapide.
Tobi

5

Contrairement aux théories communément acceptées, try/ catchpeut avoir des implications significatives sur les performances, et c'est de savoir si une exception est levée ou non!

  1. Il désactive certaines optimisations automatiques (par conception) et, dans certains cas, injecte du code de débogage , comme vous pouvez vous y attendre d'une aide au débogage . Il y aura toujours des gens qui ne seront pas d'accord avec moi sur ce point, mais le langage l'exige et le démontage le montre donc ces gens sont, par définition, illusoires .
  2. Cela peut avoir un impact négatif sur la maintenance. C'est en fait le problème le plus important ici, mais puisque ma dernière réponse (qui se concentrait presque entièrement dessus) a été supprimée, je vais essayer de me concentrer sur le problème le moins important (la micro-optimisation) par opposition au problème le plus important ( la macro-optimisation).

Le premier a été couvert dans quelques messages de blog par Microsoft MVP au fil des ans, et je vous faire confiance pouvez les trouver facilement encore StackOverflow prend soin tant au sujet du contenu , donc je vais fournir des liens vers certains d'entre eux comme filler preuve:

Il y a aussi cette réponse qui montre la différence entre le code désassemblé avec et sans utiliser try/ catch.

Il semble donc évident qu'il y a une surcharge qui est manifestement observable dans la génération de code, et que les frais généraux semble même être reconnu par les gens qui apprécient Microsoft! Pourtant, je répète Internet ...

Oui, il y a des dizaines d'instructions MSIL supplémentaires pour une ligne de code triviale, et cela ne couvre même pas les optimisations désactivées, donc techniquement, c'est une micro-optimisation.


J'ai posté une réponse il y a des années qui a été supprimée car elle se concentrait sur la productivité des programmeurs (la macro-optimisation).

C'est malheureux car aucune économie de quelques nanosecondes ici et là du temps CPU ne peut compenser de nombreuses heures d'optimisation manuelle par l'homme. Pour quoi votre patron paie-t-il le plus: une heure de votre temps ou une heure avec l'ordinateur en marche? À quel moment devons-nous débrancher la fiche et admettre qu'il est temps d' acheter un ordinateur plus rapide ?

De toute évidence, nous devrions optimiser nos priorités , pas seulement notre code! Dans ma dernière réponse, j'ai évoqué les différences entre deux extraits de code.

Utilisation de try/ catch:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Ne pas utiliser try/ catch:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Considérez-le du point de vue d'un développeur de maintenance, ce qui est plus susceptible de vous faire perdre votre temps, sinon dans le profilage / optimisation (couvert ci-dessus), ce qui ne serait probablement même pas nécessaire s'il n'y avait pas le problème try/ catch, puis en faisant défiler code source ... L'un de ceux-ci a quatre lignes supplémentaires d'ordures standard!

Au fur et à mesure que de plus en plus de champs sont introduits dans une classe, toutes ces ordures standard s'accumulent (à la fois dans le code source et dans le code désassemblé) bien au-delà des niveaux raisonnables. Quatre lignes supplémentaires par champ, et ce sont toujours les mêmes lignes ... Ne nous a-t-on pas appris à éviter de se répéter? Je suppose que nous pourrions cacher le try/ catchderrière une abstraction faite maison, mais ... alors nous pourrions tout aussi bien éviter les exceptions (c'est-à-dire l'utilisation Int.TryParse).

Ce n'est même pas un exemple complexe; J'ai vu des tentatives d'instanciation de nouvelles classes dans try/ catch. Considérez que tout le code à l'intérieur du constructeur pourrait alors être disqualifié de certaines optimisations qui seraient autrement appliquées automatiquement par le compilateur. Quelle meilleure façon de donner naissance à la théorie selon laquelle le compilateur est lent , par opposition au compilateur fait exactement ce qu'on lui dit de faire ?

En supposant qu'une exception est levée par ledit constructeur et qu'un bogue est déclenché en conséquence, le développeur de maintenance médiocre doit alors le localiser. Ce n'est peut-être pas une tâche facile, car contrairement au code spaghetti du cauchemar goto , try/ catchpeut causer des dégâts en trois dimensions , car il pourrait monter dans la pile non seulement dans d'autres parties de la même méthode, mais aussi dans d'autres classes et méthodes , qui seront tous observés par le développeur de maintenance, à la dure ! Pourtant on nous dit que "goto est dangereux", hein!

À la fin, je mentionne que try/ catcha son avantage, c'est-à- dire qu'il est conçu pour désactiver les optimisations ! C'est, si vous voulez, une aide au débogage ! C'est pour cela qu'il a été conçu et c'est pour cela qu'il devrait être utilisé ...

Je suppose que c'est aussi un point positif. Il peut être utilisé pour désactiver les optimisations qui pourraient autrement paralyser des algorithmes de transmission de messages sûrs et sains pour les applications multithread, et pour détecter d'éventuelles conditions de course;) C'est à peu près le seul scénario auquel je puisse penser pour utiliser try / catch. Même cela a des alternatives.


Que faire Optimisations try, catchet finallydésactiver?

ALIAS

Comment sont try, catchet finallydes outils utiles de débogage?

ce sont des barrières d'écriture. Cela vient de la norme:

12.3.3.13 Instructions try-catch

Pour une instruction stmt de la forme:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • L'état d'affectation défini de v au début du bloc try est le même que l'état d'affectation défini de v au début de stmt .
  • L'état d'affectation défini de v au début de catch-block-i (pour tout i ) est le même que l'état d'affectation défini de v au début de stmt .
  • L'état d'affectation défini de v à l'extrémité de stmt est définitivement attribué si (et seulement si) v est définitivement affecté à l'extrémité de try-block et de chaque catch-block-i (pour chaque i de 1 à n ).

En d'autres termes, au début de chaque trydéclaration:

  • toutes les affectations faites aux objets visibles avant d'entrer l' tryinstruction doivent être terminées, ce qui nécessite un verrou de thread pour un début, ce qui le rend utile pour le débogage des conditions de course!
  • le compilateur n'est pas autorisé à:
    • éliminer les affectations de variables inutilisées qui ont été définitivement affectées avant l' tryinstruction
    • réorganisez ou fusionnez l'une de ses affectations internes (voir mon premier lien, si vous ne l'avez pas déjà fait).
    • hisser les affectations au-dessus de cette barrière, pour retarder l'affectation à une variable dont il sait qu'elle ne sera utilisée que plus tard (voire pas du tout) ou pour avancer de manière préventive les affectations ultérieures pour rendre d'autres optimisations possibles ...

Une histoire similaire est valable pour chaque catchdéclaration; supposons que dans votre tryinstruction (ou dans un constructeur ou une fonction qu'il invoque, etc.) vous affectez à cette variable autrement inutile (disons, garbage=42;), le compilateur ne peut pas éliminer cette instruction, peu importe à quel point elle n'est pas pertinente pour le comportement observable du programme . L'affectation doit être terminée avant l' catchentrée du bloc.

Pour ce que ça vaut, finallyraconte une histoire tout aussi dégradante :

12.3.3.14 Instructions try-finally

Pour une instruction try stmt de la forme:

try try-block
finally finally-block

• L'état d'affectation défini de v au début du bloc try est le même que l'état d'affectation défini de v au début de stmt .
• L'état d'affectation défini de v au début du bloc final est le même que l'état d'affectation défini de v au début de stmt .
• L'état d'affectation défini de v au point final de stmt est définitivement attribué si (et seulement si) soit: o v est définitivement affecté au point final du bloc try o vest définitivement assigné au point final de finally-block Si un transfert de flux de contrôle (comme une instruction goto ) est effectué qui commence dans try-block et se termine en dehors de try-block , alors v est également considéré comme définitivement affecté à ce transfert de flux de contrôle si v est définitivement attribué au point final du bloc final . (Ce n'est pas seulement si - si v est définitivement attribué pour une autre raison lors de ce transfert de flux de contrôle, il est toujours considéré comme définitivement attribué.)

12.3.3.15 Instructions try-catch-finally

Analyse d'affectation définie pour une instruction try - catch - finally du formulaire:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
finally finally-block

est fait comme si l'instruction était une instruction try - finally comprenant une instruction try - catch :

try { 
    try   
    try-block
    catch ( ... ) catch-block-1
    ...   
    catch ( ... ) catch-block-n
} 
finally finally-block

3

Sans parler du fait que ce soit à l'intérieur d'une méthode fréquemment appelée, cela peut affecter le comportement général de l'application.
Par exemple, je considère l'utilisation d'Int32.Parse comme une mauvaise pratique dans la plupart des cas, car elle lève des exceptions pour quelque chose qui peut être facilement capturé autrement.

Donc, pour conclure tout ce qui est écrit ici:
1) Utilisez les blocs try..catch pour détecter les erreurs inattendues - presque aucune pénalité de performance.
2) N'utilisez pas d'exceptions pour les erreurs exceptées si vous pouvez les éviter.


3

J'ai écrit un article à ce sujet il y a quelque temps parce que beaucoup de gens posaient des questions à ce sujet à l'époque. Vous pouvez le trouver ainsi que le code de test sur http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx .

Le résultat est qu'il y a une petite quantité de surcharge pour un bloc try / catch mais si petite qu'elle devrait être ignorée. Cependant, si vous exécutez des blocs try / catch dans des boucles exécutées des millions de fois, vous pouvez envisager de déplacer le bloc en dehors de la boucle si possible.

Le problème de performances clé avec les blocs try / catch est lorsque vous interceptez réellement une exception. Cela peut ajouter un retard notable à votre demande. Bien sûr, lorsque les choses tournent mal, la plupart des développeurs (et de nombreux utilisateurs) reconnaissent la pause comme une exception sur le point de se produire! La clé ici est de ne pas utiliser la gestion des exceptions pour les opérations normales. Comme leur nom l'indique, ils sont exceptionnels et vous devez faire tout votre possible pour éviter qu'ils ne soient jetés. Vous ne devez pas les utiliser dans le cadre du flux attendu d'un programme qui fonctionne correctement.


2

J'ai fait une entrée de blog sur ce sujet l'année dernière. Vérifiez-le. En fin de compte, il n'y a presque aucun coût pour un bloc d'essai si aucune exception ne se produit - et sur mon ordinateur portable, une exception était d'environ 36 μs. C'est peut-être moins que ce à quoi vous vous attendiez, mais gardez à l'esprit que ces résultats se situent sur une pile peu profonde. De plus, les premières exceptions sont vraiment lentes.


Je n'ai pas pu accéder à votre blog (la connexion expire; utilisez-vous try/ catchtrop? Heh heh), mais vous semblez vous disputer avec la spécification du langage et certains MVP MS qui ont également écrit des blogs sur le sujet, fournissant des mesures au contraire de votre avis ... Je suis ouvert à l'idée que la recherche que j'ai faite est erronée, mais je vais avoir besoin de lire votre article de blog pour voir ce qu'il dit.
autiste

En plus de l'article de blog de @ Hafthor, voici un autre article de blog avec du code spécialement écrit pour tester les différences de performances de vitesse. Selon les résultats, si une exception se produit même 5% du temps, le code de gestion des exceptions s'exécute globalement 100 fois plus lentement que le code de gestion des exceptions. L'article cible spécifiquement le try-catchbloc par rapport aux tryparse()méthodes, mais le concept est le même.

2

Il est beaucoup plus facile d'écrire, de déboguer et de maintenir un code exempt de messages d'erreur du compilateur, de messages d'avertissement d'analyse de code et d'exceptions de routine acceptées (en particulier les exceptions qui sont levées à un endroit et acceptées à un autre). Parce que c'est plus simple, le code sera en moyenne mieux écrit et moins bogué.

Pour moi, cette surcharge de programmation et de qualité est le principal argument contre l'utilisation de try-catch pour le flux de processus.

La surcharge informatique des exceptions est insignifiante en comparaison, et généralement minime en termes de capacité de l'application à répondre aux exigences de performances du monde réel.


@Ritchard T, pourquoi? En comparaison avec les frais généraux de programmeur et de qualité, il est insignifiant.
ThunderGr

1

J'aime beaucoup l' article de blog de Hafthor , et pour ajouter mes deux cents à cette discussion, j'aimerais dire que, il a toujours été facile pour moi d'avoir le DATA LAYER lancer un seul type d'exception (DataAccessException). De cette façon, mon BUSINESS LAYER sait à quelle exception s'attendre et l'attrape. Ensuite, en fonction des règles métier supplémentaires (c'est-à-dire si mon objet métier participe au workflow, etc.), je peux lancer une nouvelle exception (BusinessObjectException) ou continuer sans relancer / lancer.

Je dirais que n'hésitez pas à utiliser try..catch chaque fois que cela est nécessaire et utilisez-le à bon escient!

Par exemple, cette méthode participe à un workflow ...

Commentaires?

public bool DeleteGallery(int id)
{
    try
    {
        using (var transaction = new DbTransactionManager())
        {
            try
            {
                transaction.BeginTransaction();

                _galleryRepository.DeleteGallery(id, transaction);
                _galleryRepository.DeletePictures(id, transaction);

                FileManager.DeleteAll(id);

                transaction.Commit();
            }
            catch (DataAccessException ex)
            {
                Logger.Log(ex);
                transaction.Rollback();                        
                throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
            }
        }
    }
    catch (DbTransactionException ex)
    {
        Logger.Log(ex);
        throw new BusinessObjectException("Cannot delete gallery.", ex);
    }
    return true;
}

2
David, encapsuleriez-vous l'appel à «DeleteGallery» dans un bloc try / catch?
Rob

Comme DeleteGallery est une fonction booléenne, il me semble que lancer une exception n'est pas utile. Cela nécessiterait que l'appel à DeleteGallery soit inclus dans un bloc try / catch. Un if (! DeleteGallery (theid)) {// handle} me semble plus significatif. dans cet exemple spécifique.
ThunderGr

1

On peut lire dans Programming Languages ​​Pragmatics de Michael L. Scott que les compilateurs actuels n'ajoutent aucun surcoût dans le cas courant, c'est-à-dire quand aucune exception ne se produit. Ainsi, chaque travail est effectué au moment de la compilation. Mais lorsqu'une exception est levée au moment de l'exécution, le compilateur doit effectuer une recherche binaire pour trouver l'exception correcte et cela se produira pour chaque nouveau lancement que vous avez effectué.

Mais les exceptions sont des exceptions et ce coût est parfaitement acceptable. Si vous essayez de gérer les exceptions sans exceptions et utilisez plutôt des codes d'erreur de retour, vous aurez probablement besoin d'une instruction if pour chaque sous-programme et cela entraînera une surcharge en temps réel. Vous savez qu'une instruction if est convertie en quelques instructions d'assemblage, qui seront exécutées à chaque fois que vous entrez dans vos sous-routines.

Désolé pour mon anglais, j'espère que cela vous aidera. Ces informations sont basées sur le livre cité, pour plus d'informations, reportez-vous au chapitre 8.5 Gestion des exceptions.


Le compilateur est hors de vue au moment de l'exécution. Il doit y avoir une surcharge pour les blocs try / catch afin que le CLR puisse gérer les exceptions. C # s'exécute sur le .NET CLR (une machine virtuelle). Il me semble que la surcharge du bloc lui-même est minime quand il n'y a pas d'exception, mais le coût du CLR qui gère l'exception est très important.
ThunderGr

-4

Analysons l'un des plus gros coûts possibles d'un bloc try / catch lorsqu'il est utilisé là où il ne devrait pas être utilisé:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Et voici celui sans try / catch:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Sans compter l'espace blanc insignifiant, on pourrait remarquer que ces deux morceaux de code équivalents ont presque exactement la même longueur en octets. Ce dernier contient 4 octets en moins d'indentation. Est-ce une mauvaise chose?

Pour ajouter l'insulte à la blessure, un élève décide de boucler tandis que l'entrée peut être analysée comme un int. La solution sans try / catch pourrait être quelque chose comme:

while (int.TryParse(...))
{
    ...
}

Mais à quoi cela ressemble-t-il lorsque vous utilisez try / catch?

try {
    for (;;)
    {
        x = int.Parse(...);
        ...
    }
}
catch
{
    ...
}

Les blocs Try / catch sont des moyens magiques de gaspiller l'indentation, et nous ne savons toujours pas pourquoi cela a échoué! Imaginez ce que ressent la personne qui effectue le débogage, lorsque le code continue de s'exécuter au-delà d'une faille logique sérieuse, plutôt que de s'arrêter avec une erreur d'exception évidente. Les blocs Try / catch sont la validation / assainissement des données d'un paresseux.

L'un des coûts les plus faibles est que les blocs try / catch désactivent effectivement certaines optimisations: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Je suppose que c'est aussi un point positif. Il peut être utilisé pour désactiver les optimisations qui pourraient autrement paralyser des algorithmes de transmission de messages sûrs et sains pour les applications multithread, et pour détecter d'éventuelles conditions de course;) C'est à peu près le seul scénario auquel je puisse penser pour utiliser try / catch. Même cela a des alternatives.


1
Je suis presque sûr que TryParse fait un essai {int x = int.Parse ("xxx"); return true;} catch {return false; } en interne. L'indentation n'est pas un problème dans la question, seulement les performances et les frais généraux.
ThunderGr

@ThunderGr Sinon, lisez la nouvelle réponse que j'ai publiée. Il contient plus de liens, dont l'un est une analyse de l'augmentation massive des performances lorsque vous évitez Int.Parseen faveur de Int.TryParse.
autiste
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.