Day 3:中间代码生成 + 优化实战

目标:理解 AST→IR 的翻译过程,掌握 LLVM 核心优化 Pass 的原理和用法,能自己写 Pass。今天最关键——Day2 学了"怎么分析",今天学"分析完了怎么改 IR"。


上午:理论(龙书第 6 章 + 第 9 章)

1. 中间代码生成(第 6 章)—— 从 AST 到三地址码

1.1 为什么不直接从 AST 生成机器码?

因为目标架构太多了——x86、ARM、RISC-V 各有不同的指令集、寄存器、寻址方式。如果 AST 直接面对几十种 CPU,每个组合都要写一套翻译逻辑,组合爆炸。

中间表示(IR)= 一种"共识语言":所有前端翻译为 IR,所有后端从 IR 翻译到目标机。代价是前端+后端要各写 N+M 套,不是 N×M 套。

1.2 三地址码(Three-Address Code, TAC)

龙书的标准三地址码:

x = y op z      二元运算: 目标 = 源1 op 源2
x = op y         一元运算
x = y            拷贝
goto L           无条件跳转
if x goto L      条件跳转 (x 为真则跳)
if x relop y goto L  关系跳转
x = &y           取地址
*x = y           间接写
x = *y           间接读
x = call p, n    调用 p,n 个参数,结果放 x
param x          传参

LLVM IR 就是三地址码

龙书 TAC:        t1 = a + b;   t2 = t1 * c;   a = t2
LLVM IR:         %t1 = add i32 %a, %b
                 %t2 = mul i32 %t1, %c
                 store i32 %t2, ptr %a

每行恰有一个操作符,恰有两个源操作数(或一个,但绝不超过三个地址)。

1.3 表达式翻译:AST 后序遍历

生成三地址码的标准算法——AST 后序遍历:

def translate_expr(node):
    """对表达式 AST 节点生成三地址码,返回存放结果的临时变量名"""
    if node.type == 'NUM':
        return str(node.value)              # 常量直接返回

    elif node.type == 'ID':
        return node.name                    # 变量名直接返回

    elif node.type in ('+', '-', '*', '/'):
        left  = translate_expr(node.left)   # 递归翻译左子树
        right = translate_expr(node.right)  # 递归翻译右子树
        temp  = new_temp()                  # 分配临时变量
        emit(f'{temp} = {left} {node.type} {right}')   # 生成一条 TAC
        return temp

    elif node.type == 'ASSIGN':
        right = translate_expr(node.right)
        # 左值是赋值的"目标地址",不需要翻译为值
        emit(f'{node.left.name} = {right}')
        return None   # 赋值语句没有返回值

示例——表达式 (a + b) * (c - d) 的 AST:

        *
       / \
      +   -
     / \ / \
    a  b c  d

后序遍历翻译过程:

访 a → "a"
访 b → "b"
出 + → emit "t1 = a + b" → 返回 "t1"
访 c → "c"
访 d → "d"
出 - → emit "t2 = c - d" → 返回 "t2"
出 * → emit "t3 = t1 * t2" → 返回 "t3"

生成的三地址码:

t1 = a + b
t2 = c - d
t3 = t1 * t2
1.4 控制流翻译:短路求值与回填

短路求值(Short-Circuit):if (a && b) 如果 a 为假,b 根本不求值。

翻译的思路——给每条布尔表达式配两个标签:

E.true  = 表达式 E 为真时跳到哪里
E.false = 表达式 E 为假时跳到哪里

对于 E1 && E2

翻译 E1:如果 E1 为真 → 继续求值 E2
        如果 E1 为假 → 整个 && 为假,跳转到 E.false
然后 E2 的 true/false 直接就是整个表达式的 true/false

实例——if (a > 0 && b > 0) { body } else { elses }

; 龙书风格的三地址码
    if a <= 0 goto Lfalse      ; a > 0 不成立 → 短路,跳假分支
    if b <= 0 goto Lfalse      ; b > 0 不成立 → 跳假分支
    ; body 代码...
    goto Lend
Lfalse:
    ; else 代码...
Lend:
; LLVM IR 等价形式
entry:
  %cmp1 = icmp sgt i32 %a, 0
  br i1 %cmp1, label %check_b, label %if.else

check_b:
  %cmp2 = icmp sgt i32 %b, 0
  br i1 %cmp2, label %if.then, label %if.else

if.then:
  ; body...

if.else:
  ; else...

if.end:
  ; 继续...

回填(Backpatching):单趟编译时,目标标签可能还没出现。

# 回填的核心数据结构
pending_jumps = []  # 待回填的跳转指令列表

def emit_jump_to_future():
    """生成一条跳转,目标暂时未知,留个坑"""
    instr = "goto ???"          # 目标地址待定
    idx = len(code)
    code.append(instr)
    pending_jumps.append(idx)

def backpatch_label(label_idx):
    """现在知道了目标位置,把所有待定的跳转填上"""
    for jump_idx in pending_jumps:
        code[jump_idx] = f"goto L{label_idx}"

Clang 的代码生成模块(clang/lib/CodeGen/CGStmt.cpp)在处理 ifwhileswitch 时大量使用回填——先留 BasicBlock* 空指针,等目标块创建后再接上。

1.5 类型检查与符号表

语义分析阶段的核心工作:

def check_binary_expr(op, left, right):
    """简单类型检查"""
    lt = left.type
    rt = right.type

    if op in ('+', '-', '*', '/'):
        if lt == 'int' and rt == 'int':
            return 'int'
        elif lt == 'float' and rt == 'float':
            return 'float'
        elif lt == 'int' and rt == 'float':
            # 隐式类型转换: int → float
            emit(f't = int_to_float({left.name})')
            return 'float'
        else:
            raise TypeError(f"不支持 {op}{lt}{rt}")

在 Clang 中,这个逻辑分布在 Sema(Semantic Analysis)模块。SemaExpr.cpp 有超过 2 万行,处理 C++ 的所有隐式转换、重载决议、模板推导。


2. 机器无关优化(第 9 章)—— 数据流分析的应用

第 4 章讲了数据流分析的框架(in/out/gen/kill/∧),第 9 章是在这个框架上实现具体优化

2.1 全局公共子表达式消除(GCSE)

