TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

8.6 KiB

局部非逃逸 Closure 原生化设计

1. 背景

TypePHP 当前把所有匿名函数和箭头函数都实现为真正的 Zend Closure。编译器先生成 一个 php::ClosureFn,PHPX 再为每个闭包创建:

  • zend_function 及其参数信息;
  • 保存 C++ 回调、绑定对象和捕获值的 ClosureCarrier;
  • 由 zend_create_fake_closure() 创建的 Zend Closure 对象。

调用闭包时仍需经过 callable 解析、Zend 调用帧和参数数组。这个实现是 PHP 可见 Closure 的必要兼容路径,但对于只在当前函数内直接调用的闭包,Closure 对象从未被 观察,以上分配和动态调用没有语义价值。

本优化把编译器能够证明不逃逸的局部闭包生成为具体 C++ lambda,并把其调用点生成为 直接 C++ 调用。不能证明安全时必须完整回退到现有 Closure Carrier 路径。

2. 目标

第一阶段目标如下:

  1. 消除局部闭包创建时的 zend_function、Carrier 和 Zend Closure 分配;
  2. 消除每次调用的 callable 查询及 Zend execute frame;
  3. 保留 PHP 的闭包创建时捕获、按值复制、可变捕获状态及参数求值顺序;
  4. 不改变任何可能观察 Closure 对象身份或 Zend 调用帧的代码;
  5. 以保守证明为前提,分析失败不产生诊断,只回退到原实现。

本优化属于 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 有两个原因:

  1. 捕获在闭包创建位置形成快照,而不是引用外层变量;
  2. 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 生成语句前运行:

  1. 收集函数顶层 $var = Closure/ArrowFunction 候选;
  2. 遍历当前函数 AST,分类该变量的每个定义和使用;
  3. 任一使用不是直接调用目标即淘汰候选;
  4. 检查闭包和调用参数是否位于第一阶段支持集合;
  5. 把证明结果写入当前 FunctionContext;
  6. 赋值生成器在原始语句位置输出 auto lambda;
  7. 函数调用生成器识别该局部变量并输出直接调用;
  8. 普通局部变量声明生成器跳过已经在源码位置声明的 lambda。

分析数据只属于当前函数的 FunctionContext,不得写入 persistent/request runtime cache,也不得跨函数复用。

8. 安全不变量

实现必须始终满足:

  1. 无误优化:不能证明时回退,绝不猜测;
  2. 创建时捕获:不能把 lambda 初始化提升到原 PHP 表达式之前;
  3. 生命周期包含:lambda 及全部调用点位于同一 C++ 函数帧;
  4. 无 PHP 可见身份:优化后的值不能进入 zval、数组、属性、参数或返回值;
  5. 求值顺序一致:实参和闭包体中的副作用顺序必须与 PHP 一致;
  6. 异常边界一致:PHP 异常仍通过现有 C++ zend_object * 路径传播;
  7. 回退等价:移除优化标记后,同一源码必须仍可由 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。