• Aucun résultat trouvé

Squelette du micro-benchmark

3.3 Implémentation d'un micro-benchmark

3.3.1 Squelette du micro-benchmark

Cette section expose une implémentation minimale et générique de micro-benchmark dont la base est reprise dans les tests de performance eectués dans les chapitres suivants. L'implémentation proposée n'utilise typiquement que deux méthodes, avec un niveau d'ité-ration en guise de warmup :

1. La méthode mesurant la durée d'exécution. Dénommée compute, elle encapsule le code testé et retourne une durée d'exécution. Elle est décrite Section 3.3.1.1 ; 2. La méthode chargée du warmup. Dénommée getBestTime, elle itère sur compute et

retourne la durée d'exécution utilisée pour le calcul des performance. Elle est décrite Section 3.3.1.2.

3.3.1.1 Méthode mesurant la durée d'exécution

Les mesures sont eectuées en utilisant des timers dans la méthode compute (cf. Table 3.1) qui encapsule le code à tester. Son rôle est de retourner avec le plus d'exactitude possible le temps d'exécution du code testé.

Le timer utilisé est la méthode System.nanoTime du JDK dont le fonctionnement et l'exac-titude sont détaillés Section 3.3.3.

Lorsque la durée d'exécution du code est du même ordre de grandeur que l'incertitude

3Le terme "timer" est un anglicisme désignant les compteurs de cycles d'horloge utilisant l'unité ma-tériel dédiée

sur la mesure, à savoir de l'ordre de la dizaine de nano-seconde (cf. Section 3.3.3.2), il est nécessaire d'itérer sur le code via une boucle an de réduire l'erreur relative répercutée sur la mesure. Un nombre important d'itération permet d'augmenter la justesse de la mesure. La boucle d'itération ajoute néanmoins des opérations à chaque itération (une addition, une comparaison et un branchement conditionnel) qui diminuent la justesse de la mesure en ajoutant un biais. L'erreur relative due à ce biais diminue quand la durée d'exécution du code testé augmente. À ces opérations peuvent s'ajouter des opérations de chargement et déchargement des registres, dont la présence dépend du code testé4. Enn la boucle ajoute également des opérations en entrée de boucle mais dont le coût relatif est négligeable car amorti rapidement par les itérations de boucle.

compute avec boucle d'itération compute sans boucle d'itération

static long compute (...){

long t0 = System . nanoTime ();

for(int i = 0; i < IT; ++i){

// Code a mesurer

}

long t1 = System . nanoTime ();

return (t1 - t0 ); }

static long compute (...){

long t0 = System . nanoTime ();

// Code a mesurer

long t1 = System . nanoTime ();

return (t1 - t0 ); }

Tab. 3.1: Méthode compute retournant la durée d'exécution du bloc de code étudié. La boucle d'itération est nécessaire lorsque la durée d'exécution du code à mesurer est du même ordre de grandeur que l'incertitude sur la mesure fournie par System.nanoTime. Un grand nombre d'itération peut permettre d'augmenter la justesse de la mesure

3.3.1.2 Méthode chargée du warmup

La performance mesurée par le micro-benchmark est calculée à partir de la valeur retournée par la méthode getBestTime (cf. Figure 3.1). Cette dernière renvoie la durée minimale d'exécution de compute parmi WU itérations (cf. Figure 3.1 pour le paramètre WU) autrement dit la durée qui maximise la performance.

La méthode getBestTime permet de garantir via une multitude d'itérations (valant plus exactement WU) que la méthode compute qui contient le code testé sera compilée par C2. Elle est donc en charge du warmup. Par ailleurs, les durées d'exécution passées dans les états transitoires ne sont pas prises en compte dans le résultat nal étant donné que la durée d'exécution minimale est considérée. Elle permet également d'atteindre, tant que possible, un état stable en considérant diérents paramètres matériels tels que les eets de cache lorsque le code lit ou écrit des données en mémoire et la prédiction de branchement lorsque le code testé contient des branchements conditionnels.

4Par exemple lorsque le code testé est un appel de méthode nécessitant la libération puis la restauration de registres pour respecter les conventions d'appel

Variance sur les mesures Le code testé et ses données d'entrée étant invariants à chaque exécution, les variations de mesure sur la série d'itération de compute sont consi-dérées comme du bruit. Ce bruit correspond à la variance sur la valeur retournée et son importance varie suivant les causes. La variance provenant du timer reste négligeable. Elle est plus signicative lorsqu'elle est causée par de la prédiction de branchement ou bien des eets de cache. Elle peut être due également à des valeurs aberrantes ponctuelles dans la suite des mesures pouvant provenir des interactions avec le système. Ces dernières re-quièrent un traitement statistique plus approfondi pour être éliminées. Considérer la durée minimale parmi une multitude d'itérations (typiquement plusieurs milliers) comme fait par getBestTime (cf. Figure 3.1) permet d'éliminer ce bruit. On obtient ainsi une valeur représentative de ce que l'on veut mesurer, à savoir un débit d'exécution lorsque le che-mins d'exécution emprunté et les données sont constants. Le nombre d'itération (xé par le paramètre WU) ne doit cependant pas être trop élevé ce qui peut amener dans certains cas avec une forte variance à considérer une valeur exceptionnellement grande et donc non représentative.

Une variation des mesures peut également apparaître lorsque le benchmark est relancé dans une nouvelle JVM (i.e. dans un nouveau processus). Son importance dépend du contenu du code testé dont la performance peut avoir une forte sensibilité aux conditions initiales (alignement du code, alignement des données, état du système...)5. Comme le code exécuté et les données d'entrée sont invariants, on admet que cette variance implique une sensibilité aux conditions initiales. Dans ce cas, l'écart-type sur la série de mesures produites en itérant les instances de benchmark est pris en considération et le résultat de performance correspond à la moyenne de la série de mesure.

static long getBestTime (...){

long bt = Long .MAX_VALUE;

for(int i = 0; i < WU; ++i){

long t = compute (...); if(t < bt) bt = t; } return bt; }

Fig. 3.1: Méthode getBestTime retournant la durée minimale d'exécution de compute. Cette dernière sert de warmup. par ailleurs, la prise en compte du minimum permet de réduire la variance sur les mesures. La durée retournée est utilisée pour le calcul de la performance

5Cette observation a été faite sur du code contenant une forte dispersion au niveau des branchements conditionnels dans l'étude du polymorphisme Chapitre 4.

Inlining de compute La méthode getBestTime n'est typiquement exécutée qu'une fois par instance de benchmark. Cependant elle peut être compilée par OSR (cf. Section 2.3.2.2 Chap. 2) si le paramètre WU est susamment grand (cf. Figure 3.1). Si cette dernière est compilée par OSR, compute peut être inlinée ce qui peut biaiser les mesures en entrelaçant des opérations entre les timers ou en permettant des optimisations modiant l'état du code. Ainsi la conguration des microbenchmarks garantit que la méthode compute n'est pas inlinée par le JIT.

Métrique de performance La performanceperf, exprimée en opérations par seconde (Ops/s), est calculée par la formule perf = nbop×IT /dt ou nbop est le nombre d'opé-rations exécutées par le code (la nature d'une opération est arbitraire cf. Section 2.1.2.3 Chap. 2),IT le nombre d'itérations eectuées sur le code entre les timers (cf. Table 3.1) et

dtla durée minimale en seconde (s) retournée par compute pour exécuter lesIT itérations.