8.6 KiB
局部非逃逸 Closure 原生化设计
1. 背景
TypePHP 当前把所有匿名函数和箭头函数都实现为真正的 Zend Closure。编译器先生成
一个 php::ClosureFn,PHPX 再为每个闭包创建:
zend_function及其参数信息;- 保存 C++ 回调、绑定对象和捕获值的
ClosureCarrier; - 由
zend_create_fake_closure()创建的 ZendClosure对象。
调用闭包时仍需经过 callable 解析、Zend 调用帧和参数数组。这个实现是 PHP 可见 Closure 的必要兼容路径,但对于只在当前函数内直接调用的闭包,Closure 对象从未被 观察,以上分配和动态调用没有语义价值。
本优化把编译器能够证明不逃逸的局部闭包生成为具体 C++ lambda,并把其调用点生成为 直接 C++ 调用。不能证明安全时必须完整回退到现有 Closure Carrier 路径。
2. 目标
第一阶段目标如下:
- 消除局部闭包创建时的
zend_function、Carrier 和 Zend Closure 分配; - 消除每次调用的 callable 查询及 Zend execute frame;
- 保留 PHP 的闭包创建时捕获、按值复制、可变捕获状态及参数求值顺序;
- 不改变任何可能观察 Closure 对象身份或 Zend 调用帧的代码;
- 以保守证明为前提,分析失败不产生诊断,只回退到原实现。
本优化属于 TypePHP 编译器内部实现,不新增 PHPX 公共 API。
3. 第一阶段适用范围
仅优化以下形态:
function calculate(int $value): int
{
$offset = 1;
$callback = static fn (int $item): int => $item + $offset;
return $callback($value);
}
候选闭包必须同时满足:
- 赋值是函数顶层的一条独立语句;
- 左值是普通局部变量;
- 该变量在函数中只有这一个定义;
- 至少存在一个调用点;
- 每个读取位置都只能是
$callback(...)的直接调用目标; - 赋值在源码和控制流上支配全部调用点;
- 调用只使用固定位置参数,暂不支持命名参数和 unpack;
- 参数没有默认值、引用或 variadic;
- 按值捕获,或对固定
int/string/float/bool/array存储的按引用捕获; - 闭包不是 generator,不按引用返回,也不包含嵌套函数或闭包;
- 闭包体不依赖 Zend 调用帧,例如不调用
func_get_args()或debug_backtrace(); - 第一阶段只处理全局函数体,不处理类方法中的
$this、词法 class scope 和 late static binding。
这里的“函数顶层”限制很重要。TypePHP 通常把普通局部变量声明提升到 C++ 函数入口,
但 C++ lambda 的匿名类型必须通过 auto 在初始化位置声明;同时 PHP 的按值捕获发生
在闭包表达式求值时,不能把初始化提前到函数入口。
4. 必须回退的情况
只要 Closure 值可能被 PHP 代码观察,就继续生成真实 Closure:
return $callback;
$array[] = $callback;
$object->callback = $callback;
$other = $callback;
array_map($callback, $values);
is_callable($callback);
new ReflectionFunction($callback);
以下情况也回退:
- 多次赋值、条件赋值、循环内创建或
unset(); - 动态
var-ref捕获、引用参数或递归闭包; - 不同闭包在同一个变量中汇合;
- generator Closure;
- 命名参数、参数展开、variadic 或默认参数;
- 捕获 Native Object、强类型容器或其他需要专门 GC root 的值;
- 嵌套闭包以及任何分析器未明确识别的使用方式。
回退不是错误,也不应产生 warning。优化器只能在正向证明完整时启用。
5. 生成模型
原始代码:
$base = 10;
$callback = static function ($value) use ($base) {
return $base + $value;
};
$result = $callback(5);
目标代码的结构为:
auto callback = [base = base](php::Var value) mutable -> php::Var {
return base + value;
};
result = callback(5);
使用显式 init-capture 有两个原因:
- 捕获在闭包创建位置形成快照,而不是引用外层变量;
mutable允许闭包内部修改自己的捕获副本,并让该状态在多次直接调用之间保留。
php::Var 的复制构造会解除普通 PHP reference,符合 use ($value) 的按值语义。
固定 int/string/float/bool/array 的 C++ 值则直接复制。第一阶段不接受需要特殊
生命周期处理的 Native Object 和 std container 捕获。
固定类型的引用捕获不需要先制造 Zend reference。因为分析器已经证明 lambda 不逃逸, 其生命周期完全包含在外层 C++ 函数帧内,所以:
$callback = function () use (&$number, &$text, &$items): void {
$number++;
$text .= '!';
$items[] = $number;
};
可直接生成:
auto callback = [&number, &text, &items]() mutable -> php::Var {
// ...
};
闭包内部把这三个名字登记为对应的 INT_REF/STR_REF/ARRAY_REF,继续复用 TypePHP
现有 typed-ref 赋值检查。这样没有 php::Ref、RefWrap、zend_reference 或写回步骤。
动态 php::Var 引用以及对象、resource、stream、Native Object、std container 等捕获
仍回退到真实 Zend Closure 路径。
第一阶段继续使用 php::Var 作为闭包参数和返回 ABI,以复用现有闭包体生成逻辑并
降低语义风险。后续可以在独立优化中,根据参数和返回声明把 ABI 收窄到
php::Int、php::Str 等固定类型。
6. 参数求值顺序
PHP 固定为从左到右求值参数,而 C++17 不保证不同函数实参之间的求值顺序。直接调用 不能简单输出:
callback(parse(a), parse(b));
多参数调用必须沿用 TypePHP 的 ordered operand 机制,在调用前按源码顺序物化存在 副作用的值,然后再调用 lambda。第一阶段即使参数表达式较简单,也不能依赖 C++ 编译器碰巧采用的顺序。
7. 分析与代码生成
新增局部 Closure 分析器,在函数 SSA 分析完成后、convert 生成语句前运行:
- 收集函数顶层
$var = Closure/ArrowFunction候选; - 遍历当前函数 AST,分类该变量的每个定义和使用;
- 任一使用不是直接调用目标即淘汰候选;
- 检查闭包和调用参数是否位于第一阶段支持集合;
- 把证明结果写入当前
FunctionContext; - 赋值生成器在原始语句位置输出
autolambda; - 函数调用生成器识别该局部变量并输出直接调用;
- 普通局部变量声明生成器跳过已经在源码位置声明的 lambda。
分析数据只属于当前函数的 FunctionContext,不得写入 persistent/request runtime
cache,也不得跨函数复用。
8. 安全不变量
实现必须始终满足:
- 无误优化:不能证明时回退,绝不猜测;
- 创建时捕获:不能把 lambda 初始化提升到原 PHP 表达式之前;
- 生命周期包含:lambda 及全部调用点位于同一 C++ 函数帧;
- 无 PHP 可见身份:优化后的值不能进入 zval、数组、属性、参数或返回值;
- 求值顺序一致:实参和闭包体中的副作用顺序必须与 PHP 一致;
- 异常边界一致:PHP 异常仍通过现有 C++
zend_object *路径传播; - 回退等价:移除优化标记后,同一源码必须仍可由 Carrier 路径编译运行。
9. 测试计划
正向测试至少覆盖:
- 无捕获箭头函数;
- 按值捕获及创建时快照;
- 捕获副本在多次调用之间保持内部修改;
- 固定 int/string/float/bool/array 的直接引用捕获;
- 普通匿名函数;
- 多次直接调用;
- 参数表达式的左到右求值。
回退测试至少覆盖:
- 作为参数传给
array_map(); - 从函数返回;
- 写入数组或属性;
- 赋给另一个变量;
- 动态
var-ref引用捕获; - 默认参数、variadic、unpack 和命名参数;
- generator、递归及嵌套 Closure;
- class method 中的
$this; ReflectionFunction、is_callable()等身份观察。
生成代码测试需要同时断言:正向候选不包含
newClosureWithParameters/ClosureCarrier 调用,而回退用例仍包含真实 Closure 创建。
10. 性能验证
benchmark/dynamic-call/benchmark.php 中的 closure_monomorphic 是第一阶段的主要性能
用例。还应保留 closure_alternating 作为不能原生化的对照组。
验证指标包括:
- Closure 创建次数和堆分配;
- 每次调用耗时;
- TypePHP/Zend PHP 比值;
- 生成 C++ 大小及编译时间;
- checksum 与 Zend PHP 一致。
性能提升不是放宽安全条件的理由。任何会暴露 Closure 对象或无法静态证明的场景, 即使处于热点,也继续使用 Closure Carrier。