爱笑的小姐姐 · 2023年08月24日

编译入门那些事儿(2):LLVM TableGen 概述

1. 基本概念

TableGen[1]是LLVM的一个工具,其可执行文件的名字为llvm-tblgen。通常在build目录下的bin目录里。

TableGen主要是帮助开发者开发和维护特定领域的信息记录(records of domain-specific information),方便开发者更好地构建这些信息记录,避免错误。尤其是在面对大量的信息记录的时候,用起来比较方便。TableGen的主要使用者是The LLVM Target-Independent Code Generator[2],即我们通常所讲的LLVM后端。通常不同架构LLVM后端有自己维护的一套LLVM Code Generator。

image.png

TableGen所做的工作通常就是分析一个文件,将其中的声明实例化(instantiates the declarations),然后将结果交给一个特定领域的后端去处理(domain-specific backend for processing)。

2. TableGen举例

TableGen 文件由两个关键部分组成:“classes”和“definitions”,两者都被视为“records”。TableGen records具有唯一名称、值列表和超类列表。值列表是TableGen为每一个record构建的主要数据,正是它保存了应用程序的领域特定信息。这些数据的解释留给特定的后端,但结构和格式规则由TableGen负责并规定。

TableGen definitions是“record”的具体形式,通常没有任何未定义的值,并用def关键字标记。

TableGen classes是用于构建和描述其他记录的抽象记录。这些类允许最终用户为其目标域或实现者构建抽象,以帮助分解出记录的公共属性。TableGen会跟踪用于构建定义的所有类,因此后端可以找到特定类的所有定义。

TableGen的语法与C++相似,具有内置类型和规范。此外,TableGen的语法还引入了一些自动化概念,如multiclass、foreach、let等[3]。语法细节在官方文档TableGen Language Reference有详细描述。

以X86架构ADD32rr指令为例,其后端需要的指令信息如下:

def ADD32rr {   // Instruction X86Inst I  
  string Namespace = "X86";  
  dag OutOperandList = (outs GR32:$dst);  
  dag InOperandList = (ins GR32:$src1, GR32:$src2);  
  string AsmString = "add{l}\t{$src2, $dst|$dst, $src2}";  
  list<dag> Pattern = [(set GR32:$dst, (add GR32:$src1, GR32:$src2))];  
  list<Register> Uses = [];  
  list<Register> Defs = [EFLAGS];  
  list<Predicate> Predicates = [];  
  int CodeSize = 3;  
  int AddedComplexity = 0;  
  bit isReturn = 0;  
  bit isBranch = 0;  
  bit isIndirectBranch = 0;  
  bit isBarrier = 0;  
  bit isCall = 0;  
  bit canFoldAsLoad = 0;  
  bit mayLoad = 0;  
  bit mayStore = 0;  
  bit isImplicitDef = 0;  
  bit isConvertibleToThreeAddress = 1;  
  bit isCommutable = 1;  
  bit isTerminator = 0;  
  bit isReMaterializable = 0;  
  bit isPredicable = 0;  
  bit hasDelaySlot = 0;  
  bit usesCustomInserter = 0;  
  bit hasCtrlDep = 0;  
  bit isNotDuplicable = 0;  
  bit hasSideEffects = 0;  
  InstrItinClass Itinerary = NoItinerary;  
  string Constraints = "";  
  string DisableEncoding = "";  
  bits<8> Opcode = { 0, 0, 0, 0, 0, 0, 0, 1 };  
  Format Form = MRMDestReg;  
  bits<6> FormBits = { 0, 0, 0, 0, 1, 1 };  
  ImmType ImmT = NoImm;  
  bits<3> ImmTypeBits = { 0, 0, 0 };  
  bit hasOpSizePrefix = 0;  
  bit hasAdSizePrefix = 0;  
  bits<4> Prefix = { 0, 0, 0, 0 };  
  bit hasREX_WPrefix = 0;  
  FPFormat FPForm = ?;  
  bits<3> FPFormBits = { 0, 0, 0 };  
}  
...  

X86 ADD32rr需要的信息有namespace、输出输入数据、汇编信息、指令选择pat、def-use寄存器信息、跳转信息、opcode以及大量的优化flags等。如果需要开发人员对每个指令都编写代码不太现实。而这恰恰是TableGen的一个关键功能,它允许用户定义他们在描述其信息时使用特定的抽象(abstractions end-user prefer)。对此ADD32rr,可以简单定义为:

let Defs = [EFLAGS],  
    isCommutable = 1,                  // X = ADD Y,Z --> X = ADD Z,Y  
    isConvertibleToThreeAddress = 1 in  // Can transform into LEA.  
