Note sur les tests de performance
29 avril 2015
Cette note a pour but de vous initiez aux bonnes pratiques li´ees aux tests
de performance ; en particulier, on va parler de compilation conditionnelle, de
fichiers jar et de script de tests. Tout au long de cette note, on va utiliser un
(mini) projet JAVA (totalement ind´ependant du TP) et c’est `a vous d’adapter
le travail pour le BE. Ce projet consiste `a g´en´erer une liste d’´el´ements (Ele-
ment est une classe contenant un attribut value de type entier), `a ordonner
par ordre croissant puis `a retourner le premier ´el´ement. T´el´echargez d’abord
l’archive sort.tar.gz puis d´ecompressez et compilez le code source avec votre
environnement de d´eveloppement pr´ef´er´e ou simplement avec la ligne de com-
mande
javac Sort/ *.java
Pour l’ex´ecution, la fonction main de la classe LaunchTest attend comme
param`etre un entier qui repr´esente la taille de la liste `a g´en´erer. Pour tester,
vous pouvez lancer la commande :
java Sort.LaunchTest 7698908
Compilation conditionnelle
La compilation conditionnelle permet au compilateur d’ignorer ou de com-
piler certaines parties du code source selon un test effectu´e au moment de la
compilation. En java, il faut d’abord d´eclarer la condition avec
public static final boolean condition
et lui attribuer la valeur faux ou vrai. Bien entendu, cette valeur ne peut ja-
mais ˆetre modifi´ee durant l’ex´ecution `a cause du mot cl´e final. Par cons´equence,
le compilateur ne va pas g´en´erer les lignes de code qui d´ependent de la condition.
A titre d’exemple, dans la classe Constants, on trouve les deux variables
printDebug et printTable qui assurent l’affichage de quelques informations du-
rant l’ex´ecution.
public static final boolean printDebug = false;
public static final boolean printTable = false;
Regardez ensuite la m´ethode public int compareTo(Element e) de la classe
Element
1
public int compareTo(Element e) {
if (Constants.printDebug)
System.out.println(" comparison between "+ this + " and "+ e);
return this.value - e.value;
}
On constate que la ligne de code
System.out.println(" comparison between "+ this + " and "+ e);
d´ependra de la valeur de Constants.printDebug. Comme cette constante vaut
‘false’, le compilateur va ignorer cette ligne puisqu’il sait d’avance qu’elle ne
sera jamais appel´ee et donc ne la g´en`ere pas.
L’utilit´e de la compilation conditionnelle est qu’elle permet de basculer entre
diff´erents ‘modes’ d’utilisation du code source (i.e. d´ebogage, test, etc). Pour voir
concr`etement son effet, changez la valeur de printDebug `a true, compilez puis
relancez la commande :
java Sort.LaunchTest 7698908
Bien entendu, pour faire les tests de performance, il est conseill´e de mettre
toutes les lignes d’affichage (e.g. println) dans un test avec compilation condi-
tionnelle.
Les fichiers jar
Un fichier jar est un fichier compress´e permettant d’archiver un projet JAVA
afin de l’ex´ecuter comme un programme ind´ependant. Pour g´en´erer le fichier jar
de notre projet, il suffit de lancer depuis le projet dans Eclipse/Netbeans :
”Export..” ou bien avec les deux lignes de commandes suivantes :
echo "Main-Class: Sort.LaunchTest" > Manifest.txt
jar cfm ./fichiertest.jar Manifest.txt Sort/*
La premi`ere ligne permet d’indiquer la classe qui contient la fonction main. Une
fois le fichier jar g´en´er´e, il suffit de le lancer depuis un terminal. Par exemple :
java -Xmx1g -jar fichiertest.jar 7698908
La valeur 7698908 repr´esente toujours le param`etre pass´e `a la fonction main
(qu’on peut bien sˆur modifier selon le test qu’on veut faire). Remarquez qu’on a
utilis´e l’option -Xmx1g avec la commande java. A vous de chercher pourquoi..
Les scripts de test
Une fois l’archive de test g´en´er´ee, on peut utiliser un script pour lancer
l’ex´ecutable .jar avec diff´erentes configurations. Une m´ethode simple pour le
faire sera de pr´eparer un script (fichier .sh par exemple) qui lance les diff´erentes
configurations s´equentiellement. Le fichier peut contenir par exemple :
java -Xmx1g -jar fichiertest.jar 91898 > 91898.txt
java -Xmx1g -jar fichiertest.jar 98123 > 98123.txt
java -Xmx1g -jar fichiertest.jar 98127 > 98127.txt
java -Xmx1g -jar fichiertest.jar 98298 > 98298.txt
2
Vous pouvez ainsi cr´eer diff´erents scripts et les lancer ensemble pour exploiter
le parall´elisme mais v´erifiez d’abord le nombre de threads que la machine offre
(`a l’aide de la commande $ cat /proc/cpuinfo).
Tests de validit´e et tests de performance
Avant de rendre votre code 1(aux enseignants, aux clients, ...) vous devez
vous assurer :
1. que le code d´evelopp´e est correct. On parle alors de tests de validit´e (ou
de tests fonctionnels).
2. que le code d´evelopp´e est efficace. On parle dans ce cas de tests de perfor-
mance.
Tests de Validit´e : est-ce que ¸ca marche ?
Lors des tests de validit´e vous devez vous assurer que les r´esultats fournis par
votre code correspondent bien aux r´esultats attendus. Pour cela, il est imp´eratif
de consid´erer des cas d’applications vari´es. Avec ces tests de validit´e, vous devez
nous convaincre que vos algorithmes fonctionnent correctement.
Tests de Performance : comment ¸ca marche ?
Lors des tests de performance, vous allez caract´eriser le fonctionnement de
votre code.
Que veut-on mesurer ? Il convient tout d’abord de d´efinir les param`etres
`a mesurer, repr´esentatifs du fonctionnement du code.
Dans quels cas d’application ? Il est n´ecessaire de s´electionner (ou de cr´eer)
des jeux de donn´ees repr´esentatifs et d’ˆetre capables de justifier les choix
effectu´es.
Comment faire les mesures ? Il faut outiller le code dont on veut ´evaluer
les performances sur chacun des jeux de donn´ees et r´ecup´erer les valeurs
mesur´ees. Voir les conseils ci-apr`es.
Que faire des mesures obtenues ? Vous devez produire un analyse critique
des valeurs mesur´ees. Ces analyses doivent permettre de caract´eriser les
performances d’un code donn´e pris isol´ement mais aussi de comparer les
performances relatives de codes entre eux. Pour cette ´etape d’analyse,
posez-vous la question de ce que signifie ”nombre de chiffres significatifs”.
Les cas d’application pour les deux types de test
Pour ce BE et pour les algorithmes de Disjktra et Disjktra-AStar, nous vous
proposons plusieurs cartes (routi`eres et non routi`eres). A vous d’imaginer des
cas d’applications vari´es sur ces diff´erentes cartes.
Il faut couvrir suffisamment de cas pour ˆetre convaincants (trajets dans
les deux sens, trajets courts, trajets longs, trajets impossibles, comparaison du
trajet ABC et des trajets AB et BC, etc.) Vous devez ˆetre capables de justifier
vos choix de cas d’application.
1. on suppose que le code d´evelopp´e r´epond au probl`eme pos´e. Dans le cas g´en´eral, il faut
´egalement s’en assurer !
3
Rapellez-vous vos cours de statistique (un test de chaque cas, c’est vari´e,
mais pas statistiquement valable).
Pour le 2eme livrable, vous devez expliciter les tests de validit´e et les tests
de performances que vous avez men´es.
Mesure du temps d’ex´ecution en Java
Compilation Just In Time (JIT)
Lors de sa compilation le code source java est compil´e dans un format binaire
(les fichiers .class). Ce format binaire n’est pas directement ex´ecutable sur le
processeur, il doit ˆetre interpr´et´e par une “Java Virtual Machine” (JVM).
Comme on peut s’y attendre, interpr´eter un format binaire est plus long
que de directement ex´ecuter des instructions sur le processeur. Pour palier `a ce
probl`eme, la JVM a des m´ecanismes de compilation Just-In-Time : quand un
morceau du code (typiquement une fonction) est utilis´e tr`es souvent, il est com-
pil´e en instructions machine (c’est `a dire qui n’ont pas besoin d’interpr´etation
par la JVM).
for(int i=0 ; i<1000 ; i++)
doSomething();
Sur l’example ci-dessus, il y a fort `a parier que le premier appel de la fonction
doSomething() soit plus long le dernier parce que entre temps la JVM aura
compil´e la fonction.
Pour rendre les choses encore plus complexes, la compilation JIT prend elle
aussi du temps de calcul. Pendant l’ex´ecution de la boucle ci-dessus, une partie
du temps d’ex´ecution sera d´edi´e `a la compilation.
Le ramasse miettes
Une autre particularit´e du Java est la gestion de la m´emoire. Comme vous
avez pu le remarquer, vous cr´eez de nouveaux objets avec le mot cl´e “new” (ce
qui revient `a allouer de la m´emoire). En revanche, vous n’avez jamais besoin de
lib´erer manuellement cette m´emoire (l’´equivalent des free/delete en C/C++).
Comme la m´emoire a besoin d’ˆetre lib´er´ee, la JVM a un ramasse-miettes (ou
garbage collector/GC) qui se charge de lib´erer la m´emoire des objets d´er´ef´erenc´es
(c’est `a dire qui n’ont plus de pointeurs sur eux et ne sont donc plus utilisables).
De temps en temps, le ramasse miettes (i) regarde tous les objets a qui de
la m´emoire a ´et´e allou´ee (ii) d´etermine ceux qui ne sont plus utilisables (c’est `a
dire d´er´ef´erenc´es) (iii) lib`ere la m´emoire de ces objets.
L`a encore cette op´eration est coˆuteuse en temps de calcul et il est difficile
de pr´evoir quand elle aura lieu.
1 i n t s t a r t D i j k s t r a = System . g etCurrentTime M i l li s ( ) ;
2 dijkstra ();
3 i n t e n d D i j k s t r a = System . g e t C u r r e n tT i m e M i l l is ( ) ;
4
5 i n t s t a r tAS t a r = System . g e t C u r r e n t T i meMillis ( ) ;
6 aSt a r ( ) ;
7 i n t endAStar = System . g e tCurrentT i m e M i l l i s ( ) ;
4
Dans le bout de programme ci-dessus la proc´edure dijkstra() va cr´eer beau-
coup d’objets (label du tas ...). L’appel suivant au ramasse miettes (apr`es la
proc´edure dijkstra) lib´erera la m´emoire de tout ces objets. On peut distinguer
plusieurs cas :
Le ramasse-miettes est invoqu´e entre les lignes 2-3. Dans ce cas le coˆut de
nettoyer la m´emoire du Dijkstra est comptabilis´e dans le temps d’ex´ecution
du Dijkstra.
Le ramasse-miette est invoqu´e entre les lignes 5-7. Dans ce cas, le coˆut
du nettoyage de la m´emoire du Djikstra est imput´e au A* (ce qui est
probl´ematique).
Le ramasse-miettes est invoqu´e entre les lignes 3-5 ou apr`es la ligne 7.
Dans ce cas le temps de travail du ramasse-miettes n’est pas comptabilis´e.
La JVM dispose d’un appel syst`eme pour forcer l’appel du ramasse-miettes :
System.gc(). Cette m´ethode peut ˆetre utilis´ee pour s’assurer que la m´emoire
du pr´ec´edent algorithme a bien ´et´e nettoy´e avant d’invoquer le second.
Conseils pratiques
Assurez vous que la JVM a bien eu le temps de faire ses optimisations (com-
pilation JIT) avant d’effectuer des mesures de performance. Un mani`ere simple
de faire ¸ca consiste `a faire tourner plusieurs fois le mˆeme algorithme et de me-
surer son temps d’ex´ecution uniquement pour la derni`ere ex´ecution.
Assurez vous que le nettoyage de la m´emoire allou´ee par un algorithme n’est
pas comptabilis´e dans le temps d’ex´ecution d’un autre algorithme. Des appels `a
System.gc() permet de forcer l’appel au ramasse miettes `a un point pr´ecis du
programme.
5
1 / 5 100%