目前,LLVM已经成为非常流行的系统,许多人积极地使用它来创建各种编译器,分析器等。 关于该主题的大量有用材料已经写好,包括俄语,这是个好消息。 但是,在大多数情况下,文章中的主要偏见是在前端和中端LLVM上进行的。 当然,当描述LLVM操作的完整方案时,并不会绕过机器代码的生成,但是基本上这个主题是随意涉及的,尤其是在俄语出版物中。 同时,LLVM具有相当灵活和有趣的机制来描述处理器体系结构。 因此,本文将专门讨论LLVM的一部分实用程序TableGen。
编译器需要了解有关每个目标平台的体系结构的信息的原因非常明显。 自然,每个处理器模型都有自己的寄存器集,自己的机器指令等。 并且编译器需要具有关于它们的所有必要信息,以便能够生成有效和高效的机器代码。 编译器解决了各种平台特定的任务:分发寄存器等。 另外,LLVM后端还已经在机器IR上进行了优化,这更接近于实际指令或汇编程序指令本身。 在此类优化中,需要替换和转换指令;因此,有关它们的所有信息都应可用。
为了解决描述处理器体系结构的问题,LLVM采用了一种格式来确定编译器所需的处理器属性。 对于每种受支持的体系结构,
.td
包含特殊形式语言的描述。 使用LLVM附带的TableGen实用程序构建编译器时,它将转换为
.inc
文件。 实际上,生成的文件是C源代码,但是很可能具有单独的扩展名,以使可以轻松地区分和过滤这些自动生成的文件。 TableGen的官方文档在
这里 ,提供了所有必要的信息,还提供
了对该语言的
正式描述和
概述 。
当然,这是一个非常广泛的主题,其中有许多您可以撰写个别文章的细节。 在本文中,即使没有概述所有功能,我们也只是考虑了处理器描述的基本要点。
.td文件中的体系结构说明
因此,TableGen中使用的形式化描述语言具有与普通编程语言类似的功能,并允许您以声明性样式描述架构的特征。 据我了解,该语言通常也称为TableGen。 即 在本文中,TableGen既使用形式语言本身的名称,又使用从中生成结果构件的实用程序。
现代处理器是非常复杂的系统,因此对它们的描述非常多就不足为奇了。 因此,要创建结构并简化
.td
文件的维护,可以使用C程序员常用的
#include
指令相互包含。 在此指令的帮助下,始终始终首先包含
Target.td
文件,其中包含与平台无关的接口,必须实施这些接口才能提供所有必需的TableGen信息。 该文件已经包含一个带有LLVM内部描述的
.td
文件,但是它本身主要包含基类,例如
Register
,
Instruction
,
Processor
等,您需要从这些基类继承以创建自己的编译器架构,基于LLVM。 从上一句话可以清楚地看出,TableGen具有所有程序员都熟悉的类的概念。
通常,TableGen仅具有两个基本实体:
类和
定义 。
班级
与所有面向对象的编程语言一样,TableGen类也是抽象的,但是它们是更简单的实体。
类可以具有参数和字段,并且它们也可以继承其他类。
例如,下面提供了一个基类。
尖括号表示分配给类属性的输入参数。 从此示例中,您还可以注意到TableGen语言是静态类型的。 TableGen中存在的类型:
bit
(值为0和1的布尔型模拟),
int
,
string
和
code
(一段代码,这是一种类型,仅因为TableGen没有通常意义上的方法和函数,所以代码行用
[{ ... }]
),位<n>,列表<type>(值使用方括号[...]设置,如Python和其他一些编程语言中一样),
class type
,
dag
。
大多数类型应该都可以理解,但是如果有疑问,它们会在语言规范中进行详细说明,可从本文开头提供的链接中获得。
继承也通过与
:
相当熟悉的语法来描述
:
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"; }
在这种情况下,创建的类当然可以使用
let
关键字覆盖基类中指定的字段的值。 并且它可以添加自己的字段,类似于上一个示例中提供的描述,指示字段的类型。
定义
定义已经是具体的实体,您可以将它们与所有对象的熟悉对象进行比较。 定义是使用
def
关键字定义的,可以实现一个类,以与上述完全相同的方式重新定义基类的字段,并且也具有自己的字段。
def i8mem : X86MemOperand<"printbytemem", X86Mem8AsmOperand>; def X86AbsMemAsmOperand : AsmOperandClass { let Name = "AbsMem"; let SuperClasses = [X86MemAsmOperand]; }
多类
自然地,处理器中的大量指令具有相似的语义。 例如,可能存在一组三地址指令,其采用两种形式
“reg = reg op reg”
和
“reg = reg op imm”
。 在一种情况下,从寄存器中获取值,结果也保存在寄存器中;在另一种情况下,第二个操作数是一个常量值(imm-立即数)。
手动列出所有组合非常繁琐;犯错的风险增加。 当然,可以通过编写简单的脚本自动生成它们,但这不是必需的,因为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)>; }
在多类内部,您需要使用
def
关键字描述所有可能的指令形式。 但这不是要生成的指令的完整形式。 同时,您可以重新定义它们中的字段,并执行常规定义中可能的所有事情。 要基于多类创建真实定义,您需要使用
defm
关键字。
因此,实际上,对于通过
defm
给出的每个这样的定义,将构造几个定义,这些定义是主指令和多类中描述的所有可能形式的组合。 结果,在此示例中将生成以下指令:
ADD_rr
,
ADD_ri
,
SUB_rr
,
SUB_ri
,
MUL_rr
,
MUL_ri
。
多类不仅可以包含带有
def
定义,还可以包含嵌套的
defm
,从而允许生成复杂形式的指令。 可以在官方文档中找到说明创建此类链的示例。
子目标
对于具有不同指令集变化的处理器,另一项基本和有用的事情是LLVM中对子目标的支持。 LLVM SPARC实现就是一个使用示例,该实现一次涵盖了SPARC微处理器体系结构的三个主要版本:版本8(V8,32位体系结构),版本9(V9,64位体系结构)和UltraSPARC体系结构。 架构之间的差异非常大,不同类型的寄存器数量不同,支持的字节顺序等。 在这种情况下,如果有多种配置,则值得为该体系结构实现
XXXSubtarget
类。 在描述中使用此类将产生新的命令行选项
-mcpu=
和
-mattr=
。
除了
Subtarget
类本身之外,
Subtarget
类
Subtarget
很重要。
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; }
在
Sparc.td
文件中,您可以找到
Sparc.td
的实现示例,该示例使您可以描述体系结构的每个子类型的一组指令的可用性。
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">;
在这种情况下,无论如何,
Sparc.td
仍然定义
Proc
类,该类用于描述SPARC处理器的特定子类型,该子类型可能具有上述属性,包括不同的指令集。
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]>;
TableGen中的指令属性与LLVM后端代码之间的关系
类和定义的属性允许您正确生成和设置体系结构功能,但是无法从LLVM后端源代码直接访问它们。 但是,有时您希望能够直接在编译器代码中获得某些平台特定的指令属性。
TSFlags
为此,
Instruction
基类有一个特殊的
TSFlags
字段,大小为64位,由TableGen转换为MCInstrDesc类的C ++对象的字段,该字段基于从TableGen描述接收的数据生成。 您可以指定存储信息所需的任何位数。 例如,这可能是一些布尔值,以表明我们正在使用标量ALU。
let TSFlags{0} = SALU;
或者我们可以存储指令类型。 那么,我们当然需要不止一位。
结果,可以从后端代码中的指令获取这些属性。
bool isSALU = MI.getDesc().TSFlags & SIInstrFlags::SALU;
如果属性更复杂,则可以将其与TableGen中描述的值进行比较,该值将添加到自动生成的枚举中。
(Desc.TSFlags & X86II::FormMask) == X86II::MRMSrcMem
函数谓词
同样,功能谓词可用于获取有关指令的必要信息。 在他们的帮助下,您可以告诉TableGen您需要生成一个函数,该函数将相应地在后端代码中可用。 下面介绍了可用于创建此类函数定义的基类。
您可以在X86后端轻松找到用法示例。 因此,有一个自己的中间类,借助于它,已经创建了必要的功能定义。
结果,可以在C ++代码中使用
isThreeOperandsLEA
方法。
if (!(TII->isThreeOperandsLEA(MI) || hasInefficientLEABaseReg(Base, Index)) || !TII->isSafeToClobberEFLAGS(MBB, MI) || Segment.getReg() != X86::NoRegister) return;
这里的TII是目标指令信息,可以使用
getInstrInfo()
方法从
MCSubtargetInfo
获取所需的体系结构。
优化过程中的指令转换。 指令映射
在编译的后期阶段执行大量优化期间,通常会出现将一种形式的全部或部分指令转换为另一种形式的指令的任务。 给定开始时描述的多类应用,我们可以拥有大量具有相似语义和属性的指令。 当然,在代码中,这些转换可以以大型
switch-case
结构的形式编写,对于每条指令,它们都会粉碎相应的转换。 部分地,这些巨大的构造可以借助宏来减少,这些宏将根据众所周知的规则形成指令的必要名称。 但是,这种方法仍然非常不便,由于所有指令名称都已明确列出,因此很难维护。 添加新指令很容易导致错误,因为 您必须记住将其添加到所有相关的转化中。 经过这种方法的折磨,LLVM创建了一种特殊的机制,可以有效地将一种形式的指令转换为另一种
Instruction Mapping
。
这个想法非常简单,有必要描述可能的模型,以便直接在TableGen中转换指令。 因此,在LLVM TableGen中,有一个用于描述此类模型的基类。
class InstrMapping {
让我们看一下文档中给出的示例。 现在,可以在源代码中找到的示例更加简单,因为最终表中仅获得两列。 在后端代码中,您可以找到使用指令映射描述的将旧形式转换为新形式的指令,mmdsp中的dsp指令等。 实际上,到目前为止,这种机制还没有得到广泛使用,仅仅是因为大多数后端是在它出现之前就开始创建的,并且为了使其正常工作,您仍然需要为指令设置正确的属性,因此切换到它并不总是那么容易,您可能需要一些重构。
因此,例如。 假设我们有没有谓词的指令形式和谓词分别为真和假的指令。 我们借助多类和特殊类来描述它们,我们将它们用作过滤器。 没有参数和许多属性的简化描述可能是这样的。
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”>; …
顺便说一下,在此示例中,还显示了如何使用
let … in
构造一次覆盖多个定义的属性。 结果,我们有许多指令存储其基本名称和属性,以唯一地描述其形式。 然后,您可以创建一个转换模型。
def getPredOpcode : InstrMapping {
结果,将从该描述生成下表。
.inc
文件中将生成一个函数
int getPredOpcode(uint16_t Opcode, enum PredSense inPredSense)
因此,它接受用于转换的指令代码和PredSense自动生成的枚举的值,该值在列中包含所有可能的值。 该功能的实现非常简单,因为 它返回我们感兴趣的指令所需的数组元素。
而且在后端代码中,只需编写生成的函数即可,而无需编写
switch-case
只需调用生成的函数即可,该函数将返回转换后指令的代码。 添加新指令的简单解决方案不会导致需要采取其他措施。
自动生成的工件( .inc
文件)
TableGen描述与LLVM后端代码之间的所有交互都由生成的包含C代码的
.inc
文件确保。 为了获得完整的图片,让我们来看看它们到底是什么。
每次构建后,对于每种体系结构,构建目录中都会有几个
.inc
文件,每个文件都存储有关体系结构的单独信息。
<TargetName>GenInstrInfo.inc
, ,
<TargetName>GenRegisterInfo.inc
, ,
<TargetName>GenAsmMatcher.inc
<TargetName>GenAsmWriter.inc
..
? , , .
<TargetName>GenInstrInfo.inc
.
namespace , , .
namespace X86 { enum { PHI = 0, … ADD16i16 = 287, ADD16mi = 288, ADD16mi8 = 289, ADD16mr = 290, ADD16ri = 291, ADD16ri8 = 292, ADD16rm = 293, ADD16rr = 294, ADD16rr_REV = 295, … }
接下来是描述指令属性的数组const MCInstrDesc X86Insts[]
。以下数组包含有关指令名称等的信息。基本上,所有信息都存储在传输和数组中。还有使用谓词描述的功能。根据上一节中讨论的函数谓词定义,将生成以下函数。 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; }
但是生成的文件和结构中有数据。在上X86GenSubtargetInfo.inc
一篇文章中,您可以找到后端代码中应使用的结构示例,以获取有关该体系结构的信息,在上一节中通过它得出了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); } };
如果用于Subtarget
描述各种配置XXXGenSubtarget.inc
,将使用SubtargetFeature
具有恒定值的数组描述的属性创建一个枚举,以指示CPU的特征和子类型,并且将生成一个函数来ParseSubtargetFeatures
处理带有选项集的字符串Subtarget
。此外,该方法XXXSubtarget
在后端代码中的实现应对应于以下伪代码,其中必须使用此功能: XXXSubtarget::XXXSubtarget(const Module &M, const std::string &FS) {
尽管.inc
文件非常庞大并且包含巨大的数组,但是由于访问数组元素的时间是恒定的,因此这使我们能够优化信息的访问时间。使用二进制搜索算法来实现按指令生成的搜索功能,以最大程度地减少操作时间。因此以这种形式存储是非常合理的。结论
因此,多亏了LLVM中的TableGen,我们以单一格式获得了易读且易于支持的体系结构描述,其中包含用于从LLVM后端源代码进行交互和访问信息以进行优化和代码生成的各种机制。同时,由于使用高效解决方案和数据结构的自动生成的代码,因此这种描述不会影响编译器的性能。