问题:如果 a+b 在两个地方都计算了,且操作数没变,第二次可以复用第一次的结果。

依赖分析:可用表达式分析(前向 ∩)

// 优化前
x = a + b;       // 定义 e1 = a+b
y = c + d;
z = a + b;       // a+b 又一次出现!但 a 和 b 都没变 → 这是公共子表达式

// 优化后
x = a + b;
y = c + d;
z = x;           // 直接复用 x

LLVM 实现GVN Pass(Global Value Numbering)就是 GCSE 的超集——它不仅消除公共子表达式,还能做常量折叠、代数简化。

# 看 GVN 的效果
cat > test_gvn.c << 'EOF'
int foo(int a, int b, int c) {
    int x = a + b;
    int y = c * 2;
    int z = a + b;   // 公共子表达式
    return x + z;
}
EOF

clang -S -emit-llvm -O0 test_gvn.c -o test_gvn.ll
opt -passes=gvn test_gvn.ll -S -o test_gvn_opt.ll
# 观察:z = a+b 会被替换为 z = x
2.2 部分冗余消除(PRE)—— 集大成者

GCSE 只能消除 所有路径上都重复 的计算。但有时一条路径重复,另一条不重复——这叫部分冗余

// 部分冗余的例子
if (cond) {
    x = a + b;         // 路径 A:计算了 a+b
}
// ...
if (cond) {
    y = a + b;         // 路径 B:又算了 a+b。如果 cond 为真,这是第二次
}

PRE 的处理方式

// PRE 优化后(代码提升)
if (cond) {
    x = a + b;         // 保留
}
// ...
t = a + b;             // 提取到公共路径!
if (cond) {
    y = t;             // 直接用
} else {
    // 如果 cond 为假,第一条路径没算 a+b,但 t 已算好,可以直接用
}

PRE 综合了:

  • 公共子表达式消除(完全冗余 → 直接删重复)
  • 循环不变量外提(循环内的冗余 → 提到 preheader)
  • 代码下沉(只在某些路径需要 → 下沉到需要的路径)

关键理解:PRE 是数据流分析的"终极应用"——它需要同时跑可用表达式(前向∩)和 anticipatable 表达式(后向∩),然后做插入+删除。算法复杂度 O(N³) 但有近似 O(N²) 的实现。LLVM 的 GVN Pass 不直接做完整的 PRE,但结合 GVN + LICM + Sink 可以达到类似效果。

2.3 循环优化——几个必须掌握的变换
a) 循环不变量外提(LICM, Loop-Invariant Code Motion)
// 优化前
for (int i = 0; i < n; i++) {
    x = a * b;          // a 和 b 在循环内不变!
    arr[i] = x + i;
}

// 优化后
x = a * b;              // 提到循环外(放入 preheader)
for (int i = 0; i < n; i++) {
    arr[i] = x + i;
}

LLVM 的实现逻辑:对每条指令,检查其操作数是否都来自循环外或循环不变量。如果是,就足够外提——但还要检查支配条件(外提后会不会在原本不执行的路径上被求值)和寄存器压力(外提太多会溢出)。

opt -passes=licm input.ll -S -o output.ll
b) 归纳变量消除(Induction Variable Elimination)
// 优化前:i 是循环变量,j = i * 4 是派生归纳变量
for (int i = 0; i < 100; i++) {
    j = i * 4;
    arr[j] = 0;    // 等价于 arr[i * 4] = 0
}

// 优化后:用 arr_ptr 的步进代替 i*4
int *ptr = &arr[0];
for (int i = 0; i < 100; i++) {
    *ptr = 0;
    ptr++;          // 用指针自增代替每次的乘法
}

强度削弱(Strength Reduction):把昂贵的操作(乘法)替换为便宜的(加法)。LLVM 的 indvars Pass 处理这一切。

c) 循环展开(Loop Unrolling)
// 展开前(每次迭代处理 1 个元素)
for (int i = 0; i < 100; i++) {
    sum += arr[i];
}

// 展开后(每次迭代处理 4 个元素)
for (int i = 0; i < 100; i += 4) {
    sum += arr[i];
    sum += arr[i+1];
    sum += arr[i+2];
    sum += arr[i+3];
}

收益:减少分支次数(100 次 → 25 次),改善指令级并行度。代价:代码膨胀。LLVM 自动权衡。

opt -passes='loop-unroll' input.ll -S
d) 循环向量化(Loop Vectorization)

SIMD 指令一次处理 4 个(或 8 个、16 个)元素:

; 标量版本
for.body:
  %v = load i32, ptr %ptr
  %sum = add i32 %sum, %v

; 向量化版本(一次处理 4 个 i32)
vector.body:
  %wide.load = load <4 x i32>, ptr %ptr   ; 一次加载 4 个 int
  %sum.vec  = add <4 x i32> %sum.vec, %wide.load  ; 一次加 4 对 int
clang -O2 -Rpass=loop-vectorize loop.c     # 看 LLVM 报告哪些循环被向量化了

下午:实战(LLVM 书第 6 章 + 第 9 章)

3. TableGen(第 6 章)—— 不是"配置文件",是 DSL

3.1 为什么需要 TableGen

LLVM 后端需要描述的信息量巨大。以 x86 为例——上千条指令,每条有不同的编码、操作数、延迟、流水线行为。手写 C++ 不仅冗长,还容易出错。

TableGen 是一种领域特定语言(DSL),专门用来描述目标架构信息:

.td 文件(声明式描述) → llvm-tblgen 工具 → C++ 头文件/源文件(自动生成)
3.2 TableGen 语法速览
// ---- 基本类型 ----
// bit: 0 或 1
// int: 整数
// string: 字符串
// list<T>: 列表
// dag: 有向无环图(用于模式匹配)

// ---- 类定义(class)----
// 类似 C++ 的类,可以有模板参数
class Instruction<string mnemonic, bits<8> opcode> {
  string Mnemonic = mnemonic;      // 助记符
  bits<8> Opcode   = opcode;       // 操作码

  bit isTerminator = 0;            // 默认不是基本块结束指令
  bit isBranch     = 0;
  bit hasSideEffects = 0;
}

