LLVM 编译器优化核心:SROA 与 mem2reg 深度解析
本文详细总结了 LLVM IR 层面将栈内存变量转化为 SSA(静态单赋值)寄存器形式的两个关键 Pass:SROA (Scalar Replacement of Aggregates) 和 mem2reg (Promote Memory to Register)。
这两个 Pass 是现代编译器优化管道的基石,它们的执行质量直接决定了后续 GVN(全局值编号)、LICM(循环不变量外提)等高级优化的效果。
1. SROA (Scalar Replacement of Aggregates)
中文名称:聚合体标量替换 核心目标:将复杂的聚合类型(结构体、数组)的 alloca 指令,拆解为多个独立的、简单的标量 alloca 指令。
1.1 核心原理:分而治之
编译器难以直接分析整个结构体的数据流,因为结构体的不同字段可能被独立访问。SROA 通过静态分析内存访问模式,将一个大的内存块(Aggregate)切分成若干个小的内存块(Scalar),从而消除聚合体的概念。
1.2 具体实现步骤
SROA 在 LLVM 中的实现主要包含以下四个阶段:
切片分析 (Slicing):
- 遍历目标
alloca指令的所有使用者(Users)。 - 对于每一个
load/store/memset/memcpy,计算其访问的 偏移量 (Offset) 和 长度 (Size)。 - 即使存在指针类型转换(bitcast),只要偏移量是静态可推导的,就将其标记为一个有效的切片。
- 遍历目标
分区规划 (Partitioning):
- 根据收集到的切片信息,在原本的内存区间上规划分区。
- 例如:
struct { int x; int y; },如果代码分别访问 offset 0 和 offset 4,SROA 将规划两个不重叠的分区。
重写指令 (Rewriting):
- 为每个分区创建新的、独立的
alloca指令(例如alloca i32)。 - 修改原本引用大结构体的
load/store指令,使其指向新的小alloca。 - 处理复杂的
memcpy:如果源或目标被拆分,将其展开为一系列对新变量的赋值操作。
- 为每个分区创建新的、独立的
清理 (Cleanup):
- 删除原始的大
alloca指令。此时,代码中只剩下若干个独立的标量alloca。
- 删除原始的大
1.3 效果示例
源代码 (C):
struct Point { int x, y; };
void func() {
struct Point p;
p.x = 10;
p.y = 20;
use(p.x);
}%p = alloca %struct.Point ; 分配整个结构体 %x_ptr = getelementptr %p, 0, 0 store 10, %x_ptr %y_ptr = getelementptr %p, 0, 1 store 20, %y_ptr SROA 处理后: Code snippet%p.x = alloca i32 ; 拆分出的 x %p.y = alloca i32 ; 拆分出的 y store 10, %p.x ; 直接存入新变量 store 20, %p.y ; 直接存入新变量 ; 原本的 %p 结构体消失 ---## 2. mem2reg (Promote Memory to Register)中文名称:内存提升至寄存器核心目标:将栈上的标量变量(alloca)提升为虚拟寄存器,并构建 SSA 形式(插入 Phi 节点)。
2.1 核心原理:构建 SSAClang 前端生成的代码默认将所有局部变量放在栈上(非 SSA)。mem2reg 负责将那些没有逃逸且为第一类类型(First Class Type)的栈变量消除,使其完全在虚拟寄存器的数据流中传递。
2.2 前置条件 (Promotability)只有满足以下条件的 alloca 才能被处理:是第一类类型:如 i32, float, ptr 等。不能是 struct 或 array(这正是 SROA 存在的意义)。未逃逸 (Not Escaped):该变量的地址没有被传递给函数,也没有被存入全局变量或堆中。地址只被用于 load 和 store。
2.3 具体实现步骤支配边界计算 (Dominance Frontier):_ 找到所有对该变量进行 store (定义) 的基本块。_ 计算这些基本块的支配边界(Dominance Frontier)。这是 Phi 节点必须放置的位置。插入 Phi 节点:_ 在支配边界的基本块开头插入 Phi 节点。_ 由于插入 Phi 也是一种定义,该过程可能需要迭代。变量重命名 (Renaming):_ 深度优先遍历支配树 (Dominator Tree)。_ 维护一个栈,记录当前变量在当前路径上的最新值 (Current Definition)。_ 遇到 store val, ptr:更新栈顶为 val,删除 store。_ 遇到 load ptr:将 load 的使用替换为栈顶的值,删除 load。* 遇到 phi:根据前驱块填充 phi 的 incoming values。
2.4 效果示例mem2reg 处理前 (SROA 的输出):Code snippetentry:
%x = alloca i32 store 0, %x br label %loop loop: %val = load %x ... store %new_val, %x br label %loop mem2reg 处理后:Code snippetentry: ; alloca 被删除 ; store 0 被删除 br label %loop loop: ; 插入 Phi 节点合并数据流 %val = phi i32 [ 0, %entry ], [ %new_val, %loop ] ... ; store %new_val 被删除,直接流回 Phi br label %loop