- 新增 AOT 编译器优化优先级重新评估文档,重点分析类型推断、去虚拟化、逃逸分析等核心优化策略 - 详细阐述 GCC/Clang 二次编译架构下的优化分工原则 - 新增 PHP Zend Optimizer/OPcache/JIT 设计分析文档,提取可借鉴的编译器优化技术 - 包含 SSA/e-SSA 构建、类型推断、SCCP、DCE、逃逸分析等关键技术的实现细节 - 提供 PHP 优化器位掩码类型系统的具体设计方案和算法实现 - 分析 PHP 的稀疏条件数据流框架(SCDF)和通用优化 pass 流水线架构pull/1/head
parent
195013ae7c
commit
46800d9dc9
3 changed files with 1673 additions and 0 deletions
@ -0,0 +1,432 @@ |
|||||||
|
# AOT 编译器优化优先级重新评估:考虑 GCC/Clang 二次编译 |
||||||
|
|
||||||
|
## 核心原则 |
||||||
|
|
||||||
|
AOT 的优化价值不在于做 GCC/Clang 已经做了的事情,而在于**提供 GCC/Clang 无法从 C++ 代码中推断出来的信息**。 |
||||||
|
|
||||||
|
GCC/Clang 在 `-O2`/`-O3` 下已经能做的: |
||||||
|
- 常量折叠、常量传播 (SCCP) |
||||||
|
- 死代码消除 (DCE) |
||||||
|
- 公共子表达式消除 (CSE) |
||||||
|
- 循环展开、向量化 |
||||||
|
- 指令选择、寄存器分配 |
||||||
|
- 函数内联 (同一编译单元内) |
||||||
|
- 分支预测优化 |
||||||
|
- SIMD 自动向量化 |
||||||
|
|
||||||
|
GCC/Clang **不能做的**——因为语义被 `php::Var`、`php::Object`、虚函数调用等抽象层遮蔽: |
||||||
|
- 将 `php::Var` 窄化为具体 C++ 类型 |
||||||
|
- 消除 `php::Object` 的虚调用 |
||||||
|
- 消除引用计数操作 |
||||||
|
- 将 `php::Array` 从堆分配移到栈分配 |
||||||
|
- 将 `php::BigInt` 降级为原生 `int64_t` |
||||||
|
- 消除 `php::call` 函数查找/分发开销 |
||||||
|
- 跨翻译单元的全局分析(LTO 部分可以,但受限于可见性) |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 重新排序的优先级 |
||||||
|
|
||||||
|
### Tier 1: 直接改善 C++ 代码形状(GCC 无法修复) |
||||||
|
|
||||||
|
这些优化改变的是**生成出来的是什么 C++ 代码**,而不是优化已达成的 C++ 代码。此处是 AOT 的最核心价值。 |
||||||
|
|
||||||
|
#### #1 类型推断 → 生成具体 C++ 类型而非 php::Var |
||||||
|
|
||||||
|
**问题:** 当前 AOT 将大量变量映射为 `php::Var`(全局类型),GCC 只能对 `php::Var` 的成员函数做有限优化。 |
||||||
|
|
||||||
|
**收益:** |
||||||
|
``` |
||||||
|
// 当前生成的代码 (GCC 能做的不多) |
||||||
|
php::Var a = php::toInt(x); |
||||||
|
php::Var b = php::toInt(y); |
||||||
|
php::Var c = php::add(a, b); // GCC 看不到这是加法 |
||||||
|
|
||||||
|
// 类型推断后 |
||||||
|
int64_t a = php::toInt(x); |
||||||
|
int64_t b = php::toInt(y); |
||||||
|
int64_t c = a + b; // GCC 可以进行所有整数优化 |
||||||
|
``` |
||||||
|
|
||||||
|
**对 GCC 二次编译的价值放大:** 当类型变成 `int64_t` 后,GCC 可以进一步: |
||||||
|
- 寄存器分配(不再走 `php::Var` 的内存布局) |
||||||
|
- 常量折叠和传播 |
||||||
|
- 循环优化(循环内的 `php::Var` 操作变成纯整数运算) |
||||||
|
- 自动 SIMD 化 |
||||||
|
|
||||||
|
**实施建议:** 这是**最高优先级**。不需要完整的 SSA 形式,只需在每个赋值点推断最精确的类型,并在 C++ 代码生成时使用该类型声明变量。已经做的 union/nullable 类型检查基础设施可以扩展为类型推断。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
#### #2 去虚拟化 (Devirtualization) |
||||||
|
|
||||||
|
**问题:** PHP 所有非 final 方法调用都是虚调用。AOT 生成 `obj->method()`(C++ 虚函数调用),即使只有一个子类实现了该方法。 |
||||||
|
|
||||||
|
**收益:** |
||||||
|
``` |
||||||
|
// 当前生成 (虚调用,GCC 不敢消除 vtable 查找) |
||||||
|
return self->foo(); // virtual call |
||||||
|
|
||||||
|
// 去虚拟化后 (GCC 可以内联这个调用!) |
||||||
|
return Aot_MyClass_foo(self); // direct function call |
||||||
|
``` |
||||||
|
|
||||||
|
**突破口:** 编译期收集所有类及其继承关系。如果: |
||||||
|
- 方法是 private → 永远直接调用 |
||||||
|
- 方法是 final → 永远直接调用 |
||||||
|
- 类在所有编译文件中只有 1 个非抽象实现 → 直接调用 |
||||||
|
- `self::foo()` 调用自己类的方法 → 可以直接调用(如果该类没有未发现的子类) |
||||||
|
|
||||||
|
**与 GCC 的协同:** 一旦变成直接调用,GCC 可以: |
||||||
|
- 内联整个函数体 |
||||||
|
- 跨函数常量传播 |
||||||
|
- 连锁消除后续的冗余操作 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
#### #3 逃逸分析 → 栈分配 + 引用计数消除 |
||||||
|
|
||||||
|
**问题:** 每个 `new` 对象/数组都在堆上分配,且通过引用计数管理生命周期。 |
||||||
|
|
||||||
|
**收益:** |
||||||
|
``` |
||||||
|
// 当前 (堆分配 + refcount) |
||||||
|
php::Array arr = php::newArray(); |
||||||
|
arr.set("key", value); // refcount 操作 |
||||||
|
return arr; // 拷贝 + refcount |
||||||
|
|
||||||
|
// 逃逸分析后 (栈分配 + 无 refcount) |
||||||
|
zend_array arr; // 栈上 |
||||||
|
zend_hash_update(&arr, "key", value); // 无 refcount |
||||||
|
return php::Array::fromStack(std::move(arr)); // 仅在返回点打包 |
||||||
|
``` |
||||||
|
|
||||||
|
**对 GCC 二次编译的影响:** 这是对 GCC 帮助**最大**的优化,因为: |
||||||
|
- 堆分配 → 栈分配:消除了 `malloc` 调用,GCC 可以完全优化栈布局 |
||||||
|
- 消除 refcount:GCC 不需要分析互锁操作的副作用,可以自由重排指令 |
||||||
|
- GCC 可以在栈变量上做 SROA(Scalar Replacement of Aggregates),把数组/对象拆散成标量 |
||||||
|
|
||||||
|
**实施建议:** 即使是简单的局部逃逸分析(只分析对象是否被传到函数外部)也能消除大量分配。复杂版本(跨函数逃逸分析)收益继续增长。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
#### #4 调用图驱动的跨文件内联 |
||||||
|
|
||||||
|
**问题:** 同一个 PHP 项目可能编译成多个 `.cc` 文件,GCC 的 LTO 可以跨文件内联但受限于编译时间。 |
||||||
|
|
||||||
|
**收益:** AOT 在编译时就知道整个调用图,可以在**生成 C++ 代码时就决定内联**: |
||||||
|
|
||||||
|
``` |
||||||
|
// 当前 |
||||||
|
auto result = aot_smallHelper(x, y); // 函数调用,即使函数体只有一行 |
||||||
|
|
||||||
|
// 内联后 |
||||||
|
auto result = x + y; // GCC 可以继续优化 |
||||||
|
``` |
||||||
|
|
||||||
|
**关键判断信息:** |
||||||
|
- 函数体大小(< 10 行 → 内联候选) |
||||||
|
- 调用次数(只调用 1 次 → 内联可以完全消除函数) |
||||||
|
- 递归标记(递归函数不内联) |
||||||
|
- 跨函数常量传播机会(实参是常量 → 内联后 GCC 可以折叠整个函数) |
||||||
|
|
||||||
|
**与 GCC 的协同:** AOT 做"决策"(该不该内联),把内联后的代码直接生成到调用点。GCC 在更大的内联体上继续做优化。AOT 掌握 GCC 不掌握的信息(来自调用图的全局视角)。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
### Tier 2: 为 GCC 提供更精确的类型信息 |
||||||
|
|
||||||
|
这些优化改善的是传递给 GCC 的 C++ 类型质量。 |
||||||
|
|
||||||
|
#### #5 整数范围推断 → 选择最优整数类型 |
||||||
|
|
||||||
|
**收益:** |
||||||
|
```php |
||||||
|
// PHP 源码: 循环计数器,0 到 100 |
||||||
|
for ($i = 0; $i < 100; $i++) { ... } |
||||||
|
|
||||||
|
// 当前 AOT 生成 |
||||||
|
int64_t i = 0; // 总是用 int64_t (防止溢出) |
||||||
|
|
||||||
|
// 范围推断后 |
||||||
|
int8_t i = 0; // 0..100 足够,更好的缓存局部性 |
||||||
|
// 或者至少 |
||||||
|
int64_t i = 0; // 但标记 "no overflow possible",不插入 BigInt 升级 |
||||||
|
``` |
||||||
|
|
||||||
|
**对 GCC 的影响:** |
||||||
|
- 更小的类型 → 更好的向量化(更多元素装入 SIMD 寄存器) |
||||||
|
- "不会溢出" 的断言 → GCC 的 VRP (Value Range Propagation) 可以基于这个前提做更激进的优化 |
||||||
|
|
||||||
|
#### #6 避免不必要的 BigInt/BigFloat 分配 |
||||||
|
|
||||||
|
**相关于 #5。** PHP 的整数运算在溢出时自动升级为浮点或 BigInt。如果 AOT 能证明不会溢出,就不需要生成升级代码。 |
||||||
|
|
||||||
|
``` |
||||||
|
// 当前 (每个 int 运算都要考虑溢出) |
||||||
|
php::Var result = php::BigInt::add(php::toBigInt(a), php::toBigInt(b)); |
||||||
|
|
||||||
|
// 范围推断后 (不会有溢出) |
||||||
|
int64_t result = a + b; // 纯整数,GCC 的世界 |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
### Tier 3: 与 GCC 部分重叠但仍有价值 |
||||||
|
|
||||||
|
#### #7 常量折叠(PHP 层级) |
||||||
|
|
||||||
|
**GCC 能做的:** C++ 层面的常量表达式在 GCC 的 SCCP pass 中完全折叠。 |
||||||
|
|
||||||
|
**AOT 独有的价值:** |
||||||
|
- PHP 特有的常量:`PHP_INT_MAX`、`PHP_VERSION`、`__DIR__` 等在编译时完全解析 |
||||||
|
- 跨 PHP 命名空间/类名的常量解析(GCC 看不到 PHP 的符号语义) |
||||||
|
- PHP 内建函数的结果:`strlen("hello")` → 5(GCC 不知道 `strlen` 的内部实现但是可以折叠已知参数的函数) |
||||||
|
|
||||||
|
**评估:** 有限价值。大部分 PHP 源码中没有复杂的编译时可求值常量表达式。 |
||||||
|
|
||||||
|
#### #8 死代码消除(PHP 层级) |
||||||
|
|
||||||
|
**GCC 能做的:** GCC 的 DCE + unreachable block elimination 非常成熟。 |
||||||
|
|
||||||
|
**AOT 独有的价值:** |
||||||
|
- 基于 PHP 类型系统消除分支:`if (false)` 在 PHP 层消除 → 不生成任何 C++ 代码 |
||||||
|
- 基于类型窄化消除分支:`if ($x instanceof Foo)` 当 `$x` 的类型已确定为 `Bar` 且 `Foo ⊄ Bar` 时,false 分支不可达 |
||||||
|
- 消除未被调用的 PHP 函数/类(跨文件死代码) |
||||||
|
|
||||||
|
**评估:** 中等价值。AOT 应该关注"基于 PHP 语义的 DCE",而不是与 GCC 竞争"基于 C++ 语义的 DCE"。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
### Tier 4: 改善编译过程而非输出质量 |
||||||
|
|
||||||
|
#### #9 文件缓存(编译加速) |
||||||
|
|
||||||
|
**GCC 能做的:** ccache。但 ccache 需要文件内容哈希匹配。 |
||||||
|
|
||||||
|
**AOT 独有的价值:** |
||||||
|
- 缓存 PHP→C++ 的翻译结果(解析过的 AST + 类型信息) |
||||||
|
- 跨重启复用 |
||||||
|
- 增量更新(只重编译改动的 PHP 文件) |
||||||
|
|
||||||
|
**评估:** 有用但非核心。先让输出代码质量达到最优,再优化编译速度。 |
||||||
|
|
||||||
|
#### #10 Pass Pipeline 架构 |
||||||
|
|
||||||
|
工程架构层面的基础设施,提高代码可维护性和扩展性。不直接影响输出质量。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 优先级汇总 |
||||||
|
|
||||||
|
``` |
||||||
|
必须做 (直接影响 GCC 看到的 C++ 类型): |
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
||||||
|
⭐ #1 类型推断 → 避免 php::Var 泛化 【最大单点收益】 |
||||||
|
⭐ #2 去虚拟化 → 消除虚调用开销 【第二大收益】 |
||||||
|
⭐ #3 逃逸分析 → 栈分配 + 消除 refcount 【对数值计算代码收益极大】 |
||||||
|
|
||||||
|
强烈建议 (给 GCC 更好的前提条件): |
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
||||||
|
⭐ #4 调用图内联 → 跨越翻译单元边界 |
||||||
|
⭐ #5 范围推断 → 最优整数类型选择 |
||||||
|
⭐ #6 消除 BigInt/BigFloat 不必要分配 |
||||||
|
|
||||||
|
锦上添花 (有独特价值但非核心): |
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
||||||
|
#7 PHP 层常量折叠 |
||||||
|
#8 PHP 语义层 DCE |
||||||
|
#9 编译缓存加速 |
||||||
|
|
||||||
|
工程基础: |
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
||||||
|
#10 Pass Pipeline 架构 |
||||||
|
``` |
||||||
|
|
||||||
|
## #1 类型推断的进一步分析 |
||||||
|
|
||||||
|
这是最大单点收益项,值得进一步展开。以下是具体的技术路径: |
||||||
|
|
||||||
|
### 关键洞察:不需要完整 SSA |
||||||
|
|
||||||
|
AOT 是**生成 C++ 代码**,不是运行优化后的字节码。这意味着: |
||||||
|
|
||||||
|
1. 不需要 φ 函数:C++ 的变量赋值天然就是 SSA 的"新鲜变量"形式 |
||||||
|
2. 不需要 dominance frontier 计算:C++ 编译器会自己做 |
||||||
|
3. 只需要在**每个表达式**计算其可能的类型,生成对应的 C++ 类型声明 |
||||||
|
|
||||||
|
### 当前问题 |
||||||
|
|
||||||
|
``` |
||||||
|
PHP 源码: $a = 1; |
||||||
|
$b = $a + 2; |
||||||
|
生成的 C++: php::Var a = php::toInt(php::toVar(1)); |
||||||
|
php::Var b = php::add(a, php::toVar(2)); // !!全 Var!! |
||||||
|
``` |
||||||
|
|
||||||
|
此时 GCC 看到的: |
||||||
|
- 所有值都是 `php::Var`(一个复杂结构体) |
||||||
|
- 运算通过 `php::add()` 函数(外部符号,不可内联) |
||||||
|
- 无法确定类型,无法窄化 |
||||||
|
|
||||||
|
### 目标 |
||||||
|
|
||||||
|
``` |
||||||
|
PHP 源码: $a = 1; |
||||||
|
$b = $a + 2; |
||||||
|
生成的 C++: int64_t a = 1; |
||||||
|
int64_t b = a + 2; // GCC 可以完整优化 |
||||||
|
``` |
||||||
|
|
||||||
|
### 实现路径 |
||||||
|
|
||||||
|
**Phase A: 局部类型传播(~300 行)** |
||||||
|
|
||||||
|
遍历 AST,在每个赋值点: |
||||||
|
1. 如果 RHS 是字面量 → 类型明确,记录 |
||||||
|
2. 如果 RHS 是已知类型的变量 → 传播类型 |
||||||
|
3. 如果 RHS 是二元运算且两操作数类型已知 → 推断结果类型 |
||||||
|
4. 如果变量被多次赋值为不同类型 → 退化为 `php::Var` |
||||||
|
|
||||||
|
**Phase B: 条件窄化(~200 行,依赖 TypeSpecifier)** |
||||||
|
|
||||||
|
``` |
||||||
|
if ($x instanceof MyClass) { // 进入此分支时 $x 的取值确定是 MyClass |
||||||
|
$x->method(); // 这里可以用直接调用 |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
**Phase C: 跨基本块交汇(~200 行)** |
||||||
|
|
||||||
|
``` |
||||||
|
if (cond) { $x = 1; } else { $x = 2; } |
||||||
|
// 交汇点: $x 是 int64_t (两个分支都是整数) |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 类型窄化的安全性约束:PHP instanceof 语义分析 |
||||||
|
|
||||||
|
### 问题 |
||||||
|
|
||||||
|
用户指出:`if ($x instanceof Foo)` 在 PHP 中的语义是——`$x` 可能是 `Foo` 的实例**或其任何子类**的实例。这与 C++ 的 `dynamic_cast<Foo*>` 相同。 |
||||||
|
|
||||||
|
这意味着: |
||||||
|
|
||||||
|
```php |
||||||
|
if ($x instanceof Foo) { |
||||||
|
$x->method(); // 虚调用!$x 的实际类可能重写了 method() |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
**不能**因为窄化为 `Foo` 就把 `$x->method()` 转为直接调用 `Foo::method()`——除非 `Foo` 是 final 类或 method 是 private。 |
||||||
|
|
||||||
|
### 窄化的三个安全层级 |
||||||
|
|
||||||
|
#### 层级 1: 类型成员可访问性(总是安全) |
||||||
|
|
||||||
|
即使不知道精确类型,只要知道 `$x` 是 `Foo` 的子类型,就可以: |
||||||
|
- 确认 `$x` 有某个方法/属性的访问能力 |
||||||
|
- 消除不可能的代码路径(`if ($x instanceof Foo)` 在 `$x` 已知为 `Bar` 且 `Foo` 与 `Bar` 不相关时为 false) |
||||||
|
- 生成正确的 C++ 类型标注(`Foo*` 而非 `void*`/`php::Var`) |
||||||
|
|
||||||
|
```php |
||||||
|
// 窄化前: $x 是 php::Var,调用 foo() 可能要抛异常 |
||||||
|
// 窄化后: $x 是 Foo 的某个子类型,一定有 foo() 方法 |
||||||
|
$x->foo(); // 可以确认方法存在(虽然仍是虚调用) |
||||||
|
``` |
||||||
|
|
||||||
|
**收益:** 消除方法不存在的运行时检查,改进错误检测(编译期发现)。 |
||||||
|
|
||||||
|
#### 层级 2: final 类 + private 方法(完全安全) |
||||||
|
|
||||||
|
| 场景 | 可窄化为精确类型? | 可直接调用? | |
||||||
|
|------|-------------------|------------| |
||||||
|
| `$x instanceof FinalClass` | 是 | 是 | |
||||||
|
| `$x->privateMethod()` | 是 (private 不可重写) | 是 | |
||||||
|
| `self::method()` 且类无子类 | 是 | 是 | |
||||||
|
| `$x instanceof NonFinalClass` | 否 | 否 | |
||||||
|
|
||||||
|
```php |
||||||
|
final class Logger { |
||||||
|
public function log(string $msg): void { ... } |
||||||
|
} |
||||||
|
|
||||||
|
if ($x instanceof Logger) { |
||||||
|
$x->log("test"); // 安全:直接调用 Aot_Logger_log($x, "test") |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
#### 层级 3: 有限子类集合 + 守卫(guard-based devirtualization) |
||||||
|
|
||||||
|
当 `Foo` 不是 final,但 AOT 编译了整个代码库,可以确定**所有 Foo 的子类**: |
||||||
|
|
||||||
|
``` |
||||||
|
已知类层次: |
||||||
|
Foo (abstract) |
||||||
|
├── SubFooA method() 实现 |
||||||
|
└── SubFooB method() 实现 |
||||||
|
``` |
||||||
|
|
||||||
|
此时 `$x instanceof Foo` 意味着 `$x` 只能是 `SubFooA` 或 `SubFooB`。可以生成: |
||||||
|
|
||||||
|
```cpp |
||||||
|
// 守卫式去虚拟化 |
||||||
|
if (x.getInstanceOf(php_get_class(SubFooA))) { |
||||||
|
Aot_SubFooA_method(x); // 直接调用 |
||||||
|
} else { |
||||||
|
Aot_SubFooB_method(x); // 直接调用(最后一种不用判断) |
||||||
|
} |
||||||
|
// 替代原来的: x->method(); (虚调用) |
||||||
|
``` |
||||||
|
|
||||||
|
**收益:** 对于已知有限子类的类层次,用 if-else 分发替代虚表查找。如果只有 1 个子类(de facto final),则完全消除分支。 |
||||||
|
|
||||||
|
### PHP 自身的处理方式 |
||||||
|
|
||||||
|
Zend SSA 的 `zend_ssa_var_info` 结构体精确地追踪了这个区别: |
||||||
|
|
||||||
|
```c |
||||||
|
typedef struct _zend_ssa_var_info { |
||||||
|
uint32_t type; |
||||||
|
zend_class_entry *ce; |
||||||
|
bool is_instanceof : 1; // 0 = class == ce, 1 = may be child of ce |
||||||
|
// ... |
||||||
|
} zend_ssa_var_info; |
||||||
|
``` |
||||||
|
|
||||||
|
- `is_instanceof = false`:变量**精确是这个类**(来自 `new Foo()` 或 `get_class($x) === 'Foo'` 检查) |
||||||
|
- `is_instanceof = true`:变量**可能是这个类或其子类**(来自 `$x instanceof Foo`) |
||||||
|
|
||||||
|
只有 `is_instanceof = false` 且 ce 非 abstract 时,才能安全地去虚拟化。 |
||||||
|
|
||||||
|
### 对 Tier 1 优化的影响 |
||||||
|
|
||||||
|
| 优化 | 受 instanceof 语义影响? | 有效范围 | |
||||||
|
|------|------------------------|---------| |
||||||
|
| #1 类型推断 (生成具体 C++ 类型) | 不直接受影响 | 窄化为 `Foo*` 仍然有价值(比 `php::Var` 好) | |
||||||
|
| #2 去虚拟化 | **是——instanceof 不提供精确类型** | 仅 final 类、private 方法、有限子类守卫 | |
||||||
|
| #3 逃逸分析 | 不直接受影响 | 不需要精确类型,只需逃逸/非逃逸判断 | |
||||||
|
| #4 调用图内联 | 不直接受影响 | 调用图分析的是函数关系,与 instanceof 窄化无关 | |
||||||
|
| #5 范围推断 | 不直接受影响 | 整数范围与 OOP 继承无关 | |
||||||
|
|
||||||
|
### 关键结论 |
||||||
|
|
||||||
|
`instanceof` 窄化虽然不足以确定性地去虚拟化所有调用,但它提供了**类型成员可见性**的保证,可以消除方法查找、改进错误检测、以及生成更精确的 C++ 类型签名(`Foo*` vs `php::Var`)。 |
||||||
|
|
||||||
|
真正能推动去虚拟化的信息来自: |
||||||
|
1. **Final 类** — 由源码声明提供 |
||||||
|
2. **全程序类层次分析** — 编译所有代码后可知哪些类是 de facto final(无子类在代码库中) |
||||||
|
3. **调用点特定的类型推断** — 从 `new Foo()` 赋值追踪到调用点 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 结论 |
||||||
|
|
||||||
|
在 GCC/Clang 二次编译的架构下,AOT 编译器的优化策略应该是: |
||||||
|
|
||||||
|
1. **把 PHP 的动态类型信息"冻结"为静态 C++ 类型**——这是 GCC 无法自己做的 |
||||||
|
2. **把虚调用"落地"为直接调用**——让 GCC 可以内联 |
||||||
|
3. **把堆分配"提升"为栈分配**——让 GCC 可以 SROA |
||||||
|
4. **不要与 GCC 竞争 SSA 层面的优化**——SCCP、DCE、CSE 留给 GCC |
||||||
|
5. **关注跨翻译单元的信息**——调用图、类层次——这是 LTO 也难以覆盖的 |
||||||
@ -0,0 +1,682 @@ |
|||||||
|
# PHP Zend Optimizer / OPcache / JIT 设计分析:可引入 AOT 编译器的机制 |
||||||
|
|
||||||
|
本文档分析了 php-src (v8.4.14) 中 Zend Optimizer、OPcache 和 JIT 的实现,识别出可引入 AOT 编译器的设计模式、算法和数据流框架。 |
||||||
|
|
||||||
|
源码位置: |
||||||
|
- 优化器: `~/soft/php/php-8.4.14/Zend/Optimizer/` |
||||||
|
- OPcache: `~/soft/php/php-8.4.14/ext/opcache/` |
||||||
|
- JIT: `~/soft/php/php-8.4.14/ext/opcache/jit/` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 概览 |
||||||
|
|
||||||
|
PHP 的优化器是本领域最成熟的动态语言高端优化器之一。它包含完整的 SSA (Static Single Assignment) 构建、e-SSA (Extended SSA) 类型/范围推断、SCCP (Sparse Conditional Constant Propagation)、逃逸分析、死代码消除、调用图分析、profile-guided tracing JIT 等完整编译器优化基础设施。 |
||||||
|
|
||||||
|
AOT 编译器可以从以下方面借鉴: |
||||||
|
|
||||||
|
| 优先级 | 设计/模块 | 实施量级 | 收益 | |
||||||
|
|--------|----------|---------|------| |
||||||
|
| P1 | Pass Pipeline 架构 | ~100 行框架 | 架构清晰、可插拔、分 O0/O1/O2 | |
||||||
|
| P1 | SSA + e-SSA 构建 | 中 | 所有高级优化的基础 | |
||||||
|
| P1 | 类型推断 (Type & Range Inference) | 中 | 精确的类型推导能力 | |
||||||
|
| P2 | SCDF 通用数据流框架 | ~300 行 | 可复用于 SCCP/类型推断/优化 | |
||||||
|
| P2 | SCCP 常量传播 | 中 | 条件常量折叠 + 不可达代码消除 | |
||||||
|
| P2 | DCE 死代码消除 | ~400 行 | worklist 驱动的精确 DCE | |
||||||
|
| P3 | 逃逸分析 (Escape Analysis) | ~500 行 | 栈分配、消除引用计数 | |
||||||
|
| P3 | 调用图 (Call Graph) | ~400 行 | 跨函数分析、内联决策、死代码 | |
||||||
|
| P4 | JIT IR 框架 | 极重 | 参考其 IR 设计的抽象层次 | |
||||||
|
| P4 | OPcache 持久化/File Cache | 轻 | 序列化优化后的 IR 缓存 | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 1. Pass Pipeline 架构 |
||||||
|
|
||||||
|
### 设计 |
||||||
|
|
||||||
|
PHP 定义了 16 个优化 Pass,每个 Pass 对应一个 bitmask 位(`zend_optimizer.h:28-46`): |
||||||
|
|
||||||
|
```c |
||||||
|
#define ZEND_OPTIMIZER_PASS_1 (1<<0) // 简单局部优化 (常量替换/折叠) |
||||||
|
#define ZEND_OPTIMIZER_PASS_2 (1<<1) // |
||||||
|
#define ZEND_OPTIMIZER_PASS_3 (1<<2) // Jump 优化 |
||||||
|
#define ZEND_OPTIMIZER_PASS_4 (1<<3) // INIT_FCALL_BY_NAME -> DO_FCALL |
||||||
|
#define ZEND_OPTIMIZER_PASS_5 (1<<4) // CFG 优化 (block pass) |
||||||
|
#define ZEND_OPTIMIZER_PASS_6 (1<<5) // DFA 优化 (type/range inference → 单函数) |
||||||
|
#define ZEND_OPTIMIZER_PASS_7 (1<<6) // CALL GRAPH 优化 (跨函数分析) |
||||||
|
#define ZEND_OPTIMIZER_PASS_8 (1<<7) // SCCP (常量传播) |
||||||
|
#define ZEND_OPTIMIZER_PASS_9 (1<<8) // 临时变量优化 |
||||||
|
#define ZEND_OPTIMIZER_PASS_10 (1<<9) // NOP 移除 |
||||||
|
#define ZEND_OPTIMIZER_PASS_11 (1<<10) // 合并相同常量 |
||||||
|
#define ZEND_OPTIMIZER_PASS_12 (1<<11) // 调整栈使用 |
||||||
|
#define ZEND_OPTIMIZER_PASS_13 (1<<12) // 移除未使用变量 |
||||||
|
#define ZEND_OPTIMIZER_PASS_14 (1<<13) // DCE (死代码消除) |
||||||
|
#define ZEND_OPTIMIZER_PASS_15 (1<<14) // 收集常量 (unsafe) |
||||||
|
#define ZEND_OPTIMIZER_PASS_16 (1<<15) // 函数内联 |
||||||
|
``` |
||||||
|
|
||||||
|
### 流水线调度 |
||||||
|
|
||||||
|
`zend_optimize()` 函数 (`zend_optimizer.c:1067-1183`) 按顺序执行各个 pass,每个 pass 只处理已执行 pass 产生的结果: |
||||||
|
|
||||||
|
``` |
||||||
|
pass1 (常量折叠) → pass3 (跳转优化) → pass4 (函数调用优化) |
||||||
|
→ pass5 (CFG) → pass6 (DFA + 类型推断) → pass9 (临时变量) |
||||||
|
→ pass10 (NOP移除) → pass11 (常量合并) → pass13 (变量清理) → ... |
||||||
|
``` |
||||||
|
|
||||||
|
当 `PASS_6 + PASS_7` 同时启用时,`zend_optimize_script()` 走更复杂的调用图路径: |
||||||
|
``` |
||||||
|
build_call_graph → zend_optimize (per-func) → analyze_call_graph |
||||||
|
→ 构建 call_map → dfa_analyze_op_array (per-func) |
||||||
|
→ dfa_optimize_op_array (per-func, with call context) |
||||||
|
→ pass9 → pass11 → pass13 → pass12 (stack adjust) → redo_pass_two |
||||||
|
``` |
||||||
|
|
||||||
|
### 关键设计点 |
||||||
|
|
||||||
|
**1. Bitmask 开关 + 注册式 Pass** |
||||||
|
|
||||||
|
用户可以按 bitmask 组合任意 pass。同时支持 `zend_optimizer_register_pass()` 注册外部 pass(如 JIT 自己的优化 pass): |
||||||
|
|
||||||
|
```c |
||||||
|
static struct { |
||||||
|
zend_optimizer_pass_t pass[ZEND_OPTIMIZER_MAX_REGISTERED_PASSES]; |
||||||
|
int last; |
||||||
|
} zend_optimizer_registered_passes; |
||||||
|
``` |
||||||
|
|
||||||
|
注册的 pass 在所有内置 pass 之后执行(`zend_optimizer_call_registered_passes`)。 |
||||||
|
|
||||||
|
**2. 每个 pass 可选 dump 输出** |
||||||
|
|
||||||
|
通过 `debug_level` 控制,支持在任意 pass 前后输出中间表示,用于调试和性能分析。 |
||||||
|
|
||||||
|
**3. Per-function 和 script-level 双层优化** |
||||||
|
|
||||||
|
- `zend_optimize(op_array, ctx)` — 单函数优化,保守(不利用跨函数信息) |
||||||
|
- `zend_optimize_script(script, ...)` — 全脚本优化 with call graph,可做跨函数优化 |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
AOT 编译器可以定义类似的 pass pipeline: |
||||||
|
|
||||||
|
```php |
||||||
|
enum AotPass: int { |
||||||
|
case CONSTANT_FOLD = 1 << 0; |
||||||
|
case TYPE_CHECK_INSERT = 1 << 1; |
||||||
|
case ESCAPE_ANALYSIS = 1 << 2; |
||||||
|
case DEVIRTUALIZE = 1 << 3; |
||||||
|
case DEAD_CODE_ELIM = 1 << 4; |
||||||
|
case FUNCTION_INLINE = 1 << 5; |
||||||
|
case LOOP_OPTIMIZE = 1 << 6; |
||||||
|
case BOX_ALLOC_ELIM = 1 << 7; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
按优化级别组合: |
||||||
|
|
||||||
|
```php |
||||||
|
const O0 = AotPass::TYPE_CHECK_INSERT->value; // 必需的基础代码生成 |
||||||
|
const O1 = O0 | AotPass::CONSTANT_FOLD->value; // 基础优化 |
||||||
|
const O2 = O1 | AotPass::DEVIRTUALIZE->value | AotPass::FUNCTION_INLINE->value; |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 2. SSA (Static Single Assignment) + e-SSA |
||||||
|
|
||||||
|
### 数据结构 |
||||||
|
|
||||||
|
SSA 构建在控制流图 (CFG) 之上: |
||||||
|
|
||||||
|
**CFG (`zend_cfg.h:84-92`):** |
||||||
|
```c |
||||||
|
typedef struct _zend_cfg { |
||||||
|
int blocks_count; // 基本块数量 |
||||||
|
int edges_count; // 边数量 |
||||||
|
zend_basic_block *blocks; // 基本块数组 |
||||||
|
int *predecessors; // 前驱列表 |
||||||
|
uint32_t *map; // opnum → block 映射 |
||||||
|
uint32_t flags; |
||||||
|
} zend_cfg; |
||||||
|
|
||||||
|
typedef struct _zend_basic_block { |
||||||
|
int *successors; // 后继块索引 |
||||||
|
uint32_t flags; |
||||||
|
uint32_t start; // 起始 opcode |
||||||
|
uint32_t len; // opcode 数 |
||||||
|
int successors_count; |
||||||
|
int predecessors_count; |
||||||
|
int idom; // 立即支配者 |
||||||
|
int loop_header; // 最近循环头 |
||||||
|
int level; // 支配树深度 |
||||||
|
int children; // 支配的子块链表 |
||||||
|
} zend_basic_block; |
||||||
|
``` |
||||||
|
|
||||||
|
**SSA (`zend_ssa.h:135-143`):** |
||||||
|
```c |
||||||
|
typedef struct _zend_ssa { |
||||||
|
zend_cfg cfg; // 控制流图 |
||||||
|
int vars_count; // SSA 变量数 |
||||||
|
int sccs; // 强连通分量数 |
||||||
|
zend_ssa_block *blocks; // 每基本块的 φ 函数 |
||||||
|
zend_ssa_op *ops; // 每指令的 use-def 信息 |
||||||
|
zend_ssa_var *vars; // 每个 SSA 变量的 def-use 链 |
||||||
|
zend_ssa_var_info *var_info; // 类型推断结果 (类型位掩码 + 范围) |
||||||
|
} zend_ssa; |
||||||
|
``` |
||||||
|
|
||||||
|
**SSA Op (`zend_ssa.h:82-92`):** |
||||||
|
```c |
||||||
|
typedef struct _zend_ssa_op { |
||||||
|
int op1_use; |
||||||
|
int op2_use; |
||||||
|
int result_use; |
||||||
|
int op1_def; // 这个指令定义的 SSA 变量 |
||||||
|
int op2_def; |
||||||
|
int result_def; |
||||||
|
int op1_use_chain; // use-def 链 |
||||||
|
int op2_use_chain; |
||||||
|
int res_use_chain; |
||||||
|
} zend_ssa_op; |
||||||
|
``` |
||||||
|
|
||||||
|
### e-SSA: Extended SSA with Pi Nodes |
||||||
|
|
||||||
|
这是 PHP 优化器最精妙的设计之一。Pi node 是一种特殊的 φ 函数,用于表示从条件分支推断出的类型/范围约束。 |
||||||
|
|
||||||
|
**Pi 约束 (`zend_ssa.h:42-59`):** |
||||||
|
```c |
||||||
|
typedef struct _zend_ssa_range_constraint { |
||||||
|
zend_ssa_range range; // 范围约束 [min, max] |
||||||
|
int min_var; // 符号范围下界变量 |
||||||
|
int max_var; // 符号范围上界变量 |
||||||
|
zend_ssa_negative_lat negative; // 否定潜力 |
||||||
|
} zend_ssa_range_constraint; |
||||||
|
|
||||||
|
typedef struct _zend_ssa_type_constraint { |
||||||
|
uint32_t type_mask; // 类型掩码 (与操作后得到的窄化类型) |
||||||
|
zend_class_entry *ce; // 类条目 (for instanceof) |
||||||
|
} zend_ssa_type_constraint; |
||||||
|
|
||||||
|
typedef union _zend_ssa_pi_constraint { |
||||||
|
zend_ssa_range_constraint range; |
||||||
|
zend_ssa_type_constraint type; |
||||||
|
} zend_ssa_pi_constraint; |
||||||
|
``` |
||||||
|
|
||||||
|
**工作原理:** 对于 `if ($x > 0)` 这样的条件: |
||||||
|
- 在 truthy 分支,插入 `Pi($x, range[1, LONG_MAX])` — 将 `$x` 的 SSA 变量限定在 > 0 的范围 |
||||||
|
- 在 falsy 分支,插入 `Pi($x, range[LONG_MIN, 0])` — 将 `$x` 限定在 ≤ 0 |
||||||
|
|
||||||
|
这允许分支内使用精化的类型/范围信息进行后续优化,而不改变原变量的显式赋值链。 |
||||||
|
|
||||||
|
### SSA 构建流程 |
||||||
|
|
||||||
|
``` |
||||||
|
1. zend_build_cfg() → 构建控制流图 (含支配树、循环检测) |
||||||
|
2. zend_build_dfg() → 构建数据流图 (计算 use/def sets) |
||||||
|
3. zend_build_ssa() → 放置 φ 函数 → 重命名变量 → 构建 SSA 形式 |
||||||
|
4. zend_ssa_compute_use_def_chains() → 连接 use-def 链 |
||||||
|
5. zend_ssa_find_sccs() → 找强连通分量 (用于类型推断) |
||||||
|
6. zend_ssa_inference() → 类型推断 + 范围推断 (填充 var_info) |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
AOT 编译器不需要 SSA 形式(因为生成的是 C++ 代码,不是直接操作寄存器),但 e-SSA 的以下概念可以直接用: |
||||||
|
|
||||||
|
1. **Pi 约束的概念:** 在条件分支中插入类型窄化标记,使分支内变量具有更精确的类型。这直接对应前文 TypeSpecifier / Type Narrowing (#7) 的实现基础。 |
||||||
|
|
||||||
|
2. **Type & Range 信息关联到每个表达式:** 类似 SSA `var_info` 的设计,可以在 AOT 的 FunctionContext 中为每个变量/表达式维护 `{type_mask, range, ce}` 三元组。 |
||||||
|
|
||||||
|
3. **Use-def 链用于优化判断:** 当需要判断一个变量是否有且仅有一个 `use` 时,SSA 的 use_chain 提供了 O(1) 查询。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 3. 类型推断 (Type & Range Inference) |
||||||
|
|
||||||
|
### 类型系统 |
||||||
|
|
||||||
|
PHP 使用位掩码表示类型信息(`zend_type_info.h` 定义),这是最具特色的设计: |
||||||
|
|
||||||
|
```c |
||||||
|
#define MAY_BE_UNDEF (1<< 0) |
||||||
|
#define MAY_BE_NULL (1<< 1) |
||||||
|
#define MAY_BE_FALSE (1<< 2) |
||||||
|
#define MAY_BE_TRUE (1<< 3) |
||||||
|
#define MAY_BE_LONG (1<< 4) |
||||||
|
#define MAY_BE_DOUBLE (1<< 5) |
||||||
|
#define MAY_BE_STRING (1<< 6) |
||||||
|
#define MAY_BE_ARRAY (1<< 7) |
||||||
|
#define MAY_BE_OBJECT (1<< 8) |
||||||
|
#define MAY_BE_RESOURCE (1<< 9) |
||||||
|
#define MAY_BE_REFERENCE (1<<10) |
||||||
|
#define MAY_BE_CALLABLE (1<<11) |
||||||
|
#define MAY_BE_ITERABLE (1<<12) |
||||||
|
#define MAY_BE_VOID (1<<13) |
||||||
|
#define MAY_BE_INDIRECT (1<<14) |
||||||
|
|
||||||
|
// 方便组合 |
||||||
|
#define MAY_BE_ANY (MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|...) |
||||||
|
#define MAY_BE_TRUTHY (MAY_BE_TRUE|MAY_BE_LONG|... /* 非 0/''/[]/null */) |
||||||
|
#define MAY_BE_FALSEY (MAY_BE_UNDEF|MAY_BE_NULL|MAY_BE_FALSE|...) |
||||||
|
``` |
||||||
|
|
||||||
|
**核心优势:** 位运算极快。类型运算(合并/交集/差集)仅需一条 AND/OR/NOT 指令: |
||||||
|
|
||||||
|
```c |
||||||
|
// 合并两个变量的类型 |
||||||
|
uint32_t result_type = info1 | info2; |
||||||
|
|
||||||
|
// 检查是否可能为 string |
||||||
|
if (info & MAY_BE_STRING) { ... } |
||||||
|
|
||||||
|
// 交集 |
||||||
|
uint32_t common = info1 & info2; |
||||||
|
``` |
||||||
|
|
||||||
|
### 范围推断 |
||||||
|
|
||||||
|
每个 SSA 变量附带 `zend_ssa_range { min, max, underflow, overflow }`: |
||||||
|
|
||||||
|
```c |
||||||
|
typedef struct _zend_ssa_range { |
||||||
|
zend_long min; |
||||||
|
zend_long max; |
||||||
|
bool underflow; // 是否有下溢风险 |
||||||
|
bool overflow; // 是否有上溢风险 |
||||||
|
} zend_ssa_range; |
||||||
|
``` |
||||||
|
|
||||||
|
核心算法(`zend_inference.c:1071`)基于 V. Campos 的 "Speed and Precision in Range Analysis, SBLP'12" 论文: |
||||||
|
|
||||||
|
1. **Warmup 阶段 (16 pass):** 在 SCC(强连通分量)上传播范围,使用 widening 加速收敛 |
||||||
|
2. **Narrowing 阶段:** 将范围逐渐收窄,消除 widening 造成的过度近似 |
||||||
|
3. **Zend Engine 专门的算术语义:** `zend_add_will_overflow()`, `zend_sub_will_overflow()` 等精确检测整数溢出 |
||||||
|
|
||||||
|
**运算符范围推断示例:** |
||||||
|
|
||||||
|
```c |
||||||
|
// ADD: 结果范围 |
||||||
|
min = OP1_MIN() + OP2_MIN() |
||||||
|
max = OP1_MAX() + OP2_MAX() |
||||||
|
overflow = OP1_RANGE_OVERFLOW() || OP2_RANGE_OVERFLOW() |
||||||
|
|| zend_add_will_overflow(OP1_MAX(), OP2_MAX()) |
||||||
|
|
||||||
|
// 结果类型: 如果 overflow 为真,类型增加 MAY_BE_DOUBLE |
||||||
|
// (PHP int 溢出自定转为 float) |
||||||
|
``` |
||||||
|
|
||||||
|
### 每个 Opcode 的 `update_type_info` |
||||||
|
|
||||||
|
`_zend_update_type_info()` 是一个巨大的 switch,针对每个 Zend opcode 精确计算结果类型和范围。例如 `ZEND_ASSIGN_DIM`(数组赋值),不仅更新被赋值元素的类型,还更新数组整体的类型,考虑 MAY_BE_PACKED_GUARD(打包数组守卫)和引用计数推断。 |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
1. **位掩码类型系统:** 是最适合 AOT 编译器的轻量类型表示方案。AOT 当前用字符串类型(`TYPE_INT = 'int'`),无法高效地进行"可能是 int 或 string"这种复合表示。位掩码提供了 O(1) 的 union/intersect/test 操作。 |
||||||
|
|
||||||
|
2. **范围推断:** 可为 C++ 代码生成选择最优的整数类型(`int32_t` vs `int64_t` vs `BigInt`),避免不必要的 BigInt 分配。 |
||||||
|
|
||||||
|
3. **Overflow 追踪:** 精确判定何时需要从 int64 转为 float/BigInt,只在真正可能溢出时才插入转换代码。 |
||||||
|
|
||||||
|
4. **每个 opcode 的类型更新表:** `_zend_update_type_info()` 的设计可以直接映射到 AOT 的 Rule 系统中——每个 opcode 对应一个 Rule,负责输出该操作的结果类型。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 4. SCCP (Sparse Conditional Constant Propagation) |
||||||
|
|
||||||
|
### 核心设计 |
||||||
|
|
||||||
|
SCCP 同时做**常量传播**和**条件常量折叠**,还可以消除不可达代码(不需要额外的死代码消除 pass)。 |
||||||
|
|
||||||
|
实现在 `sccp.c`,基于 `scdf.h`(SCDF 框架)。 |
||||||
|
|
||||||
|
### 值格 (Value Lattice) |
||||||
|
|
||||||
|
``` |
||||||
|
TOP (未定义) |
||||||
|
/ | \ |
||||||
|
C1 C2 C3 (常量值) |
||||||
|
\ | / |
||||||
|
BOT (过定义 = 非常量) |
||||||
|
``` |
||||||
|
|
||||||
|
- TOP: 还不知道这个变量的值(乐观假设) |
||||||
|
- BOT: 已知这个变量不是常量 |
||||||
|
- 常量值: 已知精确值 |
||||||
|
|
||||||
|
### 算法关键点 (来自 sccp.c:30-74 的注释) |
||||||
|
|
||||||
|
**`meet` 操作 (φ 函数的合并):** |
||||||
|
- BOT + any = BOT |
||||||
|
- TOP + any = any |
||||||
|
- C_i + C_i = C_i (两个相同常量) |
||||||
|
- C_i + C_j = BOT (两个不同常量) |
||||||
|
|
||||||
|
**指令求值:** |
||||||
|
- 任何操作数是 BOT → 结果是 BOT (例外: ASSIGN 的 op1) |
||||||
|
- 永远不能求值的指令 → BOT |
||||||
|
- 任何操作数是 TOP → 结果是 TOP |
||||||
|
- 所有操作数都是已知常量 → 尝试编译时求值 → 成功返回常量值,失败返回 BOT |
||||||
|
|
||||||
|
**分支可行性判断:** |
||||||
|
- 分支在 BOT 上 → 所有后继可行 |
||||||
|
- 分支在 TOP 上 → 都没有后继不可行(等待更多信息) |
||||||
|
- 分支在已知常量上 → 只有满足条件的分支可行 |
||||||
|
|
||||||
|
### SCDF 框架 (`scdf.h`) |
||||||
|
|
||||||
|
SCCP 被构建在 SCDF (Sparse Conditional Data Flow) 框架之上,这是一个通用的稀疏条件数据流分析引擎: |
||||||
|
|
||||||
|
```c |
||||||
|
typedef struct _scdf_ctx { |
||||||
|
zend_op_array *op_array; |
||||||
|
zend_ssa *ssa; |
||||||
|
zend_bitset instr_worklist; // 待处理的指令 |
||||||
|
zend_bitset phi_var_worklist; // 待处理的 Phi/SSA 变量 |
||||||
|
zend_bitset block_worklist; // 待处理的块 |
||||||
|
zend_bitset executable_blocks; // 可执行块 |
||||||
|
zend_bitset feasible_edges; // 可行边 |
||||||
|
|
||||||
|
struct { |
||||||
|
void (*visit_instr)(...); // 处理一条指令 |
||||||
|
void (*visit_phi)(...); // 处理一个 φ 函数 |
||||||
|
void (*mark_feasible_successors)(...); // 标记可行后继 |
||||||
|
} handlers; |
||||||
|
} scdf_ctx; |
||||||
|
``` |
||||||
|
|
||||||
|
**使用模式:** SCCP 实现了 `visit_instr` (常量求值)、`visit_phi` (meet 操作)、`mark_feasible_successors` (分支可行性)。类型推断也用类似的 worklist 传播算法。 |
||||||
|
|
||||||
|
**通用 worklist 机制:** |
||||||
|
```c |
||||||
|
// 当一个变量的值改变时,将它的所有 use 加入 worklist |
||||||
|
static inline void scdf_add_to_worklist(scdf_ctx *scdf, int var_num) { |
||||||
|
const zend_ssa_var *var = &ssa->vars[var_num]; |
||||||
|
int use; |
||||||
|
FOREACH_USE(var, use) { |
||||||
|
zend_bitset_incl(scdf->instr_worklist, use); // 标记使用该变量的指令 |
||||||
|
} |
||||||
|
FOREACH_PHI_USE(var, phi) { |
||||||
|
zend_bitset_incl(scdf->phi_var_worklist, phi->ssa_var); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
1. **SCDF 框架是最直接可借鉴的**:约 300 行 C 代码,提供了一个通用的 worklist 驱动的条件数据流引擎。AOT 可以移植为 PHP 类,复用于 SCCP、类型推断、逃逸分析等多项优化 pass。 |
||||||
|
|
||||||
|
2. **TOP/BOT 格模型:** AOT 编译器在分析类型时可以使用相同的格结构: |
||||||
|
- TOP = 未知类型 (分析初期) |
||||||
|
- BOT = 矛盾类型 (发现了不一致) |
||||||
|
- 具体值 (常量或确切类型) |
||||||
|
|
||||||
|
3. **条件分支可行性:** SCCP 的分支可行性判断可以直接帮助 AOT 在编译时消除不可达分支,生成更简化的 C++ 代码。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 5. DCE (Dead Code Elimination) |
||||||
|
|
||||||
|
### 算法 (`dce.c`) |
||||||
|
|
||||||
|
PHP 的 DCE 采用乐观策略: |
||||||
|
|
||||||
|
``` |
||||||
|
1. 假设所有指令和 φ 函数都是 dead |
||||||
|
2. 标记所有有明显副作用的指令为 live (side-effect instruction) |
||||||
|
3. 从 live 指令出发,标记其操作数的定义指令为 live (use-def 链反向传播) |
||||||
|
4. 重复直到 worklist 为空 |
||||||
|
5. 删除所有仍标记为 dead 的指令 |
||||||
|
``` |
||||||
|
|
||||||
|
**关键判断 `may_have_side_effects()` (`dce.c:74-100`):** |
||||||
|
|
||||||
|
将 Zend opcode 分为三种: |
||||||
|
- 永远无副作用(如 ADD、CONCAT、BOOL_NOT):可以被 DCE 消除 |
||||||
|
- 可能产生 notice 但无本质副作用(如 DIV_BY_ZERO 触发 warning):可配置是否消除 |
||||||
|
- 永远有副作用(如 ECHO、THROW、ASSIGN_OBJ):必须保留 |
||||||
|
|
||||||
|
**特别能力:** 可以消除"非逃逸数组/对象的冗余修改"以及"无用的数组/对象分配"。如果一个数组只在局部被构建、修改和使用,中间步骤的 ASSIGN_DIM 可能被消除。 |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
AOT 编译器的 DCE 可以更激进(因为编译时已知类型): |
||||||
|
|
||||||
|
1. **副作用分类矩阵:** 为 AOT 的表达式/语句类型建立副作用表格,精确标记哪些操作必须保留 |
||||||
|
2. **逃逸感知 DCE:** 结合逃逸分析,消除未逃逸对象的操作——这是 AOT 最大的优化机会之一 |
||||||
|
3. **Control-dependence based DCE:** PHP 明确表示当前的 DCE 没有考虑控制依赖(`dce.c:35-39` 的注释),AOT 可以做更精确的控制依赖 DCE |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 6. 逃逸分析 (Escape Analysis) |
||||||
|
|
||||||
|
### 算法 (`escape_analysis.c`) |
||||||
|
|
||||||
|
基于 Kotzmann & Mossenbock (PPPJ'05) 的经典逃逸分析算法。 |
||||||
|
|
||||||
|
**核心步骤:** |
||||||
|
|
||||||
|
1. **构建等价逃逸集 (`zend_build_equi_escape_sets`):** 使用 Union-Find 算法。如果两个 SSA 变量通过 φ 函数或 ASSIGN 关联(值相同),它们属于同一个等价类。 |
||||||
|
|
||||||
|
2. **逃逸状态传播:** 每个等价类有四种状态: |
||||||
|
``` |
||||||
|
ESCAPE_STATE_UNKNOWN → 初始状态(零初始化的 C 内存) |
||||||
|
ESCAPE_STATE_NO_ESCAPE → 确定不逃逸(最终目标) |
||||||
|
ESCAPE_STATE_FUNCTION_ESCAPE → 逃逸到被调用函数(传入参数) |
||||||
|
ESCAPE_STATE_GLOBAL_ESCAPE → 全局逃逸(返回、赋值到全局变量、抛异常等) |
||||||
|
``` |
||||||
|
|
||||||
|
3. **状态单调收敛:** 状态只能从 UNKNOWN → NO_ESCAPE/FUNCTION_ESCAPE/GLOBAL_ESCAPE,从不反转。 |
||||||
|
|
||||||
|
4. **应用逃逸信息:** |
||||||
|
- 不逃逸的数组可以在栈上分配(不需要堆分配) |
||||||
|
- 不逃逸的对象可以避免引用计数操作 |
||||||
|
- 不逃逸的变量不需要分离(ZEND_SEPARATE) |
||||||
|
|
||||||
|
**前驱/后继边:** 支持符号类型别名(SYMTABLE_ALIAS)和 HTTP 响应头别名(HTTP_RESPONSE_HEADER_ALIAS)。 |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
逃逸分析对 AOT 编译器可能价值最大: |
||||||
|
|
||||||
|
1. **Box allocation 消除:** AOT 使用 `Box<T>` 表示对象引用。逃逸分析可以确认哪些 Box 不需要堆分配,直接在栈上创建。 |
||||||
|
|
||||||
|
2. **引用计数消除:** 不逃逸的对象可以跳过 `php::Object::Ref()` / `php::Object::Unref()` 操作。 |
||||||
|
|
||||||
|
3. **Array 栈分配:** 局部数组逃逸分析后可以改用栈上的 `zend_array`。 |
||||||
|
|
||||||
|
4. **4 状态模型非常简单有效**,AOT 可以直接映射: |
||||||
|
- ESCAPE_STATE_NO_ESCAPE → 栈分配 |
||||||
|
- ESCAPE_STATE_FUNCTION_ESCAPE → 调用者决定 |
||||||
|
- ESCAPE_STATE_GLOBAL_ESCAPE → 堆分配 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 7. 调用图 (Call Graph) |
||||||
|
|
||||||
|
### 设计 (`zend_call_graph.h`) |
||||||
|
|
||||||
|
PHP 的调用图同时追踪 caller → callee 和 callee → caller 的双向关系: |
||||||
|
|
||||||
|
```c |
||||||
|
struct _zend_call_info { |
||||||
|
zend_op_array *caller_op_array; // 调用者 |
||||||
|
zend_op *caller_init_opline; // INIT_FCALL 指令 |
||||||
|
zend_op *caller_call_opline; // DO_FCALL 指令 |
||||||
|
zend_function *callee_func; // 被调用函数 |
||||||
|
zend_call_info *next_caller; // 链表: callee 的下一个 caller |
||||||
|
zend_call_info *next_callee; // 链表: caller 的下一个 callee |
||||||
|
bool recursive; // 递归调用 |
||||||
|
bool send_unpack; // 使用 SEND_UNPACK |
||||||
|
bool named_args; // 命名参数 |
||||||
|
bool is_prototype; // 可能是子类重写的方法 |
||||||
|
bool is_frameless; // frameless 函数 |
||||||
|
int num_args; |
||||||
|
zend_send_arg_info arg_info[1]; |
||||||
|
}; |
||||||
|
|
||||||
|
struct _zend_func_info { |
||||||
|
zend_ssa ssa; // 函数自身的 SSA |
||||||
|
zend_call_info *caller_info; // 谁调用了这个函数 |
||||||
|
zend_call_info *callee_info; // 这个函数调用了谁 |
||||||
|
zend_call_info **call_map; // 从 opnum 快速索引 call_info |
||||||
|
zend_ssa_var_info return_info; // 推断出的返回类型 |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
**关键功能:** |
||||||
|
|
||||||
|
1. **双向图:** `caller_info` 和 `callee_info` 分别是不同的链表,支持向上(从 callee 找 caller)和向下(从 caller 找 callee)遍历 |
||||||
|
2. **call_map:** 数组索引 opnum → call_info,O(1) 查找某个 opcode 位置对应的调用信息 |
||||||
|
3. **返回类型传播:** 被调用函数的 return_info 可以向上传播到调用者的 return_info |
||||||
|
4. **参数类型传播:** 调用者的实参类型可以向下传播到被调用者的参数类型(用于更精确的函数体优化) |
||||||
|
|
||||||
|
### 高级跨函数优化 (`zend_optimize_script:1626-1728`) |
||||||
|
|
||||||
|
``` |
||||||
|
1. build_call_graph → 构建双向调用图 |
||||||
|
2. zend_optimize (per-func) → 每个函数做独立的 local optimization |
||||||
|
3. analyze_call_graph → 推断函数信息 (递归标记、间接变量访问、func_get_args 等) |
||||||
|
4. build_call_map → 为每个函数构建 opnum→call 的索引 |
||||||
|
5. dfa_analyze_op_array → 构建 SSA + 类型推断 (per-func) |
||||||
|
6. dfa_optimize_op_array → 基于 SSA 做 SCCP + DCE + block pass |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
AOT 编译器的前两步(prepare + convert)天然构建了完整的符号依赖图。可以在此之上增加: |
||||||
|
|
||||||
|
1. **call_map 索引:** 从每个 call site 快速查找 callee 的元信息(参数类型、返回类型、是否内联候选) |
||||||
|
2. **返回类型双向传播:** 目前 AOT 的返回类型推导是自顶向下的;调用图允许从 callee 的已知返回类型回传给 caller |
||||||
|
3. **递归标记:** `ZEND_FUNC_RECURSIVE_DIRECTLY` / `ZEND_FUNC_RECURSIVE_INDIRECTLY` 对内联策略决策至关重要 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 8. JIT IR 框架 |
||||||
|
|
||||||
|
### 设计 |
||||||
|
|
||||||
|
PHP JIT 使用的 IR(Intermediate Representation)是一个通用的 SSA 派生的低级中间表示,位于 `ext/opcache/jit/ir/`。 |
||||||
|
|
||||||
|
**IR 的三个阶段:** |
||||||
|
|
||||||
|
| 阶段 | 文件 | 作用 | |
||||||
|
|------|------|------| |
||||||
|
| IR builder | `ir_builder.h`, `zend_jit_ir.c` | 从 Zend 字节码构建 IR 指令 | |
||||||
|
| IR optimizer | `ir_cfg.c`, `ir_fold.h`, `ir_gcm.c` | CFG 优化、常量折叠、全局代码移动 (GCM) | |
||||||
|
| IR emitter | `ir_emit.c`, `ir_emit_x86.h` | 从 IR 发射 x86/ARM64 机器码 | |
||||||
|
|
||||||
|
**IR 指令示例: IR_ADD, IR_MUL, IR_LOAD, IR_STORE, IR_CALL, IR_GUARD, etc.** |
||||||
|
|
||||||
|
**JIT 优化级别 (`zend_jit.h:32-37`):** |
||||||
|
```c |
||||||
|
#define ZEND_JIT_LEVEL_NONE 0 // 不启用 JIT |
||||||
|
#define ZEND_JIT_LEVEL_MINIMAL 1 // 最小 JIT (子程序线程化) |
||||||
|
#define ZEND_JIT_LEVEL_INLINE 2 // 选择性内联线程化 |
||||||
|
#define ZEND_JIT_LEVEL_OPT_FUNC 3 // 基于类型推断优化单个函数 |
||||||
|
#define ZEND_JIT_LEVEL_OPT_FUNCS 4 // 基于调用树优化 |
||||||
|
#define ZEND_JIT_LEVEL_OPT_SCRIPT 5 // 过程间分析 |
||||||
|
``` |
||||||
|
|
||||||
|
**JIT 触发模式 (`zend_jit.h:39-44`):** |
||||||
|
```c |
||||||
|
#define ZEND_JIT_ON_SCRIPT_LOAD 0 // 所有函数加载时立即编译 |
||||||
|
#define ZEND_JIT_ON_FIRST_EXEC 1 // 首次执行时编译 |
||||||
|
#define ZEND_JIT_ON_PROF_REQUEST 2 // 根据 profile 数据编译最热的函数 |
||||||
|
#define ZEND_JIT_ON_HOT_COUNTERS 3 // N 次调用/循环迭代后编译 |
||||||
|
#define ZEND_JIT_ON_HOT_TRACE 5 // N 次调用后走 tracing JIT |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
1. **IR 作为 AST→C++ 的中间载体:** AOT 当前直接从 AST 生成 C++ 代码。引入 IR 层可以: |
||||||
|
- 在 IR 层面做优化(fold、GCM、寄存器分配模拟) |
||||||
|
- 解耦前端(PHP AST)和后端(C++ codegen) |
||||||
|
|
||||||
|
2. **`ir_fold.h` 的常量折叠表:** IR 包含一个自动生成的折叠规则表 (`gen_ir_fold_hash`),定义了数百条代数简化规则。AOT 可以采用类似的"规则表" 驱动的常量折叠。 |
||||||
|
|
||||||
|
3. **JIT 的 profiling 机制:** `hot_loop` / `hot_func` 计数器 —— AOT 可以将 profile 数据嵌入生成的二进制,用于 PGO (Profile-Guided Optimization)。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 9. OPcache 持久化 & File Cache |
||||||
|
|
||||||
|
### 设计 |
||||||
|
|
||||||
|
OPcache 不只是一个缓存——它在缓存中存的是**优化后**的字节码。 |
||||||
|
|
||||||
|
``` |
||||||
|
原始 PHP 源码 |
||||||
|
→ 编译为 zend_op_array (原始字节码) |
||||||
|
→ 经过 Zend Optimizer 所有 pass (SSA + 类型推断 + SCCP + DCE + ...) |
||||||
|
→ 只保留优化后的 zend_op_array (丢弃 SSA 等临时 IR) |
||||||
|
→ zend_persist() 序列化到共享内存 / 文件缓存 |
||||||
|
``` |
||||||
|
|
||||||
|
**`zend_persist_calc` + `zend_persist`**:两阶段序列化—— |
||||||
|
1. `_calc` 计算所需的共享内存大小 |
||||||
|
2. `_persist` 进行实际序列化(所有指针调整为绝对偏移量) |
||||||
|
|
||||||
|
**文件缓存 (`zend_file_cache.c`):** 将持久化后的脚本写入文件,允许跨进程重启复用。 |
||||||
|
|
||||||
|
### AOT 借鉴 |
||||||
|
|
||||||
|
当前 AOT 编译器每次从 PHP 源码开始编译。可以借鉴 OPcache 的理念: |
||||||
|
|
||||||
|
1. **缓存优化后的 AST/类型信息:** 在 `convert()` 阶段之后,将带类型的 AST 序列化,下次编译时直接加载 |
||||||
|
2. **增量编译:** 只重新编译改动的文件及其依赖 |
||||||
|
3. **`_calc` + `_persist` 两阶段序列化:** 先计算大小再分配内存/写入,避免 realloc 碎片 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 10. 其他值得注意的设计 |
||||||
|
|
||||||
|
### zend_bitset |
||||||
|
|
||||||
|
PHP 使用自己的 bitset 实现进行高效的集合操作。优化器频繁使用 bitset 来表示 worklist、live sets、def/use sets。 |
||||||
|
|
||||||
|
### zend_worklist.h |
||||||
|
|
||||||
|
通用的 worklist 迭代宏,SCCP 和类型推断都使用相同的 worklist 机制。设计为宏以便内联性能。 |
||||||
|
|
||||||
|
### zend_arena |
||||||
|
|
||||||
|
Arena 内存分配器用于所有优化器数据结构的快速分配和整体释放。一个 arena 绑定一个 `zend_optimizer_ctx`,所有 pass 共享同一个 arena。 |
||||||
|
|
||||||
|
### Pass 间数据管理 |
||||||
|
|
||||||
|
SSA 等临时 IR 在 pass 完成后立即销毁(通过 arena free),只在最终的 `zend_op_array` 中保留优化后的结果。这保证了内存效率。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 推荐采用顺序 (针对 AOT 编译器) |
||||||
|
|
||||||
|
``` |
||||||
|
Phase 1: Pass Pipeline |
||||||
|
└── 定义 AotPass 枚举 + Pipeline runner,可插拔 pass 架构 |
||||||
|
|
||||||
|
Phase 2: 位掩码类型系统 |
||||||
|
└── 借鉴 Zend 的 type mask 设计,替代当前的字符串类型常量 |
||||||
|
└── 直接映射为 C++ 的 uint32_t 常量 |
||||||
|
|
||||||
|
Phase 3: 类型推断规则 |
||||||
|
└── 每个 AST 节点/opcode 对应一个 update_type_info |
||||||
|
└── 与 Rule 系统 (#4 设计) 结合实现 |
||||||
|
|
||||||
|
Phase 4: SCCP + DCE |
||||||
|
└── 基于 SCDF 框架的常量传播 + 死代码消除 |
||||||
|
└── 可在 C++ 代码生成之前消去冗余表达式 |
||||||
|
|
||||||
|
Phase 5: 逃逸分析 |
||||||
|
└── Union-Find 等价逃逸集 + 4 状态传播 |
||||||
|
└── 用于 Box 分配消除、引用计数消除 |
||||||
|
|
||||||
|
Phase 6: 调用图跨函数优化 |
||||||
|
└── 在已有的符号依赖图上增加 call_map + 类型传播 |
||||||
|
``` |
||||||
|
|
||||||
|
每一层都可以独立实施并立即给现有代码生成带来收益。 |
||||||
@ -0,0 +1,559 @@ |
|||||||
|
# PHPStan 设计分析:可引入 AOT 编译器的模式与模块 |
||||||
|
|
||||||
|
本文档分析了 PHPStan 项目 (`projects/phpstan-src/`) 的架构设计,识别出可引入 AOT 编译器的设计模式和模块。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 概览:四大优先级的可迁移设计 |
||||||
|
|
||||||
|
| 优先级 | 设计/模块 | 实施量级 | 前置依赖 | 收益 | |
||||||
|
|--------|----------|---------|---------|------| |
||||||
|
| P1 | Type 对象层次 (#1) | ~1000 行 | 无 | 类型推导精度质的提升 | |
||||||
|
| P1 | TrinaryLogic (#2) | ~80 行 | 无 | 类型查询不再给出错误答案 | |
||||||
|
| P1 | TypeCombinator (#3) | ~300-400 行 | #1 | 消除散落的类型字符串拼接 | |
||||||
|
| P2 | Rule 系统 (#4) | 接口+Registry ~80 行 | 基本独立 | 架构模块化,易测试易扩展 | |
||||||
|
| P2 | Collector 双阶段分析 (#5) | ~200 行 | #4 | 跨文件全局优化 | |
||||||
|
| P2 | Extension 注册机制 (#6) | ~100 行 | #4 | 框架级插件系统 | |
||||||
|
| P3 | TypeSpecifier / 类型窄化 (#7) | ~200 行 | #1, #2 | if/else 分支类型精化 | |
||||||
|
| P3 | Immutable Scope (#8) | ~300 行 | #1, #2 | 表达式级类型追踪 | |
||||||
|
| P4 | PHPDoc Pipeline (#9) | 可复用 phpstan/phpdoc-parser | #1 | 利用现有成熟解析器 | |
||||||
|
| P4 | NeverType (#10) | 含于 #1 | #1 | 死代码消除,矛盾类型检测 | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 1. Object-Oriented Type 系统(替代基于字符串的类型表示) |
||||||
|
|
||||||
|
### 现状 |
||||||
|
|
||||||
|
AOT 编译器用字符串常量表示类型: |
||||||
|
|
||||||
|
```php |
||||||
|
const TYPE_INT = 'int'; |
||||||
|
const TYPE_FLOAT = 'float'; |
||||||
|
const TYPE_STR = 'string'; |
||||||
|
// 复合类型用字符串拼接 |
||||||
|
// 'int|string', 'string|null' |
||||||
|
``` |
||||||
|
|
||||||
|
这导致类型运算(合并、比较、查询)必须手动解析和拼接字符串。 |
||||||
|
|
||||||
|
### PHPStan 方案 |
||||||
|
|
||||||
|
PHPStan 有一个 `Type` 接口,定义约 100 个方法。每种类型是一个类: |
||||||
|
|
||||||
|
``` |
||||||
|
Type (interface) |
||||||
|
├── StringType |
||||||
|
├── IntegerType |
||||||
|
├── FloatType |
||||||
|
├── BooleanType |
||||||
|
├── NullType |
||||||
|
├── MixedType (顶类型,带 subtracted type 支持) |
||||||
|
├── NeverType (底类型,无可能值) |
||||||
|
├── VoidType |
||||||
|
├── ArrayType |
||||||
|
├── ObjectType |
||||||
|
├── CallableType |
||||||
|
├── UnionType (A|B|C,持有 flat list<Type>) |
||||||
|
├── IntersectionType (A&B&C) |
||||||
|
├── ConstantStringType, ConstantIntegerType, ... |
||||||
|
├── IntegerRangeType |
||||||
|
└── Accessory*Type (用于 IntersectionType 中的精化) |
||||||
|
``` |
||||||
|
|
||||||
|
**核心设计原则:永远不要用 `instanceof` 判断类型身份。** 必须使用 `is*()` 方法: |
||||||
|
|
||||||
|
```php |
||||||
|
// 错误 —— 漏掉了 UnionType 和 IntersectionType |
||||||
|
$type instanceof StringType |
||||||
|
|
||||||
|
// 正确 —— UnionType 会委托给内部类型并组合结果 |
||||||
|
$type->isString()->yes() |
||||||
|
``` |
||||||
|
|
||||||
|
**关键子接口:** |
||||||
|
|
||||||
|
- `CompoundType`:标记需要双向类型比较的类型(UnionType, IntersectionType)。添加 `isAcceptedBy()`, `isSubTypeOf()` 方法,实现双分派协议。 |
||||||
|
- `SubtractableType`:支持差集操作(如 `mixed~null`)。 |
||||||
|
|
||||||
|
### isSuperTypeOf / accepts 双分派协议 |
||||||
|
|
||||||
|
这是 PHPStan 类型系统最核心的设计模式: |
||||||
|
|
||||||
|
``` |
||||||
|
简单类型 (StringType)::isSuperTypeOf(Type $otherType): |
||||||
|
if $otherType 是 CompoundType: |
||||||
|
return $otherType->isSubTypeOf($this) // 反向委托 |
||||||
|
// 自身逻辑 ... |
||||||
|
return No |
||||||
|
|
||||||
|
复合类型 (UnionType)::isSubTypeOf(Type $otherType): |
||||||
|
foreach innerType in $this->types: |
||||||
|
results[] = $otherType->isSuperTypeOf(innerType) |
||||||
|
return extremeIdentity(results) // ALL 必须是 subtype |
||||||
|
``` |
||||||
|
|
||||||
|
**要点:** 简单类型不需要理解复合类型的语义。新增复合类型时不需要修改任何简单类型的比较逻辑。 |
||||||
|
|
||||||
|
### AOT 采用建议 |
||||||
|
|
||||||
|
AOT 编译器不需要 PHPStan 全部的复杂度。第一版只需这些具体类型: |
||||||
|
|
||||||
|
``` |
||||||
|
IntegerType, FloatType, StringType, BoolType, |
||||||
|
NullType, MixedType, NeverType, ArrayType, ObjectType, UnionType |
||||||
|
``` |
||||||
|
|
||||||
|
不需要:IntersectionType, AccessoryType, TemplateType, Constant*Type, IntegerRangeType, EnumType。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 2. TrinaryLogic(三值逻辑) |
||||||
|
|
||||||
|
### 现状 |
||||||
|
|
||||||
|
AOT 编译器用 boolean 判断类型属性。但 `mixed` 类型意味着 "可能是 string,也可能是 int",boolean 无法表达这种不确定性。 |
||||||
|
|
||||||
|
### PHPStan 方案 |
||||||
|
|
||||||
|
```php |
||||||
|
class TrinaryLogic { |
||||||
|
public function yes(): bool; |
||||||
|
public function no(): bool; |
||||||
|
public function maybe(): bool; |
||||||
|
|
||||||
|
public static function createYes(): self; |
||||||
|
public static function createNo(): self; |
||||||
|
public static function createMaybe(): self; |
||||||
|
|
||||||
|
public function and(self ...$others): self; |
||||||
|
public function or(self ...$others): self; |
||||||
|
public function extremeIdentity(self ...$others): self; // ALL yes → yes; ALL no → no |
||||||
|
public function maxMin(self ...$others): self; // ANY yes → yes; ALL no → no |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
使用示例: |
||||||
|
|
||||||
|
```php |
||||||
|
// MixedType::isString() → maybe(mixed 可能是 string) |
||||||
|
// IntegerType::isString() → no |
||||||
|
// UnionType(int|string)::isString() → maybe |
||||||
|
|
||||||
|
$type->isString()->yes() // 确定是 string |
||||||
|
$type->isString()->no() // 确定不是 string |
||||||
|
$type->isString()->maybe() // 不确定 |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 采用建议 |
||||||
|
|
||||||
|
直接移植,约 80 行代码,无外部依赖。所有类型查询方法用 TrinaryLogic 替代 boolean。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 3. TypeCombinator —— 类型规范化引擎 |
||||||
|
|
||||||
|
### 要解决的问题 |
||||||
|
|
||||||
|
禁止直接 `new UnionType(...)`。所有 union/intersect/remove 操作必须经过 TypeCombinator,确保类型表示始终是规范化的。 |
||||||
|
|
||||||
|
### 三个核心操作 |
||||||
|
|
||||||
|
#### `union(Type ...$types): Type` |
||||||
|
|
||||||
|
**算法流程:** |
||||||
|
|
||||||
|
1. **Fast path**:0 个参数 → `NeverType`;1 个参数 → 直接返回;2 个参数检查 `never`/`mixed`/相同对象 |
||||||
|
2. **扁平化**:`union(A, union(B, C), D)` → 展开为 `[A, B, C, D]` |
||||||
|
3. **过滤 NeverType**:`union(int, never, string)` → `[int, string]` |
||||||
|
4. **分类提取**:将类型分为 scalar/array/enum/integerRange/generic 五类,分批处理 |
||||||
|
5. **标量消解**:`ConstantIntegerType(3) | IntegerType` → `IntegerType`;`true | false` → `BooleanType` |
||||||
|
6. **Pairwise 比较**: |
||||||
|
- `IntegerRangeType` 相邻区间合并:`int<0,5> | int<3,10>` → `int<0,10>` |
||||||
|
- Subtype/supertype 消除:`Foo extends Bar` ⇒ `Foo | Bar` = `Bar` |
||||||
|
- `int[] | string[]` → `(int|string)[]` |
||||||
|
7. **收尾**:0 个→NeverType;1 个→直接返回;否则 `new UnionType(array_values($types), true)` |
||||||
|
|
||||||
|
#### `intersect(Type ...$types): Type` |
||||||
|
|
||||||
|
**算法流程:** |
||||||
|
|
||||||
|
1. 有 `NeverType` → 直接返回 never |
||||||
|
2. **分配律展开**:`A & (B|C)` → `(A&B) | (A&C)`,然后对每个子项递归 |
||||||
|
3. **扁平化**:`A & (B & C)` → 展开为 `[A, B, C]` |
||||||
|
4. **Pairwise 双向比较**: |
||||||
|
- `IntegerType & ConstantIntegerType(5)` → `ConstantIntegerType(5)`(Child & Parent = Child) |
||||||
|
- `int & string` → `NeverType`(矛盾) |
||||||
|
- SubtractableType 差集交接 |
||||||
|
5. **矛盾检测**:`isSuperTypeOf` 返回 `no` → `NeverType` |
||||||
|
|
||||||
|
#### `remove(Type $fromType, Type $typeToRemove): Type` |
||||||
|
|
||||||
|
``` |
||||||
|
remove(int|string, string) = int |
||||||
|
remove(int|string|null, null) = int|string |
||||||
|
remove(int, string) = int // 要移除的不在里面 |
||||||
|
remove(string, string) = never // 完全移除 |
||||||
|
remove(mixed, Foo) = mixed~Foo // 差集类型 |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 采用建议 |
||||||
|
|
||||||
|
精简版 TypeCombinator(~300-400 行),覆盖 AOT 需要的基础类型的 union/intersect/remove。不需要 PHPStan 复杂的数组 shape 处理、accessory type 传播、IntegerRange 合并等逻辑。 |
||||||
|
|
||||||
|
### 收益 |
||||||
|
|
||||||
|
| 场景 | 当前 | TypeCombinator 后 | |
||||||
|
|------|------|-------------------| |
||||||
|
| `int\|int` 的变量赋值 | 字符串 `'int\|int'` | 自动简化为 `IntegerType` | |
||||||
|
| `mixed\|string` 的返回值 | 不知道如何简化 | 自动简化为 `MixedType` | |
||||||
|
| 两个分支类型合并 | 手动拼接 | `union()` 自动去重去 subtype | |
||||||
|
| `int & string` 交集 | 无法检测 | 返回 `NeverType`(编译错误) | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 4. Rule-based 分析 Pass 系统 |
||||||
|
|
||||||
|
### 核心接口 |
||||||
|
|
||||||
|
```php |
||||||
|
/** |
||||||
|
* @template TNodeType of Node |
||||||
|
*/ |
||||||
|
interface Rule |
||||||
|
{ |
||||||
|
/** @return class-string<TNodeType> */ |
||||||
|
public function getNodeType(): string; |
||||||
|
|
||||||
|
/** @param TNodeType $node */ |
||||||
|
public function processNode(Node $node, Scope $scope): array; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
每个 Rule 声明它关心哪种 AST 节点类型,以及在发现该节点时做什么。 |
||||||
|
|
||||||
|
### 注册机制 |
||||||
|
|
||||||
|
通过 PHP 8 Attribute 声明级别: |
||||||
|
|
||||||
|
```php |
||||||
|
#[RegisteredRule(level: 0)] |
||||||
|
final class CallMethodsRule implements Rule |
||||||
|
{ |
||||||
|
public function getNodeType(): string { return MethodCall::class; } |
||||||
|
public function processNode(Node $node, Scope $scope): array { ... } |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Registry 实现 |
||||||
|
|
||||||
|
`LazyRegistry` 从 DI 容器收集所有带 `phpstan.rules.rule` 标签的服务,按 `getNodeType()` 返回值建立索引。 |
||||||
|
|
||||||
|
**关键设计:** `getRules($nodeType)` 不仅匹配精确的类名,还匹配所有父类和接口: |
||||||
|
|
||||||
|
```php |
||||||
|
public function getRules(string $nodeType): array |
||||||
|
{ |
||||||
|
// $nodeType = MethodCall::class |
||||||
|
// parentNodeTypes = [MethodCall, Expr, NodeAbstract, Node, ...] |
||||||
|
// 匹配所有注册在这些父类/接口上的 Rule |
||||||
|
$parentNodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType); |
||||||
|
// ... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
这意味着注册为 `Node\Expr` 的 rule 会匹配**所有**表达式类型。 |
||||||
|
|
||||||
|
### 运行时调度 |
||||||
|
|
||||||
|
在 AST 遍历的每个节点上: |
||||||
|
|
||||||
|
```php |
||||||
|
$nodeType = get_class($node); |
||||||
|
foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { |
||||||
|
$ruleErrors = $rule->processNode($node, $scope); |
||||||
|
// 转换和收集错误 ... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
极其简洁,没有 switch,没有 if-else 链。 |
||||||
|
|
||||||
|
### Collector 双阶段分析 |
||||||
|
|
||||||
|
Collector 接口与 Rule 几乎相同,但返回收集的数据(而不是错误): |
||||||
|
|
||||||
|
```php |
||||||
|
interface Collector |
||||||
|
{ |
||||||
|
public function getNodeType(): string; |
||||||
|
/** @return TValue|null */ |
||||||
|
public function processNode(Node $node, Scope $scope); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
**阶段 1(per-file):** 在每个文件被分析时,collector 收集数据。 |
||||||
|
|
||||||
|
**阶段 2(global):** 所有文件分析完后,创建 `CollectedDataNode` 封装所有数据,运行注册在其上的规则: |
||||||
|
|
||||||
|
```php |
||||||
|
$node = new CollectedDataNode($analyserResult->getCollectedData(), $onlyFiles); |
||||||
|
foreach ($this->ruleRegistry->getRules(CollectedDataNode::class) as $rule) { |
||||||
|
$ruleErrors = $rule->processNode($node, $scope); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
`CollectedDataNode::get(string $collectorType): array<string, list<TValue>>` 按文件路径索引收集到的数据。 |
||||||
|
|
||||||
|
### AOT 编译器具体应用 |
||||||
|
|
||||||
|
| Rule | 触发节点 | 作用 | |
||||||
|
|------|----------|------| |
||||||
|
| `BinaryOpCodegenRule` | `Expr\BinaryOp` | 生成 C++ 运算代码 | |
||||||
|
| `MethodCallCodegenRule` | `Expr\MethodCall` | 方法调用代码生成,虚调用/直调判断 | |
||||||
|
| `TypeCheckInsertRule` | `Param` / `Return_` | 函数入口/出口插入运行时类型检查 | |
||||||
|
| `DeadCodeEliminateRule` | `CollectedDataNode` | 跨文件分析,移除未调用函数 | |
||||||
|
| `DevirtualizeRule` | `CollectedDataNode` | 单实现虚方法 → 直接调用 | |
||||||
|
| `InlineDecisionRule` | `CollectedDataNode` | 基于调用频率和函数大小决定内联 | |
||||||
|
| `ConstantFoldRule` | `Expr\BinaryOp` | 编译期常量折叠 | |
||||||
|
| `BoxOptimizationRule` | `Expr\Assign` | std container 的 Box 逃逸分析 | |
||||||
|
|
||||||
|
**O0/O1/O2 分级:** |
||||||
|
|
||||||
|
```php |
||||||
|
#[RegisteredRule(level: 0)] // 基础代码生成,永远必须 |
||||||
|
class BinaryOpCodegenRule implements Rule { ... } |
||||||
|
|
||||||
|
#[RegisteredRule(level: 1)] // O1 优化 |
||||||
|
class ConstantFoldRule implements Rule { ... } |
||||||
|
|
||||||
|
#[RegisteredRule(level: 2)] // O2 激进优化 |
||||||
|
class InlineDecisionRule implements Rule { ... } |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 采用建议 |
||||||
|
|
||||||
|
渐进式迁移,不需要一次重写整个编译器: |
||||||
|
|
||||||
|
1. 定义 `Rule` 接口和 `Registry`(约 80 行代码) |
||||||
|
2. 把 `parseStmts()` 的一个独立功能提取为第一个 Rule |
||||||
|
3. 逐步迁移剩余 switch 分支 |
||||||
|
4. 引入 `Collector` + `CollectedDataNode` 用于跨文件优化 |
||||||
|
|
||||||
|
Rule 系统可以与现有 switch 调度共存——先让 Rule 处理它能处理的节点,其他 fallback 到原有逻辑。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 5. Extension 注册机制 |
||||||
|
|
||||||
|
### PHPStan 方案 |
||||||
|
|
||||||
|
PHPStan 有数十个扩展接口,通过 DI 容器的 service tag 机制注册: |
||||||
|
|
||||||
|
``` |
||||||
|
DynamicMethodReturnTypeExtension → tag: phpstan.broker.dynamicMethodReturnTypeExtension |
||||||
|
FunctionTypeSpecifyingExtension → tag: phpstan.typeSpecifier.functionTypeSpecifyingExtension |
||||||
|
TypeNodeResolverExtension → tag: phpstan.phpdoc.typeNodeResolverExtension |
||||||
|
MethodsClassReflectionExtension → tag: phpstan.broker.methodsClassReflectionExtension |
||||||
|
PropertiesClassReflectionExtension → tag: phpstan.broker.propertiesClassReflectionExtension |
||||||
|
... |
||||||
|
``` |
||||||
|
|
||||||
|
扩展在核心逻辑之前被调用,有机会覆盖默认行为: |
||||||
|
|
||||||
|
```php |
||||||
|
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type |
||||||
|
{ |
||||||
|
foreach ($this->extensions as $extension) { |
||||||
|
$type = $extension->resolve($typeNode, $nameScope); |
||||||
|
if ($type !== null) { |
||||||
|
return $type; // 扩展处理了,短路核心逻辑 |
||||||
|
} |
||||||
|
} |
||||||
|
// 核心逻辑 ... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 应用 |
||||||
|
|
||||||
|
框架特定的编译优化可通过扩展插件提供,而不是修改编译器核心: |
||||||
|
|
||||||
|
```php |
||||||
|
interface MethodCallOptimizationExtension |
||||||
|
{ |
||||||
|
/** 返回优化后的 C++ 代码,或 null 表示不处理 */ |
||||||
|
public function optimize(MethodCall $call, Scope $scope): ?string; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 6. TypeSpecifier —— 类型窄化引擎 |
||||||
|
|
||||||
|
### 解决的问题 |
||||||
|
|
||||||
|
当 AOT 编译器遇到 `if ($x instanceof Foo)` 时,在分支内部 `$x` 的类型应该被精化为 `Foo`。这可以生成更高效的 C++ 代码(直接调用 Foo 的方法,不走虚表)。 |
||||||
|
|
||||||
|
### PHPStan 方案 |
||||||
|
|
||||||
|
`TypeSpecifier` 分析条件表达式,决定如何在 truthy/falsy 分支中窄化类型: |
||||||
|
|
||||||
|
| 条件 | Truthy 窄化为 | Falsey 窄化为 | |
||||||
|
|------|-------------|--------------| |
||||||
|
| `$x instanceof Foo` | `Foo` | 移除 `Foo` | |
||||||
|
| `$x === null` | `NullType` | 移除 `NullType` | |
||||||
|
| `is_array($x)` | `ArrayType` | 移除 `ArrayType` | |
||||||
|
| `$x`(truthy) | 移除 falsey 类型 | 仅保留 falsey 类型 | |
||||||
|
| `$x > 0` | `int<1, max>` | `int<min, 0>` | |
||||||
|
|
||||||
|
### 类型窄化流水线 |
||||||
|
|
||||||
|
``` |
||||||
|
条件表达式 |
||||||
|
→ TypeSpecifier::specifyTypesInCondition(scope, expr, context) |
||||||
|
→ SpecifiedTypes { sureTypes[], sureNotTypes[] } |
||||||
|
→ MutatingScope::filterBySpecifiedTypes(types) |
||||||
|
→ 新的 scope(类型已窄化) |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 应用 |
||||||
|
|
||||||
|
简化版 TypeSpecifier(~200 行),处理: |
||||||
|
- `instanceof` 窄化 |
||||||
|
- `=== null` / `!== null` 窄化 |
||||||
|
- `is_array()`、`is_string()`、`is_int()` 等函数窄化 |
||||||
|
- `BooleanAnd`/`BooleanOr` 链式窄化 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 7. Immutable Scope(不可变作用域) |
||||||
|
|
||||||
|
### 现状 |
||||||
|
|
||||||
|
AOT 的 `FunctionContext` 使用可变公共数组,仅按变量名追踪类型: |
||||||
|
|
||||||
|
```php |
||||||
|
class FunctionContext { |
||||||
|
public array $localVars = []; // 仅变量名 → 类型 |
||||||
|
public int $scopeLevel = 0; // 简单嵌套计数 |
||||||
|
public array $scopeLayouts = []; // ScopeContext 是空类 |
||||||
|
public bool $inLoop = false; |
||||||
|
public bool $inClosure = false; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### PHPStan 方案 |
||||||
|
|
||||||
|
`MutatingScope` 是**不可变**的持久化数据结构。每次变更返回新实例: |
||||||
|
|
||||||
|
``` |
||||||
|
scope.assignVariable('x', stringType) → new scope |
||||||
|
scope.filterByTruthyValue(instanceofExpr) → new scope (窄化后的类型) |
||||||
|
scope.filterByFalseyValue(instanceofExpr) → new scope (移除后的类型) |
||||||
|
scope.mergeWith(elseScope) → new scope (交集) |
||||||
|
``` |
||||||
|
|
||||||
|
按**表达式字符串**追踪类型,而非仅按变量名: |
||||||
|
|
||||||
|
``` |
||||||
|
'$a' → ExpressionTypeHolder($a, IntegerType, Yes) |
||||||
|
'$a[0]' → ExpressionTypeHolder($a[0], StringType, Yes) |
||||||
|
'$a->prop' → ExpressionTypeHolder($a->prop, FooType, Yes) |
||||||
|
'strlen($a)' → ExpressionTypeHolder(strlen($a), IntegerType, Yes) |
||||||
|
``` |
||||||
|
|
||||||
|
这意味着给 `$a` 赋值会使 `$a[0]` 和 `$a->prop` 的类型缓存失效。 |
||||||
|
|
||||||
|
### AOT 采用建议 |
||||||
|
|
||||||
|
精简版(~300 行),核心能力: |
||||||
|
|
||||||
|
- 不可变 scope,支持快照和合并 |
||||||
|
- 表达式键的类型追踪(至少支持变量和数组元素) |
||||||
|
- `TrinaryLogic` 确定度追踪(分支合并后的 maybe 状态) |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 8. PHPDoc Pipeline |
||||||
|
|
||||||
|
### PHPStan 方案 |
||||||
|
|
||||||
|
三阶段流水线: |
||||||
|
|
||||||
|
``` |
||||||
|
PHPDoc 注释字符串 |
||||||
|
→ Lexer + Parser (phpstan/phpdoc-parser) |
||||||
|
→ PhpDocNode (原始 AST) |
||||||
|
→ TypeNodeResolver::resolve(TypeNode, NameScope) |
||||||
|
→ PHPStan Type 对象 |
||||||
|
``` |
||||||
|
|
||||||
|
`TypeNodeResolver` 用一个 `switch` 分发到 30+ 种标识符类型: |
||||||
|
|
||||||
|
``` |
||||||
|
'int' / 'integer' → IntegerType |
||||||
|
'positive-int' → IntegerRangeType(1, null) |
||||||
|
'non-empty-string' → IntersectionType[StringType, AccessoryNonEmptyStringType] |
||||||
|
'class-string' → ClassStringType |
||||||
|
'array' → ArrayType(MixedType, MixedType) |
||||||
|
'list' → IntersectionType[ArrayType(int), AccessoryArrayListType] |
||||||
|
'mixed' → MixedType(true) |
||||||
|
'never' → NonAcceptingNeverType |
||||||
|
... |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 应用 |
||||||
|
|
||||||
|
AOT 可以复用 `phpstan/phpdoc-parser` 来解析 `@param`、`@return`、`@var` 注解,然后将类型 AST 节点映射到 AOT 自己的 Type 系统。`NameScope`(追踪 namespace + use imports)也是可直接复用的概念。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 9. NeverType(底类型) |
||||||
|
|
||||||
|
表示"不可能有任何值"的类型,即空集: |
||||||
|
|
||||||
|
``` |
||||||
|
union(int, never) = int // never 是 union 的中性元 |
||||||
|
intersect(string, never) = never // never 是 intersect 的吸收元 |
||||||
|
``` |
||||||
|
|
||||||
|
### AOT 应用 |
||||||
|
|
||||||
|
- 死代码消除:表达式窄化为 NeverType → 该路径不可达 |
||||||
|
- 错误传播:矛盾的类型组合产生 NeverType |
||||||
|
- `void` 函数返回:本质上就是 NeverType 在 return 位置 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 推荐采用顺序 |
||||||
|
|
||||||
|
``` |
||||||
|
Phase 1 (基础层): Type 接口 + 具体类型 + TypeCombinator + TrinaryLogic |
||||||
|
└── 替代字符串类型表示,预计减少散落的类型字符串操作 |
||||||
|
|
||||||
|
Phase 2 (模块化): Rule 接口 + Registry + Attribute 注册 |
||||||
|
└── 分解 parseStmts/parseExpr 的巨大 switch |
||||||
|
|
||||||
|
Phase 3 (分析层): TypeSpecifier + 基础 Immutable Scope |
||||||
|
└── 启用 if/else 分支类型窄化 |
||||||
|
|
||||||
|
Phase 4 (优化层): Collector + CollectedDataNode |
||||||
|
└── 启用跨文件全局优化(去虚拟化、死代码消除、内联决策) |
||||||
|
|
||||||
|
Phase 5 (生态层): Extension 注册 + PHPDoc Pipeline |
||||||
|
└── 启用框架插件和注解驱动的类型信息 |
||||||
|
``` |
||||||
|
|
||||||
|
每一层都建立在上一层之上,且每一层都可以独立交付价值。 |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 不应引入的 PHPStan 特性 |
||||||
|
|
||||||
|
| 特性 | 原因 | |
||||||
|
|------|------| |
||||||
|
| Generics / `@template T` | 复杂度极高,AOT 当前不需要 | |
||||||
|
| IntersectionType + Accessory types | `non-empty-string` = `string & non-empty` 这类设计过度工程化 | |
||||||
|
| BetterReflection (静态反射) | AOT 已经加载文件,不需要避免运行时副作用 | |
||||||
|
| 完整 MutatingScope (5000+ 行) | 不可变 scope 模式值得借鉴,但 300 行足够 | |
||||||
|
| ConditionalExpressionHolder | 复合条件的惰性窄化,先做基础 TypeSpecifier | |
||||||
|
| Enum / ConstantArrayType shape | 小众,初期不需要 | |
||||||
Loading…
Reference in new issue