L'architecture de CPU localisée Tencent JDK prend en charge le partage

GIAC (GLOBAL INTERNET ARCHITECTURE CONFERENCE) est une conférence annuelle d'architecture technique pour les architectes, les responsables techniques et les praticiens techniques haut de gamme lancée par la communauté des technologies d'architecture à haute disponibilité et msup qui se concentre depuis longtemps sur la technologie et l'architecture Internet. C'est la plus grande technologie en Chine L'une des réunions.

GIAC sixième sur l'Assemblée générale de cette année, sujet JAVA dans l'architecture de données volumineuses en évolution, l'ingénieur principal de Tencent Fu Jie médecin a publié un "Tencent JDK localisation CPU architecture prend en charge" partagé sur le sujet. Ce qui suit est un compte rendu des discours des invités:

Chers invités, bon après-midi à tous! Je suis très heureux d'avoir l'opportunité de partager avec vous le sujet de la prise en charge de l'architecture CPU localisée de Tencent JDK. Je suis Jiefu (Fu Jie) de l'équipe JVM de Tencent. J'ai commencé à travailler sur la recherche et le développement d'OpenJDK lors de mes études de troisième cycle à l'Institut de technologie informatique de l'Académie chinoise des sciences. Je suis actuellement membre de la communauté OpenJDK. J'ai travaillé pour Loongson et j'étais le développeur principal de la branche mips d'OpenJDK.J'ai développé et implémenté le compilateur OpenJDK C2 sur Loongson. Après avoir rejoint Tencent, il se consacre principalement à l'exploration et à la pratique de KonaJDK dans les domaines du big data et de l'apprentissage automatique.

Aujourd'hui, je vous donne d'abord une brève introduction à Tencent Kona JDK; ensuite, je développe la prise en charge de la JVM pour l'architecture CPU domestique; enfin, je vais discuter avec vous de l'impact du modèle de mémoire du processeur sur l'implémentation JVM.

Introduction à Tencent Kona JDK

Tencent Kona est un produit JDK développé par Tencent basé sur OpenJDK.Il est gratuit et open source en 2019 et fournit un support à long terme (LTS). Chaque version de Kona a été testée et vérifiée par Tencent Cloud et l'environnement de production interne actuel. Bienvenue à télécharger et à utiliser. 

Lorsque JDK14 a été publié en mars 2020, notre société était un nombre limité d'entreprises nationales et est entrée dans la liste mondiale des contributeurs / organisations exceptionnels. La liste mondiale des contributeurs OpenJDK est une statistique faisant autorité sur les contributions de diverses entreprises ou individus à travers le monde à OpenJDK, qui est annoncée par Oracle lors de la sortie de la nouvelle version de JDK.

L'équipe JVM de Tencent (y compris de nombreux auteurs / commetteurs de la communauté OpenJDK) est responsable du développement et de la maintenance de Kona. Au cours des six derniers mois seulement, l'équipe a fourni des dizaines de correctifs à la communauté OpenJDK pour corriger des bogues. Dans le même temps, Goose Factory apporte également son expérience de charge de production massive et ses pratiques de pointe à la communauté OpenJDK. À l'avenir, nous adopterons activement l'open source avec une attitude plus ouverte et continuerons à contribuer à l'open source.

Prise en charge de la JVM pour l'architecture de processeur domestique

Permettez-moi de partager avec vous le contenu pertinent de la prise en charge par JVM de l'architecture de processeur domestique. Les transformateurs nationaux sont à la base du développement chinois de l'industrie des lettres et de la création. À l'heure actuelle, les processeurs nationaux qui sont entrés dans le répertoire officiel peuvent être divisés en quatre architectures principales: ARM, MIPS, Alpha et X86. Parmi eux, ARM est représenté par Kunpeng et Feiteng, MIPS est représenté par Loongson, Alpha est représenté par Shenwei et X86 est représenté par Zhaoxin et Haiguang. Pour les quatre architectures ci-dessus, à l'exception de la prise en charge de la communauté OpenJDK pour ARM et X86, ni MIPS ni Alpha n'ont de support de la communauté, et tous doivent être développés et maintenus par eux-mêmes. Par conséquent, la maîtrise de la technologie prise en charge par JVM pour les transformateurs est d'une grande importance pour briser les monopoles étrangers et promouvoir le développement durable et sain des transformateurs nationaux.

