llvm_history

第6章:后端架构 - 从 IR 到机器码

LLVM 后端是将中间表示(IR)转换为特定目标架构机器码的复杂系统。本章深入探讨 LLVM 后端的架构设计、关键组件及其演化历程,理解从平台无关的 IR 到高度优化的机器码的转换过程。我们将重点关注 TableGen 的声明式设计、不同指令选择框架的权衡、寄存器分配算法的演进,以及机器级优化的实现策略。

6.1 TableGen:声明式后端描述的创新

6.1.1 TableGen 的设计动机

在 LLVM 项目早期(2003年),Chris Lattner 面临一个关键问题:如何高效地描述目标架构的指令集、寄存器、调用约定等信息。传统方法需要手写大量重复的 C++ 代码,不仅容易出错,而且难以维护。

TableGen 的核心思想是声明式编程:用简洁的领域特定语言(DSL)描述目标架构的特性,然后自动生成相应的 C++ 代码。这种方法带来了几个关键优势:

  1. 减少代码重复:一份描述可以生成多个组件需要的代码
  2. 提高一致性:自动生成保证了不同组件间的一致性
  3. 简化移植:新架构只需编写 .td 文件,无需深入了解后端内部
  4. 便于验证:声明式描述更容易进行形式化验证

6.1.2 TableGen 语言基础

TableGen 使用类似面向对象的语法,但本质上是一个模板展开系统:

// 定义寄存器类
class Register<string n> {
  string Name = n;
  list<string> Aliases = [];
}

// 定义具体寄存器
def RAX : Register<"rax"> {
  let Aliases = ["rax", "eax", "ax", "al"];
}

// 定义指令格式
class Instruction<dag outs, dag ins, string asm> {
  dag OutOperandList = outs;
  dag InOperandList = ins;
  string AsmString = asm;
}

6.1.3 指令描述的层次结构

TableGen 中的指令描述采用多层继承结构,从抽象到具体:

  Instruction (基类)
       ↓
  X86Inst (架构特定基类)
       ↓
  BinaryOpRR (二元寄存器操作)
       ↓
  ADD64rr (具体指令)

这种层次结构允许:

6.1.4 模式匹配与指令选择

TableGen 最强大的功能之一是描述 IR 到机器指令的映射模式:

// 将 IR 的 add 操作映射到 ADD64rr 指令
def : Pat<(add i64:$src1, i64:$src2),
          (ADD64rr $src1, $src2)>;

// 复杂模式:带立即数的加法
def : Pat<(add i64:$src, (i64 imm:$imm)),
          (ADD64ri $src, $imm)>;

这些模式在编译时被处理成高效的匹配表,运行时用于指令选择。

6.1.5 TableGen 的演化与扩展

从 2003 年至今,TableGen 经历了多次重要演进:

2003-2005:基础阶段

2006-2010:功能扩展

2011-2015:性能优化

2016-2023:现代化改进

6.1.6 TableGen 的设计权衡

优势:

  1. 可维护性高:修改指令定义只需更改 .td 文件
  2. 错误检查:编译时验证指令描述的一致性
  3. 代码生成:自动生成编码器、解码器、汇编器等

局限:

  1. 学习曲线:独特的语法需要额外学习
  2. 调试困难:生成的代码难以调试
  3. 表达能力限制:某些复杂逻辑仍需手写 C++

6.2 SelectionDAG:指令选择的图表示

6.2.1 DAG 的设计理念

SelectionDAG(选择有向无环图)是 LLVM 后端的核心组件,负责将 LLVM IR 转换为目标机器指令。Evan Cheng 在 2005-2008 年期间对其进行了重大改进,使其成为工业级的指令选择框架。

核心设计原则:

  1. 图表示:用 DAG 表示计算依赖,便于优化
  2. 类型合法化:处理目标不支持的数据类型
  3. 模式匹配:基于 TableGen 描述的高效匹配
  4. 渐进降级:分阶段将高级操作转换为机器指令

