instcombine是一个llvm标量优化pass,不仅包含常量折叠,还执行指令简化、冗余消除、比较化简等局部等价变换;它不跨基本块,依赖ir已生成结构,在optnone禁用时需显式启用。

InstCombine 本身包含常量折叠,但不止于常量折叠
instcombine 是一个 LLVM Scalar Pass,作用是在指令级别做“局部等价变换”,目标是简化 IR、暴露更多优化机会。常量折叠(constant folding)只是它做的其中一类操作——而且是最常见、最容易被注意到的一类。
- 常量折叠:当所有操作数都是常量时,直接算出结果,替换成
const指令,比如add i32 500, 100→600 -
instcombine还会做:消除冗余转换(zext (trunc x)→x)、合并相邻运算(add (mul a, 2), a→mul a, 3)、化简比较(icmp eq x, x→true)、移除无用的 phi 节点等 - 它不跨基本块,也不做控制流分析,只看单条指令及其直接操作数
为什么不能只靠 IRBuilder 的常量折叠?
LLVM IR 构建器(IRBuilder)在你调用 Builder.CreateAdd(const1, const2) 时,当场就做了常量折叠,返回的是 ConstantInt,而不是一条 add 指令。
但现实代码里,常量往往不是“裸”出现的:
- 来自宏展开或模板实例化后残留的死计算(如
200 + x + y + (100 * (1+1))) - 经过其他 Pass 处理后新生成的表达式(比如 loop-unroll 后重复出现的算式)
- 前端未完全折叠(Clang 在
-O0下默认加optnone,禁用所有优化,包括IRBuilder的部分折叠逻辑)
这时候 instcombine 就成了“兜底清理员”:它扫描已生成的 IR,把那些本该早被折叠、却因各种原因漏掉的常量表达式再扫一遍。
实际运行中容易忽略的关键点
-
instcombine不处理带副作用的指令(如call、load volatile),也不会动全局变量初始化值 - 它对
phi节点和控制流敏感指令(br,switch)基本不碰,这类需交给GVN或loop-simplify - 如果 IR 带
optnone属性(比如 Clang 默认-O0输出),instcombine会被跳过——必须显式用-Xclang -disable-O0-optnone或改用-O1及以上 - 它的输出可能触发后续 Pass(比如常量折叠后产生
icmp eq %x, 0,接着dead code elimination就能干掉整个分支)
怎么验证 instcombine 是否生效?
最直接的方式是比对前后 IR:
clang -emit-llvm -S -O0 hello.c -o hello-O0.ll clang -emit-llvm -S -O1 hello.c -o hello-O1.ll # 或手动跑 instcombine opt -passes=instcombine hello-O0.ll -S -o hello-instcombine.ll
重点看:
-
store i32 600, ptr %1是否替代了原本的add+store两步 -
%a = add i32 200, 600是否变成%a = add i32 200, 600→%a = i32 800 - 原本多个
load+add链是否被压成单个常量或更短链
真正难的不是识别“哪里能折”,而是理解哪些上下文会让折叠失效——比如指针运算、浮点异常标志、nuw/nsw 属性缺失导致不敢合并,这些细节在调试 IR 时经常卡住人。











