From 46800d9dc9174a9ab96e903e1ccfd556a3af9941 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 5 Jun 2026 13:41:33 +0800 Subject: [PATCH] =?UTF-8?q?docs(aot):=20=E6=B7=BB=E5=8A=A0=20AOT=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8=E4=BC=98=E5=8C=96=E4=BC=98=E5=85=88?= =?UTF-8?q?=E7=BA=A7=E5=92=8C=20PHP=20=E6=BA=90=E7=A0=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=99=A8=E5=88=86=E6=9E=90=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AOT 编译器优化优先级重新评估文档,重点分析类型推断、去虚拟化、逃逸分析等核心优化策略 - 详细阐述 GCC/Clang 二次编译架构下的优化分工原则 - 新增 PHP Zend Optimizer/OPcache/JIT 设计分析文档,提取可借鉴的编译器优化技术 - 包含 SSA/e-SSA 构建、类型推断、SCCP、DCE、逃逸分析等关键技术的实现细节 - 提供 PHP 优化器位掩码类型系统的具体设计方案和算法实现 - 分析 PHP 的稀疏条件数据流框架(SCDF)和通用优化 pass 流水线架构 --- docs/aot-optimization-priority.md | 432 ++++++++++++++++++ docs/php-src-optimizer-analysis.md | 682 +++++++++++++++++++++++++++++ docs/phpstan-design-analysis.md | 559 +++++++++++++++++++++++ 3 files changed, 1673 insertions(+) create mode 100644 docs/aot-optimization-priority.md create mode 100644 docs/php-src-optimizer-analysis.md create mode 100644 docs/phpstan-design-analysis.md diff --git a/docs/aot-optimization-priority.md b/docs/aot-optimization-priority.md new file mode 100644 index 00000000..bd16ae5a --- /dev/null +++ b/docs/aot-optimization-priority.md @@ -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` 相同。 + +这意味着: + +```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 也难以覆盖的 diff --git a/docs/php-src-optimizer-analysis.md b/docs/php-src-optimizer-analysis.md new file mode 100644 index 00000000..dc336cba --- /dev/null +++ b/docs/php-src-optimizer-analysis.md @@ -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` 表示对象引用。逃逸分析可以确认哪些 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 + 类型传播 +``` + +每一层都可以独立实施并立即给现有代码生成带来收益。 diff --git a/docs/phpstan-design-analysis.md b/docs/phpstan-design-analysis.md new file mode 100644 index 00000000..ebaa6f33 --- /dev/null +++ b/docs/phpstan-design-analysis.md @@ -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) +├── 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 */ + 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>` 按文件路径索引收集到的数据。 + +### 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` | + +### 类型窄化流水线 + +``` +条件表达式 + → 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 | 小众,初期不需要 |