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
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。
|
|
|