Les blocs try / catch nuisent-ils aux performances lorsque des exceptions ne sont pas levées?

274

Lors d'une révision de code avec un employé de Microsoft, nous sommes tombés sur une grande section de code à l'intérieur d'un try{}bloc. Elle et un représentant informatique ont suggéré que cela pouvait avoir des effets sur les performances du code. En fait, ils ont suggéré que la plupart du code devrait être en dehors des blocs try / catch, et que seules les sections importantes devraient être vérifiées. L'employé de Microsoft a ajouté et déclaré qu'un prochain livre blanc mettait en garde contre les blocages try / catch incorrects.

J'ai regardé autour de moi et j'ai découvert que cela pouvait affecter les optimisations , mais cela ne semble s'appliquer que lorsqu'une variable est partagée entre des étendues.

Je ne pose pas de question sur la maintenabilité du code, ni même sur la gestion des bonnes exceptions (le code en question doit sans doute être refacturé). Je ne fais pas non plus référence à l'utilisation d'exceptions pour le contrôle de flux, c'est clairement faux dans la plupart des cas. Ce sont des questions importantes (certaines sont plus importantes), mais pas le sujet ici.

Comment les blocs try / catch affectent-ils les performances lorsque des exceptions ne sont pas levées?

Kobi
la source
147
"Celui qui sacrifierait l'exactitude à la performance ne mérite ni l'un ni l'autre."
Joel Coehoorn
16
cela dit, l'exactitude ne doit pas toujours être sacrifiée pour la performance.
Dan Davies Brackett
19
Et la simple curiosité?
Samantha Branham
63
@ Joel: Peut-être que Kobi veut juste connaître la réponse par curiosité. Savoir si les performances seront meilleures ou pires ne signifie pas nécessairement qu'il va faire quelque chose de fou avec son code. La recherche de la connaissance pour elle-même n'est-elle pas une bonne chose?
LukeH
6
Voici un bon algorithme pour savoir s'il faut ou non effectuer cette modification. Tout d'abord, définissez des objectifs de performance significatifs basés sur le client. Deuxièmement, écrivez d'abord le code pour qu'il soit à la fois correct et clair. Troisièmement, testez-le par rapport à vos objectifs. Quatrièmement, si vous atteignez vos objectifs, arrêtez le travail tôt et allez à la plage. Cinquièmement, si vous n'atteignez pas vos objectifs, utilisez un profileur pour trouver le code qui est trop lent. Sixièmement, si ce code s'avère trop lent en raison d'un gestionnaire d'exceptions inutile, supprimez le gestionnaire d'exceptions uniquement. Sinon, corrigez le code qui est en fait trop lent. Revenez ensuite à l'étape trois.
Eric Lippert

Réponses:

203

Vérifie ça.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

Production:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

En millisecondes:

449
416

Nouveau code:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

Nouveaux résultats:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334
Ben M
la source
24
Pouvez-vous également les essayer dans l'ordre inverse pour être sûr que la compilation JIT n'a pas eu d'effet sur la première?
JoshJordan
28
Des programmes comme celui-ci ne semblent guère être de bons candidats pour tester l'impact de la gestion des exceptions, trop de ce qui se passerait dans les blocs try {} catch {} normaux va être optimisé. Je vais peut-être déjeuner pour ça ...
LorenVS
30
Il s'agit d'une version de débogage. Le JIT ne les optimise pas.
Ben M
7
Ce n'est pas vrai du tout, pensez-y. Combien de fois utilisez-vous try catch in a loop? La plupart du temps, vous utiliserez la boucle dans un try.c
Athiwat Chunlakhan
9
Vraiment? "Comment les blocs try / catch affectent-ils les performances lorsque des exceptions ne sont pas levées?"
Ben M
105

Après avoir vu toutes les statistiques pour avec try / catch et sans try / catch, la curiosité m'a forcé à regarder derrière pour voir ce qui est généré pour les deux cas. Voici le code:

C #:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C #:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

