本节摘要:本节用一段 12 行的 C 函数,把前七章的概念在命令行里走一遍全程:生成未优化 IR、指认 SSA 与 phi、逐个开关优化 pass 观察各自的贡献、最后对照 O2 全量产出的差异。所有命令与输出片段可直接复现。读完你应当拥有一套"亲手验证"的工作法:本册任何一个断言(外提生效了、强度削减发生了、分支被折叠了),都能用本节的流程自己验一遍。
概念看完清单,现在把机器开起来。准备一段包含"循环 + 不变量 + 常量分支"的代码——三种优化各有一道靶:
int demo(int n) { int acc = 0; int limit = 64; // 常量:常量传播的靶 for (int i = 0; i < n; i++) { int base = limit * 4; // 不变量:外提与折叠的靶 acc = acc + base / 2 + i; // 除 2 是移位的靶(强度削减的近亲) } return acc; }
用前端带 -O0 -emit-llvm 选项生成文本形态 IR(7.2 的形态知识在此上岗):
$ clang -O0 -S -emit-llvm demo.c -o demo.O0.ll
O0 的 IR 长得很有特点:所有局部变量都是栈槽(%acc = alloca i32 之类),每条赋值都是 store、每次使用都是 load——前端按"最笨但最忠实"的方式翻译,把构造 SSA 的工作(3.3)整个留给优化管线开头的 mem2reg(7.3 清单里它排第一的原因)。此时 base = limit * 4 在循环体内原封不动,每次都乘一遍。
单独跑 mem2reg 这一个 pass(优化器允许指定 pass 名单):
$ opt -passes=mem2reg demo.O0.ll -S -o demo.ssa.ll
对照输出,两个立竿见影的变化:alloca/store/load 全部消失,变量变成一串 % 名字;循环头出现一对 phi——%i 与 %acc 各一个,参数形态与 7.1 的 @sum 完全同款。这一步验证了本册的核心论断:SSA 不是前端的天赋,是管线的第一道工序;同时直观看到 7.3 说的"O0→O2 最大单项收益来自 SSA 化"——仅此一步,acc 与 i 就从每次访问一趟访存变成了寄存器世界里的值。
继续单独开 pass,每次只动一个旋钮,对照 IR 找证据:
$ opt -passes="instcombine,licm,indvars" demo.ssa.ll -S -o demo.opt1.ll
三道靶逐一验尸。常量折叠(instcombine 的一部分):limit * 4 直接变 256——常量传播证明 limit 恒 64,折叠把乘法吃掉。外提(licm):剩下的循环体里已经没有 base 的计算——它被搬到了循环入口之前(三道闸全过:无副作用、无别名风险、进圈即执行)。除法变移位(instcombine 的强度类规则):base / 2 变成 base >> 1,且 base 是编译期已知值时连移位都直接折叠掉。管线里 pass 的"分工协作"清晰可见:instcombine 反复把指令规整,licm 才能认出"这已经是常量了,搬出去"。
一键全量:
$ opt -O2 demo.O0.ll -S -o demo.O2.ll
典型产出(示意,具体指令因版本而异):
define i32 @demo(i32 %n) { entry: %cmp = icmp sgt i32 %n, 0 br i1 %cmp, label %loop, label %exit loop: %i = phi i32 [ 0, %entry ], [ %i.next, %loop ] %acc = phi i32 [ 0, %entry ], [ %acc.next, %loop ] %acc.next = add i32 %acc, %i ; 128 = 256>>1,常量直接并入 %i.next = add i32 %i, 1 %cond = icmp eq i32 %i.next, %n br i1 %cond, label %exit, label %loop exit: %r = phi i32 [ 0, %entry ], [ %acc, %loop ] ; 出口汇合 ret i32 %r }
逐块对照未优化版:栈槽没了(mem2reg)、循环头的 phi 就位(3.3 的构造)、循环体内一条乘法一条除法都不剩(折叠 + 外提 + 常量并入加法)、比较被规整成等值判断(indvars 改写循环条件与计数方向的常见副作用)。12 行 C 的全部语义,最终浓缩成循环体两条加法一条比较——这就是 7.3 那张收益构成图的实物。
⚠️ 实操注意:不同版本的工具链 pass 名与默认管线会有出入——复现时以你手上的版本为准,对照命令的帮助文档确认 pass 名单;断言"某优化没生效"之前,先确认它确实在你的档位配方里。
本节的流程是一个可复用的验证框架:提出断言 → 构造最小代码 → 生成两份 IR → 逐 pass 对照找证据。本册的每个关键断言都能塞进这个框架:"支配边界决定 phi 位置"——手算一份与 mem2reg 的输出对答案;"别名不明则外提保守"——把 limit 换成指针读法再跑 licm,看它还敢不敢搬;"内联解锁后续优化"——加一个 static 小函数跑 inline 前后对照。工具就是你的答题卡,比任何"据说"都可靠。
本节要点回顾:
手动的 pass 逐个开,终究要问一句:全量跑时谁决定顺序?下一节进 pass 管理器——编排这一切的调度中枢。