Comment écrire un micro-benchmark correct en Java?

870

Comment écrire (et exécuter) un micro-benchmark correct en Java?

Je cherche des exemples de code et des commentaires illustrant diverses choses à penser.

Exemple: Le benchmark devrait-il mesurer le temps / itération ou les itérations / temps, et pourquoi?

Connexes: l' analyse comparative du chronomètre est-elle acceptable?

John Nilsson
la source
Voir [cette question] [1] d'il y a quelques minutes pour des informations connexes. edit: désolé, ce n'est pas censé être une réponse. J'aurais dû poster un commentaire. [1]: stackoverflow.com/questions/503877/…
Tiago
C'est après avoir prévu de renvoyer l'affiche de cette question à une question comme celle-ci que j'ai constaté que cette question n'existait pas. Alors voilà, j'espère qu'il rassemblera quelques bons conseils au fil du temps.
John Nilsson
5
Java 9 pourrait fournir certaines fonctionnalités pour le micro-benchmarking: openjdk.java.net/jeps/230
Raedwald
1
@Raedwald Je pense que JEP vise à ajouter un micro benchmark au code JDK, mais je ne pense pas que jmh sera inclus dans le JDK ...
assylias
1
@Raedwald Bonjour du futur. Ça n'a pas fait la coupe .
Michael

Réponses:

787

Conseils sur l'écriture de micro-benchmarks par les créateurs de Java HotSpot :

Règle 0: Lisez un article réputé sur les JVM et le micro-benchmarking. Un bon exemple est Brian Goetz, 2005 . N'attendez pas trop des micro-benchmarks; ils ne mesurent qu'une gamme limitée de caractéristiques de performances JVM.