6.2.2 SelectionDAG 的工作流程

     LLVM IR
        ↓
   [构建初始 DAG]
        ↓
   [类型合法化]
        ↓
   [操作合法化]
        ↓
    [DAG 组合优化]
        ↓
    [指令选择]
        ↓
   [调度和生成]
        ↓
    MachineInstr

6.2.3 类型和操作合法化

类型合法化处理目标架构不直接支持的数据类型:

类型提升(Promotion)

类型拆分(Splitting)

操作展开(Expansion)

6.2.4 DAG 组合优化

在指令选择前,SelectionDAG 执行多种优化:

  1. 常量折叠:编译时计算常量表达式
  2. 强度削减:mul by 2 → shift left 1
  3. 公共子表达式消除:识别重复计算
  4. 地址模式匹配:识别复杂寻址模式

这些优化利用 DAG 的全局视图,找到 IR 级别难以发现的优化机会。

6.2.5 指令选择算法

SelectionDAG 使用基于模式匹配的指令选择:

  1. 自顶向下遍历:从 DAG 根节点开始
  2. 模式匹配:使用 TableGen 生成的匹配表
  3. 成本模型:选择最优匹配(考虑延迟、吞吐量)
  4. 失败回退:无匹配时使用默认展开
示例 DAG:
     (add)
     /   \
  (load) (mul)
         /   \
      (reg) (const)

可能的匹配:
- add + load → 带内存操作数的加法指令
- mul + const → 带立即数的乘法指令

6.2.6 SelectionDAG 的优缺点

优点

缺点

6.3 GlobalISel:下一代指令选择框架

6.3.1 为什么需要 GlobalISel

2015 年,Quentin Colombet 领导的团队开始开发 GlobalISel,目标是解决 SelectionDAG 的根本性问题:

  1. 编译速度:SelectionDAG 的多次遍历和 DAG 构建开销太大
  2. 代码复杂度:SelectionDAG 积累了太多历史包袱
  3. 可扩展性:难以支持新的编程模型(如异构计算)
  4. 调试困难:DAG 表示与原始 IR 差异太大

GlobalISel 的设计目标:

6.3.2 GlobalISel 的架构

GlobalISel 采用基于 MIR(Machine IR)的流水线架构:

    LLVM IR
       ↓
  [IRTranslator]
       ↓
   Generic MIR
       ↓
    [Legalizer]
       ↓
  Legal Generic MIR
       ↓
  [RegBankSelect]
       ↓
  RegBank-assigned MIR
       ↓
  [InstructionSelect]
       ↓
   Target MIR

6.3.3 IRTranslator:从 IR 到 MIR

IRTranslator 直接将 LLVM IR 转换为通用机器指令(Generic MIR):

; LLVM IR:
%result = add i32 %a, %b

; Generic MIR:
%result:_(s32) = G_ADD %a:_(s32), %b:_(s32)

关键特性:

6.3.4 Legalizer:类型和操作合法化

Legalizer 负责将通用操作转换为目标支持的形式:

// 合法化动作定义
getActionDefinitionsBuilder(G_ADD)
  .legalFor({s32, s64})      // 32 和 64 位加法合法
  .widenScalarToNextPow2()   // 其他宽度扩展到 2 的幂
  .clampScalar(0, s32, s64); // 限制在 32-64 位范围

合法化策略:

  1. Legal:操作已被目标支持
  2. Widen:扩展到更宽的类型
  3. Narrow:拆分为更窄的操作
  4. Custom:自定义合法化逻辑
  5. LibCall:调用运行时库

6.3.5 RegBankSelect:寄存器组分配

这是 GlobalISel 的创新之处,在指令选择前决定值存储在哪类寄存器中:

; 分配前:
%val:_(s32) = G_LOAD %ptr:_(p0)

