Lors d'une révision de code avec un employé de Microsoft, nous avons découvert une grande section de code dans un try{}
bloquer. 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 majeure partie du code soit en dehors des blocs try/catch et que seules les sections importantes soient vérifiées. L'employé de Microsoft a ajouté et a déclaré qu'un livre blanc à venir met en garde contre les blocages try/catch incorrects.
J'ai regardé autour de moi et l'ai trouvé peut affecter les optimisations , mais cela ne semble s'appliquer que lorsqu'une variable est partagée entre des étendues.
Je ne parle pas de la maintenabilité du code, ni même de la gestion des exceptions appropriées (le code en question doit sans doute être remanié, etc.). Je ne fais pas non plus allusion à l'utilisation d'exceptions pour le contrôle de flux, cela est clairement faux dans la plupart des cas. Ce sont des questions importantes (certaines sont plus importantes), mais pas l’objectif ici.
Comment les blocs try/catch affectent-ils les performances lorsque des exceptions sont et non pas ?
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);
}
Sortie:
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
Après avoir vu toutes les statistiques pour avec try/catch et sans try/catch, la curiosité m'a obligé à 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 assez identiques à celles de 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.0
. Sinon, nous passons à l'étiquette IL_002d: leave.s IL_002f
Et la fonction revient.
Je peux sans risque supposer que si aucune exception ne se produit, il s’agit de la création de variables locales pour contenir des objets d’exception. seulement et une instruction de saut.
Non. Si les optimisations triviales qu'un bloc try/finally empêche ont réellement un impact mesurable sur votre programme, vous ne devriez probablement pas utiliser .NET au départ.
Explication assez complète du modèle d'exception .NET.
Tidbits de performance de Rico Mariani: Coût d'exception: quand lancer et quand ne pas le faire
Le premier type de coût est le coût statique de la gestion des exceptions dans votre code. Les exceptions gérées fonctionnent relativement bien ici, ce qui signifie que le coût statique peut être beaucoup moins élevé qu'en C++. Pourquoi est-ce? Eh bien, le coût statique est réellement lié à deux types d’endroits: d’abord, les sites réels de try/finally/catch/throw où il y a du code pour ces constructions. Deuxièmement, dans le code non incorporé, il y a le coût furtif associé au suivi de tous les objets qui doivent être détruits dans le cas où une exception est levée. Il doit y avoir une quantité considérable de logique de nettoyage à faire et la partie sournoise est que même le code qui ne lance pas ou capture lui-même ou utilise autrement des exceptions n’a toujours pas le fardeau de savoir comment nettoyer après lui-même.
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 capture
La structure est différente dans l'exemple de Ben M. Il sera étendu dans la boucle interne for
, ce qui empêchera la 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 à l'intérieur du 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();
}
Lorsque j'ai exécuté le code de test d'origine à partir de Ben M, j'ai remarqué une différence dans la configuration Debug et Releas.
Cette version, j'ai remarqué une différence dans la version de débogage (en réalité plus que l'autre version), mais ce n'était pas différent dans la version Release.
Conclusion :
Sur la base de ces tests, je pense que nous pouvons dire que Try/Catch fait a un faible impact sur les performances.
EDIT:
J'ai essayé d’augmenter la valeur de la boucle de 10000000 à 1000000000 et j’ai ré-exécuté dans Release pour obtenir quelques différences dans la version. Le résultat est 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 réellement plus rapide!
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 fait très peu de travail (lors de mon test, j'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 effectue 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 eu une différence beaucoup plus grande en inversant l'ordre des boucles ...
les blocs catch ont un impact négligeable sur les performances, mais une exception peut être assez importante. C'est probablement là que votre collègue était dérouté.
L'essai/attrape HAS impact sur la performance.
Mais ce n'est pas un impact énorme. La complexité try/catch est généralement O (1), comme une simple affectation, sauf quand ils sont placés dans une boucle. Donc, vous devez les utiliser judicieusement.
Here est une référence sur les performances try/catch (n’explique pas la complexité de celle-ci, mais elle est implicite). Jetez un coup d'oeil à la section Jetez moins d'exceptions
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 d’autres rares, mais à peine obscures, dans lesquelles l’effet peut être perceptible. La raison en est que le code donné, tel que:
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 peut-être optimiser statement1 en se basant sur le fait que l'instruction2 est exécutée avant l'instruction3. Si le compilateur peut reconnaître que thing1 n'a pas d'effets secondaires et que thing2 n'utilise pas réellement x, il peut en toute sécurité omettre thing1. Si [comme dans ce cas] chose1 était coûteux, cela pourrait être une optimisation majeure, bien que les cas où chose1 soit coûteux soient également ceux que le compilateur serait le moins susceptible d'optimiser. Supposons que le code a été changé:
x=thing1(); // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x); // statement3
Maintenant, il existe une séquence d'événements où instruction3 pourrait s'exécuter sans que l'instruction2 soit exécutée. Même si rien dans le code de thing2
Ne peut lever une exception, il serait possible qu'un autre thread utilise un Interlocked.CompareExchange
Pour remarquer que q
a été effacé et le définir sur Thread.ResetAbort
, Puis effectuez une Thread.Abort()
avant que l'instruction2 écrive sa valeur dans x
. Ensuite, le catch
exécuterait Thread.ResetAbort()
[via délégué q
], permettant ainsi à l'exécution de continuer avec statement3. Une telle séquence d'événements serait bien sûr exceptionnellement improbable, mais un compilateur est requis pour générer un code fonctionnant 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 possibilités d'omettre de simples morceaux de code que des codes complexes. Il serait donc rare qu'un essai/capture puisse affecter considérablement les performances si les exceptions ne sont jamais levées. Néanmoins, 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.
Voir discussion sur l'implémentation try/catch pour une discussion sur le fonctionnement des blocs try/catch et sur le coût élevé de certaines implémentations, voire nul, lorsqu'aucune exception ne se produit. En particulier, je pense que l'implémentation Windows 32 bits a une surcharge, contrairement à l'implémentation 64 bits.