web-dev-qa-db-fra.com

Les exceptions .NET sont-elles lentes?

Je ne veux pas discuter de quand et de ne pas lever d'exceptions. Je souhaite résoudre un problème simple. 99% du temps, l'argument pour ne pas lever d'exceptions tourne autour de leur lenteur tandis que l'autre côté affirme (avec un test de référence) que la vitesse n'est pas le problème. J'ai lu de nombreux blogs, articles et articles concernant un côté ou l'autre. Alors c'est quoi?

Quelques liens des réponses: Skeet , Mariani , Brumme .

143
Goran

Je suis du côté "pas lent" - ou plus précisément "pas assez lent pour que cela vaille la peine de les éviter en utilisation normale". J'ai écrit deux courtarticles à ce sujet. Il y a des critiques à propos de l'aspect de référence, qui sont principalement dues à "dans la vie réelle, il y aurait plus de pile à parcourir, donc vous feriez exploser le cache, etc." - mais utiliser des codes d'erreur pour remonter la pile également souffler le cache, donc je ne vois pas cela comme un argument particulièrement bon.

Juste pour être clair - je ne supporte pas l'utilisation d'exceptions où elles ne sont pas logiques. Par exemple, int.TryParse est tout à fait approprié pour convertir les données d'un utilisateur. C'est approprié lors de la lecture d'un fichier généré par la machine, où l'échec signifie "Le fichier n'est pas dans le format qu'il est censé être, je ne veux vraiment pas essayer de gérer cela car je ne sais pas quoi d'autre pourrait mal se passer. "

Lorsque j'utilise des exceptions dans "des circonstances uniquement raisonnables", je n'ai jamais vu une application dont les performances étaient considérablement réduites par des exceptions. Fondamentalement, les exceptions ne devraient pas se produire souvent, sauf si vous avez des problèmes de correction importants, et si vous avez des problèmes de correction importants, les performances ne sont pas le plus gros problème auquel vous êtes confronté.

203
Jon Skeet