; 分配后:
%val:gpr(s32) = G_LOAD %ptr:gpr(p0)  ; 通用寄存器
; 或
%val:fpr(s32) = G_LOAD %ptr:gpr(p0)  ; 浮点寄存器

优势:

6.3.6 InstructionSelect:最终指令选择

基于 TableGen 生成的匹配表选择目标指令:

// TableGen 模式
def : GINodeEquiv<G_ADD, add>;
def : Pat<(add GPR32:$src1, GPR32:$src2),
          (ADDWrr GPR32:$src1, GPR32:$src2)>;

与 SelectionDAG 的区别:

6.3.7 GlobalISel 的优化机会

GlobalISel 提供了新的优化点:

  1. Combiner:在各阶段间插入的模式优化
  2. KnownBits 分析:跟踪位级信息
  3. CSE (Common Subexpression Elimination):MIR 级别的公共子表达式消除

6.3.8 GlobalISel vs SelectionDAG:性能对比

编译速度(2023 年数据):

采用现状:

6.3.9 GlobalISel 的未来发展

  1. 更好的优化:缩小与 SelectionDAG 的代码质量差距
  2. 更多目标支持:X86、RISC-V 的完整支持
  3. 与 MLIR 集成:探索多层 IR 的协同
  4. 机器学习引导:使用 ML 改进决策

6.4 寄存器分配:从线性扫描到 PBQP

6.4.1 寄存器分配的挑战

寄存器分配是编译器后端最关键的优化之一。它需要将无限的虚拟寄存器映射到有限的物理寄存器,同时最小化内存访问(spill/reload)。这是一个 NP 完全问题,实践中需要在编译时间和代码质量间权衡。

LLVM 的寄存器分配器演化:

  1. 2003-2008:简单的线性扫描算法
  2. 2008-2010:线性扫描的改进版本
  3. 2010-至今:Greedy 寄存器分配器(Jakob Stoklund Olesen)
  4. 实验性:PBQP(Partitioned Boolean Quadratic Programming)

6.4.2 线性扫描算法(Linear Scan)

早期 LLVM 使用的线性扫描算法基于 Poletto 和 Sarkar 的工作(1999):

算法流程:
1. 计算每个虚拟寄存器的活跃区间(Live Interval)
2. 按起始点排序活跃区间
3. 线性扫描,为每个区间分配寄存器
4. 冲突时选择溢出代价最小的寄存器

优点:

缺点:

6.4.3 Greedy 寄存器分配器

2010 年,Jakob Stoklund Olesen 设计了全新的 Greedy 分配器,至今仍是 LLVM 的默认选择:

核心思想:

  1. 优先级队列:按”重要性”排序虚拟寄存器
  2. 贪心分配:为最重要的寄存器优先分配
  3. 局部分割:必要时分割活跃区间
  4. 迭代改进:多轮优化提升质量
// 简化的分配流程
while (!Queue.empty()) {
  VirtReg = Queue.pop();
  if (PhysReg = tryAssign(VirtReg))
    assign(VirtReg, PhysReg);
  else if (canEvict(VirtReg))
    evictAndAssign(VirtReg);
  else
    splitAndEnqueue(VirtReg);
}

6.4.4 活跃区间分析(Live Interval Analysis)

活跃区间是寄存器分配的基础数据结构:

虚拟寄存器 %1 的活跃区间:
[16, 32) [48, 64) [80, 96)
    ↑        ↑        ↑
  定义点   使用点   最后使用

物理寄存器压力图:
     时间 →
R0: ████____████____
R1: __████████______
R2: ______████████__

关键概念:

6.4.5 溢出代码插入(Spill Code Insertion)

当寄存器不足时,需要将值溢出到内存:

; 溢出前:
add %r1, %r2, %r3
mul %r4, %r1, %r5