Règle 1: incluez toujours une phase de préchauffage qui exécute votre noyau de test tout au long, suffisamment pour déclencher toutes les initialisations et compilations avant de chronométrer les phases. (Moins d'itérations est OK sur la phase d'échauffement. La règle de base est de plusieurs dizaines de milliers d'itérations de boucle interne.)

Règle 2: Toujours exécuter avec -XX:+PrintCompilation, -verbose:gcetc., de sorte que vous pouvez vérifier que le compilateur et d' autres parties de la machine virtuelle Java ne font pas le travail inattendu pendant votre phase de synchronisation.

Règle 2.1: Imprimez des messages au début et à la fin des phases de chronométrage et d'échauffement, afin de pouvoir vérifier qu'il n'y a pas de sortie de la règle 2 pendant la phase de chronométrage.

Règle 3: Soyez conscient de la différence entre -clientet -server, et OSR et des compilations régulières. Le -XX:+PrintCompilationdrapeau des rapports compilations OSR avec un arobase pour indiquer le point d'entrée non initial, par exemple: Trouble$1::run @ 2 (41 bytes). Préférez le serveur au client et régulier à l'OSR, si vous recherchez les meilleures performances.

Règle 4: Soyez conscient des effets d'initialisation. N'imprimez pas pour la première fois pendant votre phase de synchronisation, car l'impression charge et initialise les classes. Ne chargez pas de nouvelles classes en dehors de la phase de préchauffage (ou de la phase de rapport final), sauf si vous testez le chargement de classe spécifiquement (et dans ce cas, ne chargez que les classes de test). La règle 2 est votre première ligne de défense contre de tels effets.

Règle 5: Soyez conscient des effets de désoptimisation et de recompilation. Ne prenez pas de chemin de code pour la première fois dans la phase de synchronisation, car le compilateur peut ordonner et recompiler le code, sur la base d'une hypothèse optimiste antérieure selon laquelle le chemin n'allait pas du tout être utilisé. La règle 2 est votre première ligne de défense contre de tels effets.

Règle 6: Utilisez les outils appropriés pour lire l'esprit du compilateur et attendez-vous à être surpris par le code qu'il produit. Inspectez le code vous-même avant de former des théories sur ce qui rend quelque chose plus rapide ou plus lent.

Règle 7: Réduisez le bruit dans vos mesures. Exécutez votre référence sur une machine silencieuse et exécutez-la plusieurs fois, en éliminant les valeurs aberrantes. Utilisez -Xbatchpour sérialiser le compilateur avec l'application et envisagez de définir -XX:CICompilerCount=1pour empêcher le compilateur de s'exécuter en parallèle avec lui-même. Faites de votre mieux pour réduire les frais généraux du GC, définissez Xmx(assez grand) égal Xmset utilisez-le UseEpsilonGCs'il est disponible.

Règle 8: Utilisez une bibliothèque pour votre référence car elle est probablement plus efficace et a déjà été déboguée à cette seule fin. Tels que JMH , Caliper ou Bill and Paul's Excellent UCSD Benchmarks for Java .

Eugene Kuleshov
la source
5
C'était aussi un article intéressant: ibm.com/developerworks/java/library/j-jtp12214
John Nilsson
143
De plus, n'utilisez jamais System.currentTimeMillis () sauf si vous êtes d'accord avec une précision de + ou - 15 ms, ce qui est typique sur la plupart des combinaisons OS + JVM. Utilisez plutôt System.nanoTime ().
Scott Carey
5
Du papier de javaOne: azulsystems.com/events/javaone_2009/session/…
bestsss
94
Il convient de noter qu'il System.nanoTime()n'est pas garanti d'être plus précis que System.currentTimeMillis(). Il est seulement garanti d'être au moins aussi précis. Cependant, il est généralement beaucoup plus précis.
Gravity du
41
La principale raison pour laquelle il faut utiliser System.nanoTime()au lieu de, System.currentTimeMillis()c'est que le premier est garanti d'augmenter de façon monotone. En soustrayant les valeurs renvoyées, deux currentTimeMillisinvocations peuvent en fait donner des résultats négatifs, probablement parce que l'heure système a été ajustée par un démon NTP.
Waldheinz
239

Je sais que cette question a été marquée comme répondue, mais je voulais mentionner deux bibliothèques qui nous aident à écrire des micro-repères

Pied à coulisse de Google

Tutoriels de démarrage

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH d'OpenJDK

Tutoriels de démarrage

  1. Éviter les pièges de l'analyse comparative sur la machine virtuelle Java
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/
Aravind Yarram
la source
37
+1, il aurait pu être ajouté en tant que règle 8 de la réponse acceptée: Règle 8: parce que tant de choses peuvent mal tourner, vous devriez probablement utiliser une bibliothèque existante plutôt que d'essayer de le faire vous-même!
assylias
8
@Pangea jmh est probablement supérieur à Caliper de nos jours, Voir aussi: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/…
assylias
87

Les choses importantes pour les benchmarks Java sont:

  • Faire chauffer le premier JIT en exécutant le code plusieurs fois avant de synchronisation , il
  • Assurez-vous de l'exécuter assez longtemps pour pouvoir mesurer les résultats en secondes ou (mieux) des dizaines de secondes
  • Bien que vous ne puissiez pas appeler System.gc()entre les itérations, c'est une bonne idée de l'exécuter entre les tests, afin que chaque test obtienne, espérons-le, un espace mémoire «propre» pour fonctionner. (Oui, gc()c'est plus un indice qu'une garantie, mais il est très probable que cela va vraiment ramasser les ordures selon mon expérience.)
  • J'aime afficher les itérations et le temps, et un score de temps / itération qui peut être mis à l'échelle de sorte que le «meilleur» algorithme obtienne un score de 1,0 et que les autres soient notés de manière relative. Cela signifie que vous pouvez exécuter tous les algorithmes pendant une longue période, en variant à la fois le nombre d'itérations et le temps, tout en obtenant des résultats comparables.

Je suis en train de bloguer sur la conception d'un cadre d'analyse comparative en .NET. J'ai un deux des postes précédents qui peuvent être en mesure de vous donner quelques idées - pas tout sera approprié, bien sûr, mais certaines d' entre elles peut - être.

Jon Skeet
la source
3
Minitpick mineur: IMO "pour que chaque test soit" devrait être "pour que chaque test puisse obtenir" car le premier donne l'impression que l'appel libère gc toujours de la mémoire inutilisée.
Sanjay T. Sharma
@ SanjayT.Sharma: Eh bien, l' intention est que ce soit le cas. Bien que ce ne soit pas strictement garanti, c'est en fait un indice assez fort. Éditera pour être plus clair.
Jon Skeet
1
Je ne suis pas d'accord avec l'appel à System.gc (). C'est un indice, c'est tout. Pas même "il faut espérer que ça fera quelque chose". Vous ne devriez jamais l'appeler. C'est de la programmation, pas de l'art.
gyorgyabraham
13
@gyabraham: Oui, c'est un indice - mais c'est celui que j'ai observé être généralement pris. Donc, si vous n'aimez pas utiliser System.gc(), comment proposez-vous de minimiser la collecte des ordures dans un test en raison des objets créés dans les tests précédents? Je suis pragmatique, pas dogmatique.
Jon Skeet
9
@gyabraham: Je ne sais pas ce que vous entendez par "grand repli". Pouvez-vous élaborer, et encore - avez-vous une proposition pour donner de meilleurs résultats? J'ai dit explicitement que ce n'était pas une garantie ...
Jon Skeet
48

jmh est un ajout récent à OpenJDK et a été écrit par certains ingénieurs de performance d'Oracle. Vaut vraiment le coup d'oeil.

Le jmh est un harnais Java pour la construction, l'exécution et l'analyse de benchmarks nano / micro / macro écrits en Java et dans d'autres langages ciblant la JVM.

Des informations très intéressantes enfouies dans les exemples de commentaires de tests .

Voir également:

assylias
la source
1
Voir également cet article de blog: psy-lob-saw.blogspot.com/2013/04/… pour plus de détails sur la mise en route de JMH.
Nitsan Wakart
Pour info, JEP 230: Microbenchmark Suite est une proposition OpenJDK basée sur ce projet Java Microbenchmark Harness (JMH) . N'a pas fait la coupe pour Java 9 mais peut être ajouté plus tard.
Basil Bourque
23

Le repère devrait-il mesurer le temps / itération ou les itérations / temps, et pourquoi?

Cela dépend de ce vous essayez de tester.

Si vous êtes intéressé par la latence , utilisez le temps / itération et si vous êtes intéressé par le débit , utilisez les itérations / temps.

Peter Lawrey
la source
16

Si vous essayez de comparer deux algorithmes, faites au moins deux tests de référence pour chacun, en alternant l'ordre. c'est à dire:

for(i=1..n)
  alg1();
for(i=1..n)
  alg2();
for(i=1..n)
  alg2();
for(i=1..n)
  alg1();

J'ai trouvé des différences notables (5-10% parfois) dans l'exécution du même algorithme dans différentes passes ..

Assurez-vous également que n est très grand, de sorte que le temps d'exécution de chaque boucle soit d'au moins 10 secondes environ. Plus il y a d'itérations, plus les chiffres sont significatifs dans votre temps de référence et plus les données sont fiables.

Kip
la source
5
La modification naturelle de l'ordre influence le temps d'exécution. Les optimisations JVM et les effets de mise en cache vont fonctionner ici. Il vaut mieux «réchauffer» l'optimisation de la JVM, effectuer plusieurs exécutions et comparer chaque test dans une JVM différente.
Mnementh
15

Assurez-vous que vous utilisez en quelque sorte les résultats qui sont calculés dans du code de référence. Sinon, votre code peut être optimisé.

Peter Štibraný
la source
13

Il existe de nombreux pièges possibles pour l'écriture de micro-benchmarks en Java.

Premièrement: vous devez calculer avec toutes sortes d'événements qui prennent du temps plus ou moins aléatoires: garbage collection, effets de mise en cache (d'OS pour les fichiers et de CPU pour la mémoire), IO etc.

