Quelle est la surcharge réelle de try / catch en C#?
Donc, je sais que try / catch ajoute de la 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?
12 réponses
Je ne suis pas un expert en implémentations de langage (alors prenez ceci avec un grain de sel), mais je pense que l'un des coûts les plus importants 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 oui, cela serait un coût caché décemment dimensionné chaque fois qu'une exception est levée... donc, ce n'est pas comme si vous sautiez d'un endroit dans le code à un autre, il se passe beaucoup de choses.
Je ne pense pas que ce soit un problème car tant que vous utilisez des exceptions pour un comportement exceptionnel (donc pas votre chemin typique et attendu à travers le programme).
Trois points à faire ici:
Premièrement, il y a peu ou pas de pénalité de performance à 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 que d'autres ont mentionnées vous devez savoir que tout un tas des choses liées à l'exécution/à la réflexion se produisent afin de remplir les membres de la classe d'exception tels que l'objet de trace de pile et les différents membres de type, etc.
Je crois que c'est l'une des raisons pour lesquelles le Conseil général si vous allez repenser l'exception est de simplement
throw;
plutôt que de lancer à nouveau l'exception ou d'en construire une nouvelle car dans ces cas toutes les informations de la pile sont récupérées alors que dans le simple lancer c'est tout préserver.
Posez-vous des questions sur les frais généraux d'utilisation de try/catch/finally lorsque les exceptions ne sont pas levées, ou sur les frais généraux d'utilisation des 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 enfant en bas âge, et les frais généraux associés tombent dans les zones suivantes:
- 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 à une page supplémentaire défauts dus à l'exception levée accédant au code et aux données non-résidents qui ne sont normalement pas dans l'ensemble de travail de votre application.
- par exemple, lancer l'exception nécessitera 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 image jusqu'à ce que l'exception soit gérée plus le bloc de filtre.
- coût de construction supplémentaire et résolution du nom afin de créer les cadres à des fins de diagnostic, y compris la lecture de les métadonnées etc.
-
Les deux éléments ci-dessus accèdent généralement au code et aux données" à froid", de sorte que les erreurs de page sont probables si vous avez une pression de mémoire:
- le CLR essaie de placer le code et les données rarement utilisés loin des données fréquemment utilisées pour améliorer la localité, donc cela fonctionne contre vous parce que vous forcez le froid à être chaud.
- Le coût des défauts de la page dure, le cas échéant, éclipsera tout le reste.
- prise typique les situations sont souvent profondes, donc les effets ci-dessus auraient tendance à être amplifiés (augmentant la probabilité de fautes de page).
Comme pour l'impact réel du coût, cela peut varier beaucoup en fonction de ce qui se passe dans votre code à la fois. 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 avez des problèmes en termes d'utilisation des exceptions au-delà de la performance.
D'après mon expérience, le plus gros frais généraux consiste à lancer une exception et à la gérer. J'ai déjà travaillé sur un projet où un code similaire à ce qui suit était utilisé pour vérifier si quelqu'un avait le droit d'éditer un objet. Cette méthode HasRight () a été utilisée partout dans la couche de présentation, et a souvent été appelée pour 100s 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 est plus complète avec les données de test, cela conduit à un ralentissement très visible lors de l'ouverture de nouveaux formulaires, etc.
Donc j'ai refactorisé il à ce qui suit, qui-selon les mesures plus tard quick 'n dirty-est d'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.
Sans oublier que si c'est à l'intérieur d'une méthode fréquemment appelée, cela peut affecter le comportement global de l'application.
Par exemple, je considère L'utilisation de Int32.Analyser comme une mauvaise pratique dans la plupart des cas, car il jette des exceptions pour quelque chose qui peut être attrapé facilement autrement.
Donc pour conclure tout ce qui est écrit ici:
1) Utilisez try..attraper des blocs pour attraper des erreurs inattendues - presque aucune pénalité de performance.
2) n'utilisez pas d'exceptions pour les erreurs exceptées si vous le pouvez l'éviter.
J'ai écrit un article à ce sujet il y a quelque temps parce qu'il y avait beaucoup de gens qui posaient des questions à ce sujet à l'époque. Vous pouvez le trouver et le code de test à http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx.
Le résultat est qu'il y a une petite quantité de frais généraux pour un bloc try/catch mais si petit qu'il devrait être ignoré. 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 cela est possible.
Le problème de performance clé avec les blocs try / catch est lorsque vous attrapez réellement une exception. Cela peut ajouter un retard notable à votre demande. Bien sûr, quand les choses vont mal, la plupart des développeurs (et beaucoup d'utilisateurs) reconnaissent la pause comme une exception qui est sur le point d'arriver! La clé ici est de ne pas utiliser la gestion des exceptions pour les opérations normales. Comme son nom l'indique, ils sont exceptionnels et vous devriez faire tout ce que vous pouvez pour éviter qu'ils soient jetés. Vous ne devriez pas utilisez - les dans le cadre du flux attendu d'un programme qui fonctionne correctement.
J'ai fait une entrée de blog sur ce sujet l'année dernière. Check it out. Bottom line est qu'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. Cela pourrait être moins que prévu, mais gardez à l'esprit que ces résultats étaient sur une pile peu profonde. En outre, les premières exceptions sont vraiment lentes.
Il est beaucoup plus facile d'écrire, de déboguer et de maintenir du code exempt de messages d'erreur du compilateur, de messages d'avertissement d'analyse de code et d'exceptions acceptées de routine (en particulier les exceptions qui sont lancées à un endroit et acceptées à un autre). Parce que c'est plus facile, le code sera en moyenne mieux écrit et moins buggé.
Pour moi, ce surcoût de programmeur et de qualité est le principal argument contre l'utilisation de try-catch pour le flux de processus.
La surcharge informatique des exceptions est insignifiant en comparaison, et généralement minuscule en termes de capacité de l'application à répondre aux exigences de performance du monde réel.
Contrairement aux théories communément admises, try
/catch
peut avoir des implications significatives sur les performances, et c'est si une exception est levée ou non!
- Il désactive certaines optimisations automatiques (par conception) , et dans certains cas injecte code de débogage, comme vous pouvez l'attendre d'une aide au débogage . Il y aura toujours des gens qui ne sont pas d'accord avec moi sur ce point, mais la langue l'exige et le démontage le montre pour que ces gens soient là définition du dictionnaire délirant.
- 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 portait presque entièrement sur elle) a été supprimée, je vais essayer de me concentrer sur le problème le moins significatif (la micro-optimisation) par opposition au problème le plus significatif (la macro-optimisation).
Le premier a été couvert dans quelques articles de blog par Microsoft MVP au fil des ans, et J'ai confiance en vous pourrait trouver facilement encore StackOverflow soucis tellement sur contenu je vais donc donner des liens vers certains de remplissage preuve:
-
incidences sur le rendement
try
/catch
/finally
(et la deuxième partie ), DE Peter Ritchie explore les optimisations quitry
/catch
/finally
désactive (et je vais aller plus loin dans cela avec des citations de la norme) -
le Profilage des Performances
Parse
vsTryParse
vs.ConvertTo
par Ian Huff déclare ouvertement que "la gestion des exceptions est très lente" et démontre ce point en opposantInt.Parse
etInt.TryParse
les uns aux autres... À toute personne qui insiste sur le fait queTryParse
utilisetry
/catch
dans les coulisses, cela devrait faire la lumière!
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 si évident qu'il y a une surcharge ce qui est manifestement observable dans la génération de code, et cette surcharge semble même être reconnue par les gens qui apprécient Microsoft! Pourtant, je suis, répéter l'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 (le macro-optimisation).
C'est regrettable car aucune économie de quelques nanosecondes ici et là de temps CPU n'est susceptible de compenser de nombreuses heures accumulées d'optimisation manuelle par les humains. Pour quoi votre patron paie-t-il le plus: une heure de votre temps, ou une heure avec l'ordinateur en marche? À quel moment tirons-nous la prise et admettons qu'il est temps de simplement acheter un ordinateur plus rapide?
Clairement, nous devrions optimiser nos priorités , pas seulement notre code! Dans mon dernier réponse j'ai tiré sur les différences entre deux extraits de code.
En utilisant 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 du point de vue d'un développeur de maintenance, qui est plus susceptible de perdre votre temps, sinon dans le profilage / optimisation (couvert ci-dessus) qui ne serait probablement même pas nécessaire si ce n'était pour le try
/catch
problème, puis en faisant défiler le code source... L'un d'eux a quatre lignes supplémentaires de passe-partout des ordures!
À mesure que de plus en plus de champs sont introduits dans une classe, toutes ces ordures s'accumulent (à la fois dans le code source et le code démonté) bien au-delà des niveaux raisonnables. Quatre lignes supplémentaires par champ, et ce sont toujours les mêmes lignes... N'avons-nous pas appris à éviter de nous répéter? Je suppose que nous pourrions cacher le try
/catch
derrière une abstraction faite maison, mais... alors nous pourrions aussi bien d'éviter les exceptions (c'est à dire utiliser 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 à le compilateur fait exactement ce qu'il est dit de faire?
En supposant qu'une exception est levée par ledit constructeur, et un bogue est déclenché en tant que résultat, le développeur de maintenance pauvre doit alors le traquer. Ce n'est peut-être pas une tâche facile, car contrairement au code spaghetti du cauchemar goto , try
/catch
peut causer des dégâts dans trois dimensions , car il pourrait remonter 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 toutes observées par le développeur de maintenance, à la dure! Pourtant on nous dit que "goto est dangereux", heh!
À la fin Je mentionne, try
/catch
a son avantage, qui est, il est conçu pour désactiver les optimisations! C'est, si vous voulez, une aide au débogage ! C'est ce qu'il a été conçu et c'est ce qu'il devrait être utilisé comme...
Je suppose que c'est un point positif aussi. 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 attraper les conditions de course possibles;) c'est à peu près le seul scénario que je peux pensez à utiliser try/catch. Même cela a des alternatives.
Quelles optimisations ne try
, catch
et finally
désactiver?
A. K. A
Comment ça va try
, catch
et finally
utile comme aides au débogage?
Ce sont des barrières à l'écriture. Cela vient de la norme:
12.3.3.13 instructions try-catch
Pour une déclaration de 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 de try-bloc est le même que l'affectation définitive de l'état de v au début de stmt.
- définitive d'attribution de l'état de v au début de catch-bloc-i (pour tout , je) est le même que l'affectation définitive de l'état de v au début de stmt.
- définitive d'attribution de l'état de v à la fin du point de stmt est certainement affecté si (et seulement si) v est définitivement attribué à la fin du point de try-bloc et tous les catch-bloc-i (pour chaque , je de 1 à n).
En d'autres termes, au début de chaque try
instruction:
- toutes les affectations effectuées sur des objets visibles avant d'entrer dans l'instruction
try
doivent être complètes, ce qui nécessite un verrou de thread pour démarrer, ce qui le rend utile pour le débogage race les conditions! - le compilateur n'est pas autorisé à:
- éliminer les affectations de variables inutilisées qui ont été définitivement assignées avant l'instruction
try
- réorganiser ou fusionner l'une de ses affectations internes (c'est-à-dire Voir mon premier lien, si vous ne l'avez pas déjà fait).
- hisse 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 (le cas échéant) ou pour déplacer de manière préventive les affectations ultérieures vers effectuer d'autres optimisations possibles...
- éliminer les affectations de variables inutilisées qui ont été définitivement assignées avant l'instruction
Une histoire similaire est valable pour chaque instruction catch
; supposons que dans votre instruction try
(ou un constructeur ou une fonction qu'il invoque, etc.) vous assigniez à cette variable autrement inutile (disons, garbage=42;
), le compilateur ne peut pas éliminer cette instruction, quelle que soit sa pertinence pour le comportement observable du programme. L'affectation doit avoir terminé avant que le bloc catch
ne soit saisi.
Pour ce que ça vaut, finally
raconte une histoire similaire dégradante:
12.3.3.14 Essayez-enfin états
Pour un try instruction stmt de la forme:
try try-block finally finally-block
• L'affectation définitive de l'état de v au début de try-bloc est le même que l'affectation définitive de l'état de v au début de stmt.
* L'état d'affectation défini de v au début de enfin-bloc est le même que l'affectation définitive de l'état de v au début de stmt.
• L'état d'affectation défini de v au point final de stmt est définitivement assigné si (et seulement si) soit: o v est définitivement attribué à la fin du point de try-bloc o v est définitivement attribué à la fin du point de enfin-bloc Si un flux de contrôle transfert (comme un goto instruction) qui commence à l'intérieur de try-bloc, et se termine à l'extérieur de try-bloc, puis v est également considéré comme définitivement attribuées sur que le contrôle de flux de transfert si v est définitivement attribué à la fin du point de enfin-bloc. (Ce n'est pas un seul if-if v est définitivement assigné pour une autre raison sur ce transfert de flux de contrôle, alors il est toujours considéré définitivement assigner.)
12.3.3.15 instructions Try-catch-finally
Analyse d'affectation définie pour un essai - attrape-enfin énoncé du formulaire:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block
Est fait comme si l'énoncé ont été un essayer-enfin déclaration en joignant une essayer-catch instruction:
try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block
J'aime vraiment le blog de Hafthor , et pour ajouter mes deux cents à cette discussion, j'aimerais dire que, il m'a toujours été facile d'avoir la couche de données lancer un seul type d'exception (DataAccessException). De cette façon, ma couche D'affaires sait à quelle exception s'attendre et l'attrape. Ensuite, en fonction d'autres règles métier (c'est-à-dire si mon objet métier participe au flux de travail, etc.), je peux lancer une nouvelle exception (BusinessObjectException) ou procéder sans re/lancer.
Je dirais ne pas hésiter à utiliser try..attrapez chaque fois que c'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;
}
On peut lire dans les langages de programmation Pragmatics de Michael L. Scott que les compilateurs de nos jours n'ajoutent pas de surcharge dans le cas commun, cela signifie, quand aucune exception ne se produit. Donc, chaque travail est fait en temps de compilation. Mais quand une exception est lancée en cours d'exécution, le compilateur doit effectuer une recherche binaire pour trouver l'exception correcte et cela se produira pour chaque nouveau lancer que vous avez fait.
Mais les exceptions sont des exceptions et ce coût est parfaitement acceptable. Si vous essayez de faire Gestion des exceptions sans exceptions et utiliser des codes d'erreur de retour à la place, 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 effectué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.
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 peut remarquer que ces deux morceaux de code équivelants ont presque exactement la même longueur en octets. Ce dernier contient 4 octets de moins d'indentation. Est-ce une mauvaise chose?
Pour ajouter une insulte à une blessure, un étudiant décide de faire une boucle alors que l'entrée peut être analysé comme un int. La solution sans try/catch peut ê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 même pas pourquoi ils ont échoué! Imaginez comment la personne qui fait le débogage se sent, quand le code continue à s'exécuter après un défaut logique sérieux, plutôt que de s'arrêter avec une belle erreur d'exception évidente. Les blocs Try/catch sont des données d'un homme paresseux validation / assainissement.
L'un des coûts les plus faibles est que les blocs try/catch désactivent en effet certaines optimisations: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx. je suppose que c'est un point positif aussi. 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 attraper les conditions de course possibles;) c'est à peu près le seul scénario que je peux pensez à utiliser try/catch. Même cela a des alternatives.