ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

php8 use-def链

2026/8/6 1:33:36 拓冰建站 浏览量
php8 use-def链

代码

$a=10;if($cond){$a=20;}echo$a;

数据结构 (zend_ssa.h)

整个 SSA 系统的核心是三个结构体:

zend_ssa (顶层容器) ├── cfg: zend_cfg ← 控制流图(基本块、前驱/后继、支配树) ├── blocks[]: zend_ssa_block ← 每个基本块的 phi 链表头 ├── ops[]: zend_ssa_op ← 每条指令的 SSA 信息(use/def 编号 + 链表) ├── vars[]: zend_ssa_var ← 每个 SSA 变量的 def-use 信息 └── vars_count ← SSA 变量总数

zend_ssa_op (每条指令在 SSA 中的视图):

typedefstruct_zend_ssa_op{intop1_use;// op1 读的是哪个 ssa_var (≥0) 或 -1intop2_use;// op2 读的是哪个 ssa_var (≥0) 或 -1intresult_use;// result 读的是哪个 ssa_var (≥0,少见) 或 -1intop1_def;// op1 定义的是哪个 ssa_var (≥0) 或 -1intop2_def;// op2 定义的是哪个 ssa_var (≥0) 或 -1intresult_def;// result 定义的是哪个 ssa_var (≥0) 或 -1intop1_use_chain;// 该 use 在使用链表中的下一个 use 指令编号intop2_use_chain;intres_use_chain;}zend_ssa_op;

zend_ssa_var (每个 SSA 变量的元信息):

typedefstruct_zend_ssa_var{intvar;// 原始变量编号(CV#0, CV#1, TMP#last_var+0...)intscc;// 强连通分量intdefinition;// 定义该 ssa_var 的指令编号(入口)intuse_chain;// 使用该 ssa_var 的第一个 use 指令编号(链表头)zend_ssa_phi*definition_phi;// 如果是 phi 定义的,指向 phizend_ssa_phi*phi_use_chain;// 该变量在 phi 中的使用列表unsignedintalias:2;// 是否可能被间接修改(符号表别名等)}zend_ssa_var;

zend_ssa_phi (phi 函数):

typedefstruct_zend_ssa_phi{zend_ssa_phi*next;// 同一 BB 中的下一个 phiintpi;// ≥0 表示 e-SSA Pi 节点(来自哪个前驱 BB)intvar;// 原始 CV/VAR/TMP 变量号intssa_var;// 该 phi 定义的 SSA 变量编号intblock;// 所在基本块zend_ssa_phi**use_chains;// 每个 source 的使用链表int*sources;// phi 的源 SSA 变量号数组(每个前驱一个)}zend_ssa_phi;

三阶段构建流程 (zend_build_ssa, zend_ssa.c:996)

阶段 0:控制流图 & 支配者树

在 SSA 构建之前,zend_build_cfg() 已经完成了:

CFG: BB0 [start=0, len=2] (ASSIGN $a,10; JMPZ $cond,BB2) │ successors: BB1, BB2 ├─▶ BB1 [start=2, len=2] (ASSIGN $a,20; JMP BB2) │ successors: BB2 └─▶ BB2 [start=4, len=2] (ECHO $a; RETURN) successors: (exit) 支配者树 (dominator tree): BB0 为根(函数入口) BB1: idom = BB0 (BB0 支配 BB1) BB2: idom = BB0 (BB0 支配 BB2) BB2 的前驱: BB0, BB1 (这是合并点!) 预排序索引: BB0: level=0, children→BB1→BB2 BB1: level=1 BB2: level=1

阶段 1: DFG 构建 zend_build_dfg() + def 传播 + Phi 放置 (zend_build_ssa, line 1017-1116)

Step 1a: DFG 收集 def/use 集合 (zend_build_dfg, zend_dfg.c:252)

遍历每个 BB 的每条指令,逐条调用 _zend_dfg_add_use_def_op() (zend_dfg.c:22):

BB0: OP0 ASSIGN $a, 10 → op1_type=IS_CV($a): zend_bitset_incl(use, $a) ← $a 的旧值被"使用"(覆盖前) → 然后 goto add_op1_def (line 213): zend_bitset_incl(def, $a) → result_def 无 BB0: use={$a} def={$a} OP1 JMPZ $cond, BB2 → op1_type=IS_CV($cond): zend_bitset_incl(use, $cond) ← 读 $cond → 无 def BB0: use={$a, $cond} def={$a} BB1: OP2 ASSIGN $a, 20 → 同上: use={$a}, def={$a} BB1: use={$a} def={$a} OP3 JMP BB2 → 无 use, 无 def BB2: OP4 ECHO $a → op1_type=IS_CV($a): zend_bitset_incl(use, $a) BB2: use={$a} def={} OP5 RETURN 1 → IS_CONST, 无 use

Step 1b: 计算 liveness (in/out) (zend_build_dfg, line 288-310)

经典的数据流不动点迭代:in = use ∪(out - def), out = ∪in(successors):

一回合: BB2: out={} in=use={$a} BB1: out=in(BB2)={$a} in=use={$a} ∪({$a} - def={$a}) = {$a} BB0: out=in(BB1)∪in(BB2)={$a} in=use={$a,$cond} ∪({$a} - {$a}) = {$a, $cond}

Step 1c: 传播 def + 确定 Phi 位置 (line 1044-1116)

这是核心。算法逐条传播 def 集合,对于每个 CFG 合并点(前驱数 > 1 的 BB),沿支配者边往上走,找到需要 phi 的变量。

初始化: def_j = 每个 BB 的局部 def 集合 phi_j = {} (空) 迭代: BB2 (predecessors_count=2 > 1, 是合并点): 对每个前驱 k: k=BB0: i=BB0, i != idom(BB2)=BB0? 否 → 停止 (BB0 直接支配 BB2) k=BB1: i=BB1, i != idom(BB2)=BB0? 是 → phi_j |= phi_j ∩ def(BB1) ∩ in(BB2) → phi(BB2) = {} ∩ {$a} ∩ {$a} = {} → 然后 i = idom(BB1) = BB0 = idom(BB2) → 停止 首次迭代 phi_2 = {},全都满足 → 不变更新 def_2 的 phi 部分 但是: def(BB0)={$a}, def(BB1)={$a}, in(BB2)={$a} 实际算法在 line 1062 用 union_with_intersection: phi_2 |= phi_2 ∩ def(i) ∩ in(BB2) 等一下,这个逻辑是: phi_j = U_{predecessor k} (phi_j ∩ def(i) ∩ in(j)) 对于所有在 idom 之上的 i。当 i != idom(j) 时继续上行。 首轮: phi_2 = {} → 交集为空 → phi_2 = {} 但 zend_bitset_subset(phi_2, def_2, set_size)? def(BB2) = {}, phi_2 = {} → 是子集 → changed=0 ... 实际上首轮不变,但问题在于 BB0 的 def={$a} 没到达 BB2 覆盖 def(BB2)... 实际上这个算法有问题需要第二轮。让我简化解释...

实际上 PHP 的算法更精妙。在标准教科书法中,phi 放置
dominance frontier 上。

PHP 采用一种变体:

foreach block j with>1predecessors:foreach predecessor k:i=kwhilei!=idom(j):phi_j|=def_i ∩ in_j// 在非直接支配者的路径上,任何"被定义且活跃"的变量需要 phii=idom(i)

实际结果(简化后的 PHP 算法):

对于 BB2 (predecessors=BB0, BB1, idom=BB0): 前驱 BB0: i=BB0 == idom → 不进入 while 前驱 BB1: i=BB1 != idom → 进入 while phi(BB2) |= def(BB1) ∩ in(BB2) |= {$a} ∩ {$a} |= {$a} i = idom(BB1) = BB0 == idom(BB2) → 退出 结果: phi(BB2) = {$a} def(BB2) = {} → phi ⊄ def → def(BB2) |= phi → def(BB2) = {$a} changed = 1 → 再次迭代 第二次迭代: phi(BB2) = {$a}, def(BB0) = {$a}, def(BB1) = {$a} phi |= phi ∩ def(BB1) ∩ in(BB2) = {$a} ∩ {$a} ∩ {$a} = {$a} 不变 → def(BB2)已经包含 {$a} → 没有新phi → 收敛

在合并点创建 phi 指令 (line 1083-1116)

在 BB2 中为 $a 创建 phi: phi->pi = -1 (普通 phi, 不是 e-SSA Pi) phi->var = 0 (CV#0, 即 $a) phi->ssa_var = -1 (renaming 阶段分配) phi->sources[0] = -1 (来自 BB0 前驱, 待填充) phi->sources[1] = -1 (来自 BB1 前驱, 待填充) phi->block = 2

阶段 2: 变量重命名 (zend_ssa_rename, line 914)

这是经典 Cytron 算法的 renaming pass。按照支配树的前序遍历进行。

关键数据结构:

var[]// var[原始var_num] = 当前活跃的 SSA 变量号// 初始值: var[0]=0 ($a), var[1]=1 ($cond) 等// 初始 for CV: var[i] = i (line 1128)// 对于 TMP_VAR: var[i] = -1 (line 1125)

递归遍历支配树 (使用 worklist 模拟递归,line 914-988):

进入 BB0 (n=0): // 1. 处理 BB0 的 phi (BB0 没有 phi) // 2. 遍历 BB0 的指令 (zend_ssa_rename_in_block, line 820) OP0 ASSIGN $a, 10 → op1_type=IS_CV → op1_use = var[0] = 0 (ssa_var[0]) ← 读 $a 的旧值 → opcode==ZEND_ASSIGN → goto add_op1_def: op1_def = ssa_vars_count = 2 ← 创建新的 ssa_var var[0] = 2 ← 更新 $a 的活跃版本 ssa_vars_count++ → 3 → ssa_ops[0]: {op1_use:0, op1_def:2} OP1 JMPZ $cond, BB2 → op1_type=IS_CV → op1_use = var[1] = 1 (ssa_var[1]) → result_type=IS_TMP_VAR → result_def = ssa_vars_count = 3 var[last_var+0] = 3 ← TMP 变量也需要 rename ssa_vars_count++ → 4 → ssa_ops[1]: {op1_use:1, result_def:3} // 3. 填充后继 BB 的 phi sources BB0 的后继: BB1, BB2 对于 BB2 的 phi $a (ssa_var 尚为 -1): phi->sources[0] = var[0] = 2 ← 填充 BB0→BB2 边上的值 // 4. 递归处理子节点 先 BB1 (level=1), 再 BB2 (level=1) 进入 BB1 (n=1): // 1. 无 phi // 2. 遍历指令 OP2 ASSIGN $a, 20 → op1_use = var[0] = 2 ← 当前 $a 活跃版本仍是 ssa_var[2](来自 BB0) → 等等! 这是 ASSIGN, no_val 标注: zend_ssa_is_no_val_use(line 221) 返回 true → op1_use 存在但被标记为 no_val (值不重要) → add_op1_def: op1_def = ssa_vars_count = 4 ← 新版本 var[0] = 4 ← $a 的活跃版本更新为 4 → ssa_ops[2]: {op1_use:2, op1_def:4} OP3 JMP BB2 → 无 use, 无 def // 3. 填充后继 BB2 的 phi sources 对于 BB2 的 phi $a: phi->sources[1] = var[0] = 4 ← 填充 BB1→BB2 边上的值 // 4. BB1 没有子节点 (leaf) → 直接回溯 var[0] 恢复为 2 (退出 BB1 时回滚 var 数组) 进入 BB2 (n=2, BB0 的另一个子节点): // 1. 处理 BB2 的 phi (zend_ssa_rename_in_block, line 829) phi for $a: phi->ssa_var = ssa_vars_count = 5 ← 为 phi 分配 ssa_var 编号 var[0] = 5 ← $a 的活跃版本现在变成 phi 的结果 OP4 ECHO $a → op1_type=IS_CV → op1_use = var[0] = 5 ← 使用 phi 结果! → ssa_ops[4]: {op1_use:5} OP5 RETURN 1 → IS_CONST → 无 SSA 参与 // 3. BB2 无后继 → 不再填充 phi sources // 4. 无子节点 → 回溯

构建 use-def 链表 (zend_ssa_compute_use_def_chains, line 1144)

这是最关键的一步。从后往前扫描所有指令,为每个被使用的 ssa_var 建立从 def 到所有 use 的单链表。

重命名后的 ssa_ops (opN_use / opN_def):

OP0 ASSIGN: op1_use=0, op1_def=2 ssa_ops[0] OP1 JMPZ: op1_use=1, result_def=3 ssa_ops[1] OP2 ASSIGN: op1_use=2, op1_def=4 ssa_ops[2] OP3 JMP: (无 SSA) ssa_ops[3] OP4 ECHO: op1_use=5 ssa_ops[4] OP5 RETURN: (无 SSA) ssa_ops[5]

BB2 phi: ssa_var=5, sources=[2, 4] (来自 BB0→2, BB1→4)

逆向扫描建立链表 (line 1167-1193):

// ssa_vars 每位初始化为:// var = -1// scc = -1// definition = -1// use_chain = -1 ← 链表头,-1 表示空for(i=op_array->last-1;i>=0;i--){// 从最后的指令往前zend_ssa_op*op=ssa->ops+i;// 链入 use 链表 (头插法!)if(op->op1_use>=0){op->op1_use_chain=ssa_vars[op->op1_use].use_chain;// 保存旧链头ssa_vars[op->op1_use].use_chain=i;// 新链头 = 当前指令}// 对 op2_use, result_use 同理// 设置 definition 指针if(op->op1_def>=0){ssa_vars[op->op1_def].definition=i;// 指向定义指令}// 对 op2_def, result_def 同理}

逆向扫描的执行过程:

i=5 OP5 RETURN: 无 use, 无 def → 无事 i=4 OP4 ECHO: op1_use=5 → ssa_ops[4].op1_use_chain = ssa_vars[5].use_chain = -1 → ssa_vars[5].use_chain = 4 ← ssa_var[5] 被 OP4 使用 ssa_vars[5]: use_chain = 4 i=3 OP3 JMP: 无 i=2 OP2 ASSIGN: op1_use=2, op1_def=4 → op1_use_chain = ssa_vars[2].use_chain = -1 → ssa_vars[2].use_chain = 2 ← ssa_var[2] 被 OP2(op1) 使用 → ssa_vars[4].definition = 2 ← ssa_var[4] 由 OP2 定义 ssa_vars[2]: use_chain = 2 ssa_vars[4]: definition = 2 i=1 OP1 JMPZ: op1_use=1, result_def=3 → op1_use_chain = ssa_vars[1].use_chain = -1 → ssa_vars[1].use_chain = 1 ← ssa_var[1] 被 OP1(op1) 使用 → ssa_vars[3].definition = 1 ← ssa_var[3] 由 OP1 定义 ssa_vars[1]: use_chain = 1 ssa_vars[3]: definition = 1 i=0 OP0 ASSIGN: op1_use=0, op1_def=2 → op1_use_chain = ssa_vars[0].use_chain = -1 → ssa_vars[0].use_chain = 0 ← ssa_var[0] 被 OP0(op1) 使用 → ssa_vars[2].definition = 0 ← ssa_var[2] 由 OP0 定义 ssa_vars[0]: use_chain = 0 ssa_vars[2]: definition = 0 ssa_vars[2]: use_chain = 2 (之前已有)

最后处理 phi 的 use 链 (line 1196-1210):

for(i=0;i<ssa->cfg.blocks_count;i++){zend_ssa_phi*phi=ssa->blocks[i].phis;while(phi){phi->block=i;ssa_vars[phi->ssa_var].var=phi->var;ssa_vars[phi->ssa_var].definition_phi=phi;// phi 是定义// ... 填充 phi 到 source 的使用链表phi=phi->next;}}// BB2 的 phi: sources=[2, 4], ssa_var=5// ssa_vars[5].definition_phi = phi// ssa_vars[5].definition = -1 (不是指令定义的,是 phi 定义的)// ssa_vars[2] 的 phi_use_chain 中包含此 phi (ssa_var[2] 是这个 phi 的一个 source)// ssa_vars[4] 的 phi_use_chain 中包含此 phi

最终完整的 use-def 链

ssa_var[0]: $a 的初始版本 (入口值) definition = -1 (函数入口,无定义指令) use_chain = 0 ──▶ OP0 (ASSIGN, op1_use, no_val) ssa_var[1]: $cond 的初始版本 definition = -1 use_chain = 1 ──▶ OP1 (JMPZ, op1_use) ssa_var[2]: $a 的第 2 个版本 (=10) definition = 0 ← 由 OP0 (ASSIGN $a, 10) 定义 use_chain = 2 ──▶ OP2 (ASSIGN, op1_use, no_val) phi_use_chain ──▶ phi in BB2, source[0] ssa_var[3]: JMPZ 结果 (条件跳转的布尔值) definition = 1 use_chain = -1 (未使用) ssa_var[4]: $a 的第 3 个版本 (=20) definition = 2 ← 由 OP2 (ASSIGN $a, 20) 定义 use_chain = -1 (通过 phi 使用,不在指令链表中) phi_use_chain ──▶ phi in BB2, source[1] ssa_var[5]: phi($a, BB0→2, BB1→4) 的结果 definition_phi = phi in BB2 definition = -1 use_chain = 4 ──▶ OP4 (ECHO, op1_use)

图示化:


遍历 use-def 链的 API

从 def 遍历所有 use:

#defineFOREACH_USE(var,use)do{\int_var_num=(var)-ssa->vars,next;\for(use=(var)->use_chain;use>=0;use=next){\next=zend_ssa_next_use(ssa->ops,_var_num,use);// zend_ssa.h:193-203 — 辅助函数staticzend_always_inlineintzend_ssa_next_use(constzend_ssa_op*ssa_op,intvar,intuse){ssa_op+=use;// 指针移到 use 所在的 ssa_opif(ssa_op->op1_use==var){returnssa_op->op1_use_chain;// 返回链表中下一个 use}elseif(ssa_op->op2_use==var){returnssa_op->op2_use_chain;}else{returnssa_op->res_use_chain;// result_use 情况}}

从 phi source 遍历 phi 使用:

// zend_ssa.h:277-284#defineFOREACH_PHI_USE(var,phi)do{\int_var_num=(var)-ssa->vars;\zend_ssa_phi*next_phi;\for(phi=(var)->phi_use_chain;phi;phi=next_phi){\next_phi=zend_ssa_next_use_phi(ssa,_var_num,phi);// zend_ssa.h:205-218staticzend_always_inline zend_ssa_phi*zend_ssa_next_use_phi(constzend_ssa*ssa,intvar,constzend_ssa_phi*p){if(p->pi>=0){returnp->use_chains[0];// Pi 节点只有一个 source}else{// 遍历 sources 找到匹配的,返回对应的 use_chainfor(intj=0;j<ssa->cfg.blocks[p->block].predecessors_count;j++){if(p->sources[j]==var){returnp->use_chains[j];}}}}

优化阶段如何使用 use-def 链

DCE 死代码消除 (dce.c)

DCE 沿 use 链反向 传播活性。从 RETURN/ECHO 等"必然活跃"的指令出发,将它们的操作数标记为活跃,再标记操作数的定义指令为活跃,如此递归。

// dce.c:294 — 操作数入队staticvoidadd_operands_to_worklists(...){if(ssa_op->op1_use>=0){if(!zend_bitset_in(instr_worklist,ssa_var[op1_use].definition)){zend_bitset_incl(instr_worklist,ssa_var[op1_use].definition);}}// op2_use 同理}// dce.c:326 — 检查变量是否"死"staticboolis_var_dead(zend_ssa*ssa,intvar_num,...){// 检查 use_chain 中是否所有 use 都是死指令// 检查 phi_use_chain 中是否所有 phi use 都是死}

具体追踪:

ECHO $a 必然 active → ssa_var[5] 活跃 → 沿着 use 链: 1. use_chain=4: OP4 活跃 (ECHO 本身) 2. ssa_var[5] 的 definition_phi 指向 BB2 的 phi → 遍历 phi 的所有 sources [2, 4]: → ssa_var[2] 活跃, ssa_var[4] 活跃 3. ssa_var[2] 活跃 → ssa_var[2].definition = 0 → OP0 活跃 → ssa_var[2] 的 phi_use_chain (作为 phi source) → 已检查 4. ssa_var[4] 活跃 → ssa_var[4].definition = 2 → OP2 活跃 5. OP0 活跃 → 但 op1_use = ssa_var[0] 是 no_val (值不重要) → ssa_var[0] 不标记活跃 (初始版本未被"真正"使用)

结果: OP0, OP2, OP4 都活着 → 没有死代码

SCCP 常量传播 (sccp.c)

SCCP 用 use-def 链的反向:从 def 找 use。

当 SCCP 发现一个变量是常量,通过 replace_constant_operands() 遍历 use 链,把所有 use 替换为常量。

// sccp.c:2377 — replace_constant_operandsstaticintreplace_constant_operands(sccp_ctx*ctx){for(inti=op_array->last_var;i<ssa->vars_count;i++){if(!IS_TOP(&ctx->values[i])&&!IS_BOT(&ctx->values[i])){// 是个常量!遍历所有 use 替换FOREACH_USE(&ssa->vars[i],use){try_replace_op1(ctx,opline+use,ssa_op+use,i,&ctx->values[i]);try_replace_op2(ctx,opline+use,ssa_op+use,i,&ctx->values[i]);}FOREACH_USE_END();// 也遍历 phi_use_chainFOREACH_PHI_USE(&ssa->vars[i],phi){// 在 phi 中也替换源}FOREACH_PHI_USE_END();}}}

类型推断 (zend_inference.c)

类型推断沿 SSA 指令顺序处理,但遇到循环时需要迭代到不动点。

对于每个 ssa_op,op1 和 op2 的 use 给出 ssa_var_info[use].type,然后根据操作码语义计算 result 类型。

// zend_inference.c 中对 ADD 的处理caseZEND_ADD:t1=ssa->var_info[ssa_op->op1_use].type;// 查 use 的类型t2=ssa->var_info[ssa_op->op2_use].type;tmp=binary_op_result_type(ssa,ZEND_ADD,t1,t2,...);UPDATE_SSA_TYPE(tmp,ssa_op->result_def);// 更新 def 的类型

总结:双向链表 vs 单向链

PHP 的 SSA use-def 是两个方向的索引:

方向存储位置用途
def → use (正向)ssa_var.use_chain + opN_use_chainDCE(反向活性传播)、SCCP(常量替换到 use 点)
use → def (反向)ssa_var.definition / ssa_var.definition_phi任何时候需要知道"谁定义了这个值"(每个优化都会用到)

链表为什么用头插法(逆向扫描)?

// zend_ssa_compute_use_def_chains, line 1167for(i=op_array->last-1;i>=0;i--){// 头插法: 更晚的 use 在链表头部op->op1_use_chain=ssa_vars[op->op1_use].use_chain;// 旧链头变新节点nextssa_vars[op->op1_use].use_chain=i;// 新链头 = 当前}

逆向+头插的结果是 use 链按指令从后往前排列,这对 DCE 有利(DCE 是反向传播,靠后的 use 先处理符合直觉),而且不需要二次遍历(一次扫描同时建立 definition 和 use 链)。

为什么 op1_use 和 op1_def 可以同时非 -1?

像 $a = $a + 1 编译为 ASSIGN_OP $a, 1(复合赋值),这条指令既读 $a 的旧值,又写 $a 的新值。
所以:

  • op1_use = ssa_var[旧版本] ← 读旧值做运算
  • op1_def = ssa_var[新版本] ← 写回新值

这就是 SSA 的核心约定:每条指令最多定义一个新的 SSA 变量号,但可以读多个旧的 SSA 变量号。