Deuxièmement: vous ne pouvez pas faire confiance à l'exactitude des temps mesurés pour des intervalles très courts.

Troisièmement: la JVM optimise votre code lors de l'exécution. Ainsi, différentes exécutions dans la même instance JVM seront de plus en plus rapides.

Mes recommandations: faites fonctionner votre benchmark quelques secondes, ce qui est plus fiable qu'un runtime sur des millisecondes. Réchauffez la JVM (signifie exécuter le benchmark au moins une fois sans mesurer, afin que la JVM puisse exécuter des optimisations). Et exécutez votre référence plusieurs fois (peut-être 5 fois) et prenez la valeur médiane. Exécutez chaque micro-benchmark dans une nouvelle instance JVM (appelez chaque benchmark nouveau Java) sinon les effets d'optimisation de la JVM peuvent influencer les tests en cours d'exécution. N'exécutez pas des choses qui ne sont pas exécutées dans la phase de préchauffage (car cela pourrait déclencher le chargement de classe et la recompilation).

Mnementh
la source
8

Il convient également de noter qu'il pourrait également être important d'analyser les résultats du micro-benchmark lors de la comparaison des différentes implémentations. Par conséquent, un test de signification doit être effectué.

Cela est dû au fait que l'implémentation Apeut être plus rapide pendant la plupart des exécutions du benchmark que l'implémentation B. Mais Apeut également avoir un écart plus élevé, de sorte que l'avantage de performance mesuré de Ane sera pas significatif par rapport àB .