Il y a la réponse définitive à cela du gars qui les a mis en œuvre - Chris Brumme. Il a écrit un excellent article de blog sur le sujet (avertissement - c'est très long) (avertissement2 - c'est très bien écrit, si vous êtes un technicien, vous le lirez jusqu'à la fin et ensuite vous devrez faire vos heures après le travail :))

Le résumé: ils sont lents. Ils sont implémentés en tant qu'exceptions Win32 SEH, donc certains passeront même la limite du CPU de l'anneau 0! De toute évidence, dans le monde réel, vous ferez beaucoup d'autres travaux, de sorte que l'exception étrange ne sera pas remarquée du tout, mais si vous les utilisez pour le déroulement du programme, à l'exception de votre application à marteler. Ceci est un autre exemple de la machine marketing MS qui nous rend un mauvais service. Je me souviens d'un microsoftie nous disant comment ils ont encouru des frais généraux absolument nuls, ce qui est complet.

Chris donne une citation pertinente:

En fait, le CLR utilise en interne des exceptions même dans les parties non gérées du moteur. Cependant, il existe un grave problème de performances à long terme avec des exceptions et cela doit être pris en compte dans votre décision.

29
gbjbaanb

Je n'ai aucune idée de quoi les gens parlent quand ils disent qu'ils sont lents seulement s'ils sont jetés.

EDIT: Si des exceptions ne sont pas levées, cela signifie que vous faites une nouvelle exception () ou quelque chose comme ça. Sinon, l'exception va entraîner la suspension du thread et la marche de la pile. Cela peut be Ok dans les petites situations, mais dans les sites Web à fort trafic, le fait de s'appuyer sur les exceptions comme flux de travail ou mécanisme de chemin d'exécution vous causera certainement des problèmes de performances. Les exceptions, en soi, ne sont pas mauvaises, et sont utiles pour exprimer des conditions exceptionnelles

Le flux de travail des exceptions dans une application .NET utilise des exceptions de première et deuxième chance. Pour toutes les exceptions, même si vous les interceptez et les manipulez, l'objet exception est toujours créé et le framework doit toujours parcourir la pile pour rechercher un gestionnaire. Si vous interceptez et relancez, bien sûr, cela va prendre plus de temps - vous allez obtenir une exception de première chance, l'attrapez, la relancez, provoquant une autre exception de première chance, qui ne trouve alors pas de gestionnaire, ce qui provoque ensuite une exception de deuxième chance.

Les exceptions sont également des objets sur le tas - donc si vous lancez des tonnes d'exceptions, vous causez des problèmes de performances et de mémoire.

En outre, selon ma copie de "Test de performances des applications Web Microsoft .NET" rédigée par l'équipe ACE:

"La gestion des exceptions est coûteuse. L'exécution du thread impliqué est suspendue pendant que CLR revient à travers la pile d'appels à la recherche du gestionnaire d'exceptions approprié, et lorsqu'il est trouvé, le gestionnaire d'exceptions et un certain nombre de blocs enfin doivent tous avoir leur chance de s'exécuter. avant qu'un traitement régulier puisse être effectué. "

Ma propre expérience dans le domaine a montré que la réduction des exceptions améliorait considérablement les performances. Bien sûr, il y a d'autres choses que vous prenez en compte lors des tests de performances - par exemple, si votre E/S de disque est exécutée ou si vos requêtes se font en quelques secondes, cela devrait être votre objectif. Mais la recherche et la suppression des exceptions devraient être un élément essentiel de cette stratégie.

8
Cory Foy

L'argument que je comprends n'est pas que lever des exceptions est mauvais, ils sont lents en soi. Au lieu de cela, il s'agit d'utiliser la construction throw/catch comme un moyen de première classe de contrôler la logique d'application normale, au lieu de constructions conditionnelles plus traditionnelles.

Souvent, dans la logique d'application normale, vous effectuez une boucle où la même action est répétée des milliers/millions de fois. Dans ce cas, avec un profilage très simple (voir la classe Chronomètre), vous pouvez constater par vous-même que lever une exception au lieu de dire une simple instruction if peut s'avérer être beaucoup plus lent.

En fait, j'ai lu une fois que l'équipe .NET de Microsoft a introduit les méthodes TryXXXXX dans .NET 2.0 à de nombreux types FCL de base, spécifiquement parce que les clients se plaignaient de la lenteur des performances de leurs applications.

Il s'avère que dans de nombreux cas, cela est dû au fait que les clients tentaient de convertir les types de valeurs dans une boucle, et chaque tentative a échoué. Une exception de conversion a été levée puis interceptée par un gestionnaire d'exceptions qui a ensuite avalé l'exception et poursuivi la boucle.

Microsoft recommande maintenant que les méthodes TryXXX soient utilisées en particulier dans cette situation pour éviter de tels problèmes de performances possibles.

Je peux me tromper, mais il semble que vous ne soyez pas certain de la véracité des "repères" dont vous avez parlé. Solution simple: essayez par vous-même.

6
Ash

Mon serveur XMPP a gagné en vitesse (désolé, pas de chiffres réels, purement observationnel) après avoir constamment essayé de les empêcher de se produire (comme vérifier si un socket est connecté avant d'essayer de lire plus de données) et me donner des moyens de les éviter (les méthodes TryX mentionnées). C'était avec seulement environ 50 utilisateurs virtuels actifs (chat).

4

Juste pour ajouter ma propre expérience récente à cette discussion: conformément à la plupart de ce qui est écrit ci-dessus, j'ai trouvé que les exceptions de lancement étaient extrêmement lentes lorsqu'elles étaient répétées, même sans que le débogueur ne fonctionne. Je viens d'augmenter de 60% les performances d'un grand programme que j'écris en modifiant environ cinq lignes de code: passer à un modèle de code retour au lieu de lever des exceptions. Certes, le code en question s'exécutait des milliers de fois et pouvait potentiellement générer des milliers d'exceptions avant que je ne le modifie. Je suis donc d'accord avec l'énoncé ci-dessus: lever des exceptions lorsque quelque chose d'important se passe réellement mal, pas comme un moyen de contrôler le flux d'application dans toutes les situations "attendues".

3
Ray Prisament

Mais le mono déclenche une exception 10 fois plus vite que le mode autonome .net, et le mode autonome .net lève l'exception 60 fois plus rapidement que le mode débogueur .net. (Les machines de test ont le même modèle de CPU)

int c = 1000000;
int s = Environment.TickCount;
for (int i = 0; i < c; i++)
{
    try { throw new Exception(); }
    catch { }
}
int d = Environment.TickCount - s;

Console.WriteLine(d + "ms / " + c + " exceptions");
2
linquize

Je n'ai jamais eu de problème de performances avec des exceptions. J'utilise beaucoup d'exceptions - je n'utilise jamais de codes retour si je le peux. Ils sont une mauvaise pratique et, à mon avis, sentent le code du spaghetti.

Je pense que tout se résume à la façon dont vous utilisez les exceptions: si vous les utilisez comme des codes de retour (chaque appel de méthode dans la pile attrape et relance), alors, oui, ils seront lents, car vous avez des frais généraux pour chaque prise/lancer unique.

Mais si vous lancez au bas de la pile et attrapez en haut (vous remplacez toute une chaîne de codes retour par un lancer/attraper), toutes les opérations coûteuses sont effectuées une fois.

À la fin de la journée, ils sont une fonctionnalité de langue valide.

Juste pour prouver mon point

Veuillez exécuter le code sur ce lien (trop gros pour une réponse).

Résultats sur mon ordinateur:

marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM

Les horodatages sont sortis au début, entre les codes retour et les exceptions, à la fin. Cela prend le même temps dans les deux cas. Notez que vous devez compiler avec des optimisations.

2
Sklivvz

Si vous les comparez pour renvoyer des codes, ils sont lents comme l'enfer. Cependant, comme les affiches précédentes l'ont déclaré, vous ne voulez pas lancer le fonctionnement normal du programme, vous n'obtenez le résultat parfait qu'en cas de problème et dans la grande majorité des cas, les performances n'ont plus d'importance (car l'exception implique un barrage routier de toute façon).

Ils valent vraiment la peine d'être utilisés par rapport aux codes d'erreur, les avantages sont vastes IMO.

2
Quibblesome

Sur le CLR Windows, pour une chaîne d'appels de profondeur 8, le déclenchement d'une exception est 750 fois plus lent que la vérification et la propagation d'une valeur de retour. (voir ci-dessous pour les repères)

Ce coût élevé pour les exceptions est dû au fait que le CLR Windows s'intègre à quelque chose appelé Windows Structured Exception Handling . Cela permet aux exceptions d'être correctement interceptées et levées sur différents runtimes et langues. Cependant, c'est très très lent.

Les exceptions dans le runtime Mono (sur n'importe quelle plate-forme) sont beaucoup plus rapides, car il ne s'intègre pas à SEH. Cependant, il y a une perte de fonctionnalité lors du passage d'exceptions sur plusieurs runtimes car il n'utilise rien de semblable à SEH.

Voici les résultats abrégés de mon benchmark des exceptions par rapport aux valeurs de retour pour le CLR Windows.

baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms
retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms
retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms
retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms
retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms
retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms
exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms
exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208     ms
exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639  ms
exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms
exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms

Et voici le code ..

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {

public class TestIt {
    int value;

    public class TestException : Exception { } 

    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    public bool baseline_null(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            return shouldfail;
        } else {
            return baseline_null(shouldfail,recurse_depth-1);
        }
    }

    public bool retval_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                return false;
            } else {
                return true;
            }
        } else {
            bool nested_error = retval_error(shouldfail,recurse_depth-1);
            if (nested_error) {
                return true;
            } else {
                return false;
            }
        }
    }

    public void exception_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                throw new TestException();
            }
        } else {
            exception_error(shouldfail,recurse_depth-1);
        }

    }

    public static void Main(String[] args) {
        int i;
        long l;
        TestIt t = new TestIt();
        int failures;

        int ITERATION_COUNT = 1000000;


        // (0) baseline null workload
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    t.baseline_null(shoulderror,recurse_depth);
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }


        // (1) retval_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    if (!t.retval_error(shoulderror,recurse_depth)) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }

        // (2) exception_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    try {
                        t.exception_error(shoulderror,recurse_depth);
                    } catch (TestException e) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));         }
        }
    }
}


}
1
David Jeske

En mode de libération, la surcharge est minime.

À moins que vous n'utilisiez les exceptions pour le contrôle de flux (par exemple, les sorties non locales) de manière récursive, je doute que vous puissiez remarquer la différence.

1
leppie

Une petite note ici sur les performances associées à la capture des exceptions.

Lorsque le chemin d'exécution entre dans un bloc "try", rien de magique ne se produit. Il n'y a aucune instruction "try" et aucun coût associé à l'entrée ou à la sortie du bloc try. Les informations sur le bloc try sont stockées dans les métadonnées de la méthode, et ces métadonnées sont utilisées lors de l'exécution chaque fois qu'une exception est levée. Le moteur d'exécution parcourt la pile à la recherche du premier appel contenu dans un bloc try. Toute surcharge associée à la gestion des exceptions se produit uniquement lorsque des exceptions sont levées.

0
Drew Noakes