// ---- 定义(def)----
// 类的实例
def ADD : Instruction<"add", 0x01> {
  let isTerminator = 0;
}

// ---- 多类(multiclass)----
// 一次生成多个 def,用模板参数
multiclass ArithI<string mnemonic, bits<8> opcode> {
  def rr : Instruction<mnemonic, opcode>;      // 寄存器-寄存器版本
  def ri : Instruction<mnemonic, opcode>;      // 寄存器-立即数版本
}
defm ADD : ArithI<"add", 0x01>;  // 生成 ADD_rr 和 ADD_ri

// ---- DAG 模式(指令选择的核心)----
// [(set 目标, (IR操作 源操作数))]  描述这条指令匹配什么样的 IR
def ADDri : Inst<(outs GR32:$rd), (ins GR32:$rs, i32imm:$imm),
                 "add $rd, $rs, $imm",
                 [(set i32:$rd, (add i32:$rs, imm:$imm))]>;
//   ↑ 意思是:当 IR 中有 (add i32:rs, 常量:imm) 且结果赋给 i32:rd →
//   ↓ 用这条指令编码,输出 "add rd, rs, imm"
3.3 TableGen 生成的 C++ 代码

运行 llvm-tblgen 可以看到生成的代码:

# 以 RISC-V 后端为例
cd llvm/lib/Target/RISCV/
llvm-tblgen RISCV.td -I ../../../include -gen-instr-info

# 输出类似(精简):
# const MCInstrDesc RISCVInsts[] = {
#   { RISCV::ADD, "add", ... },
#   { RISCV::SUB, "sub", ... },
#   ...
# };

生成的代码包含:

  • 指令描述表(MCInstrDesc
  • 寄存器信息(MCRegisterInfo
  • 调用约定信息(CallingConv
  • 指令选择匹配表(MatcherTable —— 自动机驱动的模式匹配)

关键认知:后端的指令选择器(SelectionDAG ISel)不直接读 .td 文件——它在运行时遍历一个自动机形式的匹配表,这个表是 llvm-tblgen 从 DAG 模式编译生成的。


4. SSA 优化 Pass 体系(第 9 章)—— 核心 Pass 逐个剖析

这一节是 Day3 最重要的实战内容。我们对照 .ll 文件,逐个跑 LLVM 标准优化 Pass,理解每一步做了什么、怎么做的。

4.1 mem2reg —— 构建 SSA 的入口 Pass

这是所有优化 Pass 的前置条件。它把 alloca/load/store 模式提升为 SSA 寄存器。

# 生成未优化的 IR
clang -S -emit-llvm -O0 -Xclang -disable-O0-optnone example.c -o example.ll

# 跑 mem2reg
opt -passes=mem2reg example.ll -S -o example_ssa.ll

算法回顾(从 Day2 的知识出发):

  1. 对每个 alloca 变量,找所有 store 的块
  2. DF[store块] 处插入 φ 节点(迭代直到不动点)
  3. 用 φ 参数和 store 值重命名所有使用(改名遍)
  4. 删除所有 alloca、不会到达任何 loadstore、被 φ 替代的 load
# 看 mem2reg 干了什么(需要 debug 版 LLVM)
opt -passes='print<domtree>,mem2reg,print<domtree>' example.ll -S
4.2 GVN(Global Value Numbering)—— 消除冗余

GVN 同时做全局公共子表达式消除 + 常量折叠 + 代数简化

核心思想——值编号(Value Numbering):给每个"值"分配一个编号。如果 a+bc+d 有相同的值编号(操作数同号,操作符相同),则它们等价。

// 输入
int foo(int a, int b) {
    int x = a + b;
    int y = a;          // y 和 a 同值
    int z = y + b;      // z = a + b = x
    return x + z;       // = x + x = 2*x
}

// GVN 输出(等价于)
int foo(int a, int b) {
    int x = a + b;
    return x + x;
}
opt -passes=gvn example.ll -S
4.3 LICM —— 循环不变量外提

算法

  1. 找出所有循环不变指令:操作数来自循环外或循环不变指令
  2. 对每条不变指令,检查能否移到 preheader(需要支配所有使用点)
  3. 如果能,移到 preheader
// 输入
for (int i = 0; i < n; i++) {
    x = a * b;      // a 和 b 在循环中不变 → 循环不变
    arr[i] = x + i;
}

// LICM 后
x = a * b;
for (int i = 0; i < n; i++) {
    arr[i] = x + i;
}
opt -passes=licm example.ll -S
4.4 DCE(Dead Code Elimination)—— 死代码删除

基于活跃变量分析的结果:如果一条指令定义了某个值,而这个值从不在任何"有用"的地方被引用,这条指令就是死代码。

; 输入
  %x = mul i32 %a, %b        ; %x 后面从未使用
  %y = add i32 %c, %d        ; %y 在 ret 中用到 → 不是死代码
  ret i32 %y

; DCE 后
  %y = add i32 %c, %d
  ret i32 %y                  ; %x 的指令被删了

DCE 的 “涟漪效应”:删除一条指令后,它的操作数可能也变成死代码,需要迭代删除。LLVM 用 worklist 算法实现。

opt -passes=dce example.ll -S
4.5 simplifycfg —— CFG 简化

这个 Pass 做多种 CFG 级别的简化:

a) 跳转到跳转 → 直接跳转

; 简化前
  br label %A
A:
  br label %B

; 简化后
  br label %B

b) 只有一个前驱的基本块 → 合并

; 简化前
  br label %A
A:
  %x = add i32 1, 2
  ret i32 %x

; 简化后
  %x = add i32 1, 2
  ret i32 %x

c) 条件分支的恒等式消除(If-Conversion)

; 简化前
  br i1 true, label %A, label %B

; 简化后
  br label %A                    ; 不可达的分支被删

d) switch → 跳转表 或 条件分支树,根据 case 的数量和密度自动选择。

opt -passes=simplifycfg example.ll -S
4.6 SROA(Scalar Replacement of Aggregates)

把小的结构体拆成标量成员:

// 输入
struct Point { int x; int y; };
struct Point p = {1, 2};
return p.x + p.y;

// SROA 后
int p_x = 1;
int p_y = 2;
return p_x + p_y;          // 结构体完全消失
opt -passes=sroa example.ll -S
4.7 内联(Inliner)