Il est donc également important d'écrire et d'exécuter correctement un micro-benchmark, mais aussi de l'analyser correctement.

SpaceTrucker
la source
8

Pour ajouter aux autres excellents conseils, je tiens également compte des points suivants:

Pour certains processeurs (par exemple la gamme Intel Core i5 avec TurboBoost), la température (et le nombre de cœurs actuellement utilisés, ainsi que leur pourcentage d'utilisation) affectent la vitesse d'horloge. Étant donné que les processeurs sont synchronisés dynamiquement, cela peut affecter vos résultats. Par exemple, si vous avez une application monothread, la vitesse d'horloge maximale (avec TurboBoost) est plus élevée que pour une application utilisant tous les cœurs. Cela peut donc interférer avec les comparaisons de performances mono et multi-thread sur certains systèmes. Gardez à l'esprit que la température et les volatilités affectent également la durée de maintien de la fréquence Turbo.

Peut-être un aspect plus fondamental sur lequel vous avez un contrôle direct: assurez-vous de mesurer la bonne chose! Par exemple, si vous utilisez System.nanoTime()pour comparer un morceau de code particulier, placez les appels à l'affectation dans des endroits qui ont du sens pour éviter de mesurer des choses qui ne vous intéressent pas. Par exemple, ne faites pas:

long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");

Le problème est que vous n'obtenez pas immédiatement l'heure de fin lorsque le code est terminé. Essayez plutôt ce qui suit:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");
Sina Madani
la source
Oui, il est important de ne pas effectuer de travail indépendant dans la région chronométrée, mais votre premier exemple est toujours correct. Il n'y a qu'un seul appel à println, pas une ligne d'en-tête séparée ou quelque chose, et System.nanoTime()doit être évalué comme la première étape dans la construction de l'argument de chaîne pour cet appel. Il n'y a rien qu'un compilateur puisse faire avec le premier qu'il ne puisse pas faire avec le second, et ni l'un ni l'autre ne les encourage même à faire un travail supplémentaire avant d'enregistrer une heure d'arrêt.
Peter Cordes
7

http://opt.sourceforge.net/ Java Micro Benchmark - tâches de contrôle requises pour déterminer les caractéristiques de performances comparatives du système informatique sur différentes plates-formes. Peut être utilisé pour guider les décisions d'optimisation et comparer différentes implémentations Java.

Yuriy
la source
2
Semble simplement comparer le matériel JVM +, pas un morceau arbitraire de code Java.
Stefan L