La machine virtuelle HotSpot d'OpenJDK est la machine virtuelle Java hautes performances la plus utilisée au monde. À partir du niveau de conception de macro, la machine virtuelle HotSpot peut être divisée en quatre modules: chargeur de classe, runtime, moteur d'exécution et garbage collector. Parmi eux, seuls le moteur d'exécution et l'architecture du processeur sont étroitement liés, et les trois autres modules sont presque indépendants de la plate-forme (ou seulement partiellement liés au système d'exploitation, tels que les modules d'exécution). Le moteur d'exécution de la JVM est responsable de la conversion du bytecode Java en instructions machine prises en charge par le matériel du processeur, ce module est donc principalement lié au CPU. Par conséquent, la prise en charge de JVM pour l'architecture du processeur produit localement consiste essentiellement à réaliser le moteur d'exécution JVM sur le processeur produit localement. Alors, comment le moteur d'exécution de JVM doit-il être implémenté au niveau du code?

La partie gauche du PPT sur cette page montre l'organisation du code source de la machine virtuelle HotSpot. Selon la corrélation avec le matériel et le système d'exploitation sous-jacents, le code source HotSpot est divisé en quatre sous-répertoires: cpu (lié au processeur), os (lié au système d'exploitation), os_cpu (processeur et système d'exploitation liés en même temps) et share (indépendant de la plate-forme). La partie centrale du PPT répertorie les principales fonctions implémentées par chaque sous-répertoire, parmi lesquelles les parties marquées en jaune sont des parties liées de l'architecture CPU. Le côté droit du PPT prend comme exemple l'architecture de processeur aarch64 d'ARM pour analyser quantitativement la quantité de code requise par la machine virtuelle Java pour prendre en charge une architecture de processeur. La quantité de code liée à l'architecture du processeur est d'environ 64 000 lignes et la quantité de code dans la partie restante est d'environ 70 Des millions de lignes. Par conséquent, le code requis pour la prise en charge de l'architecture du processeur représente moins de 8%. Le code lié à l'architecture comprend principalement l'assembleur, l'interpréteur et le backend du compilateur. En outre, comme le langage Java prend en charge nativement le multithreading, le processeur doit également fournir des opérations atomiques et des barrières de mémoire pour garantir l'exactitude des programmes concurrents. Ci-dessous, nous allons développer un par un à partir de l'assembleur, de l'interpréteur, du compilateur, de l'opération atomique du processeur et de la barrière mémoire.

L'assembleur est le premier module à implémenter, car la construction de l'interpréteur et du compilateur dépendent de l'assembleur pour fournir une interface. L'assembleur résume et encapsule principalement le matériel du processeur et fournit les registres et les instructions nécessaires à la programmation. L'assembleur est la fonction la plus simple parmi plusieurs modules. Cependant, du point de vue de l'ingénierie, étant donné que les processeurs modernes prennent en charge des milliers d'instructions, les tâches de mise en œuvre de l'assembleur sont ardues et des erreurs dans le format et le codage des instructions peuvent facilement être introduites. Par conséquent, les développeurs doivent se familiariser avec le jeu d'instructions du processeur et faire attention pendant le processus de codage.

Une fois l'assembleur terminé, l'interpréteur doit être implémenté immédiatement. Posez une question à tout le monde: pouvez-vous ignorer l'interpréteur et implémenter directement le compilateur de la machine virtuelle HotSpot? Certaines personnes pensent que les performances de l'interpréteur sont trop faibles et souhaitent supprimer le module d'interprétation pour réduire la charge de travail de la prise en charge de la JVM pour l'architecture du processeur. la réponse est négative. La machine virtuelle HotSpot doit s'appuyer sur la fonction de l'interpréteur. Tout d'abord, pour certaines méthodes Java spéciales (comme la grande taille), le compilateur refusera de compiler et ne pourra être interprété et exécuté que par l'interpréteur. Deuxièmement, les compilateurs de HotSpot, en particulier les compilateurs C2, utilisent largement des optimisations de compilation radicales basées sur certaines hypothèses. Mais ces hypothèses ne sont pas toujours vraies. En cas d'échec, la machine virtuelle doit revenir de la compilation et de l'exécution à l'interpréteur pour continuer l'exécution. Enfin, dans certains scénarios qui nécessitent un démarrage et une réponse rapides, l'interprétation et l'exécution directes peuvent être meilleures que la compilation puis l'exécution. Par conséquent, la construction et le soutien de l'interprète sont nécessaires.

