常量替换Pass核心是三步:匹配指令→计算常量→replaceAllUsesWith+eraseFromParent;需满足操作数为ConstantInt/ConstantFP且指令为纯计算类(如Add/Sub/Mul/ICmpInst),替换前须dyn_cast检查类型,替换后use_empty()为真才可eraseFromParent。

直接上手写一个能跑通、能验证、能改的常量替换 Pass,比看十页原理文档更管用。核心就三步:匹配指令 → 计算常量 → replaceAllUsesWith + eraseFromParent。别碰手动改操作数或重建基本块,那是在给崩溃留门。
怎么识别可常量替换的指令
不是所有二元运算都能折,得满足两个硬条件:操作数必须都是 ConstantInt(或 ConstantFP),且指令本身是纯计算类(比如 Add、Sub、Mul)。IRBuilder 在生成时会自动做简单折叠,但跨指令的(如 (1+2)+x)它不管,得靠 Pass 扫出来。
-
dyn_cast<binaryoperator>(I)</binaryoperator>判断是不是加减乘除等二元指令 -
isa<constantint>(LHS) && isa<constantint>(RHS)</constantint></constantint>保证两边都是整型常量;浮点要用isa<constantfp></constantfp> - 注意:
ICmpInst(比较指令)也能常量折叠,但结果是i1常量,别漏掉 - 别用
cast强转,先dyn_cast检查,否则遇到非预期类型直接 crash
替换时为什么必须用 replaceAllUsesWith
LLVM IR 是 SSA 形式,值被“使用”(use)的位置可能散落在多个基本块里,比如 PHINode、其他函数参数、甚至常量表达式(ConstantExpr)。手动遍历 user() 列表改操作数,90% 的概率漏掉 landingpad 或 GEP 里的引用。
-
BO->replaceAllUsesWith(result)会一次性更新所有 use 点,包括嵌套在ConstantExpr里的引用 - 替换后旧指令的
use_empty()返回 true,才能安全调用eraseFromParent() - 顺序不能反:先
replaceAllUsesWith,再eraseFromParent。反过来就是访问已释放内存 - 如果指令有副作用(比如
CallInst或带volatile的Load),不能替换——常量折叠只适用于无副作用纯计算
实际运行 Pass 的最小闭环
光编译 Pass 没用,得让它跑在真实 IR 上。最简路径是:写个 C 函数 → 用 clang-14 -S -emit-llvm 生成 .ll → 用 opt-14 加载你的 Pass。
- 编译 Pass:
clang++ -fPIC -shared -o libConstFold.so MyPass.cpp `llvm-config-14 --cxxflags --ldflags --libs` - 生成测试 IR:
echo "int f() { return 3 + 4; }" | clang-14 -x c -S -emit-llvm -o - - 运行优化:
opt-14 -load-pass-plugin ./libConstFold.so -passes="const-fold" test.ll -S -o out.ll - 检查输出:原 IR 里应有
%add = add i32 3, 4,优化后只剩ret i32 7,且%add消失
最容易被忽略的是:LLVM 的 Pass 注册名("const-fold")必须和 opt 命令里 -passes= 后面的字符串完全一致,大小写、连字符都不能错;另外 runOnFunction 返回 true 表示 IR 被修改,这会触发后续 Pass 重运行,别随手写成 false 就完事。

