把被调用函数体直接嵌入调用点:

// 内联前
static int square(int x) { return x * x; }
int foo(int a) { return square(a) + square(a+1); }

// 内联后
int foo(int a) {
    int t1 = a * a;          // square(a) 的内联体
    int t2 = (a+1) * (a+1);  // square(a+1) 的内联体
    return t1 + t2;
}

内联的代价模型:不是越大越好——内联太多→代码膨胀→指令缓存miss增加。LLVM 根据函数体大小、调用次数、调用点热度综合决定。

opt -passes=inline example.ll -S
4.8 O2 管道——这些 Pass 如何组合
# 查看 O2 的 Pass 序列
opt -passes='default<O2>' example.ll -S --debug-pass=Structure 2>&1 | head

简化版的 O2 Pass 管道(实际更复杂,此处为逻辑顺序):

Inliner(内联热点函数)
  → SROA(拆分聚合类型)
  → EarlyCSE(公共子表达式消除:简单快速版)
  → simplifycfg(简化控制流)
  → mem2reg(SSA 构造:最关键的一步)
  → GVN(全局值编号:消除冗余计算)
  → LICM(循环不变量外提)
  → indvars(归纳变量规范化 + 消除)
  → loop-unroll(选择性循环展开)
  → loop-vectorize(循环向量化:SIMD)
  → instcombine(指令合并:局部代数简化)
  → DCE(死代码消除:清理)
  → simplifycfg(再次简化控制流)

每个 Pass 只做一件事,但叠加效果惊人——这就是 Pass Pipeline 的设计哲学。


5. 写一个 LLVM Pass(完整实战)

这是今天最重要的一节。我们写两个 Pass:一个只读分析(统计指令),一个变换 IR(替换加法为减法)。

5.1 环境准备
# 推荐用 LLVM 源码构建的开发环境
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout llvmorg-18.1.0
mkdir build && cd build

# CMake 配置(Debug + 必要的组件)
cmake -G Ninja ../llvm \
  -DCMAKE_BUILD_TYPE=Debug \
  -DLLVM_ENABLE_PROJECTS="clang" \
  -DLLVM_TARGETS_TO_BUILD="X86" \
  -DCMAKE_INSTALL_PREFIX=./install

ninja opt clang   # 只需编译 opt 和 clang

如果不编译源码,也可以用系统安装的 LLVM 加载插件:

# 确认 llvm-config 可用
llvm-config --cxxflags --ldflags --libs core irreader passes support
5.2 Pass 一:指令统计(只读分析 Pass)
// InstCount.cpp —— 统计每个函数的指令数(New PassManager 写法)
#include "llvm/IR/Function.h"
#include "llvm/IR/InstIterator.h"      // inst_iterator: 遍历所有指令
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

// ── Pass 本体 ──────────────────────────────────────
namespace {
struct InstCountPass : public PassInfoMixin<InstCountPass> {
  // 这是入口:对每个 Function 调用一次
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
    int total = 0;
    int alloca_count = 0, load_count = 0, store_count = 0;
    int branch_count = 0, phi_count = 0, call_count = 0;

    for (auto &BB : F) {                    // 遍历每个基本块
      for (auto &I : BB) {                  // 遍历块内每条指令
        total++;
        // 用 isa<> / dyn_cast<> 做类型判断
        if (isa<AllocaInst>(&I))     alloca_count++;
        if (isa<LoadInst>(&I))       load_count++;
        if (isa<StoreInst>(&I))      store_count++;
        if (isa<BranchInst>(&I))     branch_count++;
        if (isa<PHINode>(&I))        phi_count++;
        if (isa<CallInst>(&I))       call_count++;
      }
    }

    errs() << "=== " << F.getName() << " ===\n"
           << "  总计: " << total << " 条指令\n"
           << "  alloca: " << alloca_count
           << "  load: " << load_count
           << "  store: " << store_count
           << "  branch: " << branch_count
           << "  phi: " << phi_count
           << "  call: " << call_count << "\n\n";

    // 我们没修改 IR,返回 all() 表示所有分析结果仍然有效
    return PreservedAnalyses::all();
  }
};
}

// ── 注册 Pass 到 PassBuilder ───────────────────────
// 注册后可以用: opt -passes="instcount" hello.ll
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
  return {
    LLVM_PLUGIN_API_VERSION,     // API 版本
    "InstCount",                 // 插件名
    LLVM_VERSION_STRING,         // LLVM 版本
    [](PassBuilder &PB) {        // 注册回调
      PB.registerPipelineParsingCallback(
        [](StringRef Name, FunctionPassManager &FPM,
           ArrayRef<PassBuilder::PipelineElement>) {
          if (Name == "instcount") {
            FPM.addPass(InstCountPass());
            return true;
          }
          return false;
        });
    }
  };
}

编译

# 编译为共享库
clang++ -fPIC -shared InstCount.cpp -o InstCount.so \
  `llvm-config --cxxflags --ldflags --libs core irreader passes support`

# 运行
opt -load-pass-plugin=./InstCount.so \
    -passes="instcount" hello.ll -disable-output

输出示例

=== add ===
  总计: 1 条指令
  alloca: 0  load: 0  store: 0  branch: 0  phi: 0  call: 0

=== main ===
  总计: 12 条指令
  alloca: 2  load: 3  store: 2  branch: 2  phi: 0  call: 1
5.3 Pass 二:变换 IR——加法变减法

一个真正修改 IR 的 Pass。把所有 add 指令替换为等价的减法(仅供教学,无实际意义):

// AddToSub.cpp —— 把所有整数 add 替换为 sub
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"      // IRBuilder: 方便的指令构造器
#include "llvm/IR/InstrTypes.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"

using namespace llvm;

namespace {
struct AddToSubPass : public PassInfoMixin<AddToSubPass> {
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
    bool changed = false;

