php7 闭包实现

代码

php 复制代码
$x = 10;
$y = 20;
$fn = function($a) use ($x, &$y) {
    $result = $x + $a + $y;
    return $result;
};

源码 → AST

解析器 (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(KaTeX parse error: Expected 'EOF', got '&' at position 4: x, &̲y):

  • $x → ZEND_AST_VAR('x'),attr = 0
  • &$y → ZEND_AST_VAR('y'),attr = 1(由 '&' T_VARIABLE 规则设定)

词法变量列表成为 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
      )

AST → 编译期 op_array

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_RECV
  4967:   zend_compile_closure_uses(uses_ast);      // 编译 use($x, &$y)
  4969:   zend_compile_stmt(stmt_ast);              // 编译闭包体
  4980:   zend_emit_final_return(NULL);            // 隐式 return null
  4982:   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 存储这个 key
  4908: 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(KaTeX parse error: Expected 'EOF', got '&' at position 4: 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) 接着做以下事情:

  1. 为变量名创建一个 CV 槽位:zend_compile_expr(&var_node, var_ast) → lookup_cv("x") → 在闭包的 op_array->vars\[\] 中分配槽位 0。对于 "y" 也是同样,分配槽位 1。

  2. 存入 static_variables:zend_hash_update(static_variables, "x", zv) 其中 zv 带有 IS_LEXICAL_VAR 标志。这是一个占位符 --- 不是实际值!

  3. 发射 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_CV
  5979: zend_compile_expr(&right_node, VAR($a))  → right_node = CV[$a], op_type=IS_CV
  5981: 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_VAR
  5979: zend_compile_expr(&right_node, ...)  → right_node = CV[$y], op_type=IS_CV
  5981: T2 是临时变量,不是 IS_CONST → 再次跳过折叠
  6025: zend_emit_op_tmp(result, ZEND_ADD, &T2, &CV[$y])
        // 发射: ZEND_ADD T3, T2, CV[$y]

就是为什么编译期常量折叠不适用于此例: x 、 x、 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        null

pass_two() --- 最终确定跳转和字面量

zend_opcode.c:577-707。到这里:

  • IS_CONST 操作数被转换为字面量表索引
  • CV/TMP_VAR 索引被规范化为绝对调用帧偏移量
  • 跳转目标被解析为相对偏移量
  • 为每条指令设置 VM 处理器:ZEND_VM_SET_OPCODE_HANDLER(opline)
  • 设置 ZEND_ACC_DONE_PASS_TWO 标志

Opcache 优化器 --- 两级常量折叠和 CFG 死代码消除

这条流水线也适用于闭包。在 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 、 x、 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);    // 构建 CFG
  2013:   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 块。

为什么在 PHP 7.0 中 SSA/类型推断不存在

复制代码
  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 是一个整数 " 、 " x 是一个整数"、" 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

总结

这条流水线中有两个架构之美:

  1. 复用 static_variables 机制。 static i = 0 和 u s e ( i = 0 和 use( i=0和use(x) 使用完全相同的哈希表 (op_array->static_variables)、完全相同 ZEND_FETCH_R + FETCH_STATIC 指令,以及完全相同的运行时路径。

只有 IS_LEXICAL_VAR | IS_LEXICAL_REF 标志位将它们区分开来。没有额外的操作码,没有额外的 VM 分支。

  1. 编译时分离,运行时缝合。 闭包的 op_array 在编译时是一个完全独立的实体(分配时的 zend_arena_alloc,在 zend_begin_func_decl
    处注册)。父函数只持有 ZEND_DECLARE_LAMBDA_FUNCTION 作为指向它的指针。

只有在运行时,zend_create_closure 执行时,两部分才通过 memcpy + zval_copy_static_var → 捕获实际 use 值 + $this 绑定重新连接起来。

这种分离使得:

  • 优化器能将闭包视为独立单元(script->function_table 循环,第 687 行)
  • 同一闭包定义的不同实例之间共享 run_time_cache(第 575-578 行)
  • "延迟绑定" --- use 变量的值在闭包定义时解析,而不是在闭包声明时
相关推荐
weixin_440784119 小时前
【HandlerThread实现原理】
android·java·开发语言
天空'之城9 小时前
C 语言工业级通用组件手写 24:互补滤波
c语言·开发语言·互补滤波·嵌入式算法·工业级组件
有梦不弃9 小时前
模块化单体架构设计方案:DDD + 六边形架构落地实践
后端·架构
XR1234567889 小时前
工厂车间无线网络方案对比:AGV漫游哪家强?
开发语言·php
傻啦嘿哟10 小时前
某短视频平台视频爬虫实战:抓取推荐流视频信息,绕过反爬的3种技巧
开发语言·爬虫·python
現実君10 小时前
【硬件进阶】AD22上位替换JLEDA教程
开发语言·php
mubei-12310 小时前
SpringDAO的用法
java·开发语言·数据库
格林威13 小时前
多相机微秒级对齐:硬件触发 vs PTP(IEEE 1588)方案实战对比
开发语言·人工智能·数码相机·机器学习·计算机视觉·视觉检测·机器视觉
江南十四行13 小时前
Spring框架核心(上)——IoC控制反转与DI依赖注入详解
java·后端·spring