À l'heure actuelle, LLVM est déjà devenu un système très populaire, que de nombreuses personnes utilisent activement pour créer divers compilateurs, analyseurs, etc. Un grand nombre de documents utiles sur ce sujet ont déjà été rédigés, y compris en russe, ce qui est une bonne nouvelle. Cependant, dans la plupart des cas, le principal biais dans les articles concerne le LLVM frontal et intermédiaire. Bien sûr, lors de la description du schéma complet du fonctionnement de LLVM, la génération de code machine n'est pas contournée, mais fondamentalement, ce sujet est abordé avec désinvolture, en particulier dans les publications en russe. Dans le même temps, LLVM dispose d'un mécanisme plutôt flexible et intéressant pour décrire les architectures de processeur. Par conséquent, ce matériel sera consacré à l'utilitaire quelque peu négligé TableGen, qui fait partie de LLVM.
La raison pour laquelle le compilateur a besoin d'informations sur l'architecture de chacune des plates-formes cibles est assez évidente. Naturellement, chaque modèle de processeur possède son propre ensemble de registres, ses propres instructions machine, etc. Et le compilateur doit avoir toutes les informations nécessaires à leur sujet afin de pouvoir générer un code machine valide et efficace. Le compilateur résout diverses tâches spécifiques à la plate-forme: distribue les registres, etc. De plus, les backends LLVM effectuent également des optimisations déjà sur la machine IR, qui est plus proche des instructions réelles, ou sur les instructions d'assembleur elles-mêmes. Dans de telles optimisations, les instructions doivent être remplacées et transformées; en conséquence, toutes les informations les concernant doivent être disponibles.
Pour résoudre le problème de description de l'architecture du processeur, LLVM a adopté un format unique pour déterminer les propriétés du processeur nécessaires au compilateur. Pour chaque architecture prise en charge, un
.td
contient une description dans un langage formel spécial. Il est converti en fichiers
.inc
lors de la construction du compilateur à l'aide de l'utilitaire TableGen inclus avec LLVM. En fait, les fichiers résultants sont de source C, mais ont probablement une extension distincte, juste pour que ces fichiers générés automatiquement puissent être facilement distingués et filtrés. La documentation officielle de TableGen est
ici et donne toutes les informations nécessaires, il y a aussi une
description formelle de la langue et une
introduction générale .
Bien sûr, il s'agit d'un sujet très complet, où il existe de nombreux détails sur lesquels vous pouvez écrire des articles individuels. Dans cet article, nous considérons simplement les points de base de la description des processeurs même sans un aperçu de toutes les fonctionnalités.
Description de l'architecture dans le fichier .td
Ainsi, le langage de description formel utilisé dans TableGen a des caractéristiques similaires aux langages de programmation ordinaires et vous permet de décrire les caractéristiques de l'architecture dans un style déclaratif. Et si je comprends bien, ce langage est aussi communément appelé TableGen. C'est-à-dire Dans cet article, TableGen utilise à la fois le nom du langage formel lui-même et l'utilitaire qui en génère les artefacts.
Les processeurs modernes sont des systèmes très complexes, il n'est donc pas surprenant que leur description soit assez volumineuse. En conséquence, pour créer la structure et simplifier la maintenance des fichiers
.td
, vous pouvez vous inclure en utilisant la directive
#include
habituelle pour les programmeurs C. Avec l'aide de cette directive, le fichier
Target.td
est toujours inclus en premier, contenant des interfaces indépendantes de la plate-forme qui doivent être implémentées pour fournir toutes les informations TableGen nécessaires. Ce fichier comprend déjà un fichier
.td
avec des descriptions intrinsèques LLVM, mais en lui-même il contient principalement des classes de base, telles que
Register
,
Instruction
,
Processor
, etc., dont vous devez hériter pour créer votre propre architecture pour un compilateur basé sur LLVM. D'après la phrase précédente, il est clair que TableGen a la notion de classes bien connue de tous les programmeurs.
En général, TableGen n'a que deux entités de base: les
classes et les
définitions .
Cours
Les classes TableGen sont également des abstractions, comme dans tous les langages de programmation orientés objet, mais ce sont des entités plus simples.
Les classes peuvent avoir des paramètres et des champs, et elles peuvent également hériter d'autres classes.
Par exemple, l'une des classes de base est présentée ci-dessous.
Les crochets angulaires indiquent les paramètres d'entrée affectés aux propriétés de la classe. Dans cet exemple, vous pouvez également remarquer que le langage TableGen est typé statiquement. Les types qui existent dans TableGen:
bit
(un analogue du type booléen avec les valeurs 0 et 1),
int
,
string
,
code
(un morceau de code, c'est un type, simplement parce qu'il n'y a pas de méthodes et de fonctions dans TableGen au sens habituel, les lignes de code sont écrites en
[{ ... }]
), bits <n>, liste <type> (les valeurs sont définies à l'aide de crochets [...] comme en Python et dans d'autres langages de programmation),
class type
,
dag
.
La plupart des types doivent être compris, mais s'ils ont des questions, ils sont tous décrits en détail dans la spécification de la langue, disponible sur le lien donné au début de l'article.
L'héritage est également décrit par une syntaxe assez familière avec:.
class X86MemOperand<string printMethod, AsmOperandClass parserMatchClass = X86MemAsmOperand> : Operand<iPTR> { let PrintMethod = printMethod; let MIOperandInfo = (ops ptr_rc, i8imm, ptr_rc_nosp, i32imm, SEGMENT_REG); let ParserMatchClass = parserMatchClass; let OperandType = "OPERAND_MEMORY"; }
Dans ce cas, la classe créée, bien sûr, peut remplacer les valeurs des champs spécifiés dans la classe de base à l'aide du mot clé
let
. Et il peut ajouter ses propres champs similaires à la description fournie dans l'exemple précédent, en indiquant le type de champ.
Définitions
Les définitions sont déjà des entités concrètes, vous pouvez les comparer avec le familier à tous les objets. Les définitions sont définies à l'aide du mot clé
def
et peuvent implémenter une classe, redéfinir les champs des classes de base exactement de la même manière que décrit ci-dessus, et également avoir leurs propres champs.
def i8mem : X86MemOperand<"printbytemem", X86Mem8AsmOperand>; def X86AbsMemAsmOperand : AsmOperandClass { let Name = "AbsMem"; let SuperClasses = [X86MemAsmOperand]; }
Multiclasses
Naturellement, un grand nombre d'instructions dans les processeurs ont une sémantique similaire. Par exemple, il peut y avoir un ensemble d'instructions à trois adresses qui prennent les deux formes
“reg = reg op reg”
et
“reg = reg op imm”
. Dans un cas, les valeurs sont extraites des registres et le résultat est également enregistré dans le registre, et dans l'autre cas, le deuxième opérande est une valeur constante (imm - opérande immédiat).
Lister toutes les combinaisons manuellement est assez fastidieux; le risque de faire une erreur augmente. Bien sûr, ils peuvent être générés automatiquement en écrivant un script simple, mais ce n'est pas nécessaire, car un concept tel que les multiclasses existe dans le langage TableGen.
multiclass ri_inst<int opc, string asmstr> { def _rr : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"), (ops GPR:$dst, GPR:$src1, GPR:$src2)>; def _ri : inst<opc, !strconcat(asmstr, " $dst, $src1, $src2"), (ops GPR:$dst, GPR:$src1, Imm:$src2)>; }
Dans les multiclasses, vous devez décrire toutes les formes d'instructions possibles à l'aide du mot clé
def
. Mais ce n'est pas une forme complète d'instructions à générer. Dans le même temps, vous pouvez redéfinir les champs qu'ils contiennent et faire tout ce qui est possible dans les définitions habituelles. Pour créer de véritables définitions basées sur une multiclasse, vous devez utiliser le mot clé
defm
.
Et en conséquence, pour chacune de ces définitions données via
defm
en fait, plusieurs définitions seront construites qui sont une combinaison de l'instruction principale et de toutes les formes possibles décrites dans la multiclasse. Par conséquent, les instructions suivantes seront générées dans cet exemple:
ADD_rr
,
ADD_ri
,
SUB_rr
,
SUB_ri
,
MUL_rr
,
MUL_ri
.
Les multiclasses peuvent contenir non seulement des définitions avec
def
, mais également des
defm
imbriquées, permettant ainsi la génération de formes complexes d'instructions. Un exemple illustrant la création de telles chaînes se trouve dans la documentation officielle.
Sous-cibles
Une autre chose fondamentale et utile pour les processeurs qui ont différentes variations du jeu d'instructions est le support de subtarget dans LLVM. Un exemple d'utilisation est l'implémentation LLVM SPARC, qui couvre trois versions principales de l'architecture de microprocesseur SPARC à la fois: version 8 (architecture V8, 32 bits), version 9 (architecture V9, 64 bits) et architecture UltraSPARC. La différence entre les architectures est assez grande, un nombre différent de registres de différents types, l'ordre des octets pris en charge, etc. Dans de tels cas, s'il existe plusieurs configurations, il vaut la peine d'implémenter la classe
XXXSubtarget
pour l'architecture. L'utilisation de cette classe dans la description entraînera de nouvelles options de ligne de commande
-mcpu=
et
-mattr=
.
En plus de la classe
Subtarget
elle-même, la classe
Subtarget
importante.
class SubtargetFeature<string n, string a, string v, string d, list<SubtargetFeature> i = []> { string Name = n; string Attribute = a; string Value = v; string Desc = d; list<SubtargetFeature> Implies = i; }
Dans le fichier
Sparc.td
, vous pouvez trouver des exemples d'implémentation de
SubtargetFeature
, qui vous permettent de décrire la disponibilité d'un ensemble d'instructions pour chaque sous-type individuel de l'architecture.
def FeatureV9 : SubtargetFeature<"v9", "IsV9", "true", "Enable SPARC-V9 instructions">; def FeatureV8Deprecated : SubtargetFeature<"deprecated-v8", "V8DeprecatedInsts", "true", "Enable deprecated V8 instructions in V9 mode">; def FeatureVIS : SubtargetFeature<"vis", "IsVIS", "true", "Enable UltraSPARC Visual Instruction Set extensions">;
Dans ce cas, de toute façon,
Sparc.td
définit toujours la classe
Proc
, qui est utilisée pour décrire des sous-types spécifiques de processeurs SPARC, qui peuvent simplement avoir les propriétés décrites ci-dessus, y compris différents ensembles d'instructions.
class Proc<string Name, list<SubtargetFeature> Features> : Processor<Name, NoItineraries, Features>; def : Proc<"generic", []>; def : Proc<"v8", []>; def : Proc<"supersparc", []>; def : Proc<"sparclite", []>; def : Proc<"f934", []>; def : Proc<"hypersparc", []>; def : Proc<"sparclite86x", []>; def : Proc<"sparclet", []>; def : Proc<"tsc701", []>; def : Proc<"v9", [FeatureV9]>; def : Proc<"ultrasparc", [FeatureV9, FeatureV8Deprecated]>; def : Proc<"ultrasparc3", [FeatureV9, FeatureV8Deprecated]>; def : Proc<"ultrasparc3-vis", [FeatureV9, FeatureV8Deprecated, FeatureVIS]>;
Relation entre les propriétés des instructions dans TableGen et le code backend LLVM
Les propriétés des classes et des définitions vous permettent de générer et de définir correctement les fonctionnalités architecturales, mais il n'y a pas d'accès direct à celles-ci depuis le code source du backend LLVM. Cependant, vous souhaitez parfois pouvoir obtenir certaines propriétés d'instructions spécifiques à la plate-forme directement dans le code du compilateur.
TSFlags
Pour ce faire, la classe de base
Instruction
possède un champ spécial, TSFlags 64 bits, qui est converti par TableGen en un champ d'objets C ++ de la classe
MCInstrDesc
, généré sur la base des données reçues de la description TableGen. Vous pouvez spécifier n'importe quel nombre de bits dont vous avez besoin pour stocker des informations. Il peut s'agir d'une valeur booléenne, par exemple, pour indiquer que nous utilisons une ALU scalaire.
let TSFlags{0} = SALU;
Ou nous pouvons stocker le type d'instruction. Ensuite, nous avons bien sûr besoin de plus d'un bit.
Par conséquent, il devient possible d'obtenir ces propriétés à partir de l'instruction dans le code principal.
bool isSALU = MI.getDesc().TSFlags & SIInstrFlags::SALU;
Si la propriété est plus complexe, vous pouvez la comparer à la valeur décrite dans TableGen, qui sera ajoutée à l'énumération générée automatiquement.
(Desc.TSFlags & X86II::FormMask) == X86II::MRMSrcMem
Prédicats de fonction
De plus, les prédicats de fonction peuvent être utilisés pour obtenir les informations nécessaires sur les instructions. Avec leur aide, vous pouvez montrer à TableGen que vous devez générer une fonction qui sera donc disponible dans le code backend. La classe de base avec laquelle vous pouvez créer une telle définition de fonction est présentée ci-dessous.
Vous pouvez facilement trouver des exemples d'utilisation dans le backend pour X86. Il y a donc sa propre classe intermédiaire, à l'aide de laquelle les définitions nécessaires des fonctions sont déjà créées.
Par conséquent, vous pouvez utiliser la méthode
isThreeOperandsLEA
dans le code C ++.
if (!(TII->isThreeOperandsLEA(MI) || hasInefficientLEABaseReg(Base, Index)) || !TII->isSafeToClobberEFLAGS(MBB, MI) || Segment.getReg() != X86::NoRegister) return;
Ici, TII est l'information d'instruction cible, qui peut être obtenue en utilisant la méthode
getInstrInfo()
du
MCSubtargetInfo
pour l'architecture souhaitée.
Transformation des instructions lors des optimisations. Mappage d'instructions
Lors d'un grand nombre d'optimisations effectuées dans les dernières étapes de la compilation, la tâche se pose souvent de convertir tout ou partie des instructions d'un formulaire en instructions d'un autre formulaire. Compte tenu de l'application des multiclasses décrites au début, nous pouvons avoir un grand nombre d'instructions avec une sémantique et des propriétés similaires. Dans le code, ces transformations, bien sûr, pourraient être écrites sous la forme de grandes constructions de
switch-case
, qui pour chaque instruction écrasaient la transformation correspondante. En partie, ces énormes constructions peuvent être réduites à l'aide de macros, qui formeraient le nom nécessaire de l'instruction selon des règles bien connues. Mais encore, cette approche est très gênante, elle est difficile à maintenir en raison du fait que tous les noms d'instructions sont explicitement répertoriés. L'ajout d'une nouvelle instruction peut très facilement entraîner une erreur, car vous devez vous rappeler de l'ajouter à toutes les conversions pertinentes. Ayant été tourmenté par cette approche, LLVM a créé un mécanisme spécial pour convertir efficacement une forme d'instruction en une autre
Instruction Mapping
.
L'idée est très simple, il faut décrire des modèles possibles pour transformer des instructions directement dans TableGen. Par conséquent, dans LLVM TableGen, il existe une classe de base pour décrire ces modèles.
class InstrMapping {
Regardons un exemple donné dans la documentation. Les exemples qui peuvent être trouvés dans le code source sont maintenant encore plus simples, car seules deux colonnes sont obtenues dans le tableau final. Dans le code principal, vous pouvez trouver la conversion d'anciens formulaires en de nouvelles formes d'instructions, des instructions dsp en mmdsp, etc., décrites à l'aide du mappage d'instructions. En fait, ce mécanisme n'est pas si largement utilisé jusqu'à présent, simplement parce que la plupart des backends ont commencé à être créés avant son apparition, et pour qu'il fonctionne, vous devez toujours définir les propriétés correctes pour les instructions, donc passer à celui-ci n'est pas toujours facile, vous pouvez en avoir besoin refactoring.
Ainsi, par exemple. Supposons que nous ayons des formes d'instructions sans prédicats et des instructions où le prédicat est respectivement vrai et faux. Nous les décrivons à l'aide d'une multiclasse et d'une classe spéciale, que nous allons simplement utiliser comme filtre. Une description simplifiée sans paramètres et de nombreuses propriétés peut ressembler à ceci.
class PredRel; multiclass MyInstruction<string name> { let BaseOpcode = name in { def : PredRel { let PredSense = ""; } def _pt: PredRel { let PredSense = "true"; } def _pf: PredRel { let PredSense = "false"; } } } defm ADD: MyInstruction<”ADD”>; defm SUB: MyIntruction<”SUB”>; defm MUL: MyInstruction<”MUL”>; …
Dans cet exemple, en passant, il est également montré comment remplacer une propriété pour plusieurs définitions à la fois en utilisant la construction
let … in
. Par conséquent, nous avons de nombreuses instructions qui stockent leur nom de base et leur propriété qui décrivent de manière unique leur formulaire. Ensuite, vous pouvez créer un modèle de transformation.
def getPredOpcode : InstrMapping {
Par conséquent, le tableau suivant sera généré à partir de cette description.
Une fonction sera générée dans le fichier
.inc
int getPredOpcode(uint16_t Opcode, enum PredSense inPredSense)
Qui, en conséquence, accepte un code d'instruction pour la conversion et la valeur de l'énumération générée automatiquement PredSense, qui contient toutes les valeurs possibles dans les colonnes. La mise en œuvre de cette fonction est très simple, car il renvoie l'élément de tableau souhaité pour l'instruction qui nous intéresse.
Et dans le code backend, au lieu d'écrire un
switch-case
suffit d'appeler simplement la fonction générée, qui renverra le code de l'instruction convertie. Une solution simple, où l'ajout de nouvelles instructions, n'entraînera pas la nécessité d'une action supplémentaire.
Artefacts générés automatiquement (fichiers .inc
)
Toute l'interaction entre la description TableGen et le code backend LLVM est assurée par les fichiers
.inc
générés qui contiennent le code C. Pour obtenir une image complète, voyons un peu ce qu'ils sont exactement.
Après chaque build, pour chaque architecture, il y aura plusieurs fichiers
.inc
dans le répertoire de build, chacun d'entre eux stockant des informations distinctes sur l'architecture.
Donc , il y a un fichier <TargetName>GenInstrInfo.inc
contenant des informations sur les instructions <TargetName>GenRegisterInfo.inc
, respectivement, qui contient des informations sur les registres, il y a des fichiers de travail directement avec l'assembleur et sa sortie <TargetName>GenAsmMatcher.inc
et <TargetName>GenAsmWriter.inc
etc.En quoi consistent ces fichiers? En général, ils contiennent des énumérations, des tableaux, des structures et des fonctions simples. Par exemple, vous pouvez consulter les informations converties sur les instructions dans <TargetName>GenInstrInfo.inc
.Dans la première partie, dans l'espace de noms avec le nom de la cible se trouve une énumération contenant toutes les instructions qui ont été décrites. namespace X86 { enum { PHI = 0, … ADD16i16 = 287, ADD16mi = 288, ADD16mi8 = 289, ADD16mr = 290, ADD16ri = 291, ADD16ri8 = 292, ADD16rm = 293, ADD16rr = 294, ADD16rr_REV = 295, … }
Vient ensuite un tableau décrivant les propriétés des instructions const MCInstrDesc X86Insts[]
. Les tableaux suivants contiennent des informations sur les noms d'instructions, etc. Fondamentalement, toutes les informations sont stockées dans des transferts et des tableaux.Il existe également des fonctions qui ont été décrites à l'aide de prédicats. Sur la base de la définition de prédicat de fonction discutée dans la section précédente, la fonction suivante sera générée. bool X86InstrInfo::isThreeOperandsLEA(const MachineInstr &MI) { switch(MI.getOpcode()) { case X86::LEA32r: case X86::LEA64r: case X86::LEA64_32r: case X86::LEA16r: return ( MI.getOperand(1).isReg() && MI.getOperand(1).getReg() != 0 && MI.getOperand(3).isReg() && MI.getOperand(3).getReg() != 0 && ( ( MI.getOperand(4).isImm() && MI.getOperand(4).getImm() != 0 ) || (MI.getOperand(4).isGlobal()) ) ); default: return false; }
Mais il y a des données dans les fichiers et les structures générés. Dans X86GenSubtargetInfo.inc
vous pouvez trouver un exemple de la structure qui devrait être utilisée dans le code principal pour obtenir des informations sur l'architecture, à travers elle dans la section précédente, nous avons obtenu TTI. struct X86GenMCSubtargetInfo : public MCSubtargetInfo { X86GenMCSubtargetInfo(const Triple &TT, StringRef CPU, StringRef FS, ArrayRef<SubtargetFeatureKV> PF, ArrayRef<SubtargetSubTypeKV> PD, const MCWriteProcResEntry *WPR, const MCWriteLatencyEntry *WL, const MCReadAdvanceEntry *RA, const InstrStage *IS, const unsigned *OC, const unsigned *FP) : MCSubtargetInfo(TT, CPU, FS, PF, PD, WPR, WL, RA, IS, OC, FP) { } unsigned resolveVariantSchedClass(unsigned SchedClass, const MCInst *MI, unsigned CPUID) const override { return X86_MC::resolveVariantSchedClassImpl(SchedClass, MI, CPUID); } };
S'il est utilisé Subtarget
pour décrire diverses configurations XXXGenSubtarget.inc
, une énumération sera créée avec les propriétés décrites à l'aide de SubtargetFeature
tableaux avec des valeurs constantes pour indiquer les caractéristiques et les sous-types du CPU, et une fonction sera générée ParseSubtargetFeatures
qui traitera la chaîne avec le jeu d'options Subtarget
. De plus, l'implémentation de la méthode XXXSubtarget
dans le code backend doit correspondre au pseudo-code suivant, dans lequel il est nécessaire d'utiliser cette fonction: XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
Malgré le fait que les .inc
fichiers sont très volumineux et contiennent d'énormes tableaux, cela nous permet d'optimiser le temps d'accès aux informations, car l'accès à un élément du tableau a un temps constant. Les fonctions de recherche générées par des instructions sont implémentées à l'aide d'un algorithme de recherche binaire pour minimiser le temps de fonctionnement. Le stockage sous cette forme est donc tout à fait justifié.Conclusion
En conséquence, grâce à TableGen dans LLVM, nous avons des descriptions d'architecture lisibles et facilement prises en charge dans un format unique avec divers mécanismes pour interagir et accéder aux informations à partir du code source backend LLVM pour les optimisations et la génération de code. Dans le même temps, une telle description n'affecte pas les performances du compilateur en raison du code généré automatiquement qui utilise des solutions et des structures de données efficaces.