LLVM TableGen杂谈
LLVM TableGen杂谈
1. 什么是tablegen
tablegen是llvm用于开发和维护编译器中公共特性的条目(e.g. 指令描述, 寄存器描述)的代码, 使之灵活的描述与构造的自动化工具. 其本质是一个parser, 将输入的td文件转化为特定的数据结构后再输出为易于阅读的cpp代码. 更多介绍可见http://llvm.org/docs/TableGen/index.html或参见docs/TableGen/下文件说明.
2. tablegen的使用方式
首次成功编译后, 在[llvm install path]/bin/下会生成可执行文件llvm-tblgen. 通常使用方法(在llvm的cmake工程中)它读入一个td文件, 并将结果输出至一个inc文件中. 以高通的Hexagon架构为例(具体使用命令可以llvm-tblgen --help查询)生成指令信息代码:
1 [12:32:11] hansy@hansy:~/llvm/llvm (master)$ ../llvm_build/bin/llvm-tblgen -gen-instr-info -I ./lib/Target/Hexagon/ -I ./include/ -I ./lib/Target/ ./lib/Target/Hexagon/Hexagon.td -o ~/test.inc
生成的inc文件实质为cpp代码, llvm工程中会包含这些文件. 当前tablegen生成的代码主要分为前端clang(target independent code, 在[llvm build path]/tools/clang/include/clang/下)以及后端llvm(target dependent code, 在[llvm build path]/lib/Target/[arch]下).
tablegen代码包含两块: 对td文件的处理, 在lib/TableGen/目录下, 包含lexer与parser, 负责解析tablegen的语法并转换为内部数据结构; 输出cpp代码, 在utils/TableGen/目录下, 用于生成我们需要的cpp代码, 这块与llvm代码逻辑强相关, 基本上一个cpp文件对应一类信息.
3. td文件的语法
tablegen的输入来源于td文件, 如要了解tablegen就先要了解td文件. 与生成的inc文件类似, td文件也包含两块内容(在[llvm source path]/对应的目录下), 后端还包含部分llvm target independent code(主要为IR相关), 暂不赘述.
仍以Hexagon架构为例, lib/Target/Hexagon/Hexagon.td是Hexagon架构td的入口(每个架构对应目录下都有一个以架构命令的td文件). 在该文件中定义了一些基础数据结构, 并包含了构建该架构后端所需的所有信息的文件.
先来看看该文件定义的数据结构. 在td中使用两个关键字定义数据结构(llvm中称为records, 其实质对应的是tablegen中的一个类/类实例), class与def. 其中classes类似于模板, 用于描述一类抽象的records(e.g. Register, RegisterClass, Instruction). 而definitions用于表达一个具体的records(可以理解为一个特定的类). 每个records包含若干数据成员, 这些成员的类型有bit(布尔量), int(整型), string(字符串), code(代码段, 包含一行或多行的字符串), bits<n>(位段)等类型. 数据成员使用let关键字进行赋值, 对于tablegen中解析的成员必须都初始化(为定义的值可以使用?初始化为'未初始化值'), 否则会导致编译失败. 若一个definition record包含一个未初始化成员, 其值将从该definition的superclass中获取. 若tablegen中未解析该成员则不赋值也不会报错. 以Asmparser为例(defined in include/llvm/Target/Target.td):
复制代码
1 class AsmParser {
2 string AsmParserClassName = "AsmParser";
3 string AsmParserInstCleanup = "";
4 bit ShouldEmitMatchRegisterName = 1;
5 bit ShouldEmitMatchRegisterAltName = 0;
6 bit AllowDuplicateRegisterNames = 0;
7 bit HasMnemonicFirst = 1;
8 bit ReportMultipleNearMisses = 0;
9 }
10 Hexagon架构的Asmarser如下(defined in lib/Target/Hexagon/Hexagon.td):
11 def HexagonAsmParser : AsmParser {
12 let ShouldEmitMatchRegisterAltName = 1;
13 bit HasMnemonicFirst = 0;
14 }
复制代码
Hexagon架构的Asmarser如下(defined in lib/Target/Hexagon/Hexagon.td):
1 def HexagonAsmParser : AsmParser {
2 let ShouldEmitMatchRegisterAltName = 1;
3 bit HasMnemonicFirst = 0;
4 }
Hexagon未定义parser的名字, 因此使用默认模板的字符串'AsmParser'; 默认的parser不匹配寄存器别名, 而Hexagon下ShouldEmitMatchegisterltName为true, 覆写了默认值.
4. tablegen选项
一般情况下并不会修改tablegen的代码, 而是通过修改td文件实现所需的目标. 对任意一个架构而言, 不是所有tablegen选项都有效(例如不支持vliw的架构就没有DFAPacketizer), 仍以Hexagon为例常见选项如下:
-gen-emitter
output: [arch]##GenMCCodeEmitter.inc
usage: used to generate binary code for given instruction. included in [arch]MCCodeEmitter.cpp
-gen-register-info
output: [arch]##GenRegisterInfo.inc
usage: output register enum and class, mask, etc. included in [arch]##BaseRegisterInfo.* and [arch]##MCTargetDesc.*
-gen-instr-info
output: [arch]##GenInstrInfo.inc
usage: output instruction enum and class, mask, etc. included in [arch]##BaseInstrInfo.* and [arch]##MCTargetDesc.*
-gen-asm-writer
output: [arch]##GenAsmWriter.inc
usage: generate assembly printer. included in [arch]##InstPrinter.cpp
-gen-asm-matcher
output: [arch]##GenAsmMatcher.inc
usage: generate assembly matcher used in assemble parser. included in [arch]##AsmParser.cpp
-gen-disassembler
output: [arch]##GenDisassemblerTables.inc
usage: generate disassembly emitter. included in [arch]##Disassembler.cpp
-gen-callingconv
output: [arch]##GenCallingConv.inc
usage: implement static function used for calling conversion.
-gen-dag-isel
output: [arch]##GenDAGISel.inc
usage: implement a DAG instruction selector. included in [arch]##ISelDAGToDAG.cpp
-gen-dfa-packetizer
output: [arch]##GenDFAPacketizer.inc
usage: generate function to check whether an instruction can be added to a VLIW. included in [arch]##InstrInfo.cpp
-gen-subtarget
output: [arch]##GenSubtargetInfo.inc
usage: generate subtarget enum.1. 语法介绍
官方文档见这里, 以下是文档的翻译.
1.1. 类型系统
tablegen是强类型语言, 其类型系统同时包含low-level(i.e. bit int)与high-level(i.e. dag). 以下是tablegen支持的内建类型.
bit: 布尔值, 0或1.
int: 表示32bit整型.
string: 字符串.
code: 一个代码片段, 通常以多行字符串的形式表示.
bits: 长度为n的bit串, 可以对其中部分进行赋值.
list: 表示类型为ty的队列容器, ty可以为任意tablegen支持的类型.
class type: class类型.
dag: 表示一个有向图.
1.2. 值与表达式
tablengen支持以下表达式.
?: 未初始化值.
0b10: bit串, 注意该位串不会被扩展或截断, 因此对类型为bits的赋值要保证长度一致.
1: 十进制常量.
0x11: 十六进制常量.
"abc": 字符串常量, 可以赋值给string类型或code类型.
value: 引用一个value.
value{n}: 访问value的第n位.
value.field: 访问value的field成员.
[x, y, z]: 队列容器赋值.
{a, b}: 位串赋值, 其总长度为位串a加位串b的长度和.
还有很多表达式就不一一列出了, 读者请自行参考文档. 另外tablegen支持的内建函数表达式(i.e. foreach strconcat listconcat)也请参见文档说明.
1.3. 类(class)与定义(definition)
类与定义是tablegen语言中关键组成, 它们又被称作条目(records).
一个records包含一个唯一的名字, 一组成员变量及一组(扩展的)超类. 对records的解释由具体的后端处理, 但records的语法格式与结构由tablegen的前端进行检查.
类是一种抽象的records, 类定义为用户提供抽象架构公共属性的能力. 类似的, multiclass提供了一组抽象的records, 对multiclass的实例化会创建一组定义.
定义是一种具体的records, 它们通过def关键字被定义.
来看个例子加深理解.
class C { bit V = 1; }
def X : C;
def Y : C {
string Greeting = "hello";
}
以上代码片段声明了类C, 类C有两个实例, X与Y. 由于定义X与定义Y均继承自类C, 所以它们都包含成员V, 而定义Y包含一个额外的字符串成员Greeting.
1.4. 赋值表达式
let表达式允许我们修改变量的值.
class C { bit V = 0; }
def D : C { let V = 1; }
类C中定义了一个bit类型变量V, 其默认值为0, 在实例D中将V修改为1.
1.5. 类的模板参数
tablegen提供了参数化类定义, 允许在定义类时指定一系列参数并绑定到对应变量.
class C<bit v> { bit V = v; }
def X : C<1>;
def Y : C<0>;
def Z : C<1> { let V = 0; }
以上文例子为例, 类C是一个模板类, 其接受一个bit类型变量v并使用该值初始化其成员变量V.
定义X与定义Y均继承自类C, 根据传入的参数, 定义X是一个V为1的类实例(等于1.4.中的D), 定义Y是一个V为0的类实例.
特殊的, 定义Z中的V值为0还是为1呢? 在模板初始化时V被设置为1, 然后let表达式生效, 所以V最终为0 (可以理解为赋值操作在模板实例化之后生效).
1.5. multiclass定义与实例
模板类给我们提供了描述一组具有公共属性的类的能力, 进一步的tablegen还提供了一种定义多个类的方式.
multiclass允许我们同时定义一组类, 继承自multiclass类会被扩展成多个类定义实例. 让我们来看个例子.
class C<bit v, bit i> { bit V = v; bit I = i; }
multiclass D<bit v> {
def X : C<v, 0>;
def Y : C<v, 1>;
}
defm V0 : D<0>;
defm V1 : D<1>;
模板类C接受两个参数v与i, 即有四种类实例. 如果使用之前的写法就需要编写四个类定义.
利用multiclass我们定义一个类D, 当实例化一个类D时我们实际实例化了两个类定义X与Y. 通过这种方式我们可以进一步减少重复代码的编写.
注意: 实例化一个multiclass时需要使用defm替换def, 并且该实例的名字是defm后接的名字与multiclass中def的名字拼接而成. i.e. 上文中实际定义了四个类实例, 名字分别为V0_X, V0_Y, V1_X, V1_Y.
从以上举例可以看到tablegen是非常灵活的语言, 易于使用者编写.
2. 语言参考
这里介绍如何编写tablegen代码.
2.1. 注释
使用//或/.../方式添加注释.
2.2. 文件包含
使用include "***"方式包含其它文件
TODO
3. tablegen后端
前文提到过tablegen由两部分组成, 前端的parser的作用是将tablegen description file (td file)翻译成records, records的含义则由后端解析.
当前clang与llvm都用到tablegen来生成代码, 前端部分不很熟悉, 因此本文简要介绍下后端自动生成代码.
这里可以了解tablegen自动生成的代码模块.
3.1. tablegen源码目录
tablegen代码由两部分组成, 前端的lexer与parser在lib/TableGen/目录下, 作用是解析td file.
后端代码在utils/TableGen/目录下, 是我们关心的逻辑代码. 在解析每个模块时我们会一一介绍.
3.2. 如何使用
这块之前有做过简要介绍. 这次以RISCV架构为例:
[00:58:41] hansy@hansy:~/llvm-mono/llvm (master)$ llvm-tblgen -I lib/Target/RISCV/ -I ./include/ ./lib/Target/RISCV/RISCV.td -o 123.inc
[00:59:36] hansy@hansy:~/llvm-mono/llvm (master)$ vim 123.inc
llvm-tblgen是tablegen源码编译出的工具, 它必须接受一个td file输入(RISCV.td), 该td file中包含的头文件索引路径通过-I参数传递, 默认不添加参数时会生成全量代码, 这里就不展示了.
这里再稍微提下llvm构建时是如何调用tablegen的. 首先在lib/Target/RISCV/CMakeLists.txt中包含如下代码:
set(LLVM_TARGET_DEFINITIONS RISCV.td)
tablegen(LLVM RISCVGenAsmMatcher.inc -gen-asm-matcher)
tablegen(LLVM RISCVGenAsmWriter.inc -gen-asm-writer)
tablegen(LLVM RISCVGenCompressInstEmitter.inc -gen-compress-inst-emitter)
tablegen(LLVM RISCVGenDAGISel.inc -gen-dag-isel)
tablegen(LLVM RISCVGenDisassemblerTables.inc -gen-disassembler)
tablegen(LLVM RISCVGenGlobalISel.inc -gen-global-isel)
tablegen(LLVM RISCVGenInstrInfo.inc -gen-instr-info)
tablegen(LLVM RISCVGenMCCodeEmitter.inc -gen-emitter)
tablegen(LLVM RISCVGenMCPseudoLowering.inc -gen-pseudo-lowering)
tablegen(LLVM RISCVGenRegisterBank.inc -gen-register-bank)
tablegen(LLVM RISCVGenRegisterInfo.inc -gen-register-info)
tablegen(LLVM RISCVGenSubtargetInfo.inc -gen-subtarget)
tablegen(LLVM RISCVGenSystemOperands.inc -gen-searchable-tables)
其中函数tablegen (defined in cmake/modules/TableGen.cmake)作用是根据传入参数配置命令行并调用llvm-tablegen. 可以看到生成的代码放在build/lib/Target/RISCV/目录下.
由于不同架构的不同硬件特性, 需要生成的代码也不相同, 下文会简要介绍几个常见的文件, 细节的内容可以根据以上介绍自行查找代码.
参考文献了解
https://www.cnblogs.com/Five100Miles/p/10666607.html
https://www.cnblogs.com/Five100Miles/p/12484104.html