L'interpréteur de HotSpot est un interpréteur basé sur des modèles haute performance. Le soi-disant «modèle» est une séquence d'instructions d'assemblage utilisées pour implémenter les fonctions sémantiques du bytecode Java. Le PPT sur cette page montre comment la méthode add est compilée en quatre bytecodes par javac, puis interprétée et exécutée. L'interprétation et l'exécution sont en fait le processus d'exécution de la séquence d'instructions dans le modèle correspondant du bytecode un par un selon le flux de contrôle du programme. Le côté droit du PPT montre le modèle d'interprétation du bytecode iadd d'addition d'entier. Les instructions machine dans la case en pointillé jaune ci-dessus sont utilisées pour récupérer les opérandes. Les instructions machine dans la case en pointillé jaune ci-dessous sont utilisées pour passer au modèle correspondant au bytecode suivant pour continuer l'exécution. Une instruction add au milieu est utilisée pour réaliser la sémantique du bytecode iadd. Les modèles de l'interpréteur suivent tous un modèle fixe, c'est-à-dire que vous prenez d'abord l'opérande, puis exécutez et enfin passez au modèle suivant pour continuer à fonctionner.

Une fois l'interpréteur débogué avec succès, vous pouvez commencer à prendre en charge le compilateur. La prise en charge du compilateur est la plus difficile et le cycle de débogage est le plus long. Deux compilateurs, C1 et C2, sont conçus dans HotSpot. Le compilateur C1 compile rapidement, mais la qualité du code généré n'est pas élevée. Il convient aux scénarios nécessitant un démarrage et une réponse rapides, il est donc également appelé compilateur de version client. Le code généré par le compilateur C2 est de haute qualité, mais la vitesse de compilation est lente. Il convient aux applications de service qui doivent être exécutées à plusieurs reprises pendant une longue période, il est donc également appelé compilateur de version serveur. Par rapport à C1, C2 utilise de plus en plus d'algorithmes d'optimisation de compilation radicaux, donc C2 est plus compliqué que C1. La structure de C1 et C2 présente de nombreuses similitudes. Prenons l'exemple de C2 plus complexe pour vous montrer comment implémenter un compilateur prenant en charge la nouvelle architecture CPU sur la JVM.

Cette page PPT montre le principe de la construction du compilateur C2. Afin de réduire la difficulté de la transplantation du compilateur, C2 est divisé en deux parties, indépendantes de la plate-forme et dépendant de la plate-forme. Le code indépendant de la plate-forme est applicable à toutes les architectures de processeur, et seul le code de la partie liée à la plate-forme doit être transplanté et adapté à l'architecture du processeur. En outre, afin de réduire la charge de travail de l'écriture manuelle d'une partie du code liée à la plate-forme, C2 utilise le compilateur ADL pour générer automatiquement du code lié à l'architecture du processeur. ADL est l'abréviation anglaise d'Architecture Description Language, qui est un langage de description d'architecture intégré dans le code open source OpenJDK. Le compilateur ADL génère du code C2 en analysant les fichiers de description d'architecture (fichiers avec un suffixe * .ad, tels que aarch64.ad). Par conséquent, la majeure partie du travail pour prendre en charge C2 sur la nouvelle architecture de processeur consiste à écrire correctement le fichier de description de l'architecture du processeur. Le fichier de description d'architecture comporte principalement trois aspects: la description du registre, la description de l'opérande et la description du jeu d'instructions.

Cette page PPT montre un exemple de description de registre utilisant Aarch64 comme exemple. Les descriptions de registre comprennent généralement des registres à usage général, des registres à virgule flottante et des registres vectoriels. Afin d'être compatible avec les systèmes d'exploitation 32 bits, une longueur de 32 bits est utilisée comme unité de description de base lors de la description du registre. Par exemple, R1 et R1_H dans la partie supérieure du PPT représentent ensemble le registre R1 64 bits. Les V0, V_H, V_J et V_K dans la moitié inférieure du PPT représentent un registre à virgule flottante V0 de 128 bits de longueur.

