LESSONS IN TABLEGEN 报告总结

报告来源

  1. https://archive.fosdem.org/2019/schedule/event/llvm_tablegen/attachments/slides/3304/export/events/attachments/llvm_tablegen/slides/3304/tablegen.pdf

  2. https://news.ycombinator.com/item?id=38620037

1. 一段话总结

该文档是Nicolai Hähnle在FOSDEM 2019上关于LLVM TableGen的分享内容,核心介绍了TableGen作为LLVM中的工具与语言 ,其工具端包含llvm-tblgenclang-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-tblgenclang-tblgen,二者共享位于lib/TableGen的前端(负责解析与求值),但拥有不同的后端(位于utils/TableGen),可生成不同类型的目标文件。

  • 生成内容 :生成文件存储于${builddir}/lib/Target/${target}/路径,核心包括:

    • MCInstrDesc(机器指令描述)
    • 指令选择(Instruction selection)代码
    • 汇编解析器(Assembly parser)与反汇编器(Disassembler)代码
  • 使用方式 :通过命令行指定所需后端(默认后端打印所有记录定义),并支持CMake集成,在debug构建时可通过LLVM_OPTIMIZED_TABLEGEN=ON优化生成过程,典型CMake配置示例:

    cmake 复制代码
    set(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/**.tdinclude/.../Intrinsics.tdOpts.td/Options.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,其值等于实例化记录的最终名称;
    • 支持默认模板参数,实例化时可省略默认参数。
  • 示例

    tablegen 复制代码
    class 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而非模板参数重写值。

  • 限制:仅能重写已有变量,不能定义新变量(否则报错)。

  • 示例

    tablegen 复制代码
    class 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文件部分逻辑(如内置函数与后端定义)
  • 多类示例

    tablegen 复制代码
    multiclass 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示例

    tablegen 复制代码
    foreach 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示例

    tablegen 复制代码
    defset 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)关键实现:参数列表生成

通过自定义类与内置函数协同,生成符合需求的参数列表,分三步:

  1. 基础参数类定义 :定义AMDGPUArg类描述单个参数(含类型与名称),makeArgList类生成基础参数列表;

    tablegen 复制代码
    class 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>));
    }
  2. 维度属性与梯度参数AMDGPUDimProps类定义图像维度属性,包含梯度参数列表(如2D数组维度的dsdhdtdh等);

  3. 参数列表拼接与调整arglistmatchshift类调整LLVMMatchType索引,arglistconcat类拼接多组参数列表(如额外地址参数、梯度参数、坐标参数),确保类型匹配。

四、未来优化方向

文档提出TableGen未来可能的改进方向,聚焦于类型系统、语法与后端:

  1. 类型系统优化
    • 考虑移除code类型,简化类型体系;
    • 引入显式的"顶类型"与"底类型"(对应unset/any),支持异构列表;
  2. 语法扩展 :将#运算符扩展至列表与dag的拼接操作;
  3. 特性清理:消除或优化多类继承(当前多类继承与let语句配合时行为不一致);
  4. 后端改进
    • 优化错误信息,提升调试体验;
    • 解决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>),无需新增类实例,极大提升了参数调整的灵活性,尤其适用于批量记录生成中局部值的微调场景。