Note - LAAS

publicité
Note sur les tests de performance
29 avril 2015
Cette note a pour but de vous initiez aux bonnes pratiques liées 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épendant du TP) et c’est à vous d’adapter
le travail pour le BE. Ce projet consiste à générer une liste d’éléments (Element est une classe contenant un attribut value de type entier), à ordonner
par ordre croissant puis à retourner le premier élément. Téléchargez d’abord
l’archive sort.tar.gz puis décompressez et compilez le code source avec votre
environnement de développement préféré ou simplement avec la ligne de commande
javac Sort/ *.java
Pour l’exécution, la fonction main de la classe LaunchTest attend comme
paramètre un entier qui représente la taille de la liste à générer. Pour tester,
vous pouvez lancer la commande :
java Sort.LaunchTest
7698908
Compilation conditionnelle
La compilation conditionnelle permet au compilateur d’ignorer ou de compiler certaines parties du code source selon un test effectué au moment de la
compilation. En java, il faut d’abord déclarer la condition avec
public static final boolean condition
et lui attribuer la valeur faux ou vrai. Bien entendu, cette valeur ne peut jamais être modifiée durant l’exécution à cause du mot clé final. Par conséquence,
le compilateur ne va pas générer les lignes de code qui dépendent 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 durant l’exécution.
public static final boolean printDebug = false;
public static final boolean printTable = false;
Regardez ensuite la méthode 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épendra 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ée et donc ne la génère pas.
L’utilité de la compilation conditionnelle est qu’elle permet de basculer entre
différents ‘modes’ d’utilisation du code source (i.e. débogage, test, etc). Pour voir
concrètement son effet, changez la valeur de printDebug à true, compilez puis
relancez la commande :
java Sort.LaunchTest
7698908
Bien entendu, pour faire les tests de performance, il est conseillé de mettre
toutes les lignes d’affichage (e.g. println) dans un test avec compilation conditionnelle.
Les fichiers jar
Un fichier jar est un fichier compressé permettant d’archiver un projet JAVA
afin de l’exécuter comme un programme indépendant. Pour générer 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ère ligne permet d’indiquer la classe qui contient la fonction main. Une
fois le fichier jar généré, il suffit de le lancer depuis un terminal. Par exemple :
java -Xmx1g -jar fichiertest.jar 7698908
La valeur 7698908 représente toujours le paramètre passé à la fonction main
(qu’on peut bien sûr modifier selon le test qu’on veut faire). Remarquez qu’on a
utilisé l’option -Xmx1g avec la commande java. A vous de chercher pourquoi..
Les scripts de test
Une fois l’archive de test générée, on peut utiliser un script pour lancer
l’exécutable .jar avec différentes configurations. Une méthode simple pour le
faire sera de préparer un script (fichier .sh par exemple) qui lance les différentes
configurations séquentiellement. Le fichier peut contenir par exemple :
java
java
java
java
-Xmx1g
-Xmx1g
-Xmx1g
-Xmx1g
-jar
-jar
-jar
-jar
fichiertest.jar
fichiertest.jar
fichiertest.jar
fichiertest.jar
2
91898
98123
98127
98298
>
>
>
>
91898.txt
98123.txt
98127.txt
98298.txt
Vous pouvez ainsi créer différents scripts et les lancer ensemble pour exploiter
le parallélisme mais vérifiez d’abord le nombre de threads que la machine offre
(à l’aide de la commande $ cat /proc/cpuinfo).
Tests de validité et tests de performance
Avant de rendre votre code 1 (aux enseignants, aux clients, ...) vous devez
vous assurer :
1. que le code développé est correct. On parle alors de tests de validité (ou
de tests fonctionnels).
2. que le code développé est efficace. On parle dans ce cas de tests de performance.
Tests de Validité : est-ce que ça marche ?
Lors des tests de validité vous devez vous assurer que les résultats fournis par
votre code correspondent bien aux résultats attendus. Pour cela, il est impératif
de considérer des cas d’applications variés. Avec ces tests de validité, vous devez
nous convaincre que vos algorithmes fonctionnent correctement.
Tests de Performance : comment ça marche ?
Lors des tests de performance, vous allez caractériser le fonctionnement de
votre code.
– Que veut-on mesurer ? Il convient tout d’abord de définir les paramètres
à mesurer, représentatifs du fonctionnement du code.
– Dans quels cas d’application ? Il est nécessaire de sélectionner (ou de créer)
des jeux de données représentatifs et d’être capables de justifier les choix
effectués.
– Comment faire les mesures ? Il faut outiller le code dont on veut évaluer
les performances sur chacun des jeux de données et récupérer les valeurs
mesurées. Voir les conseils ci-après.
– Que faire des mesures obtenues ? Vous devez produire un analyse critique
des valeurs mesurées. Ces analyses doivent permettre de caractériser les
performances d’un code donné pris isolément mais aussi de comparer les
performances relatives de codes entre eux. Pour cette étape 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ères et non routières). A vous d’imaginer des
cas d’applications variés sur ces différentes cartes.
Il faut couvrir suffisamment de cas pour être 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 être capables de justifier
vos choix de cas d’application.
1. on suppose que le code développé répond au problème posé. Dans le cas général, il faut
également s’en assurer !
3
Rapellez-vous vos cours de statistique (un test de chaque cas, c’est varié,
mais pas statistiquement valable).
Pour le 2eme livrable, vous devez expliciter les tests de validité et les tests
de performances que vous avez menés.
Mesure du temps d’exécution en Java
Compilation Just In Time (JIT)
Lors de sa compilation le code source java est compilé dans un format binaire
(les fichiers .class). Ce format binaire n’est pas directement exécutable sur le
processeur, il doit être interprété par une “Java Virtual Machine” (JVM).
Comme on peut s’y attendre, interpréter un format binaire est plus long
que de directement exécuter des instructions sur le processeur. Pour palier à ce
problème, la JVM a des mécanismes de compilation Just-In-Time : quand un
morceau du code (typiquement une fonction) est utilisé très souvent, il est compilé en instructions machine (c’est à dire qui n’ont pas besoin d’interprétation
par la JVM).
for(int i=0 ; i<1000 ; i++)
doSomething();
Sur l’example ci-dessus, il y a fort à parier que le premier appel de la fonction
doSomething() soit plus long le dernier parce que entre temps la JVM aura
compilé la fonction.
Pour rendre les choses encore plus complexes, la compilation JIT prend elle
aussi du temps de calcul. Pendant l’exécution de la boucle ci-dessus, une partie
du temps d’exécution sera dédié à la compilation.
Le ramasse miettes
Une autre particularité du Java est la gestion de la mémoire. Comme vous
avez pu le remarquer, vous créez de nouveaux objets avec le mot clé “new” (ce
qui revient à allouer de la mémoire). En revanche, vous n’avez jamais besoin de
libérer manuellement cette mémoire (l’équivalent des free/delete en C/C++).
Comme la mémoire a besoin d’être libérée, la JVM a un ramasse-miettes (ou
garbage collector/GC) qui se charge de libérer la mémoire des objets déréférencés
(c’est à 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émoire a été allouée (ii) détermine ceux qui ne sont plus utilisables (c’est à
dire déréférencés) (iii) libère la mémoire de ces objets.
Là encore cette opération est coûteuse en temps de calcul et il est difficile
de prévoir quand elle aura lieu.
1
2
3
4
5
6
7
i n t s t a r t D i j k s t r a = System . g e t C u r r e n t T i m e M i l l i s ( ) ;
dijkstra ();
i n t e n d D i j k s t r a = System . g e t C u r r e n t T i m e M i l l i s ( ) ;
i n t s t a r t A S t a r = System . g e t C u r r e n t T i m e M i l l i s ( ) ;
aStar ( ) ;
i n t endAStar = System . g e t C u r r e n t T i m e M i l l i s ( ) ;
4
Dans le bout de programme ci-dessus la procédure dijkstra() va créer beaucoup d’objets (label du tas ...). L’appel suivant au ramasse miettes (après la
procédure dijkstra) libérera la mémoire de tout ces objets. On peut distinguer
plusieurs cas :
– Le ramasse-miettes est invoqué entre les lignes 2-3. Dans ce cas le coût de
nettoyer la mémoire du Dijkstra est comptabilisé dans le temps d’exécution
du Dijkstra.
– Le ramasse-miette est invoqué entre les lignes 5-7. Dans ce cas, le coût
du nettoyage de la mémoire du Djikstra est imputé au A* (ce qui est
problématique).
– Le ramasse-miettes est invoqué entre les lignes 3-5 ou après la ligne 7.
Dans ce cas le temps de travail du ramasse-miettes n’est pas comptabilisé.
La JVM dispose d’un appel système pour forcer l’appel du ramasse-miettes :
System.gc(). Cette méthode peut être utilisée pour s’assurer que la mémoire
du précédent algorithme a bien été nettoyé avant d’invoquer le second.
Conseils pratiques
Assurez vous que la JVM a bien eu le temps de faire ses optimisations (compilation JIT) avant d’effectuer des mesures de performance. Un manière simple
de faire ça consiste à faire tourner plusieurs fois le même algorithme et de mesurer son temps d’exécution uniquement pour la dernière exécution.
Assurez vous que le nettoyage de la mémoire allouée par un algorithme n’est
pas comptabilisé dans le temps d’exécution d’un autre algorithme. Des appels à
System.gc() permet de forcer l’appel au ramasse miettes à un point précis du
programme.
5
Téléchargement