I. Le chrono et la JVM▲
Comment mesurer la performance d'un bout de code ? Le microbenchmarking, puisque c'est de cela qu'il s'agit, est une tâche en apparence simple. Prenons, par exemple, la fonction mathématique “logarithme” : mesurer le temps de calcul du logarithme d'un flottant en Java ne semble pas très compliqué ; un simple “chronométrage” en utilisant la date système dans un main(), et le tour est joué !
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public
class
MyBenchmark {
public
static
void
main
(
String[] args) {
// start stopwatch
long
startTime =
System.nanoTime
(
);
// Here is the code to measure
double
log42 =
Math.log
(
42
);
// stop stopwatch
long
endTime =
System.nanoTime
(
);
System.out.println
(
"log(42) is computed in : "
+
(
endTime -
startTime) +
" ns"
);
}
}
Malheureusement pour nous, les choses ne sont pas aussi simples. En effet, les temps que nous pouvons ainsi mesurer sont d'une part assez variables, et d'autre part pas forcement représentatifs de la réalité ; à cela, plusieurs raisons :
- la JVM n'exécute pas notre code tel qu'il est écrit : le JIT au runtime peut optimiser le code Java, réordonner les instructions, voire carrément supprimer des instructions inutiles (c'est typiquement ce qui arrive ici : la variable log42 étant inutilisée !) ;
- par ailleurs, elle n'exécute pas un même code de façon déterministe à chaque exécution : le JIT peut en effet décider de compiler à la volée le bytecode en code natif, au lieu de l'interpréter (par défaut, au 10 000ème appel) ;
- enfin, la charge physique de la machine (CPU, mémoire, autres process…) au moment du run peut varier dans le temps selon son utilisation globale, et ralentir le programme. Se baser sur une seule mesure et espérer un résultat représentatif est donc illusoire…
Mais alors, comment mesurer les performances de notre code ?
II. The right tool for the right job▲
Heureusement, il existe des outils apportant une solution aux problèmes ci-dessus ; JMHJMH en est un. Il s'agit d'un outil libre, léger, et plutôt simple à prendre en main. Voici donc, en quelques mots, les étapes qu'il vous faudra suivre pour écrire votre premier benchmark.
II-A. Déballage▲
Dans une boite de JMH, on trouve :
- un générateur de projet de benchmark JMH (l'archetype Maven jmh-java-benchmark-archetype) ;
- une collection d'annotations, pour configurer votre benchmark ;
- un Runner, pour exécuter votre benchmark ;
- une notice d'utilisation (une javadoc et des exemples de code).
II-B. Installation de JMH▲
III. Création d'un projet JMH▲
Pour générer le projet de benchmark, utilisez l'archetype jmh-java-benchmark-archetype :
2.
3.
4.
5.
6.
7.
8.
$ mvn archetype:generate \
-
DinteractiveMode=
false
\
-
DarchetypeGroupId=
org.openjdk.jmh \
-
DarchetypeArtifactId=
jmh-
java-
benchmark-
archetype \
-
DarchetypeVersion=
1.5.2
\
-
DgroupId=
fr.soat \
-
DartifactId=
jmh-
sample-
benchmak \
-
Dversion=
1.0
-
SNAPSHOT
L'archetype génère un projet JAR appelé jmh-sample-benchmak, contenant un pom.xml, déclarant les dépendances vers les JARs de JMH et les plugins nécessaires au build.
III-A. Écriture du BENCHMARK▲
L'archetype a par ailleurs généré un squelette de classe de benchmark, appelé MyBenchmark, dans lequel on retrouve une méthode testMethod(),annotée par un @Benchmark indiquant à JMH où se trouve le code à benchmarker :
2.
3.
4.
5.
6.
7.
8.
9.
public
class
MyBenchmark {
@Benchmark
public
void
testMethod
(
) {
// This is a demo/sample template for building your JMH benchmarks. Edit as needed.
// Put your benchmark code here.</h4>
}
}
On y écrira une ou plusieurs méthodes ainsi annotées, à la manière d'une classe de test JUnit ; chacune d'entre elles sera benchmarkée par JMH. On pourra ainsi comparer différents codes. Je prendrai comme exemple le calcul du logarithme, en utilisant différentes librairies :
- java.lang.Math.log() du JDK8 ;
- org.apache.commons.math3.util.FastMath.log() d'Apache commons Maths ;
- odk.lang.FastMath.log() de javafama ;
- odk.lang.FastMath.logQuick() de javafama également.
On obtient ainsi le code du benchmark suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
public
class
MyBenchmark {
@Benchmark
public
double
benchmark_logarithm_jdk
(
) {
return
java.lang.Math.log
(
42
);
}
@Benchmark
public
double
benchmark_logarithm_apache_common
(
) {
return
org.apache.commons.math3.util.FastMath.log
(
42
);
}
@Benchmark
public
double
benchmark_logarithm_jafama
(
) {
return
odk.lang.FastMath.log
(
42
);
}
@Benchmark
public
double
benchmark_logarithm_jafama_logQuick
(
) {
return
odk.lang.FastMath.logQuick
(
42
);
}
}
III-B. Build du projet▲
Avant de lancer le benchmark, il faut bien sûr faire un build Maven du projet, pour générer du code technique, l'assembler au Runner JMH, et empaqueter le tout dans un “uber” JAR benchmark.jar exécutable :
jmh-
sample-
benchmark$ mvn clean package
III-C. Exécution du BENCHMARK▲
À présent, nous allons exécuter le JAR pour démarrer le benchmark :
jmh-
sample-
benchmark$$ java -
jar target/
benchmark.jar
Voilà ! Le benchmark tourne. Les logs d'exécution s'affichent sur la sortie standard, et donnent finalement les résultats obtenus :
Benchmark |
Mode |
Cnt |
Score |
Error |
Units |
MyBenchmark.benchmark_logarithm_apache_common |
thrpt |
200 |
40,112 |
± 0,113 |
ops/us |
MyBenchmark.benchmark_logarithm_jafama |
thrpt |
200 |
95,502 |
± 0,255 |
ops/us |
MyBenchmark.benchmark_logarithm_jafama_logQuick |
thrpt |
200 |
142,486 |
± 0,604 |
ops/us |
MyBenchmark.benchmark_logarithm_jdk |
thrpt |
200 |
341,494 |
± 3,196 |
ops/us |
On obtient, par ligne, le résultat de chaque méthode testée. Le contenu des colonnes nous donne :
- le Mode de benchmark, désignant le type de mesures réalisées : ici thrpt (pour Troughput), c'est-à-dire un débit moyen d'opérations (opérations exécutées par unité de temps) ;
- Cnt (pour count) nous donne le nombre de mesures réalisées pour calculer notre score : ici, 200 mesures réalisées ;
- Score désigne la valeur du throughput moyen calculé ;
- Error représente la marge d'erreur de ce score ;
- Units est l'unité de mesure dans laquelle est affiché le score : ici opérations par microseconde.
Sur notre benchmark, java.lang.Math.log() du JDK8 obtient le meilleur résultat, avec une moyenne de 341,494 opérations par seconde !
IV. Conclusion▲
Nous venons de voir en quelques lignes les fonctionnalités de base de JMH, qui vous permettront de réaliser votre premier banc d'essai.
Ce benchmark du logarithme, qui illustre l'utilisation de JMH, n'est cependant pas très sérieux. On peut en effet en faire plusieurs critiques :
- Qu'est-ce qui nous permet de dire que les mesures faites sont représentatives ?
- Le débit “moyen” est-il un indicateur suffisant pour affirmer qu'il faut toujours utiliser le log() du JDK ? Est-il à chaque fois meilleur ?
- Il a été meilleur pour calculer log(42), mais reste-t-il le meilleur pour calculer log(42.5), log(0.0000001) ou log(10000000) ?
Dans un prochain article, nous verrons comment configurer plus finement JMH (“warm up”, cycles d'itérations…), quels autres indicateurs statistiques nous pouvons obtenir, et comment les interpréter pour en tirer des conclusions intéressantes.