    for (auto &BB : F) {
      for (auto &I : BB) {
        // 找到整数 add 指令(不包括浮点 fadd)
        if (auto *addInst = dyn_cast<BinaryOperator>(&I)) {
          if (addInst->getOpcode() == Instruction::Add) {
            // 创建减法指令: x + y → x - (-y)
            IRBuilder<> builder(addInst);

            Value *neg = builder.CreateNeg(addInst->getOperand(1),
                                           "neg");   // -y
            Value *sub = builder.CreateSub(addInst->getOperand(0),
                                           neg, "sub");  // x - (-y)

            // 替换所有使用
            addInst->replaceAllUsesWith(sub);
            changed = true;
          }
        }
      }
    }

    // 返回被 Preservation 的分析
    // 因为我们修改了 IR,支配树和循环信息可能失效
    PreservedAnalyses PA;
    if (!changed) {
      PA.preserveSet<CFGAnalyses>();  // 没修改,保留所有
    }
    return PA;
  }
};
}

extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
  return {
    LLVM_PLUGIN_API_VERSION, "AddToSub", LLVM_VERSION_STRING,
    [](PassBuilder &PB) {
      PB.registerPipelineParsingCallback(
        [](StringRef Name, FunctionPassManager &FPM,
           ArrayRef<PassBuilder::PipelineElement>) {
          if (Name == "add-to-sub") {
            FPM.addPass(AddToSubPass());
            return true;
          }
          return false;
        });
    }
  };
}

运行验证

cat > test.c << 'EOF'
int foo(int a, int b) { return a + b; }
EOF

clang -S -emit-llvm -O2 -Xclang -disable-O0-optnone test.c -o test.ll
opt -load-pass-plugin=./AddToSub.so -passes="add-to-sub" test.ll -S -o out.ll

# 验证: a + b 变成了 a - (-b)
cat out.ll
5.4 Pass 开发核心知识清单
操作代码
遍历所有函数for (auto &F : M) (Module 级 Pass 时)
遍历基本块for (auto &BB : F)
遍历指令for (auto &I : BB)
类型判断isa<AllocaInst>(&I), isa<PHINode>(&I)
类型转换auto *AI = dyn_cast<AllocaInst>(&I)
创建指令IRBuilder<> builder(BB.getFirstNonPHI()); Value *v = builder.CreateAdd(a,b);
替换使用old->replaceAllUsesWith(new);
删除指令I.eraseFromParent();
创建基本块BasicBlock::Create(Context, "name", &F);
获取上下文LLVMContext &ctx = F.getContext();
分配临时变量不需要!SSA 自动管理

分析结果保存声明

// 如果 Pass 没修改 IR(只读分析)
return PreservedAnalyses::all();

// 如果 Pass 修改了 IR 但没改 CFG
PreservedAnalyses PA;
PA.preserveSet<CFGAnalyses>();  // 支配树 & 循环信息仍然有效
return PA;

// 如果 Pass 大改了 IR
return PreservedAnalyses::none();  // 一切分析都重新计算

6. 练习

练习 1:观察 O2 管道效果(20 分钟)
# 写一个含 if/else + 循环 + 冗余计算的 C 函数
cat > pipeline_test.c << 'EOF'
int test(int a, int b, int n) {
    int sum = 0;
    int x = a * b;          // 循环不变!
    for (int i = 0; i < n; i++) {
        int y = a * b;      // x 的冗余!
        sum = sum + i + y;
        x = a * b;          // x 的又一次冗余!
    }
    return sum;
}
EOF

# 对比 -O0 和 -O2 的 IR
clang -S -emit-llvm -O0 pipeline_test.c -o pipeline_O0.ll
clang -S -emit-llvm -O2 pipeline_test.c -o pipeline_O2.ll

# 逐步跑优化
opt -passes=mem2reg pipeline_O0.ll -S -o step1.ll       # SSA 构造
opt -passes=mem2reg,gvn   pipeline_O0.ll -S -o step2.ll  # +冗余消除
opt -passes=mem2reg,gvn,licm pipeline_O0.ll -S -o step3.ll # +循环不变量外提
diff step1.ll step2.ll
diff step2.ll step3.ll

观察

  • step1:alloca 消失,出现 phi 节点(循环中的 i 和 sum)
  • step2:a*b 的第二次出现被替换
  • step3:a*b 被移到循环外
▼ 练习 1 参考答案——逐步对比 IR 变化

原始 C 代码分析

int test(int a, int b, int n) {
    int sum = 0;
    int x = a * b;           // x 在循环外定义(循环不变量!)
    for (int i = 0; i < n; i++) {
        int y = a * b;       // y = a*b —— 和 x 的值完全一样(公共子表达式)
        sum = sum + i + y;   // sum 累加
        x = a * b;           // 又一次 a*b!和 x 和 y 都一样
    }
    return sum;
}

代码中有 3 处 a * b:循环外 x、循环内 y、循环内重赋值 x。但 a 和 b 在函数内从不被修改 → a*b 是循环不变量且是全局公共子表达式。


step0(-O0 原始 IR)

define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
  ; ... alloca: sum, x, i, y ...
  store i32 0, ptr %sum
  %mul = mul nsw i32 %a, %b       ; 第1次: x = a * b
  store i32 %mul, ptr %x
  store i32 0, ptr %i
  br label %for.cond

for.cond:
  %i.val = load i32, ptr %i
  %cmp = icmp slt i32 %i.val, %n
  br i1 %cmp, label %for.body, label %for.end

for.body:
  %mul2 = mul nsw i32 %a, %b      ; 第2次: y = a * b ← 冗余!
  store i32 %mul2, ptr %y
  %sum.val = load i32, ptr %sum
  %i.val2 = load i32, ptr %i
  %y.val = load i32, ptr %y
  %add1 = add nsw i32 %sum.val, %i.val2
  %add2 = add nsw i32 %add1, %y.val
  store i32 %add2, ptr %sum
  %mul3 = mul nsw i32 %a, %b      ; 第3次: x = a * b ← 冗余!
  store i32 %mul3, ptr %x
  br label %for.inc

for.inc:
  %i.val3 = load i32, ptr %i
  %inc = add nsw i32 %i.val3, 1
  store i32 %inc, ptr %i
  br label %for.cond

for.end:
  %sum.final = load i32, ptr %sum
  ret i32 %sum.final
}

3 个 mul nsw i32 %a, %b —— 但 O0 每条都是独立计算,不共享。


step1(mem2reg 后)