Je ne suis pas un expert en IL mais nous pouvons voir qu'un objet d'exception local est créé sur la quatrième ligne .locals init ([0] class [mscorlib]System.Exception ex)après que les choses soient à peu près les mêmes que pour la méthode sans try / catch jusqu'à la ligne dix-sept IL_0021: leave.s IL_002f. Si une exception se produit, le contrôle passe à la ligne, IL_0025: ldloc.0sinon nous passons à l'étiquette IL_002d: leave.s IL_002fet la fonction revient.

Je peux supposer en toute sécurité que si aucune exception ne se produit, c'est la surcharge de la création de variables locales pour contenir uniquement les objets d'exception et une instruction de saut.

TheVillageIdiot
la source
33
Eh bien, l'IL inclut un bloc try / catch dans la même notation qu'en C #, donc cela ne montre pas vraiment combien de frais généraux un try / catch signifie dans les coulisses! Juste que l'IL n'ajoute pas beaucoup plus, ne signifie pas la même chose car il n'est pas ajouté quelque chose dans le code assemblé compilé. L'IL est juste une représentation commune de tous les langages .NET. Ce n'est PAS un code machine!
crainte
64

Non. Si les optimisations triviales qu'un bloc try / finally empêche réellement ont un impact mesurable sur votre programme, vous ne devriez probablement pas utiliser .NET en premier lieu.

John Kugelman
la source
10
C'est un excellent point - par rapport aux autres articles de notre liste, celui-ci devrait être minuscule. Nous devons faire confiance aux fonctionnalités de base du langage pour se comporter correctement et optimiser ce que nous pouvons contrôler (sql, index, algorithmes).
Kobi
3
Pensez aux boucles serrées mate. Par exemple, la boucle où vous lisez et désérialisez des objets à partir d'un flux de données de socket dans le serveur de jeu et où vous essayez de presser autant que vous le pouvez. Donc, vous MessagePack pour la sérialisation d'objet au lieu de binaryformatter, et utilisez ArrayPool <byte> au lieu de simplement créer des tableaux d'octets, etc. Certaines optimisations seront ignorées par le compilateur et la variable d'exception va à Gen0 GC. Tout ce que je dis, c'est qu'il y a "certains" scénarios où tout a un impact.
tcwicks
35

Explication assez complète du modèle d'exception .NET.

Petits morceaux de performance de Rico Mariani: coût exceptionnel: quand lancer et quand ne pas lancer

Le premier type de coût est le coût statique lié à la gestion des exceptions dans votre code. Les exceptions gérées fonctionnent en fait relativement bien ici, ce qui signifie que le coût statique peut être bien inférieur à celui de C ++. Pourquoi est-ce? Eh bien, le coût statique est vraiment engagé dans deux types d'endroits: Premièrement, les sites réels de try / finally / catch / throw où il y a du code pour ces constructions. Deuxièmement, dans le code non mélangé, il y a le coût furtif associé au suivi de tous les objets qui doivent être détruits en cas de levée d'une exception. Il y a une quantité considérable de logique de nettoyage qui doit être présente et la partie sournoise est que même le code qui ne fonctionne pas

Dmitriy Zaslavskiy:

Selon la note de Chris Brumme: Il y a aussi un coût lié au fait que certaines optimisations ne sont pas effectuées par JIT en présence de captures

arul
la source
1
La chose à propos de C ++ est qu'un très gros morceau de la bibliothèque standard lèvera des exceptions. Il n'y a rien de facultatif à leur sujet. Vous devez concevoir vos objets avec une sorte de politique d'exception, et une fois que vous avez fait cela, il n'y a plus de coût furtif.
David Thornley
Les affirmations de Rico Mariani sont complètement fausses pour le C ++ natif. "le coût statique peut être beaucoup plus bas qu'en C ++" - Ce n'est tout simplement pas vrai. Cependant, je ne sais pas quelle était la conception du mécanisme d'exception en 2003 lorsque l'article a été écrit. C ++ n'a vraiment aucun coût lorsque des exceptions ne sont pas levées, quel que soit le nombre de blocs try / catch que vous avez et où ils se trouvent.
BJovke
1
@BJovke C ++ "gestion des exceptions à coût nul" signifie uniquement qu'il n'y a pas de coût d'exécution lorsque les exceptions ne sont pas levées, mais il y a toujours un coût de taille de code majeur en raison de tout le code de nettoyage appelant des destructeurs sur les exceptions. De plus, bien qu'aucun code spécifique aux exceptions ne soit généré sur le chemin de code normal, le coût n'est toujours pas nul, car la possibilité d'exceptions restreint toujours l'optimiseur (par exemple, les éléments nécessaires en cas d'exception doivent rester quelque part -> les valeurs peuvent être rejetées de manière moins agressive -> allocation de registre moins efficace)
Daniel
24

