• Aucun résultat trouvé

Implémentation avec appel de sous-routine

5.5 Prols de performance des micro-routines

6.1.3 Implémentation des intrinsics

6.1.3.2 Implémentation avec appel de sous-routine

L'implémentation avec appel de sous-routine consiste à encapusler le code natif opti-misé dans une fonction C/C++ classique de sémantique équivalente à la méthode Java. Cette approche est similaire à celle passant par JNI, elle permet néanmoins de s'aranchir de son coût, l'intégration étant gérée à la compilation des n÷uds d'appel dénis dans l'IR.

Diérents intrinsics de HotSpot utilisent ce type d'implémentation comme la méthode System.nanoTime (décrite Section 3.3.3.2) ou encore la méthode CRC32.update pour le calcul de redondances cycliques.

Le rôle de inline_<intrinsic> (cf. Section 6.1.3.1) est d'inliner l'IR de la méthode intrin-siée dans celle de la méthode appelante en cours de compilation. Dans le cas d'un appel de sous-routine, le n÷ud généré est donc un n÷ud d'appel vers la sous-routine comme montré Figure 6.4.

La hiérarchie des diérents n÷uds d'appel dénis dans l'IR est montrée Figure 6.6. Les n÷uds utilisés pour l'appel de routines natives (i.e. respectant les conventions d'appel

C/C++) sont les n÷uds héritant de CallRuntimeNode à savoir CallLeafNode et CallLeafNoFPNode. La diérence entre ces deux n÷uds est expliquée plus loin. Les appels à des méthodes

na-tives via JNI utilisent le n÷ud CallStaticJavaNode5 qui appel avec les conventions Java une fonction (ou wrapper) qui gère l'appel à la fonction native (cf. Section 5.2.1 Chap. 5). Contrairement aux appels avec les conventions JNI (cf. Section 5.2.1.1), les appels directs ne sont pas des safepoints (cf. Section 2.3.1), ils utilisent des pointeurs directs vers les objets dans la heap et sont par conséquent bloquants pour le GC. Il leur est ainsi interdit d'appeler du code Java susceptible de déclencher un GC, c'est pourquoi ces appels sont qualiés de leaf (feuille). La Table 6.3 compare les débits d'appel mesurés pour des mé-thodes vides lorsqu'elles sont appelées avec les conventions JNI avec section critique pour l'acquisition des pointeurs d'objets (cf. Section 5.2.2.2), et lorsqu'elles sont appelées en émettant un n÷ud CallLeafNode via le mécanisme des intrinsics. Le coût d'entrée et de sortie des sections critiques augmentent avec le nombre de tableaux primitifs passés en argument comme vu Section 5.2.3.

Diérence entre les n÷uds CallLeafNoFPNode et CallLeafNode sur Intel64 Le n÷ud CallLeafNoFPNode peut être utilisé lorsque la sous-routine appelée ne modie pas l'état d'exécution des instructions ottantes ce qui est assuré si la sous-routine n'utilise pas d'instructions SSE. Sur Intel64 ces diérents états d'exécution sont :

État A : la moitié supérieure des registres YMM est considérée nulle ;

État B : la moitié supérieure des registres YMM n'est pas considérée nulle excepté pour les instructions AVX-128 ;

État C : la moitié supérieure des registres YMM n'est pas considérée nulle par les instructions SSE. Ainsi les moitiés supérieures de tous les registres YMM sont sau-vegardées puis restaurées automatiquement par le processeur.

Les transitions sont montrées Figure 6.5. Les diérents types d'instructions ottantes in-uant sur l'état d'exécution sont les instructions SSE (ou legacy-SSE i.e. des instructions SSE sans préxe VEX6) opérant sur les registres 128-bits XMM (moitiés inférieures des registres YMM), les instructions AVX-128 (instructions SSE avec préxe VEX) opérant

5Ce dernier est également utilisé pour les appels statiques et les appels virtuels optimisés

6Le préxe VEX pour Vector EXtension est un préxe d'encodage des instructions x86-64 permettant de dénir de nouvelles instructions vectorielles notamment pour l'extension AVX

sur les registres XMM et les instructions AVX-256 opérant sur les registres 256-bits YMM. La diérence entre SSE et AVX-128 est que les instructions SSE doivent conserver inchan-gée la partie supérieure des registres YMM alors que les instructions AVX-128 la charge à 0. Par ailleurs les instructions VZEROUPPER (chargeant les moitiés supérieure à zéro) et VZEROALL (chargeant les registres à zéro) permettent de revenir à l'état A.

Les transitions problématiques sont les transitions B->C ou C->B/A qui entraînent res-pectivement une sauvegarde puis restauration des moitiés supérieures des registres YMM. Ces opérations ont un coût élevé d'environ 70 cycles d'horloge par transition sur Sandy Bridge [47]. Comme le JIT ne génère pas d'instructions SSE mais AVX-128, les transi-tions problématiques sont B->C. Elles surviennent lorsque la méthode appelante utilise des instructions AVX-256 et que la sous-routine appelée utilise des instructions SSE. Dans ce cas il est important d'utiliser le n÷ud CallLeafNode pour l'appel de la sous routine qui génère l'instruction VZEROUPPER an d'éviter le coût élevé de la transition B->C. Si la sous-routine appelée n'utilise pas d'instruction SSE, comme c'est le cas pour les instrinsics implémentés, l'utilisation du n÷ud CallLeafNoFPNode est à privilégier comme il permet de s'aranchir de l'instruction VZEROUPPER même si elle ne coûte qu'un cycle d'horloge sur Intel64.

Fig. 6.5: États d'exécution des instructions ottantes sur Intel64. Le code généré par le JIT n'utilisant pas d'instructions SSE mais AVX-128, les transitions problématiques lors de l'appel de sous-routine peuvent être les transitions B->C (cas où la méthode appelante utilise des instructions AVX-256 et la sous-routine appelée des instructions SSE). Pour éviter cette transition le n÷ud d'appel CallLeafNode génère une instruction VZEROUPPER pour passer dans l'état A avant appel. Crédit image : [89]

Avantage et inconvénient de cette implémentation L'avantage majeur de cette technique d'implémentation n'est autre que sa simplicité. La sous-routine peut être implé-mentée en C/C++, assembleur ou encore provenir d'une librairie statique ou dynamique. Les n÷uds d'appel invoquant les routines natives sont déjà dénis dans l'IR. Par ailleurs cette méthode permet de réduire la durée de compilation dans la mesure où un seul n÷ud dans la représentation intermédiaire intervient.

Le principal inconvénient est que la routine ne peut pas bénécier d'optimisations de contexte provenant de l'allocation de registre, de la propagation de constante ou encore

CallNode CallJavaNode CallDynamicJavaNode CallStaticJavaNode CallRuntimeNode CallLeafNode CallLeafNoFPNode

Fig. 6.6: Hiérarchie des n÷uds d'appel dénis dans l'IR de C2 JNI

section critique Intrinsic CallLeafNode

() 147 890

(double[]) 25 820

(double[], double[]) 14 785

(double[], double[],

double[]) 9 775

Tab. 6.3: Débit d'appel pour des méthodes statiques vides en millier d'appels par seconde, pour diérentes signatures des paramètres d'entrée. La fonction native cible vide est soit appelée via JNI, soit via un intrinsic qui émet un n÷ud d'appel CallLeafNode

de l'élimination d'expression redondante. Le problème est, à ce niveau là, identique à JNI. Cette méthode est donc à privilégier pour des méthodes avec un potentiel d'optimisation de contexte ainsi qu'un coût d'appel négligeable.