define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
  %mul = mul nsw i32 %a, %b       ; 循环外的 a*b
  br label %for.cond

for.cond:
  %sum.0 = phi i32 [ 0, %entry ], [ %add2, %for.inc ]
  %i.0 = phi i32 [ 0, %entry ], [ %inc, %for.inc ]
  %x.0 = phi i32 [ %mul, %entry ], [ %mul3, %for.inc ]
  %cmp = icmp slt i32 %i.0, %n
  br i1 %cmp, label %for.body, label %for.end

for.body:
  %mul2 = mul nsw i32 %a, %b      ; ← 还在循环内!
  %add1 = add nsw i32 %sum.0, %i.0
  %add2 = add nsw i32 %add1, %mul2
  %mul3 = mul nsw i32 %a, %b      ; ← 还在循环内!
  br label %for.inc

for.inc:
  %inc = add nsw i32 %i.0, 1
  br label %for.cond

for.end:
  ret i32 %sum.0
}

变化:alloca/load/store 全部消失,出现了 3 个 φ 节点(sum, i, x)。但 %mul2%mul3 仍在循环内——mem2reg 不做优化,只做 SSA 构造。


step2(mem2reg + GVN 后)

define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
  %mul = mul nsw i32 %a, %b       ; 唯一的 a*b 计算
  br label %for.cond

for.cond:
  %sum.0 = phi i32 [ 0, %entry ], [ %add2, %for.inc ]
  %i.0 = phi i32 [ 0, %entry ], [ %inc, %for.inc ]
  %cmp = icmp slt i32 %i.0, %n
  br i1 %cmp, label %for.body, label %for.end

for.body:
  %add1 = add nsw i32 %sum.0, %i.0
  %add2 = add nsw i32 %add1, %mul  ; ← 直接用了 entry 的 %mul!
  br label %for.inc

for.inc:
  %inc = add nsw i32 %i.0, 1
  br label %for.cond

for.end:
  ret i32 %sum.0
}

变化

  • %mul2%mul3 消失了——GVN 发现它们和 entry 中的 %mul 值编号相同
  • x 的 φ 节点也消失了(x 被 GVN 发现没用——只有赋值没有使用 → 后续 DCE 会删)
  • 函数从 5 个基本块减回 4 个

但是 %mul 虽然只在 entry 中计算了一次,IR 上还在循环外,不算"外提"问题——因为它本来就不在循环内。不过 step3 会用 LICM 确保类似的情况也能处理。

GVN 怎么做到的? a*b 的值编号是 VN(mul, VN(a), VN(b))。三处 a*b 的 a 和 b 都来自函数参数(从未被修改),值编号完全相同 → GVN 直接替换为第一个。


step3(mem2reg + GVN + LICM 后)

在这个例子中,step3 和 step2 的输出几乎一样——因为 %mul 本来就不在循环内。LICM 没有额外的工作可做。

但如果把原始代码改成 a*b 只在循环内出现:

// licm_only_test.c —— 循环不变但没有在循环外提前算
int test2(int a, int b, int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += a * b;    // a*b 是循环不变但只在循环内第一次出现
    }
    return sum;
}
clang -S -emit-llvm -O0 licm_only_test.c -o licm_test.ll
opt -passes=mem2reg licm_test.ll -S -o licm_step1.ll
opt -passes=mem2reg,licm licm_test.ll -S -o licm_step2.ll
diff licm_step1.ll licm_step2.ll

LICM 前%mul = mul i32 %a, %bfor.body 中(循环内)
LICM 后%mul 被移到了 for.body.preheader(循环外的专用块),循环内直接用 %mul


最终 O2 IR 对比

clang -S -emit-llvm -O2 pipeline_test.c -o pipeline_O2.ll
define i32 @test(i32 %a, i32 %b, i32 %n) {
entry:
  %cmp4 = icmp sgt i32 %n, 0           ; n > 0?(小于等于则跳过循环)
  br i1 %cmp4, label %for.body.preheader, label %for.end

for.body.preheader:
  %mul = mul nsw i32 %a, %b            ; ← a*b 只在 preheader 中算一次!
  br label %for.body

for.body:
  %i.06 = phi i32 [ %inc, %for.body ], [ 0, %for.body.preheader ]
  %sum.05 = phi i32 [ %add, %for.body ], [ 0, %for.body.preheader ]
  %add1 = add nsw i32 %sum.05, %i.06
  %add = add nsw i32 %add1, %mul       ; ← 直接用 preheader 的结果
  %inc = add nuw nsw i32 %i.06, 1
  %cmp = icmp eq i32 %inc, %n
  br i1 %cmp, label %for.end.loopexit, label %for.body

for.end.loopexit:
  %add.lcssa = phi i32 [ %add, %for.body ]
  br label %for.end

for.end:
  %sum.0.lcssa = phi i32 [ 0, %entry ], [ %add.lcssa, %for.end.loopexit ]
  ret i32 %sum.0.lcssa
}

四个关键优化效果

优化体现
循环不变量外提a*b 从 3 处 → 1 处,且在 preheader(循环外)
公共子表达式消除循环内的两次 a*b 全部被消除
归纳变量优化i++ + i < n 变成了 %inc == %n(用 %inc 做循环出口条件,省了比较和归纳变量的维护)
LCSSA%add.lcssa 是循环闭包 SSA 形式——确保循环内定义的值在循环外通过专用 φ 引用

指令数对比:O0 约 25 条 → O2 约 12 条,省了一半多。


练习 2:写一个分析 Pass(30 分钟)

实现一个 Pass,统计每个函数中"最深的循环嵌套层数":

