报告来源
1. 一段话总结
该文档是Nicolai Hähnle在FOSDEM 2019上关于LLVM TableGen的分享内容,核心介绍了TableGen作为LLVM中的工具与语言 ,其工具端包含llvm-tblgen和clang-tblgen(共享前端、不同后端),可生成MCInstrDesc、指令选择等目标文件,语言端是记录定义的超集,支持类、多类等特性;还详细阐述了TableGen的类型系统 (如bit、dag等)、核心语言特性 (类、let语句、多类、foreach、defset、内置函数),并以AMDGPU图像内置函数与指令为实例,说明其在处理复杂指令生成中的应用,最后提及未来可能的优化方向(如类型系统改进、后端优化)。
2. 思维导图
mindmap
## 一、TableGen基础
- 定义:工具 + 语言
- 工具特性
- 核心工具:llvm-tblgen、clang-tblgen(共享lib/TableGen前端,后端不同)
- 生成文件:MCInstrDesc、指令选择、汇编解析器等(路径:${builddir}/lib/Target/${target}/)
- 编译集成:CMake支持,debug构建可设LLVM_OPTIMIZED_TABLEGEN=ON
- 语言特性:记录定义的超集,支持生成带规律性的多记录
- 记录(Records):键值字典,意义由后端定义,可继承类、可命名
- 源码结构:.td文件,支持文本包含(无包含守卫,顺序重要),典型包含链(如${target}.td→${target}InstrInfo.td等)
## 二、TableGen语言特性
- 类型系统
- 基础类型:bit、bits<N>、int、string、code、list<T>、unset
- 特殊类型:dag(类S-表达式,用于ISel模式)、class/record类型
- 特殊值:?(属于所有类型,常用于指令编码未定义值)
- 核心特性
- 类(Classes):记录模板,支持继承、默认模板参数(含隐式NAME参数)
- let语句:重写记录值(最内层生效),依赖延迟求值,不可定义新变量
- 多类(Multiclasses):多记录模板,通过defm实例化,支持继承、与类结合
- foreach:遍历列表/整数范围,可模拟if(结合BoolToList类)
- defset:捕获同类型实例化记录,用于生成派生记录
- 内置函数:前缀!,如!eq、!add、!if、!listconcat、!cast等
## 三、实例:AMDGPU图像内置函数与指令
- 挑战:地址操作数(1-12个)、数据操作数(1-4个)、指令结构复杂(多类型如IMAGE_ATOMIC_*、IMAGE_SAMPLE_*)
- 生成产物:内置函数、机器指令、通用搜索表(无SelectionDAG ISel模式)
- 关键实现:通过类(如AMDGPUArg、AMDGPUDimProps)生成参数列表,结合arglistconcat等处理LLVMMatchType调整
## 四、未来方向
- 类型系统:考虑移除code类型、引入显式顶/底类型、支持异构列表
- 语法优化:扩展#运算符至列表和dag拼接
- 特性清理:消除/优化多类继承
- 后端改进:优化错误信息、解决DAG模式痛点、提升特性正交性
3. 详细总结
一、TableGen概述:工具与语言的结合
TableGen是LLVM生态中用于生成目标代码与数据结构的核心组件,分为工具 和语言两部分,二者协同支撑LLVM的目标相关代码生成。
1.1 TableGen工具(The Tool)
-
核心工具 :包含
llvm-tblgen和clang-tblgen,二者共享位于lib/TableGen的前端(负责解析与求值),但拥有不同的后端(位于utils/TableGen),可生成不同类型的目标文件。 -
生成内容 :生成文件存储于
${builddir}/lib/Target/${target}/路径,核心包括:- MCInstrDesc(机器指令描述)
- 指令选择(Instruction selection)代码
- 汇编解析器(Assembly parser)与反汇编器(Disassembler)代码
-
使用方式 :通过命令行指定所需后端(默认后端打印所有记录定义),并支持CMake集成,在debug构建时可通过
LLVM_OPTIMIZED_TABLEGEN=ON优化生成过程,典型CMake配置示例:cmakeset(LLVM_TARGET_DEFINITIONS AMDGPU.td) tablegen(LLVM AMDGPUGenAsmMatcher.inc -gen-asm-matcher) -
架构流程:.td源码文件经前端解析与求值得到记录定义,再由不同后端(如InstrInfo后端、SearchableTables后端、DAGISel后端)处理,最终生成C++定义与代码。
1.2 TableGen语言(The Language)
- 定位:是"记录定义(Records)"的超集,开发者通过其编写代码,可生成具有规律性的大量记录,而记录是键值字典,其意义由后端定义。
- 记录特性 :
- 可从类(Classes)派生,常用于过滤逻辑;
- 可命名(部分场景必需,如内置函数、机器指令;部分可选,如ISel模式),命名有助于调试。
- 源码组织 :
- 以.td为文件后缀,支持文本
include指令,但无包含守卫,且文件包含顺序对结果有影响; - 典型.td文件包含链:
${target}/${target}.td→${target}/${target}InstrInfo.td→${target}/${target}Instr*.td→${target}/${target}Schedule.td等; - 常用基础包含文件:
include/llvm/Target/**.td、include/.../Intrinsics.td、Opts.td/Options.td等。
- 以.td为文件后缀,支持文本
二、TableGen核心语言特性
2.1 类型系统(Type System)
TableGen的类型系统涵盖基础类型与特殊类型,部分类型支持特定操作,具体分类如下表:
| 类型类别 | 具体类型 | 关键特性 | 示例 |
|---|---|---|---|
| 基础类型 | bit | 单比特值 | bit Unset; |
| bits | N位比特序列,支持切片(仅字面常量索引) | bits<32> Inst; Inst{7-0} = vsrc; |
|
| int | 整数类型,支持与bit/bits隐式转换(带范围检查) | int Size = 4; |
|
| string | 字符串类型 | "s_and_b32" |
|
| code | 代码片段,与string常隐式转换 | [{ this_is_code(); }] |
|
| list | 同类型列表,支持索引(仅字面常量索引) | list<string> names = ["s", "t"]; |
|
| unset | 非严格类型,仅表示未定义 | - | |
| 特殊类型 | dag | 类S-表达式,无类型限制,用于ISel模式 | (set i32:$sdst, (UniformBinFrag<and> i32:$src0, i32:$src1)) |
| class/record | 类或记录类型,体现继承关系 | class A { }; def MyRec : A { }; |
|
| 特殊值 | ? | 属于所有类型,常用于指令编码中未确定的值 | bits<8> vdst = { ?, ?, ?, ?, ?, ?, ?, ? }; |
2.2 核心语言结构
(1)类(Classes)
-
本质:记录的模板,语法与记录类似,支持C++风格继承,拥有独立命名空间。
-
关键特性 :
- 支持模板参数,且包含隐式模板参数NAME,其值等于实例化记录的最终名称;
- 支持默认模板参数,实例化时可省略默认参数。
-
示例 :
tablegenclass A { string Name = NAME; } // 含隐式NAME参数 class B<int x = 5> { int X = x; } // 带默认参数的类 def MyRecord : A, B<3> { int Y = 3; } // 继承A和B<3>的记录
(2)let语句与延迟求值(Let-statements & Late Evaluation)
-
功能 :重写记录或类中的值,可作用于全局、类体或记录体(多类"体"也支持),最内层let语句优先级最高。
-
核心优势:依赖"延迟求值"特性------表达式尽可能晚地求值,因此比模板参数更灵活,建议优先使用let而非模板参数重写值。
-
限制:仅能重写已有变量,不能定义新变量(否则报错)。
-
示例 :
tablegenclass A<int p> { int x = p; int y = x; } let x = 12 in { def A1 : A<1>; // x=12,y=12(延迟求值:y随x更新) def A2 : A<2> { let x = 17; } // x=17,y=17 } def A3 : A<3> { let x = 10; let y = 11; } // x=10,y=11
(3)多类、foreach与defset(Multiclasses, foreach, defset)
三者均用于生成批量记录,各有侧重,对比如下表:
| 特性 | 多类(Multiclasses) | foreach | defset |
|---|---|---|---|
| 本质 | 多记录模板 | 循环结构(遍历列表/整数范围) | 记录捕获工具(捕获同类型实例化记录) |
| 实例化方式 | 通过defm关键字 |
直接通过foreach 变量=范围 in { ... }定义 |
先定义defset 类型 名称 = { ... };,再引用 |
| 核心用途 | 生成结构相关的一组记录(如同一操作的不同位宽指令) | 生成规律性重复的记录(如不同索引的寄存器) | 收集记录后生成派生异构记录(如内置函数映射) |
| 示例 | 生成xyz三个方向的内置函数 | 生成0-15索引的TTMP寄存器 | 捕获图像原子内置函数,用于生成资源映射表 |
| 关键优势 | 符合TableGen惯用风格,可复用 | 可编程性强,支持模拟if(结合BoolToList) | 隔离.td文件部分逻辑(如内置函数与后端定义) |
-
多类示例 :
tablegenmulticlass AMDGPUReadPreloadRegisterIntrinsic_xyz_named<string prefix> { def _x : AMDGPUReadPreloadRegisterIntrinsicNamed<!strconcat(prefix, "_x")>; def _y : AMDGPUReadPreloadRegisterIntrinsicNamed<!strconcat(prefix, "_y")>; def _z : AMDGPUReadPreloadRegisterIntrinsicNamed<!strconcat(prefix, "_z")>; } defm int_amdgcn_workgroup_id : AMDGPUReadPreloadRegisterIntrinsic_xyz_named<"__builtin_amdgcn_workgroup_id">; -
foreach示例 :
tablegenforeach Index = 0-15 in { def TTMP#Index#_vi : SIReg<"ttmp"#Index, !add(112, Index)>; def TTMP#Index#_gfx9 : SIReg<"ttmp"#Index, !add(108, Index)>; } -
defset示例 :
tablegendefset list<AMDGPUImageDimIntrinsic> AMDGPUImageDimAtomicIntrinsics = { defm int_amdgcn_image_atomic_swap : AMDGPUImageDimAtomic<"ATOMIC_SWAP">; defm int_amdgcn_image_atomic_add : AMDGPUImageDimAtomic<"ATOMIC_ADD">; }
(4)内置函数(Built-ins)
- 标识 :所有内置函数以
!为前缀,支持多种操作,覆盖逻辑判断、算术、列表处理、字符串处理等场景。 - 常用内置函数分类 :
- 逻辑与算术:
!eq(等于)、!ne(不等于)、!add(加法)、!shl(左移); - 条件与列表:
!if(条件判断)、!listconcat(列表拼接)、!foreach(列表遍历)、!foldl(左折叠); - 字符串与类型:
!strconcat(字符串拼接)、!isa(类型判断)、!cast(类型转换,支持记录与字符串互转); - 特殊结构:
!dag(创建dag类型)。
- 逻辑与算术:
2.3 AMDGPU图像内置函数与指令:实例解析
(1)场景挑战
AMDGPU图像操作的复杂性体现在:
- 地址操作数数量:1-12个(因图像维度如1D/2D/3D、操作数类型如float/half不同);
- 数据操作数数量:1-4个;
- 指令类型繁多:包含IMAGE_ATOMIC_(如IMAGE_ATOMIC_ADD)、IMAGE_SAMPLE_(如IMAGE_SAMPLE_C)等数十种指令。
(2)生成产物
TableGen在此场景下生成的核心内容:
- 图像内置函数(如
@llvm.amdgcn.image.sample.1d.v4f32.f32); - 机器指令(同一基础操作码对应多份指令,差异体现在数据通道数、编码等);
- 通用搜索表(而非SelectionDAG ISel模式,因早期模式维护难度高),包括:
- BaseOpcode枚举及附加信息(如额外地址参数数量);
- 内置函数与(基础操作码、图像维度)的映射;
- 机器指令与(基础操作码、通道数、编码)的映射。
(3)关键实现:参数列表生成
通过自定义类与内置函数协同,生成符合需求的参数列表,分三步:
-
基础参数类定义 :定义
AMDGPUArg类描述单个参数(含类型与名称),makeArgList类生成基础参数列表;tablegenclass AMDGPUArg<LLVMType ty, string name> { LLVMType Type = ty; string Name = name; } class makeArgList<list<string> names, LLVMType basety> { list<AMDGPUArg> ret = !listconcat([AMDGPUArg<basety, names[0]>], !foreach(name, !tail(names), AMDGPUArg<LLVMMatchType<0>, name>)); } -
维度属性与梯度参数 :
AMDGPUDimProps类定义图像维度属性,包含梯度参数列表(如2D数组维度的dsdh、dtdh等); -
参数列表拼接与调整 :
arglistmatchshift类调整LLVMMatchType索引,arglistconcat类拼接多组参数列表(如额外地址参数、梯度参数、坐标参数),确保类型匹配。
四、未来优化方向
文档提出TableGen未来可能的改进方向,聚焦于类型系统、语法与后端:
- 类型系统优化 :
- 考虑移除
code类型,简化类型体系; - 引入显式的"顶类型"与"底类型"(对应unset/any),支持异构列表;
- 考虑移除
- 语法扩展 :将
#运算符扩展至列表与dag的拼接操作; - 特性清理:消除或优化多类继承(当前多类继承与let语句配合时行为不一致);
- 后端改进 :
- 优化错误信息,提升调试体验;
- 解决DAG模式的维护痛点,增强特性间的正交性(如复杂模式与谓词的协同)。
4. 关键问题
问题1:TableGen中的"记录(Records)"是什么?其与"类(Classes)"的关系如何?
答案
TableGen中的记录(Records)是键值字典结构 ,其具体意义由后端定义,核心用途是存储目标代码生成所需的结构化信息(如机器指令的操作数、编码等),记录可命名(部分场景如内置函数、机器指令必需命名,便于识别与调试)。
记录与类(Classes)是"实例与模板"的关系:类是记录的模板 ,记录可通过继承类(支持多继承)获取类中定义的字段与逻辑,类还支持模板参数(含隐式的NAME参数,值为记录最终名称),可灵活生成不同配置的记录;此外,类还可用于对记录进行过滤,后端可基于类的继承关系筛选目标记录。
问题2:在处理AMDGPU图像这类复杂指令时,TableGen为何放弃了早期的SelectionDAG ISel模式,转而采用通用搜索表?
答案
放弃SelectionDAG ISel模式、采用通用搜索表的核心原因是复杂指令场景下模式维护难度过高 :
AMDGPU图像指令具有极强的多样性------地址操作数(1-12个)、数据操作数(1-4个)的数量不固定,且指令类型多达数十种(如IMAGE_ATOMIC_*、IMAGE_SAMPLE_*系列),若使用SelectionDAG ISel模式,需为每种指令编写对应的模式规则,随着指令类型增加,规则数量会急剧膨胀,且后续修改(如新增指令、调整参数)需同步更新大量模式,维护成本极高。
而通用搜索表通过TableGen生成结构化的映射关系(如内置函数→基础操作码+图像维度、机器指令→基础操作码+通道数),后续指令选择可在C++代码中基于这些映射表高效查询,既降低了前期模式编写的复杂度,也提升了后期维护的灵活性。
问题3:TableGen的"延迟求值(Late Evaluation)"特性体现在哪里?该特性为何能让"let语句"比模板参数更灵活?
答案
(1)延迟求值的体现
延迟求值是指TableGen中的表达式会尽可能晚地进行求值 ,而非在定义时立即计算。例如在类中定义int y = x,若后续通过let语句修改x的值,y会随x的最终值重新计算,而非保持初始的x值(如示例中def A1 : A<1>在let x=12作用下,y最终为12,而非1)。
(2)let语句比模板参数更灵活的原因
模板参数的局限性在于:参数值在类实例化时即确定,后续无法动态修改;而let语句基于延迟求值,可在类实例化后、表达式最终求值前,多次重写记录中的值(最内层let生效) ,且无需修改类的模板定义。
例如,对于同一类A<int p>,若用模板参数修改x值,需为不同x值创建多个实例(如A<1>、A<2>);而通过let语句,可在同一实例化过程中动态调整x(如let x=12 in def A1 : A<1>),无需新增类实例,极大提升了参数调整的灵活性,尤其适用于批量记录生成中局部值的微调场景。