Files
mysysy/Pass_ID_List.md

20 KiB
Raw Permalink Blame History

记录中端遍的开发进度

名称 优化级别 开发进度
CFG优化 函数级 已完成
DCE 函数级 待正确性测试
Mem2Reg 函数级 待正确性测试
Reg2Mem 函数级 待正确性测试

部分优化遍的说明

Mem2Reg

Mem2Reg 遍的主要目标是将那些不必要的、只用于局部标量变量的内存分配 (alloca 指令) 消除,并将这些变量的值转换为 SSA 形式。这有助于减少内存访问,提高代码效率,并为后续的优化创造更好的条件。

通过Mem2Reg理解删除指令时对use关系的维护

Mem2Reg 优化遍中,当 loadstore 指令被删除时,其 use 关系(即它们作为操作数与其他 Value 对象之间的连接)的正确消除是一个关键问题,尤其涉及到 AllocaInst

结合您提供的 Mem2RegContext::renameVariables 代码和我们之前讨论的 usedelete 逻辑,下面是 use 关系如何被正确消除的详细过程:

问题回顾:Use 关系的双向性

在您的 IR 设计中,Use 对象扮演着连接 User(使用者,如 LoadInst)和 Value(被使用者,如 AllocaInst)的双向角色:

  • 一个 User 持有对其操作数 ValueUse 对象(通过 User::operands 列表)。
  • 一个 Value 持有所有使用它的 UserUse 对象(通过 Value::uses 列表)。

原始问题是:当一个 LoadInstStoreInst 被删除时,如果不对其作为操作数与 AllocaInst 之间的 Use 关系进行明确清理,AllocaInstuses 列表中就会留下指向已删除 LoadInst / StoreInstUse 对象,导致内部的 User* 指针悬空,在后续访问时引发 segmentation fault

Mem2Regload/store 指令的删除行为

Mem2RegContext::renameVariables 函数中,loadstore 指令被处理时,其行为如下:

  1. 处理 LoadInst 当找到一个指向可提升 AllocaInstLoadInst 时,其用途会被 replaceAllUsesWith(allocaToValueStackMap[alloca].top()) 替换。这意味着任何原本使用 LoadInst 本身计算结果的指令,现在都直接使用 SSA 值栈顶部的 Value重点: 这一步处理的是 LoadInst 作为被使用的值 (Value) 时,其 uses 列表的清理。即,将 LoadInst 的所有使用者重定向到新的 SSA 值,并把这些 Use 对象从 LoadInstuses 列表中移除。

  2. 处理 StoreInst 当找到一个指向可提升 AllocaInstStoreInst 时,StoreInst 存储的值会被压入值栈。StoreInst 本身并不产生可被其他指令直接使用的值(其类型是 void),所以它没有 uses 列表需要替换。 重点: StoreInst 的主要作用是更新内存状态,在 SSA 形式下,它被移除后需要清理它作为使用者 (User) 时的操作数关系。

在这两种情况下,一旦 loadstore 指令的 SSA 转换完成,它们都会通过 instIter = SysYIROptUtils::usedelete(instIter) 被显式删除。

SysYIROptUtils::usedelete 如何正确消除 Use 关系

关键在于对 SysYIROptUtils::usedelete 函数的修改,使其在删除指令时,同时处理该指令作为 UserValue 的两种 Use 关系:

  1. 清理指令作为 Value 时的 uses 列表 (由 replaceAllUsesWith 完成)usedelete 函数中,inst->replaceAllUsesWith(UndefinedValue::get(inst->getType())) 的调用至关重要。这确保了:

    • 如果被删除的 Instruction(例如 LoadInst)产生了结果值并被其他指令使用,所有这些使用者都会被重定向到 UndefinedValue(或者 Mem2Reg 中具体的 SSA 值)。
    • 这个过程会遍历 LoadInstuses 列表,并将这些 Use 对象从 LoadInstuses 列表中移除。这意味着 LoadInst 自己不再被任何其他指令使用。
  2. 清理指令作为 User 时其操作数的 uses 列表 (由 RemoveUserOperandUses 完成) 这是您提出的、并已集成到 usedelete 中的关键改进点。对于一个被删除的 Instruction(它同时也是 User),我们需要清理它自己使用的操作数所维护的 use 关系。

    • 例如,LoadInst %op1 使用了 %op1(一个 AllocaInst)。当 LoadInst 被删除时,AllocaInstuses 列表中有一个 Use 对象指向这个 LoadInst
    • RemoveUserOperandUses 函数会遍历被删除 User(即 LoadInstStoreInst)的 operands 列表。
    • 对于 operands 列表中的每个 std::shared_ptr<Use> use_ptr,它会获取 Use 对象内部指向的 Value(例如 AllocaInst*),然后调用 value->removeUse(use_ptr)
    • 这个 removeUse 调用会负责将 use_ptrAllocaInstuses 列表中删除。