Cette page PPT montre des exemples de description d'opérande. Les opérandes décrivent les types de données directement pris en charge par le processeur, y compris trois catégories: les opérandes immédiats, les opérandes de registre et les opérandes de mémoire. Dans chaque catégorie principale, il sera subdivisé en sous-types spécifiques tels que caractère, entier, virgule flottante et pointeur.

Cette page PPT montre un exemple de description d'instructions. Il convient de rappeler que la description de l'instruction décrit non seulement les instructions prises en charge par le matériel du processeur, mais affecte également la sélection d'instructions et la génération du compilateur C2, affectant ainsi les performances du compilateur. En fait, la description des instructions dans le fichier d'architecture spécifie comment utiliser les instructions de la machine CPU pour correspondre à la représentation de code intermédiaire du compilateur. La description de l'instruction addI_reg_reg sur le côté gauche du PPT correspondra au nœud AddI et à ses opérandes src1 / src2 représentés par le code intermédiaire du compilateur, comme indiqué sur la figure de droite du PPT.

Une fois les registres, opérandes et descriptions d'instructions terminés, la prise en charge par JVM de l'architecture du processeur est presque terminée. À ce stade, vous ne devez pas oublier les opérations atomiques du processeur et les barrières mémoire mentionnées précédemment. Comme indiqué dans le PPT à la page suivante, HotSpot définit des opérations atomiques très claires et des interfaces de barrière de mémoire, et il vous suffit de les implémenter une par une en fonction des caractéristiques du processeur. Tout le monde est familier avec les opérations atomiques, alors qu'est-ce qu'une barrière mémoire? Je vous donnerai une introduction détaillée dans la section suivante.

Modèle de mémoire de processeur et implémentation JVM

Discutons avec vous de l'impact du modèle de mémoire du processeur sur la conception JVM. Pourquoi énumérer ce sujet seul? Des années d'expérience pratique nous apprennent que l'implémentation de la JVM teste le plus au niveau de l'ingénieur est l'adaptation du modèle de mémoire du processeur et de la JVM. Cette partie du travail détermine si la machine virtuelle peut fonctionner de manière stable sur le processeur. J'espère que cela pourra attirer l'attention de tout le monde.

Le modèle de mémoire du processeur a des points forts et des points faibles. Le modèle de mémoire forte est représenté par X86; le modèle de mémoire faible est représenté par les architectures ARM et PowerPC. Alors, comment la force du modèle de mémoire du processeur est-elle définie? Le PPT suivant montre la base de la division de la force du modèle de mémoire: en fonction de combien le processeur permet la réorganisation des instructions d'accès à la mémoire. En général, plus la réorganisation des instructions d'accès à la mémoire est autorisée, plus le modèle de mémoire du processeur est faible et vice versa. Les instructions d'accès à la mémoire sont divisées en deux opérations: lecture (chargement) et écriture (stockage). Par conséquent, les scénarios de réorganisation possibles incluent la réorganisation en lecture-lecture (Load / Load), en lecture-écriture (Load / Store), en écriture-lecture (Store / Load) et en écriture-écriture (Store / Store). Le processeur d'architecture X86 autorise uniquement la réorganisation en écriture et en lecture (Store / Load), tandis que ARM et PowerPC autorisent les quatre réorganisations ci-dessus. Par conséquent, X86 est généralement considéré comme un modèle de mémoire solide, tandis que ARM et PowerPC sont considérés comme un modèle de mémoire faible.

Cependant, lorsque nous programmons, en particulier en programmation simultanée, nous pouvons avoir besoin d'interdire le comportement de réorganisation du processeur. À ce stade, vous devez utiliser une barrière de mémoire pour terminer. La soi-disant «barrière de mémoire» se réfère aux instructions machine prises en charge par le matériel du processeur et spécifiquement utilisées pour interdire la réorganisation d'instructions d'accès mémoire spécifiques. Comme indiqué dans le PPT à la page suivante, la machine virtuelle HotSpot fournit des interfaces de barrière de mémoire correspondantes pour quatre scénarios de réorganisation possibles. Par exemple, si vous souhaitez interdire la réorganisation en écriture et en lecture du processeur X86, il vous suffit d'appeler l'interface de barrière de mémoire OrderAccess :: storeload (). En plus des quatre interfaces de base mentionnées ci-dessus, les interfaces d'acquisition, de libération et de clôture sont également définies dans la machine virtuelle. Parmi eux, l'acquisition peut interdire la réorganisation en lecture-lecture et lecture-écriture, la libération peut interdire la réorganisation en lecture-écriture et en écriture-écriture, et la clôture interdit toute réorganisation.