La structure est différente dans l'exemple de Ben M . Il sera étendu au-dessus de l'intérieurfor boucle , ce qui entraînera une mauvaise comparaison entre les deux cas.

Ce qui suit est plus précis pour la comparaison où tout le code à vérifier (y compris la déclaration de variable) se trouve dans le bloc Try / Catch:

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Quand j'ai exécuté le code de test d'origine de Ben M , j'ai remarqué une différence dans la configuration du débogage et de la libération.

Cette version, j'ai remarqué une différence dans la version de débogage (en fait plus que l'autre version), mais il n'y avait aucune différence dans la version Release.

Conclusion :
Sur la base de ces tests, je pense que nous pouvons dire que Try / Catch ne n'ont un faible impact sur les performances.

EDIT:
J'ai essayé d'augmenter la valeur de la boucle de 10000000 à 1000000000, et j'ai exécuté à nouveau dans la version pour obtenir des différences dans la version, et le résultat était le suivant:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

Vous voyez que le résultat est sans conséquence. Dans certains cas, la version utilisant Try / Catch est en fait plus rapide!

admiration
la source
1
Je l'ai remarqué aussi, parfois c'est plus rapide avec try / catch. Je l'ai commenté sur la réponse de Ben. Cependant, contrairement à 24 électeurs, je n'aime pas ce genre de benchmarking, je ne pense pas que ce soit une bonne indication. Le code est plus rapide dans ce cas, mais le sera-t-il toujours?
Kobi
5
Cela ne prouve-t-il pas que votre machine effectuait plusieurs autres tâches en même temps? Le temps écoulé n'est jamais une bonne mesure, vous devez utiliser un profileur qui enregistre le temps processeur, pas le temps écoulé.
Colin Desmond
2
@Kobi: Je conviens que ce n'est pas le meilleur moyen de comparer si vous allez le publier comme une preuve que votre programme s'exécute plus rapidement qu'un autre ou quelque chose, mais peut vous donner en tant que développeur une indication d'une méthode plus performante qu'une autre . Dans ce cas, je pense que nous pouvons dire que les différences (au moins pour la configuration Release) sont ignorables.
admiration
1
Vous ne chronométrez pas try/catchici. Vous chronométrez 12 essais / captures entrant dans la section critique contre 10 millions de boucles. Le bruit de la boucle supprimera toute influence du try / catch. si au lieu de cela vous mettez le try / catch dans la boucle serrée, et comparez avec / sans, vous vous retrouveriez avec le coût du try / catch. (sans aucun doute, un tel codage n'est généralement pas une bonne pratique, mais si vous voulez chronométrer le temps système d'une construction, c'est comme ça que vous le faites). De nos jours, BenchmarkDotNet est l'outil incontournable pour des temporisations d'exécution fiables.
Abel
15

J'ai testé l'impact réel d'un try..catch dans une boucle serrée, et il est trop petit en soi pour être un problème de performance dans une situation normale.

Si la boucle fonctionne très peu (dans mon test, j'en ai fait un x++), vous pouvez mesurer l'impact de la gestion des exceptions. La boucle avec gestion des exceptions a pris environ dix fois plus de temps.

Si la boucle fait un travail réel (dans mon test, j'ai appelé la méthode Int32.Parse), la gestion des exceptions a trop peu d'impact pour être mesurable. J'ai obtenu une différence beaucoup plus grande en échangeant l'ordre des boucles ...

Guffa
la source
11

Les blocs try catch ont un impact négligeable sur les performances, mais le lancement d'exceptions peut être assez important, c'est probablement là que votre collègue a été confus.

RHicke
la source
8

L'essai / capture a un impact sur les performances.

Mais ce n'est pas un impact énorme. la complexité try / catch est généralement O (1), tout comme une simple affectation, sauf quand ils sont placés dans une boucle. Vous devez donc les utiliser à bon escient.

Voici une référence sur les performances try / catch (n'explique pas la complexité de cela, mais c'est implicite). Jetez un coup d'œil à la section Lancer moins d'exceptions

Isaac
la source
3
La complexité est O (1), cela ne signifie pas trop. Par exemple, si vous équipez une section de code qui est appelée très fréquemment avec try-catch (ou si vous mentionnez une boucle), les O (1) pourraient s'additionner à un nombre mesurable à la fin.
Csaba Toth
6

En théorie, un bloc try / catch n'aura aucun effet sur le comportement du code à moins qu'une exception ne se produise réellement. Il existe cependant de rares circonstances où l'existence d'un bloc try / catch peut avoir un effet majeur, et certaines rares, mais à peine obscures, où l'effet peut être perceptible. La raison en est que le code donné comme:

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

le compilateur peut être en mesure d'optimiser instruction1 sur la base du fait que l'instruction2 est garantie pour s'exécuter avant l'instruction3. Si le compilateur peut reconnaître que chose1 n'a pas d'effets secondaires et que chose2 n'utilise pas réellement x, il peut en toute sécurité omettre complètement chose1. Si [comme dans ce cas] thing1 était cher, cela pourrait être une optimisation majeure, bien que les cas où thing1 est cher sont également ceux que le compilateur serait le moins susceptible d'optimiser. Supposons que le code ait été modifié:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

Il existe maintenant une séquence d'événements où l'instruction3 pourrait s'exécuter sans que l'instruction2 ait été exécutée. Même si rien dans le code de thing2ne pouvait lever d'exception, il serait possible qu'un autre thread puisse utiliser un Interlocked.CompareExchangepour remarquer qu'il a qété effacé et le définir Thread.ResetAbort, puis effectuer une Thread.Abort()instruction before2 écrit sa valeur dans x. Ensuite, le catchexécuterait Thread.ResetAbort()[via déléguéq ], permettant à l'exécution de continuer avec instruction3. Une telle séquence d'événements serait bien sûr exceptionnellement improbable, mais un compilateur est nécessaire pour générer du code qui fonctionne conformément aux spécifications même lorsque de tels événements improbables se produisent.

En général, le compilateur est beaucoup plus susceptible de remarquer des opportunités de laisser de simples bits de code que des bits complexes, et il serait donc rare qu'un essai / capture puisse affecter les performances beaucoup si des exceptions ne sont jamais levées. Pourtant, il existe des situations où l'existence d'un bloc try / catch peut empêcher des optimisations qui - sans le try / catch - auraient permis au code de s'exécuter plus rapidement.

supercat
la source
5

Bien que "la prévention soit meilleure que la manipulation ", dans une perspective de performance et d'efficacité, nous avons pu choisir le try-catch plutôt que la pré-varication. Considérez le code ci-dessous:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Voici le résultat:

With Checking: 20367
With Exception: 13998
Ted Oddman
la source
4

Voir la discussion sur l'implémentation try / catch pour une discussion sur la façon dont les blocs try / catch fonctionnent, et comment certaines implémentations ont une surcharge élevée, et certaines ont une surcharge nulle, lorsqu'aucune exception ne se produit. En particulier, je pense que l'implémentation de Windows 32 bits a des frais généraux élevés, et l'implémentation 64 bits ne l'est pas.

Ira Baxter
la source
J'ai décrit deux approches différentes pour implémenter des exceptions. Les approches s'appliquent également au C ++ et au C #, ainsi qu'au code managé / non managé. Lesquels MS a choisi pour leur C #, je ne sais pas exactement, mais l'architecture de gestion des exceptions des applications de niveau machine fournie par MS utilise le schéma le plus rapide. Je serais un peu surpris si l'implémentation C # pour 64 bits ne l'utilisait pas.
Ira Baxter