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

16 KiB

AOT 编译器优化优先级重新评估:考虑 GCC/Clang 二次编译

核心原则

AOT 的优化价值不在于做 GCC/Clang 已经做了的事情,而在于提供 GCC/Clang 无法从 C++ 代码中推断出来的信息

GCC/Clang 在 -O2/-O3 下已经能做的:

  • 常量折叠、常量传播 (SCCP)
  • 死代码消除 (DCE)
  • 公共子表达式消除 (CSE)
  • 循环展开、向量化
  • 指令选择、寄存器分配
  • 函数内联 (同一编译单元内)
  • 分支预测优化
  • SIMD 自动向量化

GCC/Clang 不能做的——因为语义被 php::Varphp::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 源码: 循环计数器,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_MAXPHP_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 的类型已确定为 BarFoo ⊄ 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*> 相同。

这意味着:

if ($x instanceof Foo) {
    $x->method();  // 虚调用!$x 的实际类可能重写了 method()
}

不能因为窄化为 Foo 就把 $x->method() 转为直接调用 Foo::method()——除非 Foo 是 final 类或 method 是 private。

窄化的三个安全层级

层级 1: 类型成员可访问性(总是安全)

即使不知道精确类型,只要知道 $xFoo 的子类型,就可以:

  • 确认 $x 有某个方法/属性的访问能力
  • 消除不可能的代码路径(if ($x instanceof Foo)$x 已知为 BarFooBar 不相关时为 false)
  • 生成正确的 C++ 类型标注(Foo* 而非 void*/php::Var
// 窄化前: $x 是 php::Var,调用 foo() 可能要抛异常
// 窄化后: $x 是 Foo 的某个子类型,一定有 foo() 方法
$x->foo();  // 可以确认方法存在(虽然仍是虚调用)

收益: 消除方法不存在的运行时检查,改进错误检测(编译期发现)。

层级 2: final 类 + private 方法(完全安全)

场景 可窄化为精确类型? 可直接调用?
$x instanceof FinalClass
$x->privateMethod() 是 (private 不可重写)
self::method() 且类无子类
$x instanceof NonFinalClass
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 只能是 SubFooASubFooB。可以生成:

// 守卫式去虚拟化
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 结构体精确地追踪了这个区别:

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 也难以覆盖的