• Aucun résultat trouvé

4.2 États d'un site d'appel polymorphique

4.2.2 Analyse de code des diérents états

4.2.2.6 États polymorphiques

Lorsque le proling dénombre strictement plus de deux types de receveur impliqués au niveau du site d'appel, mais qu'aucun type n'est dominant au niveau des fréquences, le code généré par C2 est dans l'état polymorphique. Les diérents états possibles lors de l'exécution sont :

polymorphique-CI : état polymorphique utilisant un cache d'inline ;

dispatch-virtuel (dans le cas d'un appel virtuel invokevirtual) ou dispatch-d'interface (dans le cas d'un appel d'interface invokeinterface) : état polymorphique utilisant du dispatch.

Polymorphique-CI L'état polymorphique-CI correspond à l'état initial d'un site d'appel polymorphique. Le code associé est détaillé Figure 4.11. Une fois le code généré par C2, le site d'appel cible le runtime. À la première exécution du site d'appel, le runtime inscrit le pointeur de classe du premier receveur rencontré (nommé 1stKlass) dans le CI puis modie la cible de la méthode vers la méthode correspondant à ce receveur. 1stKlass est donc inscrit dans le CI (qui désigne l'opérande ré-inscriptible dans le code binaire), puis est chargé dans le registre dédié rax. L'appel cible ensuite l'UEP (Unveried Entry Point) de la méthode associée au type inscrit dans le CI (notée 1st.method). Chaque méthode

9Lorsque l'option -XX:UseOnlyInlinedBimorphic est activée, cet état peut aussi être généré si deux types sont impliqués mais que l'un domine (voir état bimorphique).

d'instance possède un UEP précédent son point d'entrée normal. Ce point d'entrée est spécialement prévu pour l'état avec CI. Il vérie préalablement que le pointeur de classe correspondant au type du receveur (rcvKlass) est identique à celui inscrit dans le CI (1stKlass). Si tel est le cas, le code poursuit son exécution vers le point d'entrée de la méthode. Sinon, le code appel le runtime qui modie le site d'appel vers l'état de dispatch (cf. Figure 4.4).

Pour que l'état avec CI soit bénéque, il faut que le nombre de receveurs successifs, ayant le même type que le premier receveur rencontré après la compilation, soit susant. Autrement dis, il faut que le site d'appel exécute une distribution monomorphique post-compilation et une distribution polymorphique pré-compilation. C'est le cas si la méthode contenant le site d'appel possède plusieurs contextes d'appel rendant le site d'appel polymorphique mais est inlinée dans un contexte ou elle exécutera une distribution monomorphique.

# rcv est dans rsi

movabs 1 stKlass ,% rax # Charge 1 stKlass dans rax call 1 st.method.UEP # Appel l'UEP de 1 st.method

...

1 st.method.UEP :

mov 0x8 (% rsi ),% r10d # Charge rcvKlass dans r10d , null - check implicite shl $0x3 ,% r10 # Decompresse rcvKlass dans r10

cmp %r10 ,% rax # Compare rcvKlass et 1 stKlass

jne runtime # Branche vers le runtime si non egaux

1 st.method : # Entre dans 1 st.methode si egaux

Fig. 4.11: Section d'assembleur généré par C2 correspondant l'état polymorphique-CI

États polymorphiques avec dispatch L'état polymorphique avec dispatch dière se-lon la nature de l'appel, à savoir un appel virtuel invokevirtual ou bien appel d'interface invokeinterface. Cette diérence provient du fait qu'une classe ne peut hériter que d'une seule super-classe, alors qu'elle peut hériter de plusieurs interfaces (cf. Section 2.2.4 Chap. 2).

Dispatch-virtuel Le code correspondant à l'état dispatch-virtuel est commenté Figure 4.12. Dans cet état, le CI n'est pas utilisé et contient l'entier signé -1. L'appel cible alors la section de code vtableStub qui eectue le dispatch et branche vers la bonne méthode. Le dispatch s'eectue en utilisant la table de méthodes virtuelles ou vtable. La vtable est située à un décalage constant dans la structure instanceKlass quelque soit la classe. Ses entrées sont des pointeurs de méthode (method*). Chaque méthode non-statique d'une classe possède une entrée unique dans cette table. La vtable d'une classe étend la vtable de sa superclasse en ajoutant de nouvelles entrées à la suite. Ainsi une méthode virtuelle est repérée par un indice unique, indépendamment du type qui l'implémente, qui corres-pond à son entrée dans la vtable. Plus largement, comme la vtable est situé à décalage constant et que l'index de son entrée est constant, le décalage vers le pointeur de méthode

est également constant. Dans l'exemple donné, le décalage vers le pointeur de méthode vaut 480 bytes (0x1e0 en hexadécimal). Une fois le pointeur de méthode chargé, le code branche vers le point d'entrée de la méthode. Ce dernier est chargé depuis le champ nommé from_compiled, situé dans la structure method à un décalage constant de 64 bytes (0x40 en hexadécimal), et qui contient l'entrée de la méthode à appeler depuis du code compilé10.

# rcv est dans rsi

movabs $ -1 ,% rax # Cache d'inline inactif call vtableStub # Appel vtableStub

...

vtableStub :

mov 0x8 (% rsi ),% eax # Charge rcvKlass dans eax shl $0x3 ,% rax # Decompresse rcvKlass dans rax

mov 0 x1e0 (% rax ),% rbx # Charge le pointeur de methode dans rbx jmp *0 x40 (% rbx ) # Branche vers l'entree de la methode

Fig. 4.12: Section d'assembleur généré par C2 pour l'état dispatch-virtuel

Dispatch-d'interface Le code correspondant à l'état dispatch-d'interface est commenté Figure 4.13. Le pointeur d'interface est chargé dans un registre puis l'appel cible la section de code runtime itableStub chargée d'eectuer le dispatch. Tout comme pour une méthode virtuelle, une méthode d'interface correspond à un index unique dans la vtable de l'inter-face. La section itableStub est constante pour un index donné. La vtable de l'interface est identique pour toutes les classes implémentant l'interface. Cependant, à la diérence d'un appel de méthode virtuelle avec dispatch, une classe peut hériter de plusieurs interfaces. Ainsi l'emplacement de la vtable de l'interface dans la structure instanceKlass de la classe du receveur n'est pas constant et doit être trouvé. Pour cela l'instanceKlass contient une table d'interfaces, ou itable, dont le nombre d'entrée correspond au nombre d'interfaces qu'elle implémente.

La itable est écrite en mémoire après la vtable de la classe. Comme cette dernière est de taille variable en fonction du type du receveur, sa taille est inscrite dans un champ (vtableSize) dont le décalage dans l'instanceKlass est constant. Cette dernière est ainsi chargée et uti-lisée pour localiser l'adresse de la itable dans l'instanceKlass (notée itable*).

Une entrée de la itable contient deux mots de 8 bytes. Le premier mot (i.e. l'index) de type instanceKlass* permet de déterminer à quelle interface l'entrée correspond. Le second mot contient le décalage en bytes auquel est situé sa vtable dans l'instanceKlass.

Pour trouver l'entrée de l'interface dans la itable, l'instanceKlass* de l'interface qui émet l'appel (intKlass) est écrit dans le CI. En premier lieu, l'interface contenu dans la

pre-10Ce champ contient soit le point d'entrée de la méthode compilée, soit un adaptateur (comp_2_int) permettant de basculer vers l'interpréteur. De manière similaire, un champ from_interpreted contient l'entrée de la méthode lorsque l'appelant est l'interpréteur, qui peut être un adaptateur vers du code compilé (int_to_comp).

mière entrée est comparée avec intKlass contenu dans rax. S'il s'agit de la bonne entrée, le code branche vers found. Sinon le code rentre dans lookup et itère sur le reste des en-trées tant que intKlass n'est pas trouvée. Un index nul signal la n de la itable. Si l'entrée est trouvée, l'adresse de la vtable (intVtable), située dans le second mot, est chargée et le dispatch à lieu comme pour un appel virtuel. Si cette dernière n'est pas trouvée (i.e. si l'index nul) le code branche vers notFound et lève une exception.

Le code contenant une boucle sur la itable, la performance du dispatch dépend donc de l'index de l'interface dans la itable. Le développeur peut contrôler sa valeur puisqu'il cor-respond à l'ordre de dénition des interfaces implémentées dans le prototype de la classe.

# rcv est dans rsi

mov intKlass ,% rax # Charge intKlass dans rax call itableStub # Appel itableStub

...

itableStub :

mov 0x8 (% rsi ),% r10d # Charge rcvKlass dans r10d shl $0x3 ,% r10 # Decompresse rcvKlass dans r10 mov 0 x120 (% r10 ),% r11d # Charge vtableSize dans r11d

lea 0 x1b8 (% r10 ,% r11 ,8) ,% r11d # Charge itable * dans r11d ( pointeur d'entree ) lea 0x8 (% r10 ),% r10 # Ajout du decalage de la method

mov (% r11 ),% rbx # Charge la 1 ere entree dans rbx cmp %rbx ,% rax # Test la 1 ere entree

je found

lookup : # Boucle sur les autres entrees

test %rbx ,% rbx # Test l'index nul indiquant la fin de la itable je notFound # Si vrai l'interface n'est pas trouvee

add $0x10 ,% r11 # Incremente le pointeur vers l'entree suivante mov (% r11 ),% rbx

cmp %rbx ,% rax

jne lookup found :

mov 0x8 (% r11 ),% r11d # Charge intVtable dans r11d mov (% r10 ,% r11 ,1) ,% rbx # Charge la method dans rbx jmp *0 x40 (% rbx ) # Branche vers le point d'entree

...

notFound :

jmp runtime # Appel le runtime qui lance une erreur

Fig. 4.13: Section d'assembleur généré par C2 pour l'état dispatch-d'interface