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.
 
 

231 lines
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. 第一阶段适用范围
仅优化以下形态:
```php
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:
```php
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. 生成模型
原始代码:
```php
$base = 10;
$callback = static function ($value) use ($base) {
return $base + $value;
};
$result = $callback(5);
```
目标代码的结构为:
```cpp
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++ 函数帧内,所以:
```php
$callback = function () use (&$number, &$text, &$items): void {
$number++;
$text .= '!';
$items[] = $number;
};
```
可直接生成:
```cpp
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 不保证不同函数实参之间的求值顺序。直接调用
不能简单输出:
```cpp
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。