代码
functiontest($a){$x=1;$y=$x+2;// $y 始终是 3if($a>0){$z=$y*4;// $z = 12 (分支A)}else{$z=$y*4;// $z = 12 (分支B) — 两个分支 $z 相同!}echo$z;// PHP 7: 不知道 $z 是常量; PHP 8 SCCP: 知道 $z=12return$z;}这个例子是 PHP 7 和 PHP 8 的分水岭:两个分支中 $z 都等于 12,但 PHP 7 的 block_pass 只在单个基本块内做常量传播,无法跨分支推导"两路汇合后 $z 必然是 12"。
PHP 8 的 SCCP 通过 SSA 的 φ 函数 + 格值求交,可以证明这一点。
SSA 构造
原始 opcode 序列
编译器首先生成标准的 SSA 前 opcode:
Block 0 (entry): #0 RECV $a ← 参数 #1 ASSIGN $x 1 #2 ADD $y $x 2 #3 IS_SMALLER ~0 0 $a ← $a > 0 等价于 0 < $a #4 JMPZ ~0 Block_2 ← 条件跳转 Block 1 (then — $a > 0): #5 MUL $z $y 4 #6 JMP Block_3 Block 2 (else — $a <= 0): #7 MUL $z $y 4 ← 注意:和 Block 1 中 $z 的定义完全相同! #8 JMP Block_3 Block 3 (join): #9 ECHO $z #10 RETURN $z关键观察:$z 在 Block 1 和 Block 2 中分别被定义,然后在 Block 3 中被使用。这就是需要 φ 函数的场景。
构建 CFG 和支配树
dfa_pass.c 首先构建控制流图(CFG):
CFG: Block_0 → Block_1 (JMPZ 不跳) → Block_2 (JMPZ 跳) Block_1 → Block_3 Block_2 → Block_3 Block_3 → (exit)然后构建支配者树(dominator tree):
Block_0 (entry, 支配所有) ├── Block_1 (idom = Block_0) ├── Block_2 (idom = Block_0) └── Block_3 (idom = Block_0, 但被 Block_1/Block_2 共同前驱)计算支配边界 → 放置 φ 节点
支配边界 DF(n) = “n 支配某个前驱但不严格支配的节点集合”。
对 Block_1 和 Block_2:
- DF(Block_1) = {Block_3} — Block_1 支配自身但不支配 Block_3(Block_3 有来自 Block_2 的另一条路径)
- DF(Block_2) = {Block_3} — 同上
Block_3 是一个汇合点(join point),需要为 $z 放置 φ 函数。
φ 函数的语义:$z_3 = φ($z_1: Block_1, $z_2: Block_2)
意思是:“如果控制流从 Block_1 来,则 $z_3 = $z_1;如果从 Block_2 来,则 $z_3 = $z_2”。
重命名 → 生成 SSA 形式
最后进行变量重命名(在 zend_ssa 结构中):
SSA 变量映射:
$a → ssa_0 (参数, 在 Block_0 定义) $x → ssa_1 (定义于 #1 ASSIGN) $y → ssa_2 (定义于 #2 ADD) $z → ssa_3 (定义于 Block_1 的 #5 MUL) → ssa_4 (定义于 Block_2 的 #7 MUL) → ssa_5 (由 φ 函数定义, 位于 Block_3 入口) ~0 → ssa_6 (定义于 #3 IS_SMALLER)生成的 φ 节点(zend_ssa_phi 结构):
// 位于 Block 3 入口的 φ 节点 phi[0] = { .var = z_original_index, // 原始变量编号 .ssa_var = 5, // 这个 φ 定义 ssa_5 .block = 3, // 位于 Block 3 .sources = [3, 4], // ssa_3 (来自 Block_1), ssa_4 (来自 Block_2) // sources[i] 对应 CFG 前驱 blocks[predecessors[i]] };每个 SSA 变量的完整定义信息(zend_ssa_var 结构):
ssa_vars: [0]: {.definition = #0 RECV, .definition_phi = NULL} // 指令定义 [1]: {.definition = #1 ASSIGN, .definition_phi = NULL} [2]: {.definition = #2 ADD, .definition_phi = NULL} [3]: {.definition = #5 MUL, .definition_phi = NULL} // Block 1 的定义 [4]: {.definition = #7 MUL, .definition_phi = NULL} // Block 2 的定义 [5]: {.definition = -1, .definition_phi = &phi[0]} // φ 函数定义! [6]: {.definition = #3 IS_SMALLER, .definition_phi = NULL}SCCP 初始化
值格格定义
SCCP 的值格格(sccp.c:82-91)是一个有限高度的偏序集:
// 实际常量值存储在 values[] 数组中 // values[i] 为一个 zval,只有当 var_is_const[i] 为 true 时才有意义格值初始化(sccp.c:2444-2466)
// 伪代码,对应 sccp.c 中 sccp_context_init 函数voidsccp_context_init(scdf_ctx*scdf){sccp_ctx*ctx=(sccp_ctx*)scdf;// 步骤1: 所有 SSA 变量乐观地初始化为 TOPfor(inti=0;i<ssa->vars_count;i++){ctx->values[i].type=SCCP_TOP;// "我们乐观地假设每个变量都是常量"}// 步骤2: CV 变量(前 last_var 个)→ 初始化为 BOT// 原因: if (!$undefined_var) 必须触发 "undefined variable" 警告// 不能直接优化为 if (false)for(inti=0;i<op_array->last_var;i++){ctx->values[i].type=SCCP_BOT;// $a 初始化为 BOT}// 步骤3: 别名变量 → BOT(可能被间接修改)for(inti=0;i<ssa->vars_count;i++){if(ssa_vars[i].alias){ctx->values[i].type=SCCP_BOT;}}// 步骤4: 标记入口块为可达scdf_init(ctx,scdf,op_array,ssa);// → Block 0 加入 block_worklist}初始化后的格值状态(针对我们的例子):
ssa_0 ($a): BOT ← CV 变量,初始化为 BOT ssa_1 ($x): TOP ← 乐观: 可能是常量 ssa_2 ($y): TOP ← 乐观: 可能是常量 ssa_3 ($z_A): TOP ← 乐观: 可能是常量 ssa_4 ($z_B): TOP ← 乐观: 可能是常量 ssa_5 ($z_φ): TOP ← 乐观: 可能是常量 ssa_6 (~0): TOP ← 乐观: 可能是常量SCDF 不动点求解
SCDF 求解器(scdf.c:103-182)的核心是一个三层优先级的工作列表循环:
优先级: phi_var_worklist > instr_worklist > block_worklist
主循环结构
void scdf_solve(scdf_ctx *scdf, const char *name) { scdf->instr_worklist_len = 0; scdf->phi_var_worklist_len = 0; scdf->block_worklist_len = 1; // ← Block 0 已在其中 while (phi_var_worklist_len > 0 // 优先级1: φ 函数 || instr_worklist_len > 0 // 优先级2: 指令 || block_worklist_len > 0) { // 优先级3: 新发现的可达块 // === 优先级1: 处理 φ 函数 === while (phi_var_worklist_len > 0) { uint32_t var_num = zend_bitset_pop_first(phi_var_worklist); zend_ssa_phi *phi = ssa_vars[var_num].definition_phi; if (phi && is_block_executable(phi->block)) { scdf->handlers.visit_phi(scdf, phi); // → 调用 sccp_visit_phi() } } // === 优先级2: 处理指令 === while (instr_worklist_len > 0) { uint32_t op_num = zend_bitset_pop_first(instr_worklist); zend_op *opline = &op_array->opcodes[op_num]; zend_ssa_op *ssa_op = &ssa->ops[op_num]; if (is_block_executable(block_of(opline))) { scdf->handlers.visit_instr(scdf, opline, ssa_op); // → 调用 sccp_visit_instr() } } // === 优先级3: 处理新可达块 === while (block_worklist_len > 0) { uint32_t block_num = zend_bitset_pop_first(block_worklist); mark_block_executable(block_num); // 3a. 处理该块的所有 φ 函数(但将其 phi_var 从 phi_var_worklist 移到此处处理) for_each_phi_in_block(block_num, phi) { // 对该 φ 的 SSA 变量调用 visit_phi scdf->handlers.visit_phi(scdf, phi); } // 3b. 处理该块的所有指令 for_each_op_in_block(block_num, opline, ssa_op) { scdf->handlers.visit_instr(scdf, opline, ssa_op); } // 3c. 处理后继块 if (block_has_one_successor) { scdf_mark_edge_feasible(block_num, successor); // 无条件边 } else if (block_has_two_successors) { // 条件跳转 → 由 SCCP 决定哪些后继可达 scdf->handlers.mark_feasible_successors(scdf, block_num, ...); } } } }第一轮迭代 — Block 0 的处理
scdf_solve 开始:block_worklist = {Block_0}。
处理 Block 0:
===== 处理 Block 0 中的 φ 函数 ===== (Block 0 是入口块,没有 φ 函数) ===== 处理 Block 0 中的指令 ===== 指令 #0: RECV $a sccp_visit_instr: RECV 是参数绑定, $a 初始化为 BOT(CV变量) → ssa_0 值由 BOT 不变 → 不触发使用点的重新入队 指令 #1: ASSIGN $x, 1 sccp_visit_instr: op1 = CONST{1} op1 是常量 → ctx->values[1] 从 TOP 降低为 zval{1} → scdf_add_to_worklist(ssa_1) // 所有使用 ssa_1 的地方入队 → instr_worklist 加入 #2 (使用 $x 的 ADD 指令) 格值变化: ssa_1: TOP → zval{1} 指令 #2: ADD $y, $x, 2 (因为 ssa_1 的值刚才降低了, #2 已在 instr_worklist 中) 但随着 Block 0 的顺序处理, 我们继续执行: sccp_visit_instr: op1 = values[ssa_1] = zval{1} ← 已知常量 op2 = CONST{2} ← 已知常量 两个都是常量 → ct_eval_binary_op(ZEND_ADD, 1, 2) = zval{3} → ctx->values[2] 从 TOP 降低为 zval{3} → scdf_add_to_worklist(ssa_2) // 所有使用 $y 的地方入队 → instr_worklist 加入 #5, #7 (使用 $y 的 MUL 指令) 格值变化: ssa_2: TOP → zval{3} 指令 #3: IS_SMALLER ~0, 0, $a sccp_visit_instr: op1 = CONST{0} op2 = values[ssa_0] = BOT ← $a 是 BOT BOT 参与运算 → 结果也是 BOT → ctx->values[6] 从 TOP 降低为 BOT 格值变化: ssa_6: TOP → BOT 指令 #4: JMPZ ~0, Block_2 (这是条件跳转, 在 Block 0 的最后) mark_feasible_successors(Block_0): condition = values[ssa_6] = BOT ← 条件值未知 → 两边都可能走: 标记 edge(Block_0→Block_1) 和 edge(Block_0→Block_2) → Block_1, Block_2 加入 block_worklist 第一轮结束后的格值状态: ssa_0 ($a): BOT ← CV 变量 ssa_1 ($x): zval{1} ← 已收敛为常量! ssa_2 ($y): zval{3} ← 已收敛为常量! ssa_3 ($z_A): TOP ← 尚未处理 ssa_4 ($z_B): TOP ← 尚未处理 ssa_5 ($z_φ): TOP ← 尚未处理 ssa_6 (~0): BOT ← 非常量 ($a 是运行时的)第二轮 — Block 1(then 分支)
block_worklist 现在包含 Block_1 和 Block_2。
我们先处理 Block_1:
===== 处理 Block 1 ===== 指令 #5: MUL $z, $y, 4 (已在 instr_worklist 中,因为使用 ssa_2) sccp_visit_instr: op1 = values[ssa_2] = zval{3} ← 已知常量! op2 = CONST{4} ← 已知常量! ct_eval_binary_op(ZEND_MUL, 3, 4) = zval{12} → ctx->values[3] 从 TOP 降低为 zval{12} → scdf_add_to_worklist(ssa_3) // φ 函数 φ[0] 使用 ssa_3 → phi_var_worklist 加入 ssa_5(φ 定义的变量) 格值变化: ssa_3: TOP → zval{12} 指令 #6: JMP Block_3 无条件跳转 → mark_edge_feasible(Block_1→Block_3) → Block_3 已经在可执行块中 → 只需要重新处理 φ 函数 → phi_var_worklist 加入 ssa_5 3.4 第三轮 — Block 2(else 分支) ===== 处理 Block 2 ===== 指令 #7: MUL $z, $y, 4 (已在 instr_worklist 中,因为使用 ssa_2) sccp_visit_instr: op1 = values[ssa_2] = zval{3} ← 已知常量! op2 = CONST{4} ← 已知常量! ct_eval_binary_op(ZEND_MUL, 3, 4) = zval{12} → ctx->values[4] 从 TOP 降低为 zval{12} → scdf_add_to_worklist(ssa_4) // φ 函数 φ[0] 使用 ssa_4 → phi_var_worklist 加入 ssa_5 (已在, 但会再次入队) 格值变化: ssa_4: TOP → zval{12}关键:φ 函数的求值 — sccp_visit_phi
现在 phi_var_worklist 中有 ssa_5。
SCDF 求解器进入优先级1:
// sccp.c 中 sccp_visit_phi 的实现voidsccp_visit_phi(scdf_ctx*scdf,constzend_ssa_phi*phi){sccp_ctx*ctx=(sccp_ctx*)scdf;// phi->ssa_var = 5 (这个 φ 定义了 ssa_5)// phi->sources = [ssa_3, ssa_4] ← 来自 Block_1 和 Block_2// φ 函数的 meet 操作: 对所有可行边上的源值取 meetintvar_is_const=1;zval result;intfirst=1;for(inti=0;i<phi->block->predecessors_count;i++){if(!scdf_is_edge_feasible(phi->block->predecessors[i],phi->block)){continue;// ← 跳过不可行的边}intsource=phi->sources[i];// ssa_3 (来自 Block_1) 或 ssa_4 (来自 Block_2)if(ctx->values[source].type!=SCCP_TOP){// TOP 表示"无信息",跳过if(first){result=ctx->values[source];// 第一个有效值first=0;}else{// ★MEET 操作 ★// 出: result 和 ctx->values[source] 之间的 meet// 相等常量 → 保持常量// 不等常量 → BOTif(!zval_equals(result,ctx->values[source])){var_is_const=0;// 降为 BOTbreak;}}}}if(first){// 所有值都是 TOP → 保持 TOP(尚未有信息)return;}if(var_is_const){// 所有可行边的源值都相同!ctx->values[phi->ssa_var]=result;// ssa_5 = zval{12}}else{ctx->values[phi->ssa_var].type=SCCP_BOT;}// 如果 ssa_5 的值降低了 → scdf_add_to_worklist(ssa_5)}φ 求值的具体过程:
φ 函数: ssa_5 = φ(ssa_3: Block_1, ssa_4: Block_2)
Block_1(→Block_3) 可行: source = ssa_3 = zval{12} Block_2(→Block_3) 可行: source = ssa_4 = zval{12} 迭代: i=0 (Block_1): 取 zval{12}, first=false, result=zval{12} i=1 (Block_2): src=zval{12}, 与 result(zval{12}) 相等 → 保持 → MEET(zval{12}, zval{12}) = zval{12} → ssa_5 = zval{12} ✓ φ 节点收斂为常量!处理 Block 3(汇合块)
现在 ssa_5 的值已降低为 zval{12}。使用 ssa_5 的指令 #9、#10 被加入 instr_worklist。
===== 处理 Block 3 ===== 指令 #9: ECHO $z sccp_visit_instr: op1 = values[ssa_5] = zval{12} ← 常量! → ECHO 没有定义新的 SSA 变量, 不改变格值 指令 #10: RETURN $z sccp_visit_instr: op1 = values[ssa_5] = zval{12} ← 常量!到达不动点
此时所有三个 worklist 都为空。没有任何变量的格值可以再降低。不动点达成。
最终格值状态:
ssa_0 ($a): BOT ← CV变量,运行时常量未知 ssa_1 ($x): zval{1} ← 常量 ssa_2 ($y): zval{3} ← 常量 ssa_3 ($z_A): zval{12} ← 常量 ssa_4 ($z_B): zval{12} ← 常量 ssa_5 ($z_φ): zval{12} ← 常量 ★(PHP 7 做不到的推导!) ssa_6 (~0): BOT ← 依赖于 $a 的运行时比较结果常量替换
求解完成后,replace_constant_operands(sccp.c:2377-2442)遍历所有 SSA 变量,将已知常量的使用点替换为字面量:
voidsccp_replace_constants(scdf_ctx*scdf){for(intvar=0;var<ssa->vars_count;var++){if(ctx->values[var].type==SCCP_TOP||ctx->values[var].type==SCCP_BOT){continue;// 跳过 TOP 和 BOT}// var 是一个已知常量! 找到它的定义点intdefinition=ssa_vars[var].definition;// -1 表示 φ 定义// 遍历所有使用该常量的点FOREACH_USE(ssa_vars,var,use){// 用常量字面量替换操作数if(use->type==OP1_USE){try_replace_op1(op_array,&op_array->opcodes[use->op_num],ctx->values[var]);}elseif(use->type==OP2_USE){try_replace_op2(op_array,&op_array->opcodes[use->op_num],ctx->values[var]);}}}}替换后的 opcode:
Block 0: #1 ASSIGN $x 1 ← $x = 1 (已经是常量赋值,不变) #2 QM_ASSIGN $y 3 ← ★$y = 3 (原来 ADD $y,$x,2 被替换!) #3 IS_SMALLER ~0 0 $a ← $a 是 BOT, 不替换 #4 JMPZ ~0 Block_2 Block 1: #5 QM_ASSIGN $z 12 ← ★$z = 12 (原来 MUL $z,$y,4) #6 JMP Block_3 Block 2: #7 QM_ASSIGN $z 12 ← ★$z = 12 (原来 MUL $z,$y,4) Block 3: #9 ECHO 12 ← ★echo 12 (原来 echo $z) #10 RETURN 12 ← ★return 12 (原来 return $z)指令消除 + 死代码消除
try_remove_definition
对于定义点,如果该指令的结果不再被任何地方使用,则移除:
// ssa_1 ($x) 的定义是 #1 ASSIGN → 结果 $x 在后续不再被使用 // → try_remove_definition() 将 #1 变为 NOP // ssa_2 ($y) 的定义是 #2 ADD → 结果 $y 在后续不再被使用 // → try_remove_definition() 将 #2 变为 NOP (在替换时已变为 QM_ASSIGN $y,3) // 但由于 #2 结果是 $y, 且 $y 后续被 ssa_3/ssa_4 使用... // 等等,$y 在 SSA 中被 ssa_3/ssa_4 使用,但替换后 #5/#7 用的是常量 3 // 所以 $y 的 uses 变为空 → 可以移除scdf_remove_unreachable_blocks
如果某个条件跳转的条件在 SCCP 中被证明是常量(比如 if (true)),那么不可达的分支的 blocks 会被标记为不可执行,最终从 op_array 中移除:
voidscdf_remove_unreachable_blocks(scdf_ctx*scdf){for(inti=0;i<cfg->blocks_count;i++){if(!scdf_is_block_executable(scdf,i)){// 删除该块的所有 opcodedelete_code_block(cfg->blocks[i]);}}// 重新拼接存活的块, 更新跳转偏移assemble_code_blocks(cfg,op_array);}在我们的例子中,所有 4 个块都是可达的(因为 $a > 0 的条件在编译时不确定),所以这一步不删除块。
但如果条件是 if (true),SCCP 会将其 JMPZ 的条件操作数降为常量 true,然后在 mark_feasible_successors 中只标记 true 分支为可行。
最终字节码对比
functiontest($a){$x=1;$y=$x+2;if($a>0){$z=$y*4;}else{$z=$y*4;}echo$z;return$z;}核心算法本质
以最精简的方式描述整个 SCCP 的工作机制:
SCCP() { 1. 所有变量乐观初始化为 TOP ("每个变量我都假设它是常量") 2. 不可变变量 (CV/别名) 初始化为 BOT ("这些我不能碰") 3. while (有工作要做) { 3a. 先处理 φ 函数 (优先级最高) φ(s1, s2, ...) = MEET(可行边上的所有源值) // 相等常量 → 保持; 不等 → BOT; 包含 TOP → 跳过 TOP 3b. 处理待求值的指令 如果所有操作数都是已知常量: ct_eval_*() → 计算出结果常量 降低为常量值 如果任一操作数是 BOT: 结果也降为 BOT 3c. 处理新发现的可达块 标记为可执行 处理块内所有 φ 和指令 对条件跳转: 如果条件是常量 → 只标记对应分支为可行 (不可达分支的块永远不进入可执行集) } 4. 到达不动点 5. 把所有已知常量的使用点替换为字面量 6. 移除定义被清空的指令 7. 从 op_array 中删除不可执行的块 }为什么叫"稀疏条件":
- 稀疏:只在 SSA 变量的格值降低时才传播,不扫描全部指令。通过 scdf_add_to_worklist 精确只加入受影响的 use 点。
- 条件:控制流图上的边不是全部可行——只有条件已知为常量时才选择特定后继。if (true) → 只探索 true 分支。
- 常量传播:核心目标——发现哪些变量在所有可行执行路径上都是同一个常量。
PHP 7 的 block_pass 做不到的地方:ssa_5($z 的 φ 合并值)在两个分支都产生 zval{12} 时,需要 φ 函数的 meet 操作才能发现它是常量。
PHP 7 没有 SSA,没有 φ 函数,只能在一个基本块内 forward scan,跨基本块的推导力为零。