Assez souvent sur SO, je me retrouve à comparer de petits morceaux de code pour voir quelle implémentation est la plus rapide.
Très souvent, je vois des commentaires selon lesquels le code d'analyse comparative ne prend pas en compte le jitting ou le garbage collector.
J'ai la fonction d'analyse comparative simple suivante que j'ai lentement évoluée:
static void Profile(string description, int iterations, Action func) {
// warm up
func();
// clean up
GC.Collect();
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
Usage:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
// ... code being profiled
});
Cette implémentation présente-t-elle des défauts? Est-ce suffisant pour montrer que l'implémentation X est plus rapide que l'implémentation Y sur les itérations Z? Pouvez-vous penser à des moyens d'améliorer cela?
EDIT Il est assez clair qu'une approche basée sur le temps (par opposition aux itérations), est préférée, est-ce que quelqu'un a des implémentations où les vérifications du temps n'ont pas d'impact sur les performances?
la source
Réponses:
Voici la fonction modifiée: comme recommandé par la communauté, n'hésitez pas à modifier ce wiki communautaire.
Assurez-vous de compiler dans Release avec les optimisations activées et d'exécuter les tests en dehors de Visual Studio . Cette dernière partie est importante car le JIT effectue ses optimisations avec un débogueur attaché, même en mode Release.
la source
La finalisation ne sera pas nécessairement terminée avant les
GC.Collect
retours. La finalisation est mise en file d'attente puis exécutée sur un thread distinct. Ce fil pourrait encore être actif pendant vos tests, affectant les résultats.Si vous voulez vous assurer que la finalisation est terminée avant de commencer vos tests, vous pouvez appeler
GC.WaitForPendingFinalizers
, qui bloquera jusqu'à ce que la file d'attente de finalisation soit effacée:la source
GC.Collect()
une fois?Collect
est donc là pour s'assurer que les objets «finalisés» sont également collectés.Si vous souhaitez supprimer les interactions GC de l'équation, vous souhaiterez peut-être exécuter votre appel "warm up" après l'appel GC.Collect, pas avant. De cette façon, vous savez que .NET aura déjà suffisamment de mémoire allouée par le système d'exploitation pour l'ensemble de travail de votre fonction.
Gardez à l'esprit que vous effectuez un appel de méthode non intégré pour chaque itération, alors assurez-vous de comparer les éléments que vous testez à un corps vide. Vous devrez également accepter que vous ne pouvez chronométrer de manière fiable que les choses qui sont plusieurs fois plus longues qu'un appel de méthode.
De plus, en fonction du type de contenu que vous profilez, vous voudrez peut-être exécuter votre chronométrage pendant un certain temps plutôt que pendant un certain nombre d'itérations - cela peut avoir tendance à conduire à des nombres plus facilement comparables sans avoir un très court terme pour la meilleure implémentation et / ou un très long pour le pire.
la source
J'éviterais du tout de passer le délégué:
Un exemple de code menant à l'utilisation de la fermeture:
Si vous n'êtes pas au courant des fermetures, jetez un œil à cette méthode dans .NET Reflector.
la source
IDisposable
.Je pense que le problème le plus difficile à surmonter avec des méthodes d'analyse comparative comme celle-ci est de tenir compte des cas extrêmes et des imprévus. Par exemple - "Comment les deux extraits de code fonctionnent-ils sous une charge CPU élevée / utilisation du réseau / battage de disque / etc." Ils sont parfaits pour les vérifications logiques de base afin de voir si un algorithme particulier fonctionne beaucoup plus rapidement qu'un autre. Mais pour tester correctement la plupart des performances du code, vous devez créer un test qui mesure les goulots d'étranglement spécifiques de ce code particulier.
Je dirais quand même que tester de petits blocs de code a souvent peu de retour sur investissement et peut encourager l'utilisation d'un code trop complexe au lieu d'un simple code maintenable. Ecrire un code clair que d'autres développeurs, ou moi-même 6 mois plus tard, pouvons comprendre rapidement aura plus d'avantages en termes de performances qu'un code hautement optimisé.
la source
J'appelais
func()
plusieurs fois pour l'échauffement, pas une seule.la source
Suggestions d'amélioration
Détecter si l'environnement d'exécution est bon pour l'analyse comparative (comme détecter si un débogueur est attaché ou si l'optimisation jit est désactivée, ce qui entraînerait des mesures incorrectes).
Mesurer des parties du code indépendamment (pour voir exactement où se trouve le goulot d'étranglement).
Concernant le n ° 1:
Pour détecter si un débogueur est attaché, lisez la propriété
System.Diagnostics.Debugger.IsAttached
(n'oubliez pas de gérer également le cas où le débogueur n'est initialement pas attaché, mais est attaché après un certain temps).Pour détecter si l'optimisation jit est désactivée, lisez la propriété
DebuggableAttribute.IsJITOptimizerDisabled
des assemblys appropriés:Concernant le n ° 2:
Cela peut être fait de plusieurs manières. Une façon est de permettre à plusieurs délégués d'être fournis, puis de mesurer ces délégués individuellement.
Concernant le n ° 3:
Cela pourrait également être fait de nombreuses manières, et différents cas d'utilisation exigeraient des solutions très différentes. Si le benchmark est appelé manuellement, l'écriture sur la console peut être correcte. Cependant, si le benchmark est effectué automatiquement par le système de construction, alors l'écriture sur la console n'est probablement pas si bien.
Une façon de procéder consiste à renvoyer le résultat de référence sous la forme d'un objet fortement typé qui peut facilement être utilisé dans différents contextes.
Etimo.Benchmarks
Une autre approche consiste à utiliser un composant existant pour effectuer les benchmarks. En fait, dans mon entreprise, nous avons décidé de publier notre outil de référence dans le domaine public. À la base, il gère le ramasse-miettes, la gigue, les échauffements, etc., comme le suggèrent certaines des autres réponses ici. Il présente également les trois fonctionnalités que j'ai suggérées ci-dessus. Il gère plusieurs des problématiques abordées dans le blog d'Eric Lippert .
Il s'agit d'un exemple de sortie où deux composants sont comparés et les résultats sont écrits sur la console. Dans ce cas, les deux composants comparés sont appelés 'KeyedCollection' et 'MultiplyIndexedKeyedCollection':
Il existe un package NuGet , un exemple de package NuGet et le code source est disponible sur GitHub . Il y a aussi un article de blog .
Si vous êtes pressé, je vous suggère d'obtenir l'exemple de package et de simplement modifier les exemples de délégués si nécessaire. Si vous n'êtes pas pressé, il peut être judicieux de lire l'article du blog pour en comprendre les détails.
la source
Vous devez également exécuter une passe de "préchauffage" avant la mesure réelle pour exclure le temps que le compilateur JIT passe à jitting votre code.
la source
Selon le code que vous comparez et la plate-forme sur laquelle il s'exécute, vous devrez peut-être tenir compte de la façon dont l'alignement du code affecte les performances . Pour ce faire, il faudrait probablement un wrapper externe qui a exécuté le test plusieurs fois (dans des domaines d'application ou des processus distincts?), Parfois en appelant d'abord le «code de remplissage» pour le forcer à être compilé en JIT, de manière à ce que le code soit benchmarkés pour être alignés différemment. Un résultat de test complet donnerait le meilleur et le pire des cas pour les différents alignements de code.
la source
Si vous essayez d'éliminer l'impact du nettoyage de la mémoire du benchmark terminé, cela vaut-il la peine de le définir
GCSettings.LatencyMode
?Si ce n'est pas le cas, et que vous voulez que l'impact des déchets créés dans
func
fasse partie du benchmark, ne devriez-vous pas également forcer la collecte à la fin du test (à l'intérieur du minuteur)?la source
Le problème fondamental de votre question est l'hypothèse qu'une seule mesure peut répondre à toutes vos questions. Vous devez mesurer plusieurs fois pour obtenir une image efficace de la situation et en particulier dans un langage garbage collector comme C #.
Une autre réponse donne une bonne façon de mesurer les performances de base.
Cependant, cette mesure unique ne tient pas compte du garbage collection. Un profil approprié représente également les pires performances du garbage collection réparties sur de nombreux appels (ce nombre est en quelque sorte inutile car la machine virtuelle peut s'arrêter sans jamais collecter les déchets restants, mais est toujours utile pour comparer deux implémentations différentes de
func
.)Et on peut également vouloir mesurer les pires performances du garbage collection pour une méthode qui n'est appelée qu'une seule fois.
Mais plus important que de recommander des mesures supplémentaires spécifiques possibles pour établir le profil est l'idée qu'il faut mesurer plusieurs statistiques différentes et pas seulement un type de statistique.
la source