• Aucun résultat trouvé

3.5 Conclusion

4.1.1 Contenu du code mesuré

Version sans timers

static long compute ( AbstractType [] data ){

long t0 = System . nanoTime ();

int n = data .length;

for (int i = 0; i < n; ++i) data [i]. method ();

long t1 = System . nanoTime ();

long dt = (t1 - t0 );

return dt; }

Version avec timers

Fig. 4.1: Méthode compute implémentant le pattern appel polymorphique dans une boucle. La seconde version est la version avec timer mesurant la durée d'exécution servant au calcul de la performance n/dt

fondamental an de, un, garantir la généricité du benchmark (i.e. son indépendance vis à vis d'un traitement spécique), deux, se placer dans un cas d'utilisation du polymorphisme critique et, trois, avoir une mesure qui soit juste. Une description détaillée des méthodes intervenant est faite Section 4.1.1.

4.1.1 Contenu du code mesuré

Cette section détaille le contenu des méthode impliquées dans le micro-benchmark (montré Figure 4.1) à savoir la méthode method Section 4.1.1.1 et la méthode compute Section 4.1.1.2.

4.1.1.1 Méthodes virtuelles ciblées

Dans ce type de pattern, l'utilisation du polymorphisme ne peut être remise en ques-tion que si le coût relatif de l'appel n'est pas négligeable par rapport à celui des méthodes ciblées. Ainsi une condition susante1 pour que le polymorphisme soit problématique est que le coût d'appel ne soit pas entièrement masqué par le coût des méthodes ciblées2. Pour satisfaire cette condition, on se place dans un cas extrême où toutes les versions de la méthode method sont vides. Le coût relatif de l'appel par rapport aux méthodes appelées est alors théoriquement maximal.

Une des conséquences de l'utilisation de méthodes vides est que l'inlining doit être désac-tivé. Autrement, la boucle et le site d'appel dans compute seraient éliminés à la compilation et la mesure perdrait son sens. On peut considérer que la désactivation de l'inlining n'est pas représentative d'un cas ou le polymorphisme est problématique, car les méthodes ci-blées (supposées de faible complexité relativement au site d'appel), seraient probablement inlinées par le JIT. Ainsi, pour que le benchmark soit représentatif, on doit s'assurer que le site d'appel avec ou sans inlining est similaire. Cette similarité signie que le site d'appel sans inlining est équivalent au site d'appel avec inlining excepté que le corps des méthodes inlinées est remplacé par une instruction d'appel ; le dispatch demeurant similaire. Cette hypothèse est vraie, excepté dans le cas bimorphique esposé plus tard où l'inlining peut modier l'état du site d'appel (cf. Figure 4.9).

Une autre approche peut être plus judicieuse, aurait été d'eectuer les tests en activant l'inlining mais avec des méthodes cibles eectuant une opération élémentaire comme in-crémenter un champ de classe statique pour éviter l'élimination de code mort. L'avantage étant que le coût de la méthode aurait été moins important. En eet, une méthode vide contient des opérations constantes comprenant la création et la restauration du contexte sur la pile ainsi qu'une barrière de synchronisation (ou safepoint, cf. Section 2.3.1.4) avant retour. Le contenu d'une méthode vide est montré Figure 4.2.

mov %eax ,- max_offset (% rsp ) # verifie le depassement de pile

push % rbp # sauve le frame pointer

sub $frame_size ,% rsp # alloue la frame

...

add $frame_size ,% rsp # desalloue la frame

pop % rbp # restore le frame pointer

test %eax ,0 xb5ea71f (% rip ) # safepoint ret

Fig. 4.2: Code généré par C2 pour une méthode d'instance vide

1Cette condition est susante car les problèmes de prédiction de branchement dues au polymorphisme persistent indépendamment de cette condition.

2Si il y a une grande disparité entre les diérents coût des méthodes ciblées, il est judicieux de considérer un coût moyen

4.1.1.2 Méthode contenant les timers

La méthode compute itère sur un tableau d'objets de type AbstractType passé en argument (data). Le type AbstractType désigne soit une interface, dans ce cas le bytecode d'appel correspondant est invokeinterface, ou bien une classe et dans ce cas le bytecode d'appel correspondant est invokevirtual.

A chaque itération, un objet est chargé depuis le tableau data et la méthode method est appelée avec cet objet comme receveur. Les performances sont mesurées en Mop/s (méga-opérations par seconde) ou une opération désigne l'exécution du site d'appel. Le temps d'exécution des appels est obtenu en ajoutant des timers en entrée et sortie de méthode (cf. Section 3.3).

La méthode compute est un exemple d'utilisation intensive du polymorphisme, l'appel polymorphique étant la seule opération eectuée dans le corps de boucle.

Le paramètre data est utilisé pour faire varier la distribution des types qui est la principale variable dont dépend la performance comme détaillé Section 4.1.2.

Opérations mesurées par compute L'opération d'intérêt mesurée par compute est la durée d'exécution du site d'appel. Les autres opérations comme la durée d'exécution de la méthode ciblée sont considérée comme du bruit. L'objectif de l'implémentation est de maintenir l'intensité de ce bruit minimal, on se place ainsi and le cas extrême où le coût du polymorphisme (i.e. du site d'appel) est maximal. Cette approche permet d'obtenir une borne supérieure concernant l'impact d'une optimisation ou d'un paramètre sur le coût du polymorphisme.

Parmi ces opérations, on a vu dans la section précédente le contenu des méthodes ciblées qui sont maintenues vides pour limiter leur contribution au bruit. Les autres opérations s'ajoutant au bruit sont sont : le chargement du receveur depuis la mémoire ; le coût de la boucle et des timers ; des opérations de déchargement (spilling) et de restauration (lling) des registres de part et d'autre du site d'appel. Le coût de la boucle qui correspond à un test et un branchement à chaque itération est considéré négligeable. Il peut être amorti en déroulant la boucle (ce qui n'est pas eectué par le JIT pour des boucles contenant des appels de méthode). Le coût des timers étant constant, il est amorti par la taille de la boucle (cf. Section 3.3.3, Chap. 3). Pour réduire le coût des accès mémoires, la taille de data est xée à 2048 bytes an qu'il puisse loger dans le cache L1d. Sa taille doit par ailleurs être assez grande pour qu'une distribution de type aléatoire puisse mettre à mal la prédiction de branchement. La localité des données est augmentée en allouant un unique objet par type considéré et en réutilisant sa référence dans data.