• Aucun résultat trouvé

4.4 Alternatives pour l'optimisation des performances

4.4.3 Dispatch explicite

Un dispatch explicite peut constituer une alternative ou un complément à l'utilisation du polymorphisme an d'améliorer les performances pour certaines distributions polymor-phiques. Il peut notamment permettre de combiner les diérents états générés par le JIT an de bénécier de l'inlining.

La stratégie utilisée par le JIT est de placer les branchements conditionnels directs par ordre de probabilité d'exécution, ces derniers pouvant être inlinés, et de placer en dernier le dispatch via la table des méthodes (utilisant des branchements indirectes), ce qui permet également de réduire le coût du dispatch (cf. Eq. 4.1 Section 4.3.5) .

Ces optimisations peuvent être appliquées à des distributions polymorphiques hiérarchi-sées, non considérées par le JIT, en utilisant un dispatch explicite.

Application du dispatch explicite pour une distribution La Figure 4.4.3 montre une implémentation de dispatch explicite optimisée pour une distribution polymorphique avec le type Type0 dominant suivi du type Type1. La Figure 4.20 montre les performances obtenues pour diérentes distributions respectant cette hiérarchie. En moyenne pour les distributions considérées, elle permet d'obtenir un facteur d'accélération de 270% par rap-port à l'état dispatch-virtuel, qui est celui généré par défaut, et de 156% par raprap-port à l'état polymorphique-domination. Pour des distributions 3-polymorphiques et 4-polymorphiques, cette implémentation permet de contourner l'utilisation de la vtable et permet l'inlining de toutes les méthodes. En eet le troisième branchement générera l'état monomorphique pour une distribution 3-polymorphique, et bimorphique pour une distribution 4-polymorphique. L'implémentation utilise l'opérateur instanceof pour tester le type du receveur. La re-dondance fonctionnelle des opérations checkcast (générée par la conversion de type du receveur) et de instanceof est optimisée sans problème par C2.

public void dispatch_method ( AbstractType rcv ) {

if ( rcv instanceof Type0 ) { Type0 rcv0 = ( Type0 ) rcv ; rcv0 . method (); } else { if ( rcv instanceof Type1 ) { Type1 rcv1 = ( Type1 ) rcv ; rcv1 . method (); } else rcv . method (); } }

Fig. 4.19: Méthode dispatch_method eectuant un dispatch explicite vers la méthode method (de signature void(void)), optimisée pour une distribution polymorphique hiérar-chique avec Type0 dominant suivie de Type1, le reste de la distribution étant indéterminé Paramétrisation de la distribution La distribution peut être connue mais variable par instance d'application. Dans ce cas, la méthode eectuant le dispatch peut être une méthode virtuelle pour permettre la paramétrisation de la distribution. L'implémentation dépend alors du type de distribution qui implémente sa version la méthode eectuant le dispatch. La Figure 4.4.3 reprend le pattern étudié (cf. Figure 4.1) en intégrant le dispatch explicite qui dépend du type de distribution distType. En considèrant que la distribution

Al. 50-40-10 Al. 60-30-10 Al. 70-20-10 Al. 50-40-5-5 Al. 60-30-5-5 Al. 70-20-5-5

Distributions polymorphiques

0

100

200

300

400

500

Performance [Mop/s]

État du site d'appel

Dispatch explicite

Polymorphique domination (dispatch virtuel)

Dispatch virtuel

Fig. 4.20: Performances pour diérentes distributions polymorphiques hiérarchiques. L'état dispatch-virtuel est l'état adapté généré par défaut.

est unique par instance d'exécution, la méthode de dispatch sera inlinée par le JIT.