总结

通过在 SysYIROptUtils::usedelete 中同时执行这两个步骤:

  • replaceAllUsesWith:处理被删除指令作为结果被使用时的 use 关系。
  • RemoveUserOperandUses:处理被删除指令作为使用者User其操作数use 关系。

这就确保了当 Mem2Reg 遍历并删除 loadstore 指令时,无论是它们作为 Value 的使用者,还是它们作为 User 的操作数,所有相关的 Use 对象都能被正确地从 Valueuses 列表中移除,从而避免了悬空指针和后续的 segmentation fault

最后,当所有指向某个 AllocaInstloadstore 指令都被移除后,AllocaInstuses 列表将变得干净(只包含 Phi 指令,如果它们在 SSA 转换中需要保留 Alloca 作为操作数),这时在 Mem2RegContext::cleanup() 阶段,SysYIROptUtils::usedelete(alloca) 就可以安全地删除 AllocaInst 本身了。

Reg2Mem

我们的Reg2Mem 遍的主要目标是作为 Mem2Reg 的一种逆操作,但更具体是解决后端无法识别 PhiInst 指令的问题。主要的速录是将函数参数和 PhiInst 指令的结果从 SSA 形式转换回内存形式,通过插入 alloca、load 和 store 指令来实现。其他非 Phi 的指令结果将保持 SSA 形式。

SCCP

SCCP稀疏条件常量传播是一种编译器优化技术它结合了常量传播和死代码消除。其核心思想是在程序执行过程中尝试识别并替换那些在编译时就能确定其值的变量常量同时移除那些永远不会被执行到的代码块不可达代码

以下是 SCCP 的实现思路:

  1. 核心数据结构与工作列表:

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) 指令。当一个指令的任何操作数的状态发生变化时,该指令就会被添加到这个列表,需要重新评估。

  1. 初始化:

所有 Value 的状态都被初始化为 Top。

所有基本块都被初始化为不可执行。