def ADD32rr  : I<0x01, MRMDestReg, (outs GR32:$dst),  
                                   (ins GR32:$src1, GR32:$src2),  
                 "add{l}\t{$src2, $dst|$dst, $src2}",  
                 [(set GR32:$dst, (add GR32:$src1, GR32:$src2))]>;  

其中使用let赋值需要的flags,未赋值使用默认值,例如打开交换率和打开三地址寻址。定义ADD32rr class继承class X86Inst用来描述该类指令共有的特征,如opcode、输入输出信息等。

3. LLVM Target-Independent Code Generator

LLVM Target-Independent Code Generator旨在支持标准基于寄存器的微处理器的高效和高质量代码生成。此模型中的代码生成分为以下阶段:

  1. Instruction Selection
  2. Scheduling and Formation
  3. SSA-based Machine Code Optimizations
  4. Register Allocation
  5. Prolog/Epilog Code Insertion
  6. Late Machine Code Optimizations
  7. Code Emission

通常来说,上述指令选择和指令调度需要TableGen生成对应的DAGISel.inc,寄存器分配需要TableGen生成对应的RegisterInfo.inc,Code Emission需要TableGen生成对应的MCCodeEmitter.inc。其他基于目标指令优化需要TableGen生成对应的InstrInfo.inc。因此,TableGen生成的信息会被整个LLVM后端使用[4]。

4. 运行TableGen

llvm-tblgen作为LLVM开源工具可以运行.td生成所需信息。llvm-tblgen –help 可以查看所有的功能,以X86为例,我们可以打印reg信息和instr信息:llvm-tblgen X86.td -print-enums -class=Register -class=Instruction

$ llvm-tblgen X86.td -print-enums -class=Register  
AH, AL, AX, BH, BL, BP, BPL, BX, CH, CL, CX, DH, DI, DIL, DL, DX, EAX, EBP, EBX,  
ECX, EDI, EDX, EFLAGS, EIP, ESI, ESP, FP0, FP1, FP2, FP3, FP4, FP5, FP6, IP,  
MM0, MM1, MM2, MM3, MM4, MM5, MM6, MM7, R10, R10B, R10D, R10W, R11, R11B, R11D,  
R11W, R12, R12B, R12D, R12W, R13, R13B, R13D, R13W, R14, R14B, R14D, R14W, R15,  
R15B, R15D, R15W, R8, R8B, R8D, R8W, R9, R9B, R9D, R9W, RAX, RBP, RBX, RCX, RDI,  
RDX, RIP, RSI, RSP, SI, SIL, SP, SPL, ST0, ST1, ST2, ST3, ST4, ST5, ST6, ST7,  
XMM0, XMM1, XMM10, XMM11, XMM12, XMM13, XMM14, XMM15, XMM2, XMM3, XMM4, XMM5,  
XMM6, XMM7, XMM8, XMM9,  
   
$ llvm-tblgen X86.td -print-enums -class=Instruction  
ABS_F, ABS_Fp32, ABS_Fp64, ABS_Fp80, ADC32mi, ADC32mi8, ADC32mr, ADC32ri,  
ADC32ri8, ADC32rm, ADC32rr, ADC64mi32, ADC64mi8, ADC64mr, ADC64ri32, ADC64ri8,  
ADC64rm, ADC64rr, ADD16mi, ADD16mi8, ADD16mr, ADD16ri, ADD16ri8, ADD16rm,  
ADD16rr, ADD32mi, ADD32mi8, ADD32mr, ADD32ri, ADD32ri8, ADD32rm, ADD32rr,  
ADD64mi32, ADD64mi8, ADD64mr, ADD64ri32, ...  

5. 总结

TableGen的用途是创建包含表的巨大的include文件,文件内容各个平台相互独立,为相关平台LLVM后端服务,如果没有LLVM后端使用创建的include文件,TableGen就没有真正的意义。TableGen生成的信息会被整个LLVM后端使用,因此,一个高质高效的TableGen设计对编译时间和编译结果至关重要。

尽管TableGen非常通用,但是仍有一些不足。主要体现在虽然TableGen允许构建领域特定的语言,但创建的最终语言缺乏其他 DSL(领域特定语言) 的能力,这反过来又大大增加了 TableGen 文件的大小和复杂性。

参考

  1. https://llvm.org/docs/TableGen/index.html
  2. https://llvm.org/docs/TableGen/LangIntro.html
  3. https://llvm.org/docs/TableGen/LangRef.html
  4. https://www.youtube.com/watch?v=dIEVUlsiktQ
作者:赵炅东
文章来源:毕昇编译

推荐阅读

更多嵌入式AI干货请关注嵌入式AI专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
18799
内容数
1346
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息