Le compilateur doit s'adapter pleinement aux caractéristiques du modèle de mémoire du processeur pendant l'étape de génération d'instructions. Le PPT suivant montre le nœud intermédiaire MemBarStoreStore du compilateur C2, la génération du code cible sur l'architecture X86 et l'architecture Aarch64. La sémantique du nœud intermédiaire MemBarStoreStore est d'interdire au processeur de réorganiser les écritures et les écritures. Étant donné que le modèle de mémoire X86 ne permet pas de réorganiser les écritures et les écritures, le nœud intermédiaire n'a pas besoin de générer des instructions machine supplémentaires sur l'architecture X86 pour garantir l'exactitude sémantique. Le processeur d'architecture Aarch64 lui-même permet la réorganisation écriture-écriture, donc une barrière mémoire écriture-écriture supplémentaire est nécessaire pour implémenter correctement la sémantique du nœud. En général, l'architecture de modèle de mémoire faible doit généralement générer plus de barrières de mémoire.

Que se passe-t-il si la JVM n'adapte pas correctement le modèle d'accès au processeur? Cela causera certainement des bugs. Ces bogues ont généralement les caractéristiques du hasard, de la divergence et des apparences diverses, ce qui rend l'analyse et le débogage difficiles. Permettez-moi de partager avec vous un bug (JDK-8229169) que j'ai résolu et que le modèle d'accès à la mémoire OpenJDK n'est pas adapté correctement. Ce bogue a d'abord été corrigé dans jdk14, puis rétroporté vers des versions LTS telles que jdk8 et jdk11.

Ce bogue est situé dans la phase de vol de travail du framework de récupération de place HotSpot et affecte tous les récupérateurs de place à l'exception du GC série. Le mécanisme du bogue est que lorsque le processeur exécute la méthode GenericTaskQueue :: pop, les deux opérations de lecture de _age (affichées dans la police jaune dans le PPT sur la page suivante) sont dans le désordre par le processeur. La solution consiste à ajouter une barrière mémoire de lecture (représentée en police verte dans le PPT) entre les deux opérations de lecture pour empêcher le processeur de lire dans le désordre. Quelqu'un peut demander: Étant donné que le processeur X86 ne permet pas la lecture dans le désordre, il n'est pas nécessaire d'ajouter cette barrière de mémoire sur X86. Pourquoi ne pas utiliser la méthode de modification dans le coin inférieur droit du PPT? La bonne réponse à cette question est que X86 doit également être corrigé en ajoutant OrderAccess :: loadload (). Cela est dû au fait que bien que X86 ne réorganise pas les opérations de lecture pendant l'exécution, le compilateur peut réorganiser ce code lors de la compilation de ce code. Afin d'interdire au code d'être réorganisé lors de la compilation, X86 a également besoin de ce correctif. Il n'est pas difficile de voir à partir de l'analyse ci-dessus que la barrière d'accès à la mémoire OrderAccess dans la JVM a également pour fonction d'interdire la réorganisation du processeur et du compilateur. Veuillez prêter plus d'attention à ce point dans le processus de développement futur.

Ce qui précède est ce que j'ai partagé avec vous aujourd'hui. Merci à tous! De plus, tout le monde est invité à prêter attention et à la star Tencent Kona JDK 8: 

https://github.com/Tencent/TencentKona-8

Dans le même temps, tous les développeurs exceptionnels sont également invités à rejoindre l'équipe R&D de Tencent JVM, à scanner le code QR ci-dessous ou à cliquer sur [ Lire le texte intégral ] pour nous rejoindre!

Les mots-clés de réponse dans les coulisses [GIAC] peuvent amener les invités à partager PPT.

Je suppose que tu aimes

Origine blog.csdn.net/Tencent_TEG/article/details/108301544
conseillé
Classement