• Aucun résultat trouvé

Contrôle du code généré

7.2 Techniques d'optimisations dynamiques

7.2.1 Contrôle du code généré

7.2.1.1 Implémentation d'intrinsics du compilateur dynamique

Le Chapitre 6 propose l'implémentation d'intrinsic dans le compilateur dynamique C2 de la JVM HotSpot. Les intrinsics sont des méthodes particulières pour lesquelles, le code généré par le compilateur est prédéni et n'est pas à la charge de ce dernier. Leur mécanisme est ainsi, comme étayé dans [50], une des modalités permettant de réconcilier programmation haut niveau et programmation bas niveau. L'implémentation d'intrinsic est très ecace pour trois raisons :

1. Elle permet un contrôle niveau assembleur du code généré et donc de produire un code optimum sur l'architecture sous-jacente ;

2. Elle permet l'inlining du code dans le contexte courant et donc des optimisations locales (propagation de constante, allocation de registre...) ;

9On pense par exemple à l'allocation de registre [186], la vectorisation [98], la recherche de sous-expression commune [28], l'analyse de localité [156] ou encore la dés-optimisation [88]

3. Elle ore la possibilité de spécialiser le code aux données prolées par le runtime. Cette technique a permis d'obtenir un facteur d'accélération de ×2,5 sur une méthode générique de très faible granularité (trois instructions machine) ainsi qu'un facteur ×16,7 sur une méthode application-spécique de granularité plus élevée (de l'ordre d'une dizaine d'instructions).

Le troisième potentiel d'amélioration n'est cependant pas exploité dans les expérimenta-tions conduites du fait, d'une part, des méthodes considérées, dont le potentiel de spéciali-sation au données prolées est nul, mais également des dicultés techniques d'implémen-tation induites.

Le principal inconvénient de l'implémentation des intrinsics concerne l'eort de dévelop-pement et de maintenance supplémentaire. Elle nécessite, en eet, une connaissance du fonctionnement interne du JIT notamment concernant sa représentation intermédiaire, ses mécanismes de gestion et la dénition d'instructions dans le back-end.

An de limiter cette contrainte, une implémentation basique par appel de sous-routine a été proposée. Cependant cette dernière n'est pas optimum et entraîne un sur-coût important en particulier sur les routines de faible granularité. Ainsi aucune accélération n'est observée sur la routine de faible granularité considérée dont la performance demeure similaire à celle de la version générée par C2. Une diérence de 13% avec l'implémentation optimum est observée sur la routine de plus forte granularité.

An de permettre aux développeurs Java de contrôler le code généré diérentes techniques facilitant le développement sont présentées Section 7.2.1.4.

7.2.1.2 Solutions similaires

Intrinsics au sein des JVM de production Beaucoup d'intrinsics sont déjà implé-mentés dans des JVM de production telles que HotSpot ou J9 et leurs performances re-posent en partie dessus [68, 119, 61]. Leur nombre ne fait que croitre compte tenu des nouveaux jeux d'instructions intégrés dans les nouvelles architectures. Par exemple la mé-thode Math.fma10est l'un des intrinsics introduit dans la version Java 9 de HotSpot [122] an de bénécier de l'extension FMA11 présente dans les architectures Intel à partir de la génération Haswell [66]

Néanmoins, concernant les environnements d'exécution, deux contraintes se posent sur la nature des intrinsics :

Ils doivent correspondre à des méthodes génériques an qu'ils soient bénéques à toutes les applications ;

Ils ne doivent pas être, contrairement aux intrinsics de compilateur statique comme GCC, spéciques à une architecture particulière an de respecter la portabilité Java.

10La méthode retourne la valeur de l'opérationa×b+c(fused multiply-add) aveca,b,cpour paramètres

11FMA (Floating-point Multiply Add) est une extension du jeu d'instruction x86 fournissant des ins-tructions SIMD du type fused multiply-add (cf. note 10)

La première contrainte a motivé l'intégration d'intrinsic application spécique. La seconde contrainte interdit, par exemple, d'exposer une API fournissant des instructions AVX. Java Vectorization Interface Dans [115], Nie et Al. ont néanmoins cassé la seconde contrainte en implémentant une API Java nommée JVI (Java Vectorization Interface). Cette dernière expose aux développeurs Java des types vectoriels ainsi que des méthodes pour exploiter les jeux d'instructions SSE et AVX (les méthodes et types sont d'ailleurs dénis dans un package nommé com.intel.jvi qui marque explicitement la dépendance du code à l'architecture). JVI ne repose pas sur une extension du langage Java pour la dénition des types vectoriels qui correspondent à des classes. Néanmoins un vecteur n'est jamais instancié et JVI repose un support de l'interpréteur et du JIT. De ce point de vue, son usage est hors-spécication comme le code utilisant l'API ne peut pas fonctionner sur d'autres JVM. Le support du JIT utilise le mécanisme des intrinsics. La solution a été expérimentée dans le contexte de la JVM Apache Harmony (intégrant le compilateur dy-namique Jittrino) qui n'est plus maintenue depuis 2011 [166]. Les résultats de performance montrent des gains de respectivement +55% et +107% sur les benchmarks scimark.fft et scimark.lu [168] exécutés sur un c÷ur Intel Nehalem exposant uniquement l'exten-sion SSE (les gains seraient à priori supérieurs sur une architecture supportant l'extenl'exten-sion AVX).

7.2.1.3 Limitations pour la génération de code dynamique