; 溢出 %r1 后:
add %r1, %r2, %r3
str %r1, [sp, #8]   ; spill
... 其他代码 ...
ldr %r1, [sp, #8]   ; reload
mul %r4, %r1, %r5

溢出决策因素:

  1. 使用频率:循环内的变量避免溢出
  2. 活跃长度:长活跃区间更可能溢出
  3. 溢出代价:考虑 load/store 延迟

6.4.6 寄存器类和约束处理

现代架构有复杂的寄存器约束:

// X86 的寄存器类定义
def GR32 : RegisterClass<[i32], 32, [EAX, ECX, EDX, ...]>;
def FR32 : RegisterClass<[f32], 32, [XMM0, XMM1, ...]>;

// 特殊约束
def : Constraint<"$src = $dst">; // two-address 约束

分配器必须处理:

6.4.7 PBQP 寄存器分配器

PBQP(Partitioned Boolean Quadratic Programming)是一种基于图的分配方法:

构建 PBQP 图:
- 节点:虚拟寄存器
- 边:寄存器间的干扰
- 权重:分配代价

求解过程:
1. 图简化(移除度数小的节点)
2. 启发式选择
3. 回溯优化

优势:

劣势:

6.4.8 寄存器分配的性能影响

寄存器分配对性能的影响巨大:

基准测试结果(SPEC CPU2017):
优秀的分配 vs 差的分配:
- 性能差异:10-30%
- 代码大小:15-25% 差异
- 能耗:相应增加

关键指标:

6.4.9 未来方向:机器学习引导的分配

最新研究探索使用机器学习改进寄存器分配:

  1. 特征提取:程序特征(循环深度、数据流)
  2. 模型训练:基于历史数据训练决策模型
  3. 在线决策:运行时选择分配策略
  4. 强化学习:通过反馈持续改进

Google 的 MLGO 项目已经展示了初步成果,在某些场景下超越传统启发式算法。

6.5 机器级优化和指令调度

6.5.1 机器级优化的重要性

机器级优化发生在指令选择之后、代码发射之前,直接操作目标机器指令。这个阶段的优化对性能至关重要,因为它们了解具体的硬件特性:

6.5.2 指令调度(Instruction Scheduling)

LLVM 支持多种调度策略:

1. 列表调度(List Scheduling)

基本算法:
1. 构建依赖图(DAG)
2. 计算每个指令的优先级
3. 维护就绪队列
4. 贪心选择下一条指令

2. 调度区域

6.5.3 机器调度模型

TableGen 描述目标的调度模型:

// 定义处理器模型
def CortexA57Model : SchedMachineModel {
  let IssueWidth = 3;          // 每周期可发射3条指令
  let MicroOpBufferSize = 128; // 微操作缓冲区大小
  let LoadLatency = 4;         // Load 指令延迟
  let MispredictPenalty = 16;  // 分支预测错误代价
}

// 定义指令调度类
def : WriteRes<WriteALU, [A57UnitI]> {
  let Latency = 1;  // ALU 操作延迟1周期
}

6.5.4 关键路径优化

调度器识别并优化关键路径:

原始序列:
1: load  r1, [r0]     ; 4 cycles
2: load  r2, [r0+4]   ; 4 cycles
3: add   r3, r1, #1   ; 1 cycle (依赖1)
4: add   r4, r2, #2   ; 1 cycle (依赖2)
5: mul   r5, r3, r4   ; 3 cycles (依赖3,4)
总延迟:4 + 1 + 3 = 8 cycles

优化后(指令重排):
1: load  r1, [r0]     ; 4 cycles
2: load  r2, [r0+4]   ; 4 cycles (并行)
3: add   r3, r1, #1   ; 1 cycle
4: add   r4, r2, #2   ; 1 cycle (并行)
5: mul   r5, r3, r4   ; 3 cycles
总延迟:max(4+1, 4+1) + 3 = 8 cycles
但利用了并行执行单元

6.5.5 寄存器压力感知调度

平衡指令级并行和寄存器压力:

// 调度策略选择
enum SchedulingStrategy {
  ILP,      // 最大化指令级并行
  RegPress, // 最小化寄存器压力
  Hybrid    // 混合策略
};

// 根据区域特征选择策略
if (RegisterPressure > Threshold)
  Strategy = RegPress;  // 降低压力,避免溢出
else
  Strategy = ILP;       // 提高并行度

6.5.6 机器级窥孔优化(Peephole Optimization)

识别并优化局部指令模式:

; 优化前:
mov r1, #0
cmp r1, #0
beq label

; 优化后:
mov r1, #0
b   label    ; 无条件跳转,因为比较结果已知

; 另一个例子 - 合并 load/store:
; 优化前:
ldr r1, [r0]
ldr r2, [r0+4]

; 优化后(如果目标支持):
ldp r1, r2, [r0]  ; 加载对

6.5.7 指令合并和宏融合

现代处理器支持指令融合:

; 比较和分支融合(ARM64)
cmp  x0, x1
b.eq label
; 处理器可能将这两条指令作为一个微操作执行

; 地址计算融合(x86)
lea rax, [rbx + rcx*4 + 8]
; 复杂地址计算在一个周期内完成

LLVM 的 MacroFusion pass 识别这些机会:

// MacroFusion 的模式识别
if (isCompareInstr(First) && isCondBranch(Second)) {
  if (canFuseInstructions(First, Second))
    markAsFused(First, Second);
}

6.5.8 延迟槽填充(Delay Slot Filling)

某些架构(如 MIPS、SPARC)有延迟槽:

; MIPS 的延迟槽
beq $t0, $t1, label
add $t2, $t3, $t4  ; 延迟槽,总是执行

; 填充策略:
; 1. 从前面移动无关指令
; 2. 从目标块移动安全指令
; 3. 插入 NOP(最后选择)

6.5.9 后寄存器分配调度

寄存器分配后的再调度,利用确定的寄存器信息:

// PostRA 调度器
class PostRAScheduler {
  void schedule() {
    // 已知物理寄存器,可以精确建模:
    // - 寄存器依赖
    // - 反依赖(WAR)
    // - 输出依赖(WAW)
    
    buildDependencies();
    computeSchedule();
    applySchedule();
  }
};

优势:

6.5.10 分支优化

机器级的分支优化:

1. 分支消除

; 条件移动替代分支
; 优化前:
cmp r0, #0
beq skip
mov r1, #1
skip:

; 优化后:
cmp r0, #0
movne r1, #1  ; 条件执行

2. 分支对齐

; 对齐热点分支目标到缓存行边界
.align 64
hot_loop:
  ; 循环体

3. 尾调用优化

; 优化前:
call function
ret

; 优化后:
jmp function  ; 尾调用

6.5.11 代码布局优化

优化基本块布局以改善分支预测和缓存行为:

原始 CFG:
  BB1
  / \
BB2 BB3
  \ /
  BB4

优化后(基于 profile):
BB1 → BB3 → BB4 → BB2
(将热路径排成直线)

策略:

  1. 热路径优先:频繁执行的路径顺序排列
  2. 减少跳转:fall-through 优于显式跳转
  3. 循环旋转:将循环入口移到末尾

6.5.12 高级话题:FastISel vs SelectionDAG vs GlobalISel

三种指令选择框架的机器级优化能力对比:

FastISel

SelectionDAG

GlobalISel

本章小结

本章深入探讨了 LLVM 后端架构的核心组件和演化历程。我们学习了:

  1. TableGen 的声明式设计哲学,它如何简化目标描述并自动生成大量后端代码
  2. SelectionDAG 作为成熟的指令选择框架,通过 DAG 表示实现高质量的代码生成
  3. GlobalISel 作为下一代框架,通过模块化设计提供更快的编译速度和更好的可扩展性
  4. 寄存器分配 从简单的线性扫描到复杂的 Greedy 算法,以及 PBQP 等高级技术
  5. 机器级优化 包括指令调度、窥孔优化、代码布局等直接影响性能的关键技术

关键洞察:

未来趋势:

练习题

基础题

练习 6.1 TableGen 的核心价值是什么?列举三个 TableGen 自动生成的组件。

答案 TableGen 的核心价值是通过声明式编程减少代码重复、提高一致性、简化新架构移植。 自动生成的组件包括: 1. 指令编码器/解码器 2. 汇编器/反汇编器 3. 寄存器信息表 4. 指令选择匹配表 5. 调度模型信息

练习 6.2 解释 SelectionDAG 中”类型合法化”和”操作合法化”的区别,各举一个例子。

答案 类型合法化:处理目标不支持的数据类型 - 例子:在 32 位架构上,i64 类型被拆分为两个 i32 操作合法化:处理目标不支持的操作 - 例子:如果目标不支持硬件除法,UDIV 操作被展开为库函数调用

练习 6.3 在 GlobalISel 框架中,为什么要在指令选择之前进行 RegBankSelect?这带来什么优势?

答案 RegBankSelect 在指令选择前决定值存储在哪类寄存器(如通用寄存器或浮点寄存器)。 优势: 1. 避免后期修复的复杂性 2. 简化指令选择逻辑 3. 更好支持异构架构(CPU+GPU) 4. 早期决策可以影响后续优化

练习 6.4 比较线性扫描和 Greedy 寄存器分配算法的时间复杂度和代码质量。

答案 线性扫描: - 时间复杂度:O(n log n) - 代码质量:较好,但不如 Greedy - 适用场景:JIT 编译 Greedy: - 时间复杂度:O(n² log n) 最坏情况 - 代码质量:优秀,接近图着色算法 - 适用场景:AOT 编译,-O2/-O3 优化级别

挑战题

练习 6.5 设计一个简单的 TableGen 描述,定义一个假想架构的 ADD 指令(包括寄存器-寄存器和寄存器-立即数两种形式)。说明你的设计考虑。

答案 ```tablegen // 定义寄存器类 def GPR : RegisterClass<[i32], 32, (sequence "R%u", 0, 15)>; // 定义指令基类 class MyInst<dag outs, dag ins, string asm> : Instruction { let OutOperandList = outs; let InOperandList = ins; let AsmString = asm; } // ADD 寄存器-寄存器形式 def ADDrr : MyInst<(outs GPR:$dst), (ins GPR:$src1, GPR:$src2), "add $dst, $src1, $src2"> { let Pattern = [(set GPR:$dst, (add GPR:$src1, GPR:$src2))]; } // ADD 寄存器-立即数形式 def ADDri : MyInst<(outs GPR:$dst), (ins GPR:$src1, i32imm:$imm), "add $dst, $src1, $imm"> { let Pattern = [(set GPR:$dst, (add GPR:$src1, imm:$imm))]; } ``` 设计考虑: 1. 分离两种形式便于模式匹配 2. 使用 Pattern 关联 IR 操作 3. 清晰的操作数列表定义 4. 统一的汇编语法

练习 6.6 给定以下指令序列和延迟信息,手工进行指令调度以最小化总执行时间。假设有两个执行单元可以并行执行独立指令。

1: load r1, [r0]      // 3 cycles
2: load r2, [r0+4]    // 3 cycles  
3: add r3, r1, #1     // 1 cycle, depends on 1
4: mul r4, r2, r1     // 2 cycles, depends on 1,2
5: store r3, [r0+8]   // 2 cycles, depends on 3
6: store r4, [r0+12]  // 2 cycles, depends on 4
答案 优化调度: ``` Cycle 0-2: load r1, [r0] | load r2, [r0+4] Cycle 3: add r3, r1, #1 | mul r4, r2, r1 (开始) Cycle 4: mul继续 | store r3, [r0+8] (开始) Cycle 5: store r3继续 | store r4, [r0+12] (开始) Cycle 6: store r4继续 | 完成 ``` 总时间:7 cycles(原始串行执行需要 11 cycles) 关键优化: 1. 并行执行两个 load 2. add 和 mul 尽早开始 3. 两个 store 可以部分重叠

练习 6.7 分析 GlobalISel 和 SelectionDAG 在编译以下 IR 时的主要差异:

define i32 @foo(i32 %a, i32 %b) {
  %1 = add i32 %a, %b
  %2 = mul i32 %1, 2
  ret i32 %2
}
答案 SelectionDAG 流程: 1. 构建 DAG:创建节点和边表示依赖 2. 合法化:类型和操作都已合法 3. DAG 优化:mul by 2 → shift left 1 4. 指令选择:匹配目标指令 5. 调度:确定指令顺序 GlobalISel 流程: 1. IRTranslator:直接生成 G_ADD, G_MUL, G_RET 2. Legalizer:操作已合法 3. RegBankSelect:分配到通用寄存器 4. Combiner:mul by 2 → shift 5. InstructionSelect:选择目标指令 主要差异: - GlobalISel 无需构建 DAG,更快 - GlobalISel 的优化发生在 MIR 级别 - SelectionDAG 有更成熟的优化 - GlobalISel 更模块化,易于调试

练习 6.8 设计一个寄存器分配场景,展示 Greedy 算法相比线性扫描的优势。说明两种算法的不同决策。

答案 场景:3 个物理寄存器,4 个虚拟寄存器 ``` v1: [0, 10] 权重: 100 (循环内) v2: [2, 8] 权重: 80 (循环内) v3: [4, 12] 权重: 20 (循环外) v4: [6, 14] 权重: 10 (循环外) ``` 线性扫描(按起始位置): 1. v1 → r1 2. v2 → r2 3. v3 → r3 4. v4 → 溢出 (没有空闲寄存器) Greedy(按权重): 1. v1 → r1 (最高权重) 2. v2 → r2 (次高权重) 3. v3 → 溢出 (与 v1、v2 冲突) 4. v4 → r3 (可用) 结果:Greedy 保留了高权重(循环内)的变量在寄存器中,性能更好。

常见陷阱与错误

1. TableGen 陷阱

陷阱:过度使用 TableGen,将复杂逻辑塞入 .td 文件

// 错误:复杂计算不适合 TableGen
def ComplexPattern : Pat<...> {
  let Predicate = [{
    // 100 行 C++ 代码
  }];
}

解决:复杂逻辑应该在 C++ 中实现,TableGen 只做声明

2. 寄存器分配陷阱

陷阱:忽视寄存器压力导致过度溢出

// 在循环中创建太多临时变量
for (...) {
  int t1 = ..., t2 = ..., t3 = ..., t4 = ...;
  // 寄存器压力过高
}

解决:使用寄存器压力跟踪,必要时调整优化策略

3. 指令选择陷阱

陷阱:模式匹配顺序错误

// 错误:通用模式在特殊模式前
def : Pat<(add i32:$a, i32:$b), (ADD32rr $a, $b)>;
def : Pat<(add i32:$a, 1), (INC32r $a)>;  // 永远不会匹配

解决:特殊模式应该有更高优先级

4. 调度陷阱

陷阱:忽视内存依赖

store [addr], r1
load r2, [addr]  ; 必须在 store 后执行

解决:正确建模内存依赖,使用别名分析

5. GlobalISel 迁移陷阱

陷阱:假设 GlobalISel 总是更快

// GlobalISel 可能在某些模式上更慢
-mllvm -global-isel=1  // 盲目启用

解决:基准测试,根据具体场景选择

最佳实践检查清单

TableGen 最佳实践

指令选择最佳实践

寄存器分配最佳实践

机器级优化最佳实践

调试和验证最佳实践