En este momento, LLVM ya se ha convertido en un sistema muy popular, que muchas personas usan activamente para crear varios compiladores, analizadores, etc. Ya se ha escrito una gran cantidad de materiales útiles sobre este tema, incluso en ruso, lo cual es una buena noticia. Sin embargo, en la mayoría de los casos, el sesgo principal en los artículos se realiza en el LLVM frontend y middleend. Por supuesto, cuando se describe el esquema completo de la operación LLVM, no se omite la generación de código de máquina, pero básicamente este tema se trata de manera casual, especialmente en publicaciones en ruso. Al mismo tiempo, LLVM tiene un mecanismo bastante flexible e interesante para describir arquitecturas de procesador. Por lo tanto, este material se dedicará a la utilidad un tanto descuidada TableGen, que forma parte de LLVM.
La razón por la que el compilador necesita tener información sobre la arquitectura de cada una de las plataformas de destino es bastante obvia. Naturalmente, cada modelo de procesador tiene su propio conjunto de registros, sus propias instrucciones de máquina, etc. Y el compilador necesita tener toda la información necesaria sobre ellos para poder generar un código de máquina válido y eficiente. El compilador resuelve varias tareas específicas de la plataforma: distribuye registros, etc. Además, los backends de LLVM también llevan a cabo optimizaciones en el IR de la máquina, que está más cerca de las instrucciones reales o de las instrucciones del ensamblador. En tales optimizaciones, las instrucciones deben reemplazarse y transformarse; en consecuencia, toda la información sobre ellas debe estar disponible.
Para resolver el problema de describir la arquitectura del procesador, LLVM adoptó un formato único para determinar las propiedades del procesador necesarias para el compilador. Para cada arquitectura compatible, un
.td
contiene una descripción en un lenguaje formal especial. Se convierte en archivos
.inc
cuando se compila el compilador utilizando la utilidad TableGen incluida con LLVM. Los archivos resultantes, de hecho, son fuente C, pero tienen una extensión separada, muy probablemente, solo para que estos archivos generados automáticamente puedan distinguirse y filtrarse fácilmente. La documentación oficial de TableGen está
aquí y proporciona toda la información necesaria, también hay una
descripción formal del idioma y una
introducción general .
Por supuesto, este es un tema muy extenso, donde hay muchos detalles sobre los cuales puede escribir artículos individuales. En este artículo, simplemente consideramos los puntos básicos de la descripción de los procesadores, incluso sin una descripción general de todas las características.
Descripción de la arquitectura en archivo .td
Entonces, el lenguaje de descripción formal utilizado en TableGen tiene características similares a los lenguajes de programación ordinarios y le permite describir las características de la arquitectura en un estilo declarativo. Y según tengo entendido, este lenguaje también se llama comúnmente TableGen. Es decir En este artículo, TableGen usa tanto el nombre del lenguaje formal como la utilidad que genera los artefactos resultantes a partir de él.
Los procesadores modernos son sistemas muy complejos, por lo que no es sorprendente que su descripción sea bastante voluminosa. En consecuencia, para crear la estructura y simplificar el mantenimiento de los archivos
.td
pueden incluir entre sí utilizando la directiva
#include
habitual para programadores en C. Con la ayuda de esta directiva, el archivo
Target.td
siempre se incluye primero y contiene interfaces independientes de la plataforma que deben implementarse para proporcionar toda la información necesaria de TableGen. Este archivo ya incluye un archivo
.td
con descripciones intrínsecas de LLVM, pero por sí mismo contiene principalmente clases base, como
Register
,
Instruction
,
Processor
, etc., de las cuales debe heredar para crear su propia arquitectura para el compilador basado en LLVM. De la oración anterior, está claro que TableGen tiene la noción de clases bien conocidas por todos los programadores.
En general, TableGen tiene solo dos entidades básicas:
clases y
definiciones .
Clases
Las clases TableGen también son abstracciones, como en todos los lenguajes de programación orientados a objetos, pero son entidades más simples.
Las clases pueden tener parámetros y campos, y también pueden heredar otras clases.
Por ejemplo, una de las clases base se presenta a continuación.
Los corchetes angulares indican los parámetros de entrada que se asignan a las propiedades de la clase. A partir de este ejemplo, también puede observar que el lenguaje TableGen está estáticamente escrito. Los tipos que existen en TableGen:
bit
(un análogo del tipo booleano con valores 0 y 1),
int
,
string
,
code
(un fragmento de código, este es un tipo, simplemente porque TableGen no tiene métodos y funciones en el sentido habitual, las líneas de código están escritas en
[{ ... }]
), bits <n>, lista <tipo> (los valores se establecen entre corchetes [...] como en Python y algunos otros lenguajes de programación),
class type
,
dag
.
La mayoría de los tipos deben entenderse, pero si tienen preguntas, todos se describen en detalle en la especificación del idioma, disponible en el enlace que se encuentra al principio del artículo.
La herencia también se describe mediante una sintaxis bastante familiar con
:
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"; }
En este caso, la clase creada, por supuesto, puede anular los valores de los campos especificados en la clase base usando la palabra clave
let
. Y puede agregar sus propios campos similares a la descripción proporcionada en el ejemplo anterior, indicando el tipo de campo.
Definiciones
Las definiciones ya son entidades concretas, puede compararlas con lo familiar para todos los objetos. Las definiciones se definen usando la palabra clave
def
y pueden implementar una clase, redefinir campos de clases base exactamente de la misma manera que se describió anteriormente, y también tienen sus propios campos.
def i8mem : X86MemOperand<"printbytemem", X86Mem8AsmOperand>; def X86AbsMemAsmOperand : AsmOperandClass { let Name = "AbsMem"; let SuperClasses = [X86MemAsmOperand]; }
Multiclases
Naturalmente, una gran cantidad de instrucciones en los procesadores tienen una semántica similar. Por ejemplo, puede haber un conjunto de instrucciones de tres direcciones que toman las dos formas
“reg = reg op reg”
y
“reg = reg op imm”
. En un caso, los valores se toman de los registros y el resultado también se guarda en el registro, y en el otro caso, el segundo operando es un valor constante (imm - operando inmediato).
Listar todas las combinaciones manualmente es bastante tedioso; aumenta el riesgo de cometer un error. Por supuesto, pueden generarse automáticamente escribiendo un script simple, pero esto no es necesario, porque existe un concepto como multiclases en el lenguaje 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)>; }
Dentro de las multiclases, debe describir todas las formas posibles de instrucciones utilizando la palabra clave
def
. Pero esta no es una forma completa de instrucciones que se generarán. Al mismo tiempo, puede redefinir los campos en ellos y hacer todo lo posible en las definiciones habituales. Para crear definiciones reales basadas en una multiclase, debe usar la palabra clave
defm
.
Y como resultado, para cada definición dada a través de
defm
de hecho, se construirán varias definiciones que son una combinación de la instrucción principal y todas las formas posibles descritas en la multiclase. Como resultado, se generarán las siguientes instrucciones en este ejemplo:
ADD_rr
,
ADD_ri
,
SUB_rr
,
SUB_ri
,
MUL_rr
,
MUL_ri
.
Las multiclases pueden contener no solo definiciones con
def
, sino también
defm
anidadas, lo que permite generar formas complejas de instrucciones. Un ejemplo que ilustra la creación de tales cadenas se puede encontrar en la documentación oficial.
Metas secundarias
Otra cosa básica y útil para los procesadores que tienen diferentes variaciones del conjunto de instrucciones es el soporte de subtarget en LLVM. Un ejemplo de uso es la implementación LLVM SPARC, que cubre tres versiones principales de la arquitectura de microprocesador SPARC a la vez: Versión 8 (V8, arquitectura de 32 bits), Versión 9 (V9, arquitectura de 64 bits) y arquitectura UltraSPARC. La diferencia entre las arquitecturas es bastante grande, un número diferente de registros de diferentes tipos, orden de bytes admitido, etc. En tales casos, si hay varias configuraciones, vale la pena implementar la clase
XXXSubtarget
para la arquitectura. El uso de esta clase en la descripción dará como resultado nuevas opciones de línea de comando
-mcpu=
y
-mattr=
.
Además de la clase
Subtarget
sí, la clase
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; }
En el archivo
Sparc.td
, puede encontrar ejemplos de la implementación de
SubtargetFeature
, que le permiten describir la disponibilidad de un conjunto de instrucciones para cada subtipo individual de la arquitectura.
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">;
En este caso, de todos modos,
Sparc.td
todavía define la clase
Proc
, que se utiliza para describir subtipos específicos de procesadores SPARC, que podrían tener las propiedades descritas anteriormente, incluidos diferentes conjuntos de instrucciones.
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]>;
Relación entre las propiedades de las instrucciones en TableGen y el código de fondo de LLVM
Las propiedades de las clases y definiciones le permiten generar y establecer correctamente las características arquitectónicas, pero no hay acceso directo a ellas desde el código fuente del back-end LLVM. Sin embargo, a veces desea poder obtener algunas propiedades de instrucciones específicas de la plataforma directamente en el código del compilador.
TSFlags
Para hacer esto, la clase base
Instruction
tiene un campo
TSFlags
especial, de 64 bits de tamaño, que TableGen convierte en un campo de objetos C ++ de la clase
MCInstrDesc
, generado sobre la base de los datos recibidos de la descripción de TableGen. Puede especificar cualquier cantidad de bits que necesite para almacenar información. Esto puede ser algún valor booleano, por ejemplo, para indicar que estamos usando una ALU escalar.
let TSFlags{0} = SALU;
O podemos almacenar el tipo de instrucción. Entonces necesitamos, por supuesto, más de un bit.
Como resultado, es posible obtener estas propiedades de la instrucción en el código de fondo.
bool isSALU = MI.getDesc().TSFlags & SIInstrFlags::SALU;
Si la propiedad es más compleja, puede compararla con el valor descrito en TableGen, que se agregará a la enumeración generada automáticamente.
(Desc.TSFlags & X86II::FormMask) == X86II::MRMSrcMem
Predicados de funciones
Además, los predicados de función se pueden usar para obtener la información necesaria sobre las instrucciones. Con su ayuda, puede mostrar a TableGen que necesita generar una función que estará disponible en el código de fondo. La clase base con la que puede crear dicha definición de función se presenta a continuación.
Puede encontrar fácilmente ejemplos de uso en el backend para X86. Por lo tanto, existe su propia clase intermedia, con la ayuda de la cual ya se crean las definiciones necesarias de funciones.
Como resultado, puede usar el método
isThreeOperandsLEA
en código C ++.
if (!(TII->isThreeOperandsLEA(MI) || hasInefficientLEABaseReg(Base, Index)) || !TII->isSafeToClobberEFLAGS(MBB, MI) || Segment.getReg() != X86::NoRegister) return;
Aquí TII es la información de instrucción de destino, que se puede obtener utilizando el método
getInstrInfo()
del
MCSubtargetInfo
para la arquitectura deseada.
Transformación de instrucciones durante optimizaciones. Mapeo de instrucciones
Durante una gran cantidad de optimizaciones realizadas en las etapas posteriores de la compilación, la tarea a menudo surge de convertir todas o solo una parte de las instrucciones de un formulario en instrucciones de otro formulario. Dada la aplicación de las multiclases descritas al principio, podemos tener una gran cantidad de instrucciones con semánticas y propiedades similares. En el código, estas transformaciones, por supuesto, podrían escribirse en forma de grandes construcciones de
switch-case
, que para cada instrucción aplastan la transformación correspondiente. Parcialmente, estas enormes construcciones se pueden reducir con la ayuda de macros, que formarían el nombre necesario de la instrucción de acuerdo con reglas bien conocidas. Pero aún así, este enfoque es muy inconveniente, es difícil de mantener debido al hecho de que todos los nombres de instrucciones se enumeran explícitamente. Agregar una nueva instrucción puede conducir fácilmente a un error, porque debe recordar agregarlo a todas las conversiones relevantes. Después de haber sido atormentado con este enfoque, LLVM creó un mecanismo especial para convertir eficientemente una forma de instrucción en otra
Instruction Mapping
.
La idea es muy simple, es necesario describir posibles modelos para transformar instrucciones directamente en TableGen. Por lo tanto, en LLVM TableGen hay una clase base para describir tales modelos.
class InstrMapping {
Veamos un ejemplo que se da en la documentación. Los ejemplos que se pueden encontrar en el código fuente ahora son aún más simples, ya que solo se obtienen dos columnas en la tabla final. En el código de back-end puede encontrar la conversión de formas antiguas a nuevas formas de instrucciones, instrucciones dsp en mmdsp, etc., descritas mediante el mapeo de instrucciones. De hecho, este mecanismo no se usa tan ampliamente hasta ahora, simplemente porque la mayoría de los backends comenzaron a crearse antes de que apareciera, y para que funcione, aún necesita establecer las propiedades correctas para las instrucciones, por lo que cambiar a él no siempre es fácil, puede necesitar algunos refactorización.
Entonces, por ejemplo. Supongamos que tenemos formas de instrucciones sin predicados e instrucciones donde el predicado es respectivamente verdadero y falso. Los describimos con la ayuda de una clase múltiple y una clase especial, que solo usaremos como filtro. Una descripción simplificada sin parámetros y muchas propiedades puede ser algo como esto.
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”>; …
En este ejemplo, por cierto, también se muestra cómo anular una propiedad para varias definiciones a la vez usando la construcción
let … in
. Como resultado, tenemos muchas instrucciones que almacenan su nombre base y propiedad que describe de forma única su forma. Entonces puedes crear un modelo de transformación.
def getPredOpcode : InstrMapping {
Como resultado, la siguiente tabla se generará a partir de esta descripción.
Se generará una función en el archivo
.inc
int getPredOpcode(uint16_t Opcode, enum PredSense inPredSense)
Que, en consecuencia, acepta un código de instrucciones para la conversión y el valor de la enumeración generada automáticamente de PredSense, que contiene todos los valores posibles en las columnas. La implementación de esta función es muy simple, porque devuelve el elemento de matriz deseado para la instrucción que nos interesa.
Y en el código de back-end, en lugar de escribir un
switch-case
suficiente simplemente llamar a la función generada, que devolverá el código de la instrucción convertida. Una solución simple, donde agregar nuevas instrucciones, no conducirá a la necesidad de una acción adicional.
Artefactos generados automáticamente (archivos .inc
)
Toda la interacción entre la descripción de TableGen y el código de fondo de LLVM está asegurada por los archivos
.inc
generados que contienen el código C. Para obtener una imagen completa, veamos un poco qué son exactamente.
Después de cada compilación, para cada arquitectura, habrá varios archivos
.inc
en el directorio de compilación, cada uno de los cuales almacena información separada sobre la arquitectura.
Así que hay un archivo <TargetName>GenInstrInfo.inc
que contiene información sobre las instrucciones <TargetName>GenRegisterInfo.inc
, respectivamente, que contiene información sobre los registros, hay archivos para trabajar directamente con el ensamblador y su salida <TargetName>GenAsmMatcher.inc
y <TargetName>GenAsmWriter.inc
etc.Entonces, ¿en qué consisten estos archivos? En general, contienen enumeraciones, matrices, estructuras y funciones simples. Por ejemplo, puede ver la información convertida en las instrucciones en <TargetName>GenInstrInfo.inc
.En la primera parte, en el espacio de nombres con el nombre del objetivo hay una enumeración que contiene todas las instrucciones que se han descrito. namespace X86 { enum { PHI = 0, … ADD16i16 = 287, ADD16mi = 288, ADD16mi8 = 289, ADD16mr = 290, ADD16ri = 291, ADD16ri8 = 292, ADD16rm = 293, ADD16rr = 294, ADD16rr_REV = 295, … }
El siguiente es un conjunto que describe las propiedades de las instrucciones const MCInstrDesc X86Insts[]
. Las siguientes matrices contienen información sobre nombres de instrucciones, etc. Básicamente, toda la información se almacena en transferencias y matrices.También hay funciones que se han descrito utilizando predicados. Según la definición del predicado de la función discutida en la sección anterior, se generará la siguiente función. 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; }
Pero hay datos en los archivos y estructuras generados. En X86GenSubtargetInfo.inc
usted puede encontrar un ejemplo de la estructura que debe usarse en el código de back-end para obtener información sobre la arquitectura, a través de ella en la sección anterior obtuvimos 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); } };
Si se usa Subtarget
para describir varias configuraciones XXXGenSubtarget.inc
, se creará una enumeración con las propiedades descritas usando SubtargetFeature
matrices con valores constantes para indicar las características y subtipos de la CPU, y se generará una función ParseSubtargetFeatures
que procesa la cadena con el conjunto de opciones Subtarget
. Además, la implementación del método XXXSubtarget
en el código de fondo debe corresponder al siguiente pseudocódigo, en el que es necesario usar esta función: XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
A pesar de que los .inc
archivos son muy voluminosos y contienen grandes matrices, esto nos permite optimizar el tiempo de acceso a la información, ya que el acceso a un elemento de matriz tiene un tiempo constante. Las funciones de búsqueda generadas por instrucciones se implementan utilizando un algoritmo de búsqueda binario para minimizar el tiempo de funcionamiento. Por lo tanto, el almacenamiento en esta forma está bastante justificado.Conclusión
Como resultado, gracias a TableGen en LLVM, tenemos descripciones de arquitectura legibles y fácilmente compatibles en un solo formato con varios mecanismos para interactuar y acceder a la información del código fuente de back-end LLVM para optimizaciones y generación de código. Al mismo tiempo, dicha descripción no afecta el rendimiento del compilador debido al código autogenerado que utiliza soluciones eficientes y estructuras de datos.