9.5 KiB
记录中端遍的开发进度
| 名称 | 优化级别 | 开发进度 |
|---|---|---|
| CFG优化 | 函数级 | 已完成 |
| DCE | 函数级 | 待正确性测试 |
| Mem2Reg | 函数级 | 待正确性测试 |
| Reg2Mem | 函数级 | 待正确性测试 |
部分优化遍的说明
Mem2Reg
Mem2Reg 遍的主要目标是将那些不必要的、只用于局部标量变量的内存分配 (alloca 指令) 消除,并将这些变量的值转换为 SSA 形式。这有助于减少内存访问,提高代码效率,并为后续的优化创造更好的条件。
Reg2Mem
我们的Reg2Mem 遍的主要目标是作为 Mem2Reg 的一种逆操作,但更具体是解决后端无法识别 PhiInst 指令的问题。主要的速录是将函数参数和 PhiInst 指令的结果从 SSA 形式转换回内存形式,通过插入 alloca、load 和 store 指令来实现。其他非 Phi 的指令结果将保持 SSA 形式。
SCCP
SCCP(稀疏条件常量传播)是一种编译器优化技术,它结合了常量传播和死代码消除。其核心思想是在程序执行过程中,尝试识别并替换那些在编译时就能确定其值的变量(常量),同时移除那些永远不会被执行到的代码块(不可达代码)。
以下是 SCCP 的实现思路:
- 核心数据结构与工作列表:
Lattice 值(Lattice Value): SCCP 使用三值格(Three-Valued Lattice)来表示变量的状态:
Top (T): 初始状态,表示变量的值未知,但可能是一个常量。
Constant (C): 表示变量的值已经确定为一个具体的常量。
Bottom (⊥): 表示变量的值不确定或不是一个常量(例如,它可能在运行时有多个不同的值,或者从内存中加载)。一旦变量状态变为 Bottom,它就不能再变回 Constant 或 Top。
SSAPValue: 封装了 Lattice 值和常量具体值(如果状态是 Constant)。
valState (map<Value, SSAPValue>):* 存储程序中每个 Value(变量、指令结果等)的当前 SCCP Lattice 状态。
ExecutableBlocks (set):* 存储在分析过程中被确定为可执行的基本块。
工作列表 (Worklists):
cfgWorkList (queue<pair<BasicBlock, BasicBlock>>):** 存储待处理的控制流图(CFG)边。当一个块被标记为可执行时,它的后继边会被添加到这个列表。
ssaWorkList (queue):* 存储待处理的 SSA (Static Single Assignment) 指令。当一个指令的任何操作数的状态发生变化时,该指令就会被添加到这个列表,需要重新评估。
- 初始化:
所有 Value 的状态都被初始化为 Top。
所有基本块都被初始化为不可执行。
函数的入口基本块被标记为可执行,并且该块中的所有指令被添加到 ssaWorkList。
- 迭代过程 (Fixed-Point Iteration):
SCCP 的核心是一个迭代过程,它交替处理 CFG 工作列表和 SSA 工作列表,直到达到一个不动点(即没有更多的状态变化)。
处理 cfgWorkList:
从 cfgWorkList 中取出一个边 (prev, next)。
如果 next 块之前是不可执行的,现在通过 prev 块可达,则将其标记为可执行 (markBlockExecutable)。
一旦 next 块变为可执行,其内部的所有指令(特别是 Phi 指令)都需要被重新评估,因此将它们添加到 ssaWorkList。
处理 ssaWorkList:
从 ssaWorkList 中取出一个指令 inst。
重要: 只有当 inst 所在的块是可执行的,才处理该指令。不可执行块中的指令不参与常量传播。
计算新的 Lattice 值 (computeLatticeValue): 根据指令类型和其操作数的当前 Lattice 状态,计算 inst 的新的 Lattice 状态。
常量折叠: 如果所有操作数都是常量,则可以直接执行运算并得到一个新的常量结果。
Bottom 传播: 如果任何操作数是 Bottom,或者运算规则导致不确定(例如除以零),则结果为 Bottom。
Phi 指令的特殊处理: Phi 指令的值取决于其所有可执行的前驱块传入的值。
如果所有可执行前驱都提供了相同的常量 C,则 Phi 结果为 C。
如果有任何可执行前驱提供了 Bottom,或者不同的可执行前驱提供了不同的常量,则 Phi 结果为 Bottom。
如果所有可执行前驱都提供了 Top,则 Phi 结果仍为 Top。
更新状态: 如果 inst 的新计算出的 Lattice 值与它当前存储的值不同,则更新 valState[inst]。
传播变化: 如果 inst 的状态发生变化,那么所有使用 inst 作为操作数的指令都可能受到影响,需要重新评估。因此,将 inst 的所有使用者添加到 ssaWorkList。
处理终结符指令 (BranchInst, ReturnInst):
对于条件分支 BranchInst,如果其条件操作数变为常量:
如果条件为真,则只有真分支的目标块是可达的,将该边添加到 cfgWorkList。
如果条件为假,则只有假分支的目标块是可达的,将该边添加到 cfgWorkList。
如果条件不是常量(Top 或 Bottom),则两个分支都可能被执行,将两边的边都添加到 cfgWorkList。
这会影响 CFG 的可达性分析,可能导致新的块被标记为可执行。
- 应用优化 (Transformation):
当两个工作列表都为空,达到不动点后,程序代码开始进行实际的修改:
常量替换:
遍历所有指令。如果指令的 valState 为 Constant,则用相应的 ConstantValue 替换该指令的所有用途 (replaceAllUsesWith)。
将该指令标记为待删除。
对于指令的操作数,如果其 valState 为 Constant,则直接将操作数替换为对应的 ConstantValue(常量折叠)。
删除死指令: 遍历所有标记为待删除的指令,并从其父基本块中删除它们。
删除不可达基本块: 遍历函数中的所有基本块。如果一个基本块没有被标记为可执行 (ExecutableBlocks 中不存在),则将其从函数中删除。但入口块不能删除。
简化分支指令:
遍历所有可执行的基本块的终结符指令。
对于条件分支 BranchInst,如果其条件操作数在 valState 中是 Constant:
如果条件为真,则将该条件分支替换为一个无条件跳转到真分支目标块的指令。
如果条件为假,则将该条件分支替换为一个无条件跳转到假分支目标块的指令。
更新 CFG,移除不可达的分支边和其前驱信息。
computeLatticeValue 的具体逻辑:
这个函数是 SCCP 的核心逻辑,它定义了如何根据指令类型和操作数的当前 Lattice 状态来计算指令结果的 Lattice 状态。
二元运算 (Add, Sub, Mul, Div, Rem, ICmp, And, Or):
如果任何一个操作数是 Bottom,结果就是 Bottom。
如果任何一个操作数是 Top,结果就是 Top。
如果两个操作数都是 Constant,执行实际的常量运算,结果是一个新的 Constant。
一元运算 (Neg, Not):
如果操作数是 Bottom,结果就是 Bottom。
如果操作数是 Top,结果就是 Top。
如果操作数是 Constant,执行实际的常量运算,结果是一个新的 Constant。
Load 指令: 通常情况下,Load 的结果会被标记为 Bottom,因为内存内容通常在编译时无法确定。但如果加载的是已知的全局常量,可能可以确定。在提供的代码中,它通常返回 Bottom。
Store 指令: Store 不产生值,所以其 SSAPValue 保持 Top 或不关心。
Call 指令: 大多数 Call 指令(尤其是对外部或有副作用的函数)的结果都是 Bottom。对于纯函数,如果所有参数都是常量,理论上可以折叠,但这需要额外的分析。
GetElementPtr (GEP) 指令: GEP 计算内存地址。如果所有索引都是常量,地址本身是常量。但 SCCP 关注的是数据值,因此这里通常返回 Bottom,除非有特定的指针常量跟踪。
Phi 指令: 如上所述,基于所有可执行前驱的传入值进行聚合。
Alloc 指令: Alloc 分配内存,返回一个指针。其内容通常是 Bottom。
Branch 和 Return 指令: 这些是终结符指令,不产生一个可用于其他指令的值,通常 SSAPValue 保持 Top 或不关心。
类型转换 (ZExt, SExt, Trunc, FtoI, ItoF): 如果操作数是 Constant,则执行相应的类型转换,结果仍为 Constant。对于浮点数转换,由于 SSAPValue 的 constantVal 为 int 类型,所以对浮点数的操作会保守地返回 Bottom。
未处理的指令: 默认情况下,任何未明确处理的指令都被保守地假定为产生 Bottom 值。
浮点数处理的注意事项:
在提供的代码中,SSAPValue 的 constantVal 是 int 类型。这使得浮点数常量传播变得复杂。对于浮点数相关的指令(kFAdd, kFMul, kFCmp, kFNeg, kFNot, kItoF, kFtoI 等),如果不能将浮点值准确地存储在 int 中,或者不能可靠地执行浮点运算,那么通常会保守地将结果设置为 Bottom。一个更完善的 SCCP 实现会使用 std::variant<int, float> 或独立的浮点常量存储来处理浮点数。
后续优化可能涉及的改动
1)将所有的alloca集中到entryblock中
好处:优化友好性,方便mem2reg提升 目前没有实现这个机制,如果想要实现首先解决同一函数不同域的同名变量命名区分 需要保证符号表能正确维护域中的局部变量
关于中端优化提升编译器性能的TODO
usedelete_withinstdelte方法
这个方法删除了use关系并移除了指令,逻辑是根据Instruction* inst去find对应的迭代器并erase 有些情况下外部持有迭代器和inst,可以省略find过程