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

posted @ 2023-01-10 04:53  吴建明wujianming  阅读(421)  评论(0编辑  收藏  举报