修复了ir-opt的编译不通过、用例不通过等问题

develop^2
Junhe Wu 1 week ago
parent 8b6ad43df0
commit 75c65e4728

@ -0,0 +1,89 @@
# Fix: IR O1 优化导致 09_BFS/10_DFS 输出错误及编译器崩溃
**日期**: 2026-05-19
**分支**: fix/ir-opt
**测试结果**: O1 前 422 项测试全部通过(后续 dead-code-elimination 性能问题为已存在缺陷)
## 问题描述
运行 `bash ./scripts/run_ir_test.sh --O1 --run` 时:
1. 编译器在生成 `testdata/h_functional/10_DFS.sy` 的 IR 时崩溃/卡死
2. `testdata/h_functional/09_BFS.sy` 的输出结果不正确(全为 0期望有 1
3. `testdata/h_functional/10_DFS.sy` 的可执行文件运行时死循环
## 根因分析
共发现 4 个缺陷:
### 缺陷 1 (关键): ConstProp 删除指令前未清理操作数引用
`src/ir/passes/ConstProp.cpp` 中 ZExt/SIToFP/FPToSI 折叠后直接调用 `RemoveFromParent()` 销毁指令,未先调用 `SetOperand(i, nullptr)` 断开 use-def 链。导致指令操作数的 `uses_` 列表中残留指向已释放内存的悬空指针,后续 `ReplaceAllUsesWith` 遍历 use 列表时访问已释放内存造成崩溃UB
### 缺陷 2 (关键): ConstFold 的 `removed` 集合存储悬空指针
`src/ir/passes/ConstFold.cpp``DetachAndRemove` 销毁指令后将其地址存入 `std::unordered_set<void*> removed`。若新指令被分配到同一地址,会被错误跳过,导致漏优化。
### 缺陷 3 (关键): CSE 跨 Store/Call 指令错误消除 Load
`src/ir/passes/CSE.cpp` 的局部值编号不感知内存副作用:`Store`/`Call` 指令可能修改全局变量,但 `available` 映射中缓存的 `Load` 结果不会被失效。例如 `pop_queue()` 中:
```c
h = h + 1; // store @h
int res = que[h]; // load @h 应读到新值,但 CSE 用 store 前的旧值替代
return que[h]; // 同上
```
导致 `que[old_h]` 被返回而非 `que[new_h]`BFS 路径搜索失败。
### 缺陷 4: SanitizePhis 使用错误的 undef 类型
`src/ir/passes/PassManager.cpp``SanitizePhis` 固定使用 `ctx.GetConstInt(0)` 作为浮点 PHI 节点的 undef 值,造成 LLVM IR 类型不匹配。同时修复无效 incoming 时过于激进——value 和 block 只有一个为 null 时却同时替换两者。
## 修改内容
### 修改 1: ConstProp.cpp — 移除冗余折叠,委托给 ConstFold
ConstFold 已使用 `DetachAndRemove`(先清操作数再删除)正确处理 ZExt/SIToFP/FPToSI 折叠。移除 ConstProp 中的重复实现,改为直接返回 false
```cpp
bool RunConstProp(Function& func, Context& ctx) {
(void)func; (void)ctx;
return false;
}
```
### 修改 2: ConstFold.cpp — 用 GetParent() 检查替代 removed 集合
```cpp
// Before:
std::unordered_set<void*> removed;
if (removed.count(inst)) continue;
removed.insert(inst);
// After:
if (inst->GetParent() == nullptr) continue;
```
### 修改 3: CSE.cpp — Store/Call 指令清空 available 映射
```cpp
// 新增: Store/Call 可能修改内存,保守清空 available 表
if (ip->GetOpcode() == Opcode::Store ||
ip->GetOpcode() == Opcode::Call) {
available.clear();
continue;
}
```
### 修改 4: PassManager.cpp — SanitizePhis 类型感知 + 精准修复
```cpp
// 根据 PHI 类型选择 undef 值
Value* undef_val = phi->GetType()->IsFloat32()
? static_cast<Value*>(ctx.GetConstFloat(0.0f))
: static_cast<Value*>(ctx.GetConstInt(0));
// 仅修复确实为 null 的操作数
if (!inc_val) phi->SetOperand(i * 2, undef_val);
if (!inc_bb) phi->SetOperand(i * 2 + 1, entry);
```
## 效果
- 09_BFS/10_DFS 全部通过 O1 测试
- 编译器不再崩溃或卡死
- O1 生成的 IR 通过 llc 验证
- 前 422 项测试 0 失败
## 已知遗留问题
dead-code-elimination-1.sy100,000 个局部变量的压力测试)在 O1 下编译极慢(~940s根因是 Mem2Reg 中 `BasicBlock::RemoveInstruction` 的 O(n) 线性查找被调用 O(n) 次,整体 O(n²)。此问题在本次修复前即存在(之前被 10_DFS 卡死掩盖),留待后续修复。

@ -0,0 +1,80 @@
# Fix: Mem2Reg 中 RemoveInstruction 的 O(n²) 性能问题导致 dead-code-elimination 编译极慢
**日期**: 2026-05-19
**分支**: fix/ir-opt
**测试结果**: 全部 737 项测试通过dead-code-elimination-1/2/3 编译时间从 ~940s 降至 ~2-3s
## 问题描述
`dead-code-elimination-1/2/3.sy`(三个完全相同的 100,000 局部变量压力测试仅输入规模不同163/16384/1638400在 O1 下编译极慢(~940s远超合理范围。
## 根因分析
`BasicBlock::RemoveInstruction``std::vector<std::unique_ptr<Instruction>>` 做指针线性扫描获取迭代器,再执行 `vector::erase`(需要移动后续所有元素),每次调用均为 O(n),其中 n 为基本块内指令数。
Mem2Reg 提升 SSA 后需删除所有已提升的 load、store 和 alloca 指令。在 100K 变量的压力测试中,所有指令集中在单个入口基本块内(~300K 条指令Mem2Reg 对每条指令调用一次 `RemoveFromParent()``RemoveInstruction()`
- 调用次数:~300K 次
- 每次平均扫描:~150K 个元素
- 总比较次数:~45B 次
- 每次 erase 还需移动平均 150K 个 `unique_ptr`
## 修改内容
### 修改 1: IR.h — 新增 `SweepDeadInstructions()` 声明和 `dead_instructions_` 成员
```cpp
// public 区域InsertBefore 之后):
void SweepDeadInstructions();
// private 区域instructions_ 之后):
std::unordered_set<Instruction*> dead_instructions_;
```
### 修改 2: BasicBlock.cpp — RemoveInstruction 改为 O(1) 惰性标记
```cpp
// Before:
void BasicBlock::RemoveInstruction(Instruction* inst) {
for (auto it = instructions_.begin(); it != instructions_.end(); ++it) {
if (it->get() == inst) {
instructions_.erase(it);
return;
}
}
}
// After: O(1) 标记 + 惰性清除
void BasicBlock::RemoveInstruction(Instruction* inst) {
dead_instructions_.insert(inst);
inst->SetParent(nullptr);
}
void BasicBlock::SweepDeadInstructions() {
if (dead_instructions_.empty()) return;
auto it = std::remove_if(instructions_.begin(), instructions_.end(),
[this](const std::unique_ptr<Instruction>& p) {
return dead_instructions_.count(p.get()) > 0;
});
instructions_.erase(it, instructions_.end());
dead_instructions_.clear();
}
```
### 修改 3-6: 各 pass 在删除循环后调用 SweepDeadInstructions
- **Mem2Reg.cpp**: 删除所有 load/store/alloca 后遍历所有 block 调用 `SweepDeadInstructions()`**主要修复点**
- **DCE.cpp**: 每个 block 的 `to_remove` 循环后调用 `SweepDeadInstructions()`
- **CSE.cpp**: 每个 block 的 `to_remove` 循环后调用 `SweepDeadInstructions()`
- **ConstFold.cpp**: 每个 block 的折叠循环后调用 `SweepDeadInstructions()`
## 效果
| 测试用例 | 修复前 | 修复后 |
|----------|--------|--------|
| dead-code-elimination-1 | ~940s | ~2s |
| dead-code-elimination-2 | ~940s | ~3s |
| dead-code-elimination-3 | ~940s | ~3s |
| 全部 737 项测试 | — | 0 失败 |
约 300-400x 加速。三个用例输出与预期完全一致。

@ -410,6 +410,8 @@ class BasicBlock : public Value {
void RemoveInstruction(Instruction* inst);
// 在 before 之前插入指令before 为 nullptr 时追加到末尾
void InsertBefore(Instruction* inst, Instruction* before);
// 批量清除已标记的 dead 指令(单次 O(n) sweep
void SweepDeadInstructions();
template <typename T, typename... Args>
T* Append(Args&&... args) {
@ -449,6 +451,7 @@ class BasicBlock : public Value {
private:
Function* parent_ = nullptr;
std::vector<std::unique_ptr<Instruction>> instructions_;
std::unordered_set<Instruction*> dead_instructions_;
std::vector<BasicBlock*> predecessors_;
std::vector<BasicBlock*> successors_;
};

@ -62,12 +62,19 @@ void BasicBlock::AddSuccessor(BasicBlock* bb) {
void BasicBlock::ClearSuccessors() { successors_.clear(); }
void BasicBlock::RemoveInstruction(Instruction* inst) {
for (auto it = instructions_.begin(); it != instructions_.end(); ++it) {
if (it->get() == inst) {
instructions_.erase(it);
return;
}
}
dead_instructions_.insert(inst);
inst->SetParent(nullptr);
}
void BasicBlock::SweepDeadInstructions() {
if (dead_instructions_.empty()) return;
auto it = std::remove_if(instructions_.begin(), instructions_.end(),
[this](const std::unique_ptr<Instruction>& p) {
return dead_instructions_.count(p.get()) > 0;
});
instructions_.erase(it, instructions_.end());
dead_instructions_.clear();
}
void BasicBlock::InsertBefore(Instruction* inst, Instruction* before) {

@ -92,13 +92,18 @@ bool RunCSE(Function& func) {
for (auto& inst : bb->GetInstructions()) {
auto* ip = inst.get();
std::string key = MakeKey(ip);
if (key.empty()) {
// 不可消除的指令:如果它有结果,可以考虑将其加入可用集
// 但为了简单,这里不处理
// Store/Call 指令可能修改内存,使之前缓存的 Load/Gep 结果失效。
// 保守地清空整个 available 表,避免跨副作用的错误 CSE。
if (ip->GetOpcode() == Opcode::Store ||
ip->GetOpcode() == Opcode::Call) {
available.clear();
continue;
}
std::string key = MakeKey(ip);
if (key.empty()) continue;
auto it = available.find(key);
if (it != available.end()) {
// 找到已有的等价指令,替换使用
@ -115,6 +120,7 @@ bool RunCSE(Function& func) {
ip->SetOperand(i, nullptr);
ip->RemoveFromParent();
}
bb->SweepDeadInstructions();
}
return changed;

@ -135,7 +135,6 @@ bool FoldBinaryWithCtx(BinaryInst* bin, Context& ctx) {
bool RunConstFold(Function& func, Context& ctx) {
bool changed = false;
std::unordered_set<void*> removed;
bool any_changed = true;
while (any_changed) {
@ -147,7 +146,7 @@ bool RunConstFold(Function& func, Context& ctx) {
insts.push_back(inst.get());
for (auto* inst : insts) {
if (removed.count(inst)) continue;
if (inst->GetParent() == nullptr) continue;
bool folded = false;
switch (inst->GetOpcode()) {
case Opcode::Add: case Opcode::Sub: case Opcode::Mul:
@ -174,11 +173,11 @@ bool RunConstFold(Function& func, Context& ctx) {
default: break;
}
if (folded) {
removed.insert(inst);
any_changed = true;
changed = true;
}
}
bb->SweepDeadInstructions();
}
}
return changed;

@ -11,53 +11,12 @@
namespace ir {
bool RunConstProp(Function& func, Context& ctx) {
bool changed = false;
for (auto& bb : func.GetBlocks()) {
std::vector<Instruction*> insts;
for (auto& inst : bb->GetInstructions())
insts.push_back(inst.get());
for (auto* inst : insts) {
if (inst->GetParent() == nullptr) continue;
// 检查是否为"复制"类指令:直接将一个操作数作为结果传播
// 实际上常量传播由 ConstFold 配合 use-def 链完成
// 这里处理简单的常量替换
switch (inst->GetOpcode()) {
case Opcode::ZExt: {
auto* ze = static_cast<ZExtInst*>(inst);
if (auto* ci = dynamic_cast<ConstantInt*>(ze->GetSrc())) {
ze->ReplaceAllUsesWith(ctx.GetConstInt(ci->GetValue() != 0 ? 1 : 0));
ze->RemoveFromParent();
changed = true;
}
break;
}
case Opcode::SIToFP: {
auto* si = static_cast<SIToFPInst*>(inst);
if (auto* ci = dynamic_cast<ConstantInt*>(si->GetSrc())) {
si->ReplaceAllUsesWith(
ctx.GetConstFloat(static_cast<float>(ci->GetValue())));
si->RemoveFromParent();
changed = true;
}
break;
}
case Opcode::FPToSI: {
auto* fp = static_cast<FPToSIInst*>(inst);
if (auto* cf = dynamic_cast<ConstantFloat*>(fp->GetSrc())) {
fp->ReplaceAllUsesWith(
ctx.GetConstInt(static_cast<int>(cf->GetValue())));
fp->RemoveFromParent();
changed = true;
}
break;
}
default: break;
}
}
}
return changed;
// ZExt/SIToFP/FPToSI 常量折叠已在 ConstFold 中统一处理(使用
// DetachAndRemove 安全断开 use-def 链后再删除指令),此处不再重复处理,
// 否则 RemoveFromParent 直接销毁指令会导致其操作数的 uses_ 列表残留悬空指针。
(void)func;
(void)ctx;
return false;
}
} // namespace ir

@ -60,6 +60,7 @@ bool RunDCE(Function& func) {
ip->RemoveFromParent();
changed = true;
}
bb->SweepDeadInstructions();
}
return changed;

@ -184,6 +184,11 @@ bool RunMem2Reg(Function& func, Context& ctx) {
info.alloca->RemoveFromParent();
}
// 批量清除已标记的指令(一次 O(n) sweep 替代每次 O(n) 删除)
for (auto& bb : func.GetBlocks()) {
bb->SweepDeadInstructions();
}
return true;
}

@ -31,9 +31,13 @@ static void SanitizePhis(Function& func, Context& ctx) {
if (phis.empty()) continue;
auto& preds = bb->GetPredecessors();
Value* undef_val = ctx.GetConstInt(0);
for (auto* phi : phis) {
// 根据 PHI 节点的类型选择正确的 undef 值
Value* undef_val = phi->GetType()->IsFloat32()
? static_cast<Value*>(ctx.GetConstFloat(0.0f))
: static_cast<Value*>(ctx.GetConstInt(0));
// 收集已有的 incoming 块
std::unordered_set<BasicBlock*> existing;
for (size_t i = 0; i < phi->GetNumIncoming(); ++i) {
@ -47,14 +51,12 @@ static void SanitizePhis(Function& func, Context& ctx) {
}
}
// 修复无效的 incoming
// 修复无效的 incoming:仅修复确实为 null 的操作数
for (size_t i = 0; i < phi->GetNumIncoming(); ++i) {
auto* inc_val = phi->GetIncomingValue(i);
auto* inc_bb = phi->GetIncomingBlock(i);
if (!inc_val || !inc_bb) {
phi->SetOperand(i * 2, undef_val);
phi->SetOperand(i * 2 + 1, entry);
}
if (!inc_val) phi->SetOperand(i * 2, undef_val);
if (!inc_bb) phi->SetOperand(i * 2 + 1, entry);
}
}
}

Loading…
Cancel
Save