函数的入口基本块被标记为可执行,并且该块中的所有指令被添加到 ssaWorkList。

  1. 迭代过程 (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 的可达性分析,可能导致新的块被标记为可执行。

  1. 应用优化 (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> 或独立的浮点常量存储来处理浮点数。

LoopSR循环归纳变量强度削弱 关于魔数计算的说明

魔数除法的核心思想是:将除法转换为乘法和移位

数学原理x / d ≈ (x * m) >> (32 + s)

m 是魔数 (magic number) s 是额外的移位量 (shift)

是算术右移

2^(32+s) / d ≤ m < 2^(32+s) / d + 2^s / d

cd /home/downright/Compiler_Opt/mysysy && python3 -c "

真正的迭代原因:精度要求

def explain_precision_requirement(): d = 10

print('魔数算法需要找到精确的边界值:')
print('目标2^p > d * (2^31 - r)其中r是余数')
print()

# 模拟我们算法的迭代过程
p = 31
two_p = 2**p
r = two_p % d  # 余数
m = two_p // d  # 商

print(f'初始状态 (p={p}):')
print(f'  2^{p} = {two_p:,}')
print(f'  2^{p} / {d} = {m:,} 余 {r}')
print(f'  需要检查: 2^{p} > {d} * (2^31 - {r}) = {d * (2**31 - r):,}')
print(f'  条件满足: {two_p > d * (2**31 - r)}')
print()

# 继续迭代直到条件满足
iteration = 0
while r != 0 and m < 2**31:
    iteration += 1
    p += 1
    two_p *= 2
    m *= 2
    r *= 2
    if r >= d:
        m += 1
        r -= d
        
    print(f'迭代 {iteration} (p={p}):')
    print(f'  m = {m:,}, r = {r}')
    print(f'  m < 2^31? {m < 2**31} (2^31 = {2**31:,})')
    
    if m >= 2**31:
        print(f'  ✗ 魔数 {m:,} 超出32位范围!')
        break
    elif r == 0:
        print(f'  ✓ 余数为0找到精确解')
        break
    
    if iteration > 10:  # 防止无限循环
        print('  ... (限制迭代次数)')
        break

explain_precision_requirement() "

问题在于精度要求。理论魔数需要非常精确,不能简单地用 2^32 / d。让我展示真正的迭代原因

cd /home/downright/Compiler_Opt/mysysy && python3 -c "

真正的迭代原因:精度要求

def explain_precision_requirement(): d = 10

print('魔数算法需要找到精确的边界值:')
print('目标2^p > d * (2^31 - r)其中r是余数')
print()

# 模拟我们算法的迭代过程
p = 31
two_p = 2**p
r = two_p % d  # 余数
m = two_p // d  # 商

print(f'初始状态 (p={p}):')
print(f'  2^{p} = {two_p:,}')
print(f'  2^{p} / {d} = {m:,} 余 {r}')
print(f'  需要检查: 2^{p} > {d} * (2^31 - {r}) = {d * (2**31 - r):,}')
print(f'  条件满足: {two_p > d * (2**31 - r)}')
print()

# 继续迭代直到条件满足
iteration = 0
while r != 0 and m < 2**31:
    iteration += 1
    p += 1
    two_p *= 2
    m *= 2
    r *= 2
    if r >= d:
        m += 1
        r -= d
        
    print(f'迭代 {iteration} (p={p}):')
    print(f'  m = {m:,}, r = {r}')
    print(f'  m < 2^31? {m < 2**31} (2^31 = {2**31:,})')
    
    if m >= 2**31:
        print(f'  ✗ 魔数 {m:,} 超出32位范围!')
        break
    elif r == 0:
        print(f'  ✓ 余数为0找到精确解')
        break
    
    if iteration > 10:  # 防止无限循环
        print('  ... (限制迭代次数)')
        break

explain_precision_requirement() "

cd /home/downright/Compiler_Opt/mysysy && python3 -c "

检查初始状态是否会溢出

def check_initial_overflow(): int32_max = 2**31 - 1

print('检查不同除数的初始魔数是否会溢出:')
print(f'32位有符号整数最大值: {int32_max:,}')
print()

# 检查各种除数
divisors = [2, 3, 5, 7, 9, 10, 11, 13, 17, 25, 100, 1000]

for d in divisors:
    # 初始魔数 = 2^31 / d
    initial_magic = (2**31) // d
    
    overflow = initial_magic > int32_max
    status = \"溢出\" if overflow else \"安全\"
    
    print(f'd={d:4d}: 初始魔数 = 2^31/{d} = {initial_magic:10,} [{status}]')
    
print()
print('结论: 初始状态下魔数不会溢出,溢出发生在迭代过程中')

check_initial_overflow() "

总结 迭代的必要性:

不是为了避免初始溢出(初始状态安全) 是为了找到最精确的魔数,减少舍入误差 每次迭代提高一倍精度,但魔数也翻倍 溢出发生时机:

初始状态2^31 / d 总是在32位范围内 迭代过程2^32 / d, 2^33 / d, ... 逐渐超出32位范围 回退值的正确性:

回退值是基于数学理论和实践验证的标准值 来自LLVM、GCC等成熟编译器的实现 通过测试验证,对各种输入都能产生正确结果 算法设计哲学:

先尝试最优解:通过迭代寻找最精确的魔数 检测边界条件当超出32位范围时及时发现 智能回退:使用已验证的标准值保证正确性 保持通用性:对于没有预设值的除数仍然可以工作

死归纳变量消除

整体架构和工作流程 当前的归纳变量消除优化分为三个清晰的阶段:

识别阶段:找出所有潜在的死归纳变量 安全性分析阶段:验证每个变量消除的安全性 消除执行阶段:实际删除安全的死归纳变量

逃逸点检测 (已修复的关键安全机制) 数组索引检测GEP指令被正确识别为逃逸点 循环退出条件:用于比较和条件分支的归纳变量不会被消除 控制流指令condBr、br、return等被特殊处理为逃逸点 内存操作store/load指令经过别名分析检查

后续优化可能涉及的改动

1将所有的alloca集中到entryblock中已实现

好处优化友好性方便mem2reg提升 目前没有实现这个机制,如果想要实现首先解决同一函数不同域的同名变量命名区分 需要保证符号表能正确维护域中的局部变量

关于中端优化提升编译器性能的TODO

usedelete_withinstdelte方法

这个方法删除了use关系并移除了指令逻辑是根据Instruction* inst去find对应的迭代器并erase 有些情况下外部持有迭代器和inst,可以省略find过程