提示:
- 用 LoopInfo 分析: auto &LI = FAM.getResult<LoopAnalysis>(F);
- LI.getLoopsInPreorder() 获取所有循环
- Loop::getLoopDepth() 获取嵌套深度
- 参考: llvm/include/llvm/Analysis/LoopInfo.h
▼ 练习 2 参考答案——完整 Pass 代码
// MaxLoopDepth.cpp —— 统计函数中最深的循环嵌套层数
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/IR/Function.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
struct MaxLoopDepthPass : public PassInfoMixin<MaxLoopDepthPass> {
  // 需要 LoopInfo 分析结果 → 声明依赖
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) {
    // 获取 LoopInfo 分析结果(FAM 自动管理缓存)
    auto &LI = FAM.getResult<LoopAnalysis>(F);

    int max_depth = 0;
    int loop_count = 0;

    // 遍历所有循环(前序遍历:外层先于内层)
    for (auto *L : LI.getLoopsInPreorder()) {
      loop_count++;
      int depth = L->getLoopDepth();
      if (depth > max_depth)
        max_depth = depth;

      // 额外信息:循环头基本块名、是否有子循环
      errs() << "  循环 at "
             << L->getHeader()->getName()
             << " (深度 " << depth << ")";
      if (!L->getSubLoops().empty())
        errs() << " [含 " << L->getSubLoops().size() << " 个子循环]";
      errs() << "\n";
    }

    errs() << "=== " << F.getName() << " ===\n"
           << "  循环总数: " << loop_count << "\n"
           << "  最大嵌套深度: " << max_depth << "\n\n";

    // 只读 Pass,不修改 IR
    return PreservedAnalyses::all();
  }

  // 声明:这个 Pass 需要 LoopAnalysis 的结果
  // 这样 PassManager 会在运行此 Pass 前确保 LoopInfo 已计算
  static void registerCallbacks() {} // 在注册时用下面的方法
};
}

// ── 注册 ──────────────────────────────────────
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
  return {
    LLVM_PLUGIN_API_VERSION, "MaxLoopDepth", LLVM_VERSION_STRING,
    [](PassBuilder &PB) {
      // 注册为 Function Pass(Pipeline 元素)
      PB.registerPipelineParsingCallback(
        [](StringRef Name, FunctionPassManager &FPM,
           ArrayRef<PassBuilder::PipelineElement>) {
          if (Name == "max-loop-depth") {
            FPM.addPass(MaxLoopDepthPass());
            return true;
          }
          return false;
        });
      // 注册分析依赖:告诉 PassBuilder 我们需要 LoopAnalysis
      PB.registerAnalysisRegistrationCallback(
        [](FunctionAnalysisManager &FAM) {
          FAM.registerPass([&] { return LoopAnalysis(); });
        });
    }
  };
}

编译和使用

# 编译
clang++ -fPIC -shared MaxLoopDepth.cpp -o MaxLoopDepth.so \
  `llvm-config --cxxflags --ldflags --libs core analysis passes support`

# 准备测试用例
cat > nested_loops.c << 'EOF'
void matmul(int n, int m, int p, float A[n][m], float B[m][p], float C[n][p]) {
    for (int i = 0; i < n; i++)           // 深度 1
        for (int j = 0; j < p; j++)       // 深度 2
            for (int k = 0; k < m; k++)   // 深度 3
                C[i][j] += A[i][k] * B[k][j];
}

void simple(int n) {
    for (int i = 0; i < n; i++)           // 深度 1
        n--;
}
EOF

clang -S -emit-llvm -O0 nested_loops.c -o nested_loops.ll
opt -load-pass-plugin=./MaxLoopDepth.so \
    -passes="max-loop-depth" nested_loops.ll -disable-output

输出

  循环 at for.cond (深度 1) [含 1 个子循环]
  循环 at for.cond1 (深度 2) [含 1 个子循环]
  循环 at for.cond4 (深度 3)
=== matmul ===
  循环总数: 3
  最大嵌套深度: 3

  循环 at for.cond (深度 1)
=== simple ===
  循环总数: 1
  最大嵌套深度: 1

关键 API 说明

// 获取 LoopInfo
auto &LI = FAM.getResult<LoopAnalysis>(F);

// 遍历所有循环(前序:外→内)
for (Loop *L : LI.getLoopsInPreorder()) { ... }

// 获取顶层循环(没有外层循环的)
for (Loop *L : LI) { ... }  // LI 本身是一个顶层循环的容器

// 循环信息
L->getHeader();          // 循环头 BasicBlock*
L->getLoopDepth();       // 嵌套深度(顶层=1)
L->getSubLoops();        // 子循环列表
L->getExitingBlock();    // 循环出口块
L->getLoopLatch();       // latch 块
L->getLoopPreheader();   // preheader 块(需要 LoopSimplify 先跑)
L->isLoopSimplifyForm(); // 是否已被规范化

练习 3:用 TableGen 定义一条指令(15 分钟)

创建一个最小的 .td 文件,定义一条自定义指令:

// mini.td —— 最小的 TableGen 文件
def R0 : Register<"r0">;
def GR8 : RegisterClass<"MyTarget", [i8], 8, (add R0)>;

def ADDi8 : Instruction {
  let OutOperandList = (outs GR8:$rd);
  let InOperandList  = (ins GR8:$rs, i8imm:$imm);
  let AsmString      = "add $rd, $rs, $imm";
  let Pattern        = [(set i8:$rd, (add i8:$rs, imm:$imm))];
}

llvm-tblgen 处理:

llvm-tblgen mini.td -print-records
▼ 练习 3 参考答案——运行结果与解读

完整可运行的 mini.td 文件

// mini.td —— 完整的最小 TableGen 文件
// 如果要在 LLVM build 目录下直接运行,需要 include 基础定义

// ---- 第1步:定义寄存器 ----
def R0 : Register<"r0">;
def R1 : Register<"r1">;

// ---- 第2步:定义寄存器类(一类寄存器的集合) ----
// 参数: 目标名称, 可存类型列表, 对齐(字节), 寄存器列表
def GR8 : RegisterClass<"MyTarget", [i8], 8, (add R0, R1)>;

// ---- 第3步:定义指令 ----
def ADDi8 : Instruction {
  let OutOperandList = (outs GR8:$rd);        // 输出:目标寄存器
  let InOperandList  = (ins GR8:$rs, i8imm:$imm); // 输入:源寄存器+8位立即数
  let AsmString      = "add $rd, $rs, $imm";        // 汇编格式
  let Pattern        = [(set i8:$rd, (add i8:$rs, imm:$imm))];  // IR匹配模式
}

// ---- 第4步:再定义一条减法指令,演示比较 ----
def SUBi8 : Instruction {
  let OutOperandList = (outs GR8:$rd);
  let InOperandList  = (ins GR8:$rs, i8imm:$imm);
  let AsmString      = "sub $rd, $rs, $imm";
  let Pattern        = [(set i8:$rd, (sub i8:$rs, imm:$imm))];
}

