概述
linearize 函数将控制流图(CFG)转换为线性指令序列,这是代码生成的最后一步。
它通过深度优先遍历基本块,正确处理分支和跳转,确保每个基本块只出现一次,并生成可直接汇编的顺序指令列表。
代码
rust
/// `linearize` 函数:将控制流图线性化为指令序列
///
/// 返回值:Vec<RV64Operation> - 线性化的RISC-V指令序列
///
/// 功能:执行深度优先的图遍历,将控制流图转换为线性指令序列
/// 这是代码生成的最后一步,生成可以直接汇编的指令序列
pub fn linearize(self) -> Vec<RV64Operation> {
// 步骤1:初始化数据结构
// placed: 记录已经放置到线性序列中的基本块标签
let mut placed = HashSet::new();
// worklist: 工作队列,用于深度优先遍历控制流图
// 使用VecDeque作为双端队列,支持前端和后端操作
let mut worklist = VecDeque::new();
// 将入口块添加到工作队列
worklist.push_back(self.get_block(self.get_entry()));
// result: 存储最终的线性指令序列
let mut result = Vec::new();
// 步骤2:主循环 - 深度优先遍历控制流图
// 使用while let模式匹配,从工作队列中取出下一个要处理的基本块
while let Some(to_place) = worklist.pop_front() {
// 步骤2.1:检查基本块是否已经处理过
// 避免重复处理同一个基本块(循环控制流)
if placed.contains(&to_place.label) {
continue; // 已处理,跳过
}
// 步骤2.2:为当前基本块插入标签指令
// PSEUD_LABEL是伪指令,表示基本块的标签
result.push(RV64Operation::PSEUD_LABEL(Rc::clone(&to_place.label)));
// 步骤2.3:遍历当前基本块中的所有指令
for op in to_place.body.iter() {
// 根据指令类型进行不同的处理
match op {
// 情况1:处理无条件跳转指令 PSEUD_J
// 格式:PSEUD_J(target_label)
RV64Operation::PSEUD_J(t) => {
// 检查目标标签是否已经放置
if placed.contains(t) {
// 目标已放置:直接添加跳转指令
result.push(op.clone());
} else {
// 目标未放置:将目标块添加到工作队列前端
// 使用前端添加实现深度优先遍历
worklist.push_front(
self.blocks.iter().find(|b| &b.label == t).unwrap(),
);
}
}
// 情况2:处理条件分支指令
// 包括:BEQ(相等分支)、BGE(大于等于分支)、BL(小于分支)
// 格式:BEQ/BGE/BL(reg1, reg2, target_label)
RV64Operation::BEQ(_, _, l)
| RV64Operation::BGE(_, _, l)
| RV64Operation::BL(_, _, l) => {
// 断言:条件分支指令必须有两个后继
debug_assert_eq!(to_place.children.len(), 2);
// 获取false分支(第二个后继)
let r = self.get_block(to_place.children[1]);
// 添加条件分支指令到结果序列
result.push(op.clone());
// 检查false分支是否已经放置
if placed.contains(&r.label) {
// false分支已放置:添加无条件跳转到false分支
result.push(RV64Operation::PSEUD_J(Rc::clone(&r.label)));
} else {
// false分支未放置:将false分支添加到工作队列前端
worklist.push_front(r);
}
// 将true分支(目标标签)添加到工作队列后端
// 使用后端添加,确保true分支在false分支之后处理
worklist.push_back(self.blocks.iter().find(|b| &b.label == l).unwrap());
}
// 情况3:处理其他所有指令
// 包括:算术指令、内存访问指令、伪指令等
op => {
// 直接添加到结果序列
result.push(op.clone());
}
}
}
// 步骤2.4:标记当前基本块为已处理
placed.insert(Rc::clone(&to_place.label));
}
// 步骤3:返回线性化的指令序列
result
}
主要作用
将图状结构线性化 :CFG 中的基本块通过边(分支/跳转)连接,无法直接输出为指令流。linearize 通过遍历将这些块按深度优先顺序排列,使生成的指令序列在逻辑上等价于原控制流。
插入标签 :为每个基本块生成一个标签(PSEUD_LABEL),作为跳转目标
处理跳转:
- 对于无条件跳转(
PSEUD_J):如果目标块尚未放置,则将其加入工作队列前端(深度优先);如果已放置,则直接生成跳转指令。 - 对于条件分支(
BEQ/BGE/BL):先输出分支指令,然后将 false 分支(fall-through 路径)加入队列前端(优先处理),将 true 分支(显式跳转目标)加入队列后端(稍后处理),并在 false 分支已放置时插入一个显式的无条件跳转以维持控制流。
保证单次访问 :使用 placed 集合记录已处理的基本块,避免重复处理(例如循环)。
举例说明
假设有以下简单控制流图(用伪代码表示):
text
BB0:
inst0
beq r1, r2, BB2 ; 如果 r1 == r2,跳转到 BB2,否则执行 BB1
BB1:
inst1
j BB3
BB2:
inst2
j BB3
BB3:
inst3
ret
执行过程
- 初始化:工作队列 worklist 包含入口块 BB0,placed 为空。
- 处理 BB0 :
- 输出 PSEUD_LABEL BB0。
- 处理 inst0,直接输出。
- 遇到条件分支 beq r1, r2, BB2:
- 输出该分支指令。
- false 分支为 BB1(children[1]),未放置 → 将 BB1 加入队列前端。
- true 分支 BB2 未放置 → 将 BB2 加入队列后端。
- 标记 BB0 为已放置。此时工作队列:[BB1, BB2]。
- 标记 BB0 为已放置。
- 此时工作队列:[BB1, BB2]。
- 处理 BB1 (从队列前端取出):
- 输出 PSEUD_LABEL BB1。
- 处理 inst1,直接输出。
- 遇到无条件跳转 j BB3:
- 检查 BB3 是否已放置(尚未)→ 将 BB3 加入队列前端。
- 标记 BB1 已放置。
- 工作队列:[BB3, BB2]。
- 处理 BB3 :
- 输出 PSEUD_LABEL BB3。
- 处理 inst3 和 ret,直接输出。
- 标记 BB3 已放置。工作队列:[BB2]。
- 处理 BB2 :
- 输出 PSEUD_LABEL BB2。
- 处理 inst2,直接输出。
- 遇到无条件跳转 j BB3:
- BB3 已放置 → 直接输出该跳转指令。
- 标记 BB2 已放置。工作队列为空,结束。
最终线性指令序列
text
PSEUD_LABEL BB0
inst0
beq r1, r2, BB2
PSEUD_LABEL BB1
inst1
j BB3
PSEUD_LABEL BB3
inst3
ret
PSEUD_LABEL BB2
inst2
j BB3
该序列保持了与原控制流图相同的语义:
- 从
BB0开始,根据条件分支进入BB1或BB2 BB1和BB2最终都跳转到BB3执行ret
为什么使用深度优先策略?
局部性:深度优先遍历倾向于将基本块按执行路径紧密排列,有利于指令缓存。
简单性:利用双端队列,可以自然地实现分支路径的优先处理(将 fall-through 路径优先输出,减少跳转)。
该函数是编译器后端从中间表示到最终汇编输出的关键桥梁,确保生成的机器码可以正确执行。