把同一份 IR 依次交给 mem2reg 和 instcombine,观察分析与变换、流水线层级和验证日志,并排查 opt 没有改变函数的原因。
把一次改写变成可单独观察的步骤
看到 O0 和 O2 的 IR 不同,只能说明两套编译配置产生了不同结果。要继续问“哪一步改了它”,就需要把过程拆开。LLVM Pass 是组织分析和变换的工作单元;流水线决定这些工作在什么范围、按什么顺序执行。
不是所有 Pass 都负责把程序改短。分析可以计算支配关系等信息;变换则可能改写 IR。一个变换也不保证每次都改变输入,更不保证每次都让真实程序更快。本文只用两个明确的 Pass,避免从一整套 O2 输出倒猜原因。
先固定同一份输入
实验环境:Ubuntu 24.04.4(WSL2,x86-64),Clang/LLVM 18.1.3,GNU binutils 2.42。
把下面这份手写 IR 保存为 pipeline.ll。它没有从某个 C++ 优化级别隐式带来函数属性,适合单独观察 Pass:
define i32 @twice(i32 %x) {
entry:
%slot = alloca i32, align 4
store i32 %x, ptr %slot, align 4
%v = load i32, ptr %slot, align 4
%plus = add i32 %v, 0
%result = mul i32 %plus, 2
ret i32 %result
}这段计算先把 x 存入栈槽,再读出,加零、乘二并返回。这里刻意保留可消除的操作,不代表普通程序一定会生成这样的 IR。无额外溢出标志的 i32 算术按相应位宽处理,测试端用 unsigned 对照。
先mem2reg,再instcombine
opt-18 -passes=mem2reg -verify-each -S pipeline.ll -o promoted.ll本次输出的函数主体为:
entry:
%plus = add i32 %x, 0
%result = mul i32 %plus, 2
ret i32 %result栈槽及其读写消失了,但加零和乘二仍在。继续运行:
opt-18 -passes=instcombine -verify-each -S promoted.ll -o combined.ll本次结果变为:
entry:
%result = shl i32 %x, 1
ret i32 %result现在可以把观察说得具体:在固定输入和版本下,mem2reg 提升了这个栈槽;随后 instcombine 消除了加零,并把乘二整理为左移。不能由此说“所有乘法都更慢”,也不能据 IR 指令数量推断最终执行时间。
把步骤放进一条明确的流水线
opt-18 '-passes=function(mem2reg,instcombine)' \
-debug-pass-manager -verify-each -S pipeline.ll -o transformed.llfunction(…) 指出这一组工作面向函数。Pass 管理器还组织模块、调用图相关结构和循环等层级;跨层级需要适配,不能把任意名称无条件排成一串。
-debug-pass-manager把实际调度写到诊断输出中;-verify-each在中间步骤加入验证。这样出现非法 IR 时,可以比“最后一个文件坏了”更早定位。验证器仍不是程序正确性证明,不会替你知道业务上应当乘二还是乘三。
opt-18 '-passes=default<O2>' -print-pipeline-passes -disable-output pipeline.ll这条命令本次打印了当前版本的 O2 默认流水线。它适合查当前配置,不适合作为永远不变的顺序表。LLVM 版本、目标条件和构建的工具组件都可能影响可用 Pass 与默认安排。重复运行某些工作也可能有用,因为前一步会暴露新的机会。
分析结果为什么需要管理
opt-18 '-passes=print<domtree>' -disable-output pipeline.ll本次命令打印了该函数的支配树信息。它回答的是图上的关系,没有要求生成新的目标程序。分析结果可以被后续变换使用;当变换改变了它依赖的结构,缓存就可能失效,必须正确保留、更新或重算。
因此 Pass 接口不只是“传进 IR、传出 IR”。它也要交代修改之后哪些分析仍然可信。这个契约对生态接入同样关键:加入一个设备相关 Pass,如果错误地保留过期分析,表面上省了重算,实际会破坏后续判断。
opt没有改变函数,先看它有没有运行
我们还做了一个对照:用 Clang 默认 O0 生成 IR,再尝试 instcombine。该函数带有 optnone,诊断显示 InstCombinePass 被跳过。原因是输入属性影响了是否运行,不是命令退出 0 就必然做了优化。
clang++-18 -O0 -S -emit-llvm twice.cpp -o optnone.ll
opt-18 -passes=instcombine -debug-pass-manager -S optnone.ll -o skipped.ll这里的 twice.cpp 仍是前面系列使用的 unsigned 乘二加零函数。排查时依次看:命令是否成功;Pass 是否进入;函数属性是否要求跳过;输入是否存在该变换能够处理的模式。没有变化还可能只是已经达到该 Pass 的稳定形式。
需要教学对照时,本次在 Clang18 使用 -Xclang -disable-O0-optnone重新生成输入,再运行 mem2reg、instcombine,实际得到了左移。这个内部选项有版本边界;不要机械删除所有函数属性。它们有的承载语言或目标语义,随意删除会让比较失去依据。
检查这次输出有没有算对
为了核对运行结果,本次将变换后的 IR 与下面的入口链接。保存为 main.cpp:
#include <cstdio>
extern "C" unsigned twice(unsigned);
int main() {
for (unsigned x=0; x<1000u; ++x) if (twice(x) != x*2u) return 1;
if (twice(~0u) != ~0u*2u) return 2;
std::puts("1001 cases passed");
}clang++-18 transformed.ll main.cpp -o demo
./demo本次 1000 个连续输入及 UINT_MAX,共 1001 组检查通过。有限测试覆盖的是这些输入,不是数学证明;本期也没有性能对比。测试失败时,应先保留原始 IR、流水线、版本与失败输入,而不是继续叠加 Pass 碰运气。
把输入固定、把步骤拆开、把失败保留下来,才能从“好像优化了”走到可核对的判断。下一篇沿着同一个 instcombine 入口进入 llvm-project,找到实现与回归测试的位置。
如果你正遇到“命令成功但 IR 没变”,可单独阅读知乎排查篇:为什么 opt 跑完以后,LLVM IR 一点没变?
为什么Pass次序会影响机会,而不仅影响输出长相
mem2reg把可提升栈槽上的读写转为直接值关系,使后续局部化简更容易看到“某值加零再乘二”。如果先运行只处理显式值模式的变换,可能因为输入仍经由内存间接连接而错过机会。前置变换改变了可见结构,而不只是美化文本。
但这不推出任何固定顺序都最优。某次内联可能暴露常量与循环,也可能增大代码和寄存器压力;一次规范化可能让一个Pass获益,却令另一个模式匹配不再触发。流水线要在编译时间、目标代价和运行表现之间评价。
对“为什么这一步没有改变”的调查,应分别检查运行与匹配:Pass是否被调度,函数是否被属性要求跳过,前提是否满足,输入是否已规范化,变换是否判断收益不足。退出码0只说明命令按工具定义完成,不保证任何具体改写。
分析缓存的正确性,也是变换契约的一部分
一个Pass若改变CFG,原有支配树或循环分析可能不再正确。后续Pass若继续消费过期结果,错误可能在更晚阶段才出现。局部输出看似合法,不意味着整条管线的内部状态可靠。
新Pass管理器让变换报告哪些分析被保留;这是关于依赖关系的声明,必须与实际修改一致。报告全部保留但未维护相关分析,是错误;一律丢弃虽可能避免使用旧结果,也会产生重算成本,且不能替代变换本身的语义正确性。
LLVM 18.1新Pass管理器文档描述分析管理与失效。阅读某个实际Pass时,应将run的改写、返回的PreservedAnalyses与相关分析使用一并检查。
本例的1001组测试只对所取unsigned输入核对结果。若要提升覆盖,可加入系统边界、随机差分与缩减失败输入;若要做性能结论,则需另设目标负载与测量。本轮没有把这些建议登记成已执行。
2026-10-18内容修订:补Pass机会、失效契约与测试证明力。本次为文献与语义审校,未新增实验;首次发布日期、原网址及历史运行版本保持。
来源、验证范围与阅读路线
本期实测采用18.1.3;下列版本化文档来自18.1系列的18.1.8文档站,命令输出以本文实测为准。持续更新的官方页面用于补充原理,不据此改写旧版运行结果。
同一手写IR:mem2reg去栈槽,instcombine变shl;pipeline调度与domtree已记录;O0的optnone导致InstCombine跳过;1001组检查通过。未知Pass预期失败。没有性能结论。
上一篇 · 下一篇:第一次阅读 llvm-project:源码目录、构建与调试入口 · 文章归档 · 知识体系
勘误:2026-10-07,初版。发现问题时请附工具版本、命令与关键输出;确认后在此记录修改。
写下你的评论
要发表评论,您必须先登录。