Mesurer les performances d’un programme¶
L’objectif premier de cet exercice est de savoir instrumenter un code source Java de sorte à pouvoir faire certaines mesures lors de l’exécution du programme :
mesurer les temps d’exécution des différentes méthodes,
compter les appels aux différentes méthodes,
Un autre objectif de ce TP est de s’initier à la complexité algorithmique, simplement en comptant les instructions arithmétiques et les accès mémoire des programmes analysés.
La mécanique : comment/pourquoi ?¶
Pour faire des mesures comme le temps de traitement et le nombre d’appels effectifs à une méthode, pour un programme en cours d’exécution, il existe plusieurs techniques et outils.
note
Quel qu’il soit, un dispositif de mesure influe sur le système mesuré. Donc l’emploi d’outils de profilage a un impact sur l’exécution du programme.
Des profileurs sont des programmes capables d’intercepter les communications et appels du programme avec l’OS pour en retirer des informations de toutes sortes (nb d’appels, temps, utilisation mémoire fine : pile, tas, défauts de cache, …).
Dans le cas particulier de Java, le compilateur javac ne produit pas directement un programme exécutable, au contraire du C/C++ par exemple. L’exécution du programme en Java passe par une machine virtuelle et ainsi les profileurs génériques ne sont d’aucune utilité, sauf à profiler la machine virtuelle Java elle même, ce qui n’est a priori pas notre but.
Il existe toutefois des profileurs dédiés à Java, mais leur emploi est souvent trop complexe pour notre niveau d’expertise. Dans Idea, deux profileurs sont proposés par défaut : Java Flight Recorder et Async Profiler.
Sans profileur, on peut simplement instrumenter notre code en y insérant des instructions de mesure/comptage.
Toutefois, on veut modifier le moins possible le code à analyser ; ainsi l’insertion des outils ne compromettra pas ou peu la lisibilité et la remise en configuration de production.
La solution la plus simple pour analyser une méthode est de la passer en paramètre à une méthode profileuse qui pourra ainsi dissimuler les instructions d’analyse. Ainsi, si on imagine par exemple que, dans une classe A, on cherche à analyser une méthode
double oneMethod(double x){...}
alors, au lieu de l’appeler classiquement par exemple avec
double r = oneMethod(0.6);
on l’appellera, dans le cadre du profilage, par
double r = Profiler.analyse(A::oneMethod, 0.6);
La classe Profiler : pas à pas¶
On va donc créer une classe Profiler et lui ajouter une méthode analyse() ayant pour entête :
public static double analyse(Function<Double, Double> oneMethod, double x)
Les interfaces fonctionnelles¶
Il s’agit d’un concept avancé de Java que l’on abordera plus en détails en deuxième année. Pour simplifier, il s’agit de classes qui n’ont qu’une seule méthode abstraite. Elles nous permettent en particulier de passer en paramètre une référence de méthode et c’est cet aspect qui nous conduit à les employer ici.
La bibliothèque Java définit ainsi trois catégories d’interfaces :
les suppliers qui ne prennent aucun paramètre mais retournent un résultat. Exemple : IntSupplier, Double Supplier, etc.
Les consumers qui ne retournent rien mais prennent un seul paramètre. Exemple : IntConsumer, DoubleConsumer, etc.
les functions qui prennent un paramètre et retournent un résultat, comme dans l’exemple ci-dessus. Il existe aussi pas mal de prototypes prédéfinis de Functions aux noms plus exotiques les uns que les autres.
Enfin, il nous est aussi possible de définir nos propres interfaces si celles prédéfinies ne satisfont pas nos besoins particuliers.
Mesurer le temps d’exécution¶
Il existe, dans la classe System, une méthode nanotime() qui fournit le temps système, en nanosecondes (ce qui ne garantit pas une précision à la nanoseconde). On va l’utiliser pour concevoir 2 définitions d’une méthode timestamp() :
une version sans paramètre qui retourne le résultat de nanotime()
une version avec un paramètre clock0 de type long et qui renvoie une chaîne de caractères informative représentant le temps écoulé depuis clock0.
Cette méthode timestamp() peut alors être utilisée pour encadrer les appels aux méthodes dont on veut mesurer les temps d’exécution.
Note
Si on veut mesurer les temps d’exécution d’instructions qui ne constituent pas une méthode entière, on construira alors une méthode pour les encapsuler (les instructions).
À ce stade, on a donc un squelette de classe Profiler qui ressemble à
import java.util.function.Function;
public class Profiler {
public static double analyse(Function<Double, Double> oneMethod, double p){
double res = oneMethod.apply(p);
return res;
}
/**
* Si clock0 est >0, retourne une chaîne de caractères
* représentant la différence de temps depuis clock0.
* @param clock0 instant initial
* @return expression du temps écoulé depuis clock0
*/
public static String timestamp(long clock0) {
String result = null;
if (clock0 > 0) {
double elapsed = (System.nanoTime() - clock0) / 1e9;
String unit = "s";
if (elapsed < 1.0) {
elapsed *= 1000.0;
unit = "ms";
}
result = String.format("%.4g%s elapsed", elapsed, unit);
}
return result;
}
/**
* retourne l'heure courante en ns.
* @return
*/
public static long timestamp() {
return System.nanoTime();
}
}
Travail à réaliser
Reprendre le code du défi
Erdös
. Vous allez tout d’abord analyser le temps d’exécution de la méthodepublic static double traceSegments(double p){...}
Pour cela, afin de simplifier cette première mesure, nous avons modifié légèrement la structure du programme de sorte à ce que la méthode à analyser ne prenne qu’un paramètre. Pour cela, nous avons transformé le paramètre nombre de points en attribut de la classe (ce qui est d’ailleurs plus propre).
Ajouter les instructions nécessaires à la méthode Profiler::analyse de sorte à pouvoir mesurer le temps d’exécution de la méthode passée en paramètre.
Modifier le code du main() de la classe Erdos pour que l’analyse de la méthode traceSegments() ait bien lieu.
Ajouter une définition adéquate de la méthode Profiler::analyse qui permettrait maintenant de pouvoir mesurer le temps consommé par la méthode
public static boolean tireProba(double p)
Les grandeurs cumulées¶
Comme on vient de le constater, la technique que l’on a choisie fournit le temps d’exécution de chaque appel à la méthode analysée, pris individuellement. Il est cependant courant de vouloir mesurer le temps total consommé par l’ensemble des appels.
Pour cela, il nous suffit de raffiner un peu la classe Profiler et de lui ajouter, par exemple, un attribut static long globalTime. Cet attribut sera affecté à chaque appel de la méthode d’analyse.
Une simple méthode Profiler::getGlobalTime permettra de récupérer la valeur de l’attribut aux instants choisis.
Note
La même technique s’applique à d’autres grandeurs cumulées, comme par exemple le nombre d’appels d’une méthode particulière.
Travail à réaliser
Modifier la classe Profiler pour lui ajouter l’attribut globalTime
Modifier alors la méthode Profiler::analyse pour qu’elle tienne à jour la valeur de l’attribut globalTime à chaque appel.
Ajouter les méthodes Profiler::init et Profiler::getGlobalTime dont la définition est évidente.
Ajouter enfin les instruction nécessaires au main() du programme Erdos permettant de connaître le coût total, en millisecondes, de la méthode Erdos::tireproba.
Concevoir les modifications de la classe Profiler permettant de compter le nombre d’appel total de la méthode Erdos::tireproba.
Définir une interface fonctionnelle personnalisée¶
Dans le TP StdDraw, le défi Von Koch
ne comporte qu’une seule méthode (autre que main()). Il s’agit de la méthode récursive vonKoch, qui prend en paramètre : 1 int et 4 float et ne renvoie rien.
Un tel prototype n’existe pas parmi les interfaces prédéfinies du Java. Si on veut analyser notre méthode, et c’est tout particulièrement crucial dans le cas d’une méthode récursive, il nous faut définir une interface adaptée.
Dans ce cas particulier, on peut définir dans la classe Profiler l’interface fonctionnelle suivante, adaptée à notre cas :
@FunctionalInterface
interface IntFloat4Consumer {
void apply(int n, float xa, float ya, float xb, float yb);
}
Si vous avez bien compris le mécanisme, il devient clair que nous pouvons alors fournir une définition supplémentaire à la méthode Profiler::analyse, qui corresponde à cette interface :
public static void analyse(IntFloat4Consumer oneMethod, int n, float xa, float ya, float xb, float yb){....}
Travail à réaliser
Compléter les codes de la méthode ci-dessus de sorte à obtenir une analyse du nombre d’appels total à la méthode vonKonch et le temps total d’exécution qu’elle aura consommé.