C #: Cette classe d’parsing comparative est-elle précise?

J’ai créé une classe simple pour comparer certaines de mes méthodes. Mais est-ce exact? Je suis un peu novice en matière de benchmarking, de timing, etc. De plus, si c’est bon, quelqu’un d’autre pourrait peut-être l’utiliser aussi 🙂

public static class Benchmark { public static IEnumerable This(Action subject) { var watch = new Stopwatch(); while (true) { watch.Reset(); watch.Start(); subject(); watch.Stop(); yield return watch.ElapsedTicks; } } } 

Vous pouvez l’utiliser comme ceci:

 var avg = Benchmark.This(() => SomeMethod()).Take(500).Average(); 

Tous les commentaires? Cela semble-t-il assez stable et précis ou ai-je oublié quelque chose?

Il s’agit d’être aussi précis que possible pour un repère simple. Mais il y a des facteurs qui ne sont pas sous votre contrôle:

  • charger sur le système à partir d’autres processus
  • état du tas avant / pendant le benchmark

Vous pouvez faire quelque chose à propos de ce dernier point: un sharepoint repère est l’une des rares situations dans lesquelles l’appel de GC.Collect peut être défendu. Et vous pouvez appeler le subject une fois à l’avance pour éliminer tout problème de JIT. Mais cela nécessite que les appels soient subject pour être indépendants.

 public static IEnumerable This(Action subject) { subject(); // warm up GC.Collect(); // compact Heap GC.WaitForPendingFinalizers(); // and wait for the finalizer queue to empty var watch = new Stopwatch(); while (true) { watch.Reset(); watch.Start(); subject(); watch.Stop(); yield return watch.Elapsed; // TimeSpan } } 

Pour les bonus, votre classe devrait vérifier le champ System.Diagnostics.Stopwatch.IsHighResolution . Si cette option est désactivée, vous n’avez qu’une résolution très grossière (20 ms).

Mais sur un PC ordinaire, avec de nombreux services exécutés à l’arrière-plan, cela ne sera jamais très précis.

Quelques problèmes ici.

Tout d’abord, rappelez-vous que la première fois que vous exécutez le code, la fermeture transitive de ses appels de méthode est ignorée. Cela signifie que la première exécution aura probablement un coût plus élevé que chaque exécution ultérieure. Selon que vous comparez les temps «froids» ou les temps «chauds», cela peut faire la différence. J’ai vu des méthodes où le coût de la méthode était plus élevé que tous les autres appels mis ensemble!

Deuxièmement, rappelez-vous que le ramasse-miettes s’exécute sur un autre thread. Si vous faites des ordures en une fois, le coût de nettoyage de ces ordures peut ne pas être réalisé avant des exécutions ultérieures. Vous ne parvenez donc pas à comptabiliser le coût total d’une exécution en la reportant à des exécutions ultérieures.

Ces deux indicateurs sont révélateurs de la faiblesse de tout étalonnage: l’parsing comparative est par nature irréaliste et, par conséquent, d’une valeur limitée. Dans le code du monde réel, le CPG va fonctionner, la gigue va s’exécuter, etc. Il est fréquent que les performances référencées ne soient en aucun cas comparables aux performances réelles, car elles ne tiennent pas compte de la variabilité des coûts réels inhérents à un système de grande taille. Plutôt que d’parsingr les caractéristiques des performances de manière isolée, je préfère examiner celles-ci dans des scénarios réalistes auxquels sont confrontés de vrais clients.

Vous devez absolument renvoyer ElapsedMilliseconds au lieu de ElapsedTicks. La valeur renvoyée par ElapsedTicks dépend de la fréquence du chronomètre, qui peut être différente sur différents systèmes. Il ne correspondra pas nécessairement à la propriété Ticks d’un object Timespan ou DateTime.

Voir http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.elapsedticks.aspx .

Si vous souhaitez une résolution supplémentaire des ticks, vous devez renvoyer watch.Elapsed.Ticks (c.-à-d. Timestamp.Ticks) au lieu de watch.ElapsedTicks (il peut s’agir de l’une des plus subtiles erreurs potentielles de .Net). De MSDN:

Les ticks du chronomètre sont différents des DateTime.Ticks. Chaque tick dans la valeur DateTime.Ticks représente un intervalle de 100 nanosecondes. Chaque tick dans la valeur ElapsedTicks représente l’intervalle de temps égal à 1 seconde divisé par la fréquence.

En dehors de cela, je suppose que votre code est correct, bien que je pense que vous incluriez une partie de la surcharge des appels de méthodes dans vos mesures, ce qui pourrait être significatif si les méthodes elles-mêmes prennent très peu de temps à exécuter. En outre, vous voudrez probablement exclure le premier appel de la méthode de votre moyenne calculée, mais je ne suis pas sûr de savoir comment procéder dans votre classe.

Un dernier point, qui ne serait probablement pas pertinent pour la plupart des utilisations de cette classe: Le chronomètre tourne un peu vite par rapport à l’heure système. Sur mon ordinateur, cela prend environ 5 secondes (c’est-à-dire quelques secondes , pas des millisecondes) après 24 heures, et sur d’autres machines, cette dérive peut être encore plus grande. C’est donc un peu trompeur de dire que c’est très précis , alors que c’est en fait très granulaire . Pour chronométrer des méthodes de courte durée, ce ne serait évidemment pas un problème important.

Et un dernier point, qui est certainement pertinent: j’ai souvent remarqué lors de l’parsing comparative que je vais obtenir une série de temps d’exécution qui sont tous regroupés dans une plage de valeurs étroite (par exemple 80, 80, 79, 82, etc.). , mais il arrive parfois que quelque chose d’autre se produise dans Windows (comme ouvrir un autre programme ou que mon anti-virus s’exécute ou quelque chose d’autre) et que j’obtiens une valeur farfelue avec les autres (par exemple 80, 80, 79, 271, 80, etc. .) Je pense qu’une solution simple à ce problème aberrant consiste à utiliser la médiane de vos mesures au lieu de la moyenne . Je ne sais pas si Linq le supporte automatiquement ou non.

Comme je ne suis pas un programmeur C #, je ne peux pas dire avec beaucoup d’exactitude si cette classe est une implémentation appropriée pour compter la durée d’exécution d’une fonction. Cependant, il faut garder à l’esprit la répétabilité et l’exactitude.

Je ne suis pas au courant des divers avantages et inconvénients du .NET Framework, mais selon la manière dont il est compilé en code natif, il est possible que toute compilation affecte les résultats du test de performance. En outre, le fait qu’une fonction soit ou non dans le cache peut également faire la différence. Vous voudrez donc parcourir votre fonction pour vous assurer que la compilation n’a pas de succès et que tout est chargé et prêt. Une fois cela fait, vous pourrez peut-être commencer.

D’autres auront probablement de meilleures informations et connaissances sur .NET que moi.