php $x = 10;$y = 20;$fn = function($a) use ($x, &$y) {$result = $x + $a + $y;return$result;};
解析器 (zend_language_parser.y:962-966) 匹配闭包语法并生成一个 ZEND_AST_CLOSURE 节点:
php $fn = function($a) use ($x, &$y) { $result = $x + $a + $y; return$result; }; ~~~~~~~~ ~~ ~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | | | | decl | uses list body stmts params
lexical_vars 规则 (第 987-999 行) 解析 use($x, &$y):
词法变量列表成为 ZEND_AST_CLOSURE_USES 链表(zend_ast_create_list 的第 3 个参数)。
最终 AST 形状:
c ZEND_AST_STMT_LIST ├── ZEND_AST_ASSIGN($x, 10) ├── ZEND_AST_ASSIGN($y, 20) └── ZEND_AST_ASSIGN($fn, ZEND_AST_CLOSURE ├── child[0]: params = [ZEND_AST_PARAM($a)] ├── child[1]: uses = ZEND_AST_CLOSURE_USES[VAR('x',attr=0), VAR('y',attr=1)] ├── child[2]: stmts = STMT_LIST[ │ ZEND_AST_ASSIGN( │ $result, │ ZEND_AST_BINARY_OP(ZEND_ADD, │ ZEND_AST_BINARY_OP(ZEND_ADD, $x, $a), ← 左→右 → $x + $a 先算 │ $y │ ) │ ), │ ZEND_AST_RETURN($result) │ ] └── child[3]: return_type = NULL )
zend_compile_func_decl() — 创建独立的 op_array
zend_compile.c:4915。关键流程:
c 4915: zend_compile_func_decl(result, ast)4924: orig_op_array = CG(active_op_array); // 保存父作用域4925: op_array = zend_arena_alloc(...); // 新的独立内存块4928: init_op_array(op_array, ...); // 归零所有字段(static_variables=NULL, last_var=0...)4937: if (decl->kind == ZEND_AST_CLOSURE)4938: op_array->fn_flags |= ZEND_ACC_CLOSURE; // ← 标记为闭包4945: zend_begin_func_decl(result, op_array, decl); // ← 发射 ZEND_DECLARE_LAMBDA_FUNCTION 到父函数4948: CG(active_op_array) = op_array; // 切换作用域到闭包4965: zend_compile_params(params_ast, ...); // 编译参数 $a → ZEND_RECV4967: zend_compile_closure_uses(uses_ast); // 编译 use($x, &$y)4969: zend_compile_stmt(stmt_ast); // 编译闭包体4980: zend_emit_final_return(NULL); // 隐式 return null4982: pass_two(op_array); // 最终确定 jumps、字面量、CV 偏移量4988: CG(active_op_array) = orig_op_array; // 恢复父作用域
zend_begin_func_decl() — 父函数得到 ZEND_DECLARE_LAMBDA_FUNCTION
zend_compile.c:4867。即使 CG(active_op_array) 仍然指向父函数,也会在父函数中发射一条关键指令:
c 4893: if (op_array->fn_flags & ZEND_ACC_CLOSURE) {4894: opline = zend_emit_op_tmp(result, ZEND_DECLARE_LAMBDA_FUNCTION, NULL, NULL);// ^^^^^^ 写入 CG(active_op_array) — 即父函数的 op_array! 然后生成一个运行时 key 并将闭包注册到 CG(function_table):4903: zend_string *key = zend_build_runtime_definition_key(lcname, decl->lex_pos);// key = "\0{closure}\0filename:3" (第 3 行)4905: opline->op1_type = IS_CONST;4906: LITERAL_STR(opline->op1, key); // 父函数字面量表 #N 存储这个 key4908: zend_hash_update_ptr(CG(function_table), key, op_array);// 运行时 VM 通过此 key 从 EG(function_table) 查找 op_array
zend_compile_closure_uses() — use 变量得到 FETCH_STATIC 占位符
zend_compile.c:4674。对于 use($x, &$y) 中的每个变量:
对于 $x (by_ref=false):
c 4689: ZVAL_NULL(&zv);4690: Z_CONST_FLAGS(zv) = IS_LEXICAL_VAR; // 标志:值拷贝4692: zend_compile_static_var_common(var_ast, &zv, 0);
对于 &$y (by_ref=true):
c 4689: ZVAL_NULL(&zv);4690: Z_CONST_FLAGS(zv) = IS_LEXICAL_REF; // 标志:引用绑定4692: zend_compile_static_var_common(var_ast, &zv, 1);
zend_compile_static_var_common (zend_compile.c:3490) 接着做以下事情:
为变量名创建一个 CV 槽位:zend_compile_expr(&var_node, var_ast) → lookup_cv(“x”) → 在闭包的 op_array->vars[] 中分配槽位 0。对于 “y” 也是同样,分配槽位 1。
存入 static_variables:zend_hash_update(static_variables, “x”, zv) 其中 zv 带有 IS_LEXICAL_VAR 标志。这是一个占位符 — 不是实际值!
发射 FETCH + ASSIGN 指令:
c ZEND_FETCH_R T0 [ext=ZEND_FETCH_STATIC, "x"] // 运行时从 static_variables 取 "x"ZEND_ASSIGN CV[$x], T0 // 存到 CV 槽位ZEND_FETCH_R T1 [ext=ZEND_FETCH_STATIC, "y"] // 运行时从 static_variables 取 "y"ZEND_ASSIGN CV[$y], T1
注意:$y 在这里不是 ZEND_FETCH_W 因为 by_ref 控制的是静态变量赋值是否按引用(第 3516-3521 行),而不是 use 变量的 FETCH 类型。对于use 变量,by_ref 传入的是 var_ast->attr。 而对应的功能在 zend_compile_closure_uses 中: zend_bool by_ref = var_ast->attr;
然后在 zend_compile_static_var_common 中: opline = zend_emit_op(&result, by_ref ? ZEND_FETCH_W : ZEND_FETCH_R, &var_node, NULL); opline->extended_value = ZEND_FETCH_STATIC;
所以对于 $x:是 ZEND_FETCH_R + FETCH_STATIC。对于 &$y:是 ZEND_FETCH_W + FETCH_STATIC。编译闭包体 — 表达式 $result = $x + $a + $y 的常量折叠
闭包体从左到右编译。表达式 $x + $a + $y 解析为:
c ZEND_AST_BINARY_OP(ZEND_ADD, ZEND_AST_BINARY_OP(ZEND_ADD, VAR($x), VAR($a)), ← $x + $a VAR($y) ← + $y )
zend_compile_binary_op (zend_compile.c:5971) 对 $x + $a:
c 5978: zend_compile_expr(&left_node, VAR($x)) → left_node = CV[$x], op_type=IS_CV5979: zend_compile_expr(&right_node, VAR($a)) → right_node = CV[$a], op_type=IS_CV5981: if (left_node.op_type == IS_CONST && right_node.op_type == IS_CONST) ← FALSE!// $x 和 $a 都是 CV(已编译变量),不是编译时常量// → 跳过常量折叠6025: zend_emit_op_tmp(result, ZEND_ADD, &CV[$x], &CV[$a])// 发射: ZEND_ADD T2, CV[$x], CV[$a]
然后对 T2 + $y(外层二进制操作):
c 5978: zend_compile_expr(&left_node, ...) → left_node = T2, op_type=IS_TMP_VAR5979: zend_compile_expr(&right_node, ...) → right_node = CV[$y], op_type=IS_CV5981: T2 是临时变量,不是 IS_CONST → 再次跳过折叠6025: zend_emit_op_tmp(result, ZEND_ADD, &T2, &CV[$y])// 发射: ZEND_ADD T3, T2, CV[$y]
就是为什么编译期常量折叠不适用于此例:$x、$a、$y 都是运行时变量(CV/临时变量),不是字面量。
只有当两边都是 IS_CONST(字面量,如 2 和 3)时才会触发折叠。
例如,如果是 function() { return 2 + 3; },编译器会计算 5 并完全消除 ZEND_ADD 指令。
到 zend_compile_func_decl 的第 4982 行结束时,闭包的原始字节码如下:
; === 闭包 {closure}:3 字节码(在 pass_two 之前,简化显示)=== #0 ZEND_FETCH_R T0 ext=FETCH_STATIC ; 取 use 变量 "x" #1 ZEND_ASSIGN CV[$x], T0 ; → CV 槽位 0 #2 ZEND_FETCH_W T1 ext=FETCH_STATIC ; 取 use 变量 "y"(引用) #3 ZEND_ASSIGN CV[$y], T1 ; → CV 槽位 1 #4 ZEND_RECV CV[$a] ; 接收参数 #5 ZEND_ADD T2, CV[$x], CV[$a] ; $x + $a #6 ZEND_ADD T3, T2, CV[$y] ; ($x + $a) + $y #7 ZEND_ASSIGN CV[$result], T3 ; $result = ... #8 ZEND_RETURN CV[$result] ; return $result #9 ZEND_RETURN null ; 隐式 final return ; === 父函数字节码 === #0 ZEND_ASSIGN CV[$x], 10 #1 ZEND_ASSIGN CV[$y], 20 #2 ZEND_DECLARE_LAMBDA_FUNCTION CONST["\0{closure}\0file:3"] → T0 #3 ZEND_ASSIGN CV[$fn], T0 #4 ZEND_RETURN nullpass_two() — 最终确定跳转和字面量zend_opcode.c:577-707。到这里:
这条流水线也适用于闭包。在 zend_accel_script_optimize (zend_optimizer.c:669) 中:
c 681: zend_accel_optimize(&script->main_op_array, &ctx); // 优化主脚本683: for each entry in script->function_table: // ← 闭包在这里!687: zend_accel_optimize(op_array, &ctx); // 独立优化每个闭包
zend_accel_optimize (第 557 行) 首先撤销 pass_two(将跳转和常量转换回绝对索引),运行优化 pass,然后重做 pass_two。
Pass 1:操作码级常量折叠 (pass1_5.c:40-103)扫描闭包的操作码。对于每条 ZEND_ADD 指令,检查两边是否都是 IS_CONST:
#5 ZEND_ADD T2, CV[$x], CV[$a] 操作数 1 = CV → 不是 IS_CONST → 不折叠。继续。 #6 ZEND_ADD T3, T2, CV[$y] 操作数 1 = TMP_VAR → 不是 IS_CONST → 不折叠。同样,没有折叠发生。$x、$a、$y 是运行时变量,优化器无法在编译时求值。
但 pass1 确实 折叠了一个相关的模式:FETCH_R(IS_CONST) + 常量传播。zend_optimizer_replace_by_const(第 362-483 行)通过所有后续使用向前传播折叠后的常量,用字面量替换临时变量引用,并 NOP 掉原来的指令。
Pass 3:复合赋值优化 (pass3.c:56-159)寻找 ZEND_ADD $var, expr 后跟 ZEND_ASSIGN $var, result 的模式,并重写为 ZEND_ASSIGN_ADD:
#5 ZEND_ADD T2, CV[$x], CV[$a] 操作数1 = CV[$x]#6 ZEND_ADD T3, T2, CV[$y] 操作数1 = T2#7 ZEND_ASSIGN CV[$result], T3 操作数1 = CV[$result]检查 #5+#6 或 #6+#7:对于 #6→#7,ZEND_OP1(#6) = T2 与 ZEND_OP1(#7) = CV[$result] — 不匹配。没有复合赋值机会。
但如果代码是 $result = $result + $x,pass3 会将其优化为 $result += $x,节省一条指令。
Pass 5:CFG 优化 (block_pass.c:1975-2044) — 死代码消除
构建控制流图(CFG)并对每个基本块运行三级优化:
c 1975: optimize_cfg(op_array, ctx)1989: if (fn_flags & ZEND_ACC_HAS_FINALLY_BLOCK) return; // 跳过有 finally 的函数1995: find_code_blocks(op_array, &cfg, ctx); // 构建 CFG2013: for (pass = 0; pass < 3; pass++):2016: zend_t_usage(blocks, ...); // 计算数据依赖2023: zend_optimize_block(block, ...); // 优化每个块(折叠 ADD、JMP 等)2031: zend_jmp_optimization(block, ...); // 优化跨块跳转2034: zend_rebuild_access_path(&cfg, ...); // 标记可达块// ^^^^^^^ 不可达块被标记为 access=0 ^^^^^^^2040: assemble_code_blocks(&cfg, op_array); // 删除不可达块,重写 op_array
对于我们的闭包:字节码是直线型的 — 没有 JMPZ、JMP、JMPNZ 或其他分支指令。
CFG 只包含从第一条到最后一条指令的单个基本块。所有代码都是可达的,没有不可达块被消除。
但如果有这样的代码:
php function($a) use ($x) {if (false) { $x = 999; } // ← JMPZ 以常量 false,跳过此块return$a + $x;}
那么 pass2 (pass2.c:114-145) 会将常量条件跳转 (JMPZ with IS_CONST op1 = false) 转换为无条件 JMP,跳过 if-body 块。
然后 assemble_code_blocks 在重写 op_array 时物理移除从未被访问的 if-body 块。
ext\opcache\Optimizer\ 文件列表是决定性的: block_pass.c — 每个基本块的折叠、跳转优化、不可达代码消除 compact_literals.c — 合并重复的字面量 nop_removal.c — 剥离 NOP optimize_func_calls.c — INIT_FCALL_BY_NAME → DO_FCALL optimize_temp_vars_5.c — 合并临时变量 pass1_5.c — 操作码级常量折叠 pass2.c — 常量数字转换、常量 JMP 折叠 pass3.c — $i = $i + x → $i += x, JMP 链 zend_optimizer.c — 编排器没有 zend_ssa.c,没有 zend_type_inference.c。 SSA 构造和基于类型的推导(“$x 是一个整数”、“$a 永远不会是 null”)是 PHP 7.1 中添加的全新通道(ext/opcache/Optimizer/zend_ssa.c、ext/opcache/Optimizer/zend_inference.c)。
在 7.1 之前,优化器只作用于操作码结构(哪些是常量、哪些临时变量被复用、哪些块从控制流图可达),而从不作用于操作数的语义类型。
由于应用到此特定闭包的优化为零(没有常量可折叠,没有复合赋值模式匹配,无法到达死代码),最终字节码仅通过 pass_two 最终确定并进行了 compact-literals 处理:
; === {closure}:3 (pass_two 最终确定) ===#0 ZEND_FETCH_R CV[$x] ext=FETCH_STATIC#1 ZEND_FETCH_W CV[$y] ext=FETCH_STATIC#2 ZEND_RECV CV[$a]#3 ZEND_ADD T0, CV[$x], CV[$a]#4 ZEND_ADD T1, T0, CV[$y]#5 ZEND_ASSIGN CV[$result], T1#6 ZEND_RETURN CV[$result]#7 ZEND_RETURN null这条流水线中有两个架构之美:
只有 IS_LEXICAL_VAR | IS_LEXICAL_REF 标志位将它们区分开来。没有额外的操作码,没有额外的 VM 分支。
只有在运行时,zend_create_closure 执行时,两部分才通过 memcpy + zval_copy_static_var → 捕获实际 use 值 + $this 绑定重新连接起来。
这种分离使得: