Skip to content

算法Algorithm

稀疏条件常量传播

Sparse conditional constant propagation · SCCP

共同增长可能执行的控制边与SSA值的三层抽象,只在已激活前驱上合流phi,用双工作队列排除假分支并传播常量。

形式陈述 ​

两份状态要分开存 ​

输入是良构SSA:每个名字一个静态定义,普通使用受定义支配,φ 参数按对应入边读取。运算纯粹、确定、总定义;输入参数可以取任意声明类型的值。整数按数学整数,没有未初始化、poison、内存或异常;这些不是从 LLVM 自动继承的默认规则。

每个 SSA 名字保存以下抽象值:

  • ⊥:目前尚未从已激活路径得到值
  • 常量 c:目前所有已获证据一致为 c
  • ⊤:不再能用一个常量概括,可能为不同值

次序为 ⊥⊑c⊑⊤,不同常量彼此不可比。合流满足 ⊥⊔c=c、c⊔c=c、c⊔d=⊤(c≠d)。未知输入从一开始就是 ⊤,不能用“尚未分析” ⊥ 代替;两者会对条件分支产生不同动作。

另一份状态是控制边集合 X,初始只有人工 START→E。边只增加、不删除。称它们“已激活的可能执行边”更准确:一条边被加入,不证明一定有真实输入走它;算法保守地承认这种可能。

两种消息驱动同一个求解器 ​

维护“新控制边”队列和“值发生变化”队列,也可以像下载实现那样用一个带类型标签的队列。预先建立每个 SSA 定义的使用者列表。

收到尚未加入 X 的 P→B 时,加入它并重新计算 B 顶部的全部 φ。若 B 第一次激活,还访问它的普通指令与末端跳转。B已经激活时,新增入边主要改变φ,不能因为“这个块访问过”就跳过。

普通运算若全部输入为常量,按源语义算出常量;若有 ⊥,暂留 ⊥;其余情况返回 ⊤。这是保守的基础规则,没有利用 0*x=0 等额外恒等式。新结果再与原保存值做 join,保证只沿偏序上升,不能把已知多值的 ⊤ 改回某次新算出的常量。

φ 的规则是

A(v)=⨆(P,B)∈XA(arg(v,P→B)).

无激活入边时合流为 ⊥。分支条件为常量真假时只激活对应边,为 ⊤ 时两边都激活,为 ⊥ 时先等待。值一旦上升,通知所有使用者,包括普通运算、φ及使用它的分支;无条件跳转在所属块激活时直接加入后继边。

对于循环,第一次可能只知道一个φ入口值;后来回边激活或回边值从常量变多值,会重新通知φ。边集合不会撤回,值也不会从 ⊤“猜回去”。这种单调性正是有限高度不动点终止论证的基础。

直觉

普通常量传播看到一条路传来10、另一条路传来未知输入,就只能说汇合结果未知。但如果第二条路根本走不到呢?SCCP 一边找常量,一边确定哪些控制边需要考虑:常量能排除分支,排除分支又能使更多值成为常量。这两个过程必须一起求稳定结果。

例子与边界

一个完整的常量链 ​

text
E: a := 4
   b := 6
   c := (a == 4)
   if c goto T else F
T: t := a+b
   goto J
F: f := q                 // q是任意整数输入
   goto J
J: v := phi(T:t,F:f)
   w := v*2
   d := (w == 20)
   if d goto R else B
R: return w
B: return 99

激活 E 后,a=4、b=6、c=true,于是只激活 E→T。T 给出 t=10,再激活 T→J。J 的 φ 只看已激活的 T→J 槽,所以 v=10;随后 w=20、d=true,只激活 J→R。最终可达块集合为 {E,T,J,R},q虽为 ⊤,却没有通过未激活的 F→J 污染 v。

边是否激活决定φ参与合流的输入