Deux autres inconvénients, qui dépendent néanmoins du compilateur dynamique, peuvent être relevés concernant l'implémentation d'intrinsics :

Le premier concerne la gamme des informations dynamiques utilisables qui sont relativement limitées (cf. Section 3.3.2.2 Chap. 3), et peuvent être insusantes pour produire un code spécialisé optimum. Par exemple, hormis si ces dernières sont des constantes de compilation auquel cas il est possible de bénécier de la propagation de constante, les valeurs des paramètres d'entrée ne peuvent pas être prises en compte pour spécialiser le code ;

Le second concerne l'émission du code optimisé qui est inliné dans la méthode appe-lante. Cet aspect peut limiter le bénéce des optimisations optimistes comme, en cas d'invalidation du code, la dés-optimisation, qui est un mécanisme coûteux, aecte toute la méthode appelante.

Ces deux inconvénients limitent l'utilisation des intrinsics pour faire de la génération de code dynamique. La génération de code dynamique consiste, dans le cas d'un langage com-pilé statiquement, à remplacer un site d'appel par un générateur de code. Ce générateur est en charge, un, de générer un code spécialisé aux données d'entrée, et deux, d'appeler le code optimisé. Diérentes stratégies peuvent être utilisées pour supporter l'invalidation des optimisations allant du dispatch explicite vers diérentes versions à la régénération complète du code.

Cette technique est ecace pour des méthodes de granularité assez forte pour couvrir le temps de génération de code (qui dans le pire cas, a lieu à chaque appel) et ayant un fort potentiel de gain par spécialisation aux données d'entrée. Elle s'avère au contraire impra-ticable pour des routines de très faible granularité comme celles considérées Chapitre 6 Section 6.2 pour l'expérimentation sur les intrinsics.

L'outil deGoal [24] permet d'embarquer, au sein des applications, des générateurs dyna-miques de code appelés compilettes. L'usage des compilettes expose, sur des processeurs embarqués STxP70, des temps de génération de code au moins 10 fois inférieurs à ceux des JIT traditionnels tout en garantissant un code généré optimum. L'accélération obtenue pour le temps de génération de code est néanmoins à relativiser pour l'usage des intrinsics, dont le mécanisme s'apparente à de la génération de code mais au niveau de la représen-tation intermédiaire du JIT au lieu du niveau code machine.

Actuellement l'outil deGoal ne supporte pas le langage Java. Par ailleurs un tel mécanisme, dans un contexte de compilation dynamique, doit logiquement être pris en charge par le compilateur dynamique qui dénit la politique de compilation. On peut cependant imagi-ner d'utiliser des générateurs dynamiques de bytecode implémentés en Java (dont l'usage et courant) et eectuant la spécialisation, le bytecode spécialisé sera ensuite compilé dy-namiquement et optimisé par le JIT. Mint [182] est une extension de Java permettant d'implémenter et d'utiliser des générateurs de bytecode dans du code applicatif. Il est basé sur la programmation MSP (Multi-Stage Programming) [165]. Également basé sur la pro-grammation MSP, l'outil LMS (Lightweight Modular Staging) [144], sur lequel sont basés diérents projets [120,163,145], permet d'implémenter des générateurs de bytecode mais dans le langage Scala. De nombreuses APIs Java (comme Javassist [84] ou Byte Buddy [189]) permettent également de générer du bytecode à la volée.

7.2.1.4 Alternatives

Le compilateur Graal [169] ore des perspectives importantes pour réconcilier la pro-grammation Java et la propro-grammation bas-niveau [138, 193]. Sa particularité est d'être écrit en Java, ce qui permet par exemple d'étendre sa représentation intermédiaire en dé-nissant de nouveaux n÷uds ainsi que les optimisations locales associées [37].

Le compilateur Graal implémente le concept de snippets [155]. Les snippets sont des mé-thodes statiques Java repérées par l'annotation @Snippets dont la sémantique dénit à la fois le bytecode correspondant mais également la représentation intermédiaire à générer à la compilation. Ils reprennent le mécanisme des intrinsics mais facilitent leur implémentation comme elle s'eectue en Java. Graal introduit également des méthodes natives spéciales appelées node intrinsics qui permettent de faire référence à des n÷uds particuliers dans la représentation intermédiaire. Les snippets orent de surcroit la possibilité de forcer la spécialisation du code via des annotations portant sur les paramètres d'entrée (comme @ConstantParameter forçant la spécialisation du code à la valeur d'entrée) et de contrôler la désoptimsation du code. Cette possibilité est un pas en avant vers la prise en charge de

la génération de code dynamique par le compilateur.

Un exemple d'utilisation des snippets concerne la conversion double vers int. Comme vu Chapitre 6, Section 6.2.1, les spécications du bytecode d2i eectuant cette conversion peuvent limiter les performances. La classe AMD64ConvertSnippets dénit ainsi le snippet d2i eectuant cette conversion avec les spécications de l'instruction x86 CVTTSD2SI (uti-lisée pour l'optimisation des performances de la méthode floorD2I Section 6.2.1).

Les snippets ne permettent cependant pas, contrairement aux intrinsics, d'inliner de l'assembleur. Leur ecacité suppose une couverture exhaustive des instructions machines par les n÷uds de la représentation intermédiaire de Graal ce qui n'est pas garanti étant donnée la portabilité de cette dernière. Ainsi, si une instruction n'est pas dénie dans le back-end du compilateur, comme c'était le cas pour l'instruction ROUNDSD (cf. Section 6.2.1), les snippets ne permettront pas d'en faire usage.