运行 TableGen

# 最简单的用法:看 TableGen 怎么解析
llvm-tblgen mini.td -print-records

# 如果需要 include 基础定义(大多数实际 .td 文件):
llvm-tblgen mini.td -I /path/to/llvm/include -print-records

-print-records 输出(精简解读)

------------- Classes -----------------
class Instruction {
  ...
}
class Register<string n> {
  string AsmName = n;
  ...
}

------------- Defs -----------------
def ADDi8 {          // Instruction 的实例
  dag OutOperandList = (outs GR8:$rd);
  dag InOperandList = (ins GR8:$rs, i8imm:$imm);
  string AsmString = "add $rd, $rs, $imm";
  dag Pattern = (set (i8 GR8:$rd), (add (i8 GR8:$rs), (imm i8imm:$imm)));
}
def R0 {             // Register 的实例
  string AsmName = "r0";
}
...

TableGen 类型和关键概念解读

TableGen 概念对应的 C++ 含义
dag有向无环图——指令选择的 IR 模式。(set dst, (add src, imm))
bits<n>位字段,用于编码操作码/寄存器号
RegisterClass一组可互换的寄存器——指令的操作数类型
Pattern指令选择器用来匹配 IR 的"模板"。告诉编译器"遇到这个 IR 模式就用这条指令"
Operand操作数类型定义——寄存器、立即数、内存地址等

Pattern 的 DAG 语法详解

let Pattern = [(set i8:$rd, (add i8:$rs, imm:$imm))];
//             └────────┬──────┘
//             这是一个 dag 表达式
//
// 解读:
//   (set 目标, 操作)  — set 是最外层,表示"赋值"操作
//      目标: i8:$rd   — 赋值目标是 $rd(类型 i8)
//      操作: (add i8:$rs, imm:$imm) — IR add 操作,两个操作数是 $rs 和 $imm
//
// 这条 Pattern 匹配的 IR:
//   %rd = add i8 %rs, 立即数
//
// 匹配后,指令选择器输出:
//   add %rd, %rs, <立即数>

生成不同输出

# 生成指令信息(编码、操作数等)
llvm-tblgen mini.td -I llvm/include -gen-instr-info

# 生成寄存器信息
llvm-tblgen mini.td -I llvm/include -gen-register-info

# 生成指令选择匹配表(实际用 TableGen 后端 -gen-dag-isel)
llvm-tblgen mini.td -I llvm/include -gen-dag-isel

# 列出所有可用的生成器
llvm-tblgen -help-list

常见输出

  • -gen-instr-infoMCInstrDesc 数组(指令描述)
  • -gen-register-infoMCRegisterInfo(寄存器信息)
  • -gen-dag-isel → 指令选择匹配表(NDFA 自动机)
  • -gen-asm-matcher → 汇编解析器匹配表
  • -gen-disassembler → 反汇编表
  • -gen-callingconv → 调用约定实现

如果遇到 include 依赖问题,创建一个自给自足的版本:

// standalone.td —— 完全独立的 TableGen 文件,无需 include
// 手动定义所有需要的基类

class Register<string name> {
  string AsmName = name;
}

class RegisterClass<string namespace, list<ValueType> regTypes,
                    int align, dag regList> {
  string Namespace = namespace;
  list<ValueType> RegTypes = regTypes;
  int Alignment = align;
  dag MemberList = regList;
}

class Instruction {
  dag OutOperandList;
  dag InOperandList;
  string AsmString = "";
  dag Pattern;
  bit isTerminator = 0;
  bit isBranch = 0;
}

// 操作符定义(用于 Pattern DAG)
def set;    // set 操作
def add;    // add 操作
def sub;    // sub 操作

// 值类型
def i8  : ValueType<8>;
def i16 : ValueType<16>;
def i32 : ValueType<32>;

// 立即数操作数
def i8imm  : Operand<i8>;
def i16imm : Operand<i16>;

def imm : Operand<i32>;  // 通用立即数(用于匹配模式)

// ---- 自定义目标开始 ----
def R0 : Register<"r0">;
def R1 : Register<"r1">;
def R2 : Register<"r2">;
def R3 : Register<"r3">;

def GR8 : RegisterClass<"MyTarget", [i8], 8, (add R0, R1, R2, R3)>;

def ADDi8 : Instruction {
  let OutOperandList = (outs GR8:$rd);
  let InOperandList  = (ins GR8:$rs, i8imm:$imm);
  let AsmString      = "add $rd, $rs, $imm";
  let Pattern        = [(set i8:$rd, (add i8:$rs, imm:$imm))];
}

这个独立版本可以直接用 llvm-tblgen standalone.td -print-records 运行。


今日小结

上午(龙书理论):
  三地址码: 表达式 AST 后序遍历生成 TAC
  短路求值: true/false 标签 + 回填
  GCSE: 可用表达式分析 → 消除重复计算
  PRE: 部分冗余 → 代码提升 + 插入 + 删除
  循环优化: LICM / 归纳变量 / 展开

下午(LLVM 实战):
  TableGen: 声明式描述目标架构 → 自动生成 C++
  Pass 体系: mem2reg → GVN → LICM → indvars → loop-unroll/vectorize → DCE → simplifycfg
  写 Pass: InstCount(分析)/ AddToSub(变换)
  IRBuilder: 创建/替换/删除指令的 API

关键认知

  1. 优化 = 数据流分析 + 重写规则。每个 Pass 都有明确的数学基础(GVN=可用表达式,LICM=循环不变分析,DCE=活跃变量),不是"聪明地猜到"优化机会。

  2. Pass 管道是分层的。先 mem2reg(建立 SSA)→ 再做 GVN/LICM(依赖 SSA 的简洁性)→最后 DCE(清理)。顺序错了效果就出不来。

  3. 写 Pass 是理解 LLVM 的最佳途径。当你需要遍历 CFG、插入指令、替换 use 时,你自然就深入了 LLVM 的 IR 数据结构。

明天:把这些优化后的 IR 变成真正的机器码——指令选择、寄存器分配、指令调度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值