php8 常量折叠

发布时间:2026/8/6 1:35:16
php8 常量折叠 代码functiontest($a){$x1;$y$x2;// $y 始终是 3if($a0){$z$y*4;// $z 12 (分支A)}else{$z$y*4;// $z 12 (分支B) — 两个分支 $z 相同}echo$z;// PHP 7: 不知道 $z 是常量; PHP 8 SCCP: 知道 $z12return$z;}这个例子是 PHP 7 和 PHP 8 的分水岭两个分支中 $z 都等于 12但 PHP 7 的 block_pass 只在单个基本块内做常量传播无法跨分支推导两路汇合后 $z 必然是 12。PHP 8 的 SCCP 通过 SSA 的 φ 函数 格值求交可以证明这一点。SSA 构造原始 opcode 序列编译器首先生成标准的 SSA 前 opcodeBlock 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 首先构建控制流图CFGCFG: Block_0 → Block_1 (JMPZ 不跳) → Block_2 (JMPZ 跳) Block_1 → Block_3 Block_2 → Block_3 Block_3 → (exit)然后构建支配者树dominator treeBlock_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_2DF(Block_1) {Block_3} — Block_1 支配自身但不支配 Block_3Block_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(inti0;issa-vars_count;i){ctx-values[i].typeSCCP_TOP;// 我们乐观地假设每个变量都是常量}// 步骤2: CV 变量前 last_var 个→ 初始化为 BOT// 原因: if (!$undefined_var) 必须触发 undefined variable 警告// 不能直接优化为 if (false)for(inti0;iop_array-last_var;i){ctx-values[i].typeSCCP_BOT;// $a 初始化为 BOT}// 步骤3: 别名变量 → BOT可能被间接修改for(inti0;issa-vars_count;i){if(ssa_vars[i].alias){ctx-values[i].typeSCCP_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 初始化为 BOTCV变量 → 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 1then 分支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 2else 分支 处理 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_const1;zval result;intfirst1;for(inti0;iphi-block-predecessors_count;i){if(!scdf_is_edge_feasible(phi-block-predecessors[i],phi-block)){continue;// ← 跳过不可行的边}intsourcephi-sources[i];// ssa_3 (来自 Block_1) 或 ssa_4 (来自 Block_2)if(ctx-values[source].type!SCCP_TOP){// TOP 表示无信息跳过if(first){resultctx-values[source];// 第一个有效值first0;}else{// ★MEET 操作 ★// 出: result 和 ctx-values[source] 之间的 meet// 相等常量 → 保持常量// 不等常量 → BOTif(!zval_equals(result,ctx-values[source])){var_is_const0;// 降为 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].typeSCCP_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} 迭代: i0 (Block_1): 取 zval{12}, firstfalse, resultzval{12} i1 (Block_2): srczval{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_operandssccp.c:2377-2442遍历所有 SSA 变量将已知常量的使用点替换为字面量voidsccp_replace_constants(scdf_ctx*scdf){for(intvar0;varssa-vars_count;var){if(ctx-values[var].typeSCCP_TOP||ctx-values[var].typeSCCP_BOT){continue;// 跳过 TOP 和 BOT}// var 是一个已知常量! 找到它的定义点intdefinitionssa_vars[var].definition;// -1 表示 φ 定义// 遍历所有使用该常量的点FOREACH_USE(ssa_vars,var,use){// 用常量字面量替换操作数if(use-typeOP1_USE){try_replace_op1(op_array,op_array-opcodes[use-op_num],ctx-values[var]);}elseif(use-typeOP2_USE){try_replace_op2(op_array,op_array-opcodes[use-op_num],ctx-values[var]);}}}}替换后的 opcodeBlock 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(inti0;icfg-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){$x1;$y$x2;if($a0){$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跨基本块的推导力为零。