若忽略条件、把两条边预先都激活,f便为 ⊤,φ得到 10⊔⊤=⊤,无法继续发现 w=20。这是条件分析带来的精度,SSA使用链则承担稀疏传播的效率;两项贡献不同。

三项边界防止过度承诺 ​

把 E 的条件改成布尔输入p。若 T给10、F给14,则两边激活,v和w都是 ⊤,J 的两个出口都保留。若改为两边都给10,即使p未知,φ仍为10,后面的 d仍为真。下载实现同时检查这两个迁移。

再考虑两条分支分别产生 (x,y)=(1,1) 和 (2,2),汇合后计算 z=x−y。逐变量常量域把x、y都视为 ⊤,所以基础规则无法推出z=0;两者相等这一关系已丢失。SCCP可靠,并不意味着找到所有语义常量。

这里的 ⊥ 也不是目标语言的任意位模式或LLVM的undef。把编译器中间状态与源程序未定义语义混用,会让等待规则或删除规则失去依据。真实工业IR还需单独处理异常、浮点、内存、poison和可见效果。

推论与应用

为什么稳定后的删除可信 ​

中间时刻的未激活边还可能在以后被发现,因此不能提前把它删掉;同样,中间的常量以后可能升到 ⊤。只有两个队列都为空,并且所有上述约束满足,才可以使用结果改写程序。

对任一真实有限执行前缀按步数归纳。入口边已激活,输入参数由 ⊤ 覆盖。若真实执行到普通赋值,各输入此前的真实值已被抽象值覆盖;相应转移便覆盖这次结果。若到φ,实际前驱边已被纳入 X,它的槽值参与合流,因此覆盖所选值。若遇分支,稳定值要么是匹配真实结果的常量,要么是 ⊤,算法都已经纳入实际下一条边。沿前缀归纳,所有真实执行边都在 X 中。

这也排除了“真实走到条件却永久为 ⊥”的情况:良构、已定义的输入和有限执行前缀会逐层提供值证据,队列稳定时不能仍遗漏这些约束。因而未激活边不可真实执行;稳定常量在每次实际定义执行时都取该值。程序可以删除未激活块、折叠已证常量分支,并把相关读取改成常量,仍要维护剩余φ槽和CFG良构性。

本页检查器完整实现该分析,输出边、值和事件列表;它没有自动发出清理后的SSA。主例可手工读成直接返回20,是独立的终点答案,不把分析结果文件误称为编译器已完成全部清理。

复杂度必须说明φ的实现 ​

设 N 个指令与块、M 条控制边、U 次定义—使用出现。每个值最多发生两次严格上升,每条控制边最多激活一次;固定元数普通运算的工作因此与这些事件数线性相关。

但若每次有φ消息都重新扫描它的 k 个槽,该φ最多收到 O(k) 次新边或输入变化消息,代价可能为 O(k²)。下载检查器就是这种简单实现,故总界为 O(N+M+U+∑ϕkϕ2),另计常量算术位成本,存储为 O(N+M+U)。维护每个槽的已合流贡献并增量更新,可以将φ工作降到线性;没有实施这种维护时不能直接引用原论文的线性结论。

终点请先不运行脚本,写出主例从START→E到J→R的全部激活边,列出f为何仍为 ⊥ 而q为 ⊤。再完成p未知的两种迁移,并解释检查器的有限常量链不证明任意源程序一定终止。

参考资料
  • Mark N. Wegman and F. Kenneth Zadeck, “Constant Propagation with Conditional Branches,” ACM TOPLAS 13(2), 1991, pp. 181–210,论文全文,§3.4 的稀疏条件算法、§4 的可靠性、§5.4 的表达式求值成本。原文使用与本文相反的格绘制方向;本页始终明确以信息覆盖增大的 ⊥→c→⊤ 口径叙述。
  • Brown 技术报告 CS-91-22核对作者、题名与1991版本。本文SSA、具体输入和简单φ重扫实现均单独规定。
关系图谱13 个相邻概念 · 1 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组