static void compute ( AbstractType [] data , distType dist ){

int n = data .length;

for (int i = 0; i < n; ++i)

dist . dispatch_method ( data [i); }

Fig. 4.21: Méthode compute implémentant le pattern appel polymorphique dans une boucle avec dispatch explicite dépendant du type de distribution distType qui dénit la méthode dispatch_method. Une version de cette méthode pour une distribution polymorphique hiérarchique est donnée Figure 4.4.3

4.5 Conclusion

Cette étude propose une analyse des performances du polymorphisme. Dans un pre-mier temps, une métrique de performance du polymorphisme est dénie. Cette dernière repose sur la conception d'un micro-benchmark générique i.e. indépendant de tout traite-ment applicatif spécique. Cette généricité a pour objectif de favoriser l'extrapolation des

résultats à des patterns similaires au sein de l'application et d'obtenir une quantication des performances.

Dans un second temps, l'étude consiste en l'analyse des diérents états pouvant être gé-nérés par le JIT relativement à la distribution des types au niveau du site d'appel. Cette étape à pour objectifs, d'une part, de permettre aux développeurs de connaître les optimi-sations eectuées par le JIT et, d'autre part, de savoir si ces dernières son appliquées aux cas d'utilisation applicatifs.

Les concepts d'états adaptés et d'états stables ont été dénis an de couvrir tous les cas possibles pouvant se présenter lors de l'exécution de l'application. Cette étude a permis de quantier les baisses potentielles de performance et de mettre en avant des opportunités d'optimisation en considérant les deux paramètres fondamentaux que sont l'état du site d'appel et la prédiction de branchement.

L'état du site d'appel peut être amélioré dans les cas où il y a un défaut de proling ou dans les cas non pris en charge par le JIT (cf. Section 4.4.1). Dans ces cas, une connais-sance applicative de la distribution mise en jeu, peut, avec du dispatch explicite emmener à améliorer les performances. Pour certaine distribution, le réglage des paramètres du JIT peut également permettre d'améliorer les performances.

La prédiction de branchement repose principalement sur les capacités matérielles de l'unité de prédiction de branchement, néanmoins, au niveau code source Java, le tri de la distri-bution par type permet d'améliorer substantiellement les performances en réduisant la variance sur les branches d'exécution. Un potentiel d'optimisation existe également en pre-nant en compte les informations sur l'unité de prédiction de branchements. Ces dernières doivent néanmoins être eectuées côté runtime considérant la forte dépendance à l'archi-tecture et la forte compétence métier qu'elles requièrent.

L'approche utilisée dans cette étude, qui vise à quantier les performances d'un bytecode donné (en l'occurrence invokevirtual et invokeinterface) relativement aux paramètres impactant l'état du code, peut être généralisée à d'autres bytecodes. En particulier ceux bé-néciant d'information dynamique pour lesquels l'état du code généré possède une grande variabilité. Cette connaissance globale améliorait nettement l'analyse des performances du code Java et favoriserait également l'identication du code critique.

Notons pour nir que les optimisations identiées sont relatives à HotSpot. Cette démarche s'inscrit donc dans une approche par programmation JIT-Friendly pour l'amélioration des performances (cf. Section 7.1.1 Chap. 7).

Chapitre 5

Compromis qualité du code

natif/coût d'intégration via JNI

Ce chapitre considère l'utilisation de JNI (Java Native Interface) an d'améliorer les performances d'application Java faisant intensivement appel à des routines calculatoires. JNI désigne à la fois une spécication [101] et une API C du JDK permettant à du code Java d'appeler des fonctions natives depuis des librairies dynamiques et de manipuler des objets Java depuis ce code natif.

Son ecacité pour l'amélioration des performances repose sur le compromis coût d'inté-gration/qualité du code natif. Ce compromis signie que l'on accepte, en utilisant JNI, de payer un coût constant élevé à chaque appel de méthode native (par rapport à celui de méthodes Java non natives) si le code natif appelé est plus performant que celui généré par le JIT et permet d'obtenir une accélération.

Cette condition dépend de la quantité de calcul exécutée à chaque appel qui doit être suf-sante pour couvrir le coût d'intégration via JNI.

On se propose dans ce chapitre de comparer les performances d'implémentations Java et JNI de diérentes routines élémentaires en faisant varier la quantité de calcul an de dé-terminer quelle est la meilleure implémentation sur un intervalle donné.

La Section 5.1 souligne les motivations et le problème traité. La Section 5.2 propose un background sur JNI en décrivant l'origine de son coût d'intégration élevé. Dans la Section 5.3 on dénit le terme de prol de performance ainsi que son usage. Dans la Section 5.4 on distinguera la nature des routines considérées pour l'analyse par prol de performance, ainsi que les optimisations permises par l'utilisation de code natif. Enn la Section 5.5 est consacrée à la description des micro-routines traitées et à l'analyse des prols de performance.

5.1 Problématique et motivations

Lorsqu'il implémente une méthode, le développeur Java dispose de deux choix : 1. L'implémentation Java-pure (i.e. bytecode) constituant la quasi-totalité du code ; 2. L'usage de JNI qui reste marginal comme il peut impacter la portabilité de

l'appli-cation et ajoute un eort de maintenance.

L'utilisation de JNI est habituellement orientée à des ns fonctionnelles (par exemple accé-der à des fonctionnalités système ou relever des compteurs CPU [51,72]), néanmoins l'idée d'utiliser JNI an d'améliorer les performances est apparue dans les premières versions de Java [57].

JNI est ici évalué pour appeler des routines calculatoires de faible granularité, critiques en terme de performance. Ces routines représentant une portion de code négligeable dans l'application, l'usage de solutions non conventionnelles et plus fastidieuses à maintenir est donc envisageable. L'utilisation de JNI repose sur deux motivations :

1. Le manque d'expressivité et le haut niveau d'abstraction du langage Java ne permet pas de faire de l'optimisation bas niveau sur l'architecture considérée ;

2. Le niveau d'optimisation du JIT peut être moindre que celui d'un compilateur sta-tique comme GCC ou que de l'optimisation manuelle bas-niveau.

Néanmoins, en supposant que le code natif soit de meilleure qualité, deux autres aspects mitigeant son ecacité doivent également être considérés :

1. La spécialisation du code Java permise par la compilation dynamique (proling et inlining) ore un potentiel d'optimisation dont ne disposent pas les fonctions natives ; 2. Le coût d'intégration via JNI ralentit les performances par rapport aux méthodes

Java qui, de surcroît, peuvent être inlinées.

Pour les routines calculatoires considérées, travaillant sur des structures de données en mé-moire, le potentiel d'optimisation provenant de la spécialisation du code par propagation de constante est faible. Ainsi le second aspect, à savoir le coût d'intégration via JNI, est le facteur limitant à prendre en considération. Ce coût d'intégration se manifeste par un coût constant relativement élevé, payé à chaque appel de méthode native.

Considérant le coût d'intégration via JNI, on propose de déterminer à partir de quelle quantité de calcul eectuée par la routine l'utilisation de JNI est protable. Pour cela on considère le prol de performance des diérentes implémentations de la routine. Le prol de performance est la courbe représentant les performances en fonction de la quantité de calcul. L'objectif est ainsi de déterminer la meilleure implémentation sur un intervalle de quantité de calcul donné et, si il y en a, les points de croisement entre implémentations Java et implémentations JNI.