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

KPHP 编译器 Review:设计、优化、工具链与语法兼容性分析

2026-06-11 · 基于 /home/swoole/workspace/cpp/kphp 源码审查


一、架构概览

维度 KPHP AOT Compiler
编译目标 PHP → C++ → 二进制 PHP → C++ → 二进制
IR 形式 自定义 vertex (op_*) 树 PHP-Parser AST → C++ 字符串
类型推理 迭代收敛型图推断(收敛于泛化方向) SSA + 手动类型注释
中间优化 多 pass AST 重写(~60+ pipe) 直接 AST → C++ 翻译 + 少量优化
运行时 自研(allocator/string/array/mixed) phpx(C++ RAII 包装 Zend API)
并发模型 单线程 + reactor/epoll 依赖 Zend/TSRM
线程安全 无(内存分配器无锁) Zend TSRM
代码量(编译器) ~18k 行 pipe 代码 ~6k 行 CompilerBase

二、可借鉴的创新设计

2.1 Rewrite Rules DSL(模式匹配优化规则)

文件: compiler/rewrite-rules/early_opt.rules

声明式 DSL 描述 AST 重写,编译期生成 C++ 优化代码。规则格式为 (pattern) => (replacement),支持条件子句和 C++ 表达式嵌入:

;; strlen 常量折叠
(op_func_call {"strlen"} arg:(op_string))
  => (op_int_const { std::to_string(arg->str_val.size()) })

;; explode 索引直接访问 → 特化版本
(op_index (op_func_call {"explode"} delim s) k:(op_int_const))
  => (op_func_call {"_explode_nth"} delim s k)

;; ("" . $x) → (string)$x — 消除无意义拼接
(op_concat (op_string {""}) x) => (op_conv_string x)

;; 子串类型优化:conv(substr(...)) → conv(_tmp_substr(...))
(op_conv_int x) if let x2 { to_tmp_string_expr(x) } => (op_conv_int x2)

AOT 借鉴优先级: P0

当前 FuncCallOptimizerstrlen/count 等优化是硬编码的。引入类似规则引擎可以:

  • 声明式添加优化,降低维护成本
  • 通过 if let 条件做局部模式变量绑定
  • 规则文件与编译器分离,热加载可行

实现建议:PHP 层面实现一套 RewriteRule 类,在 FuncCallOptimizer 中加载并匹配。


2.2 Smart instanceof / Smart Casts(类型收窄)

文件: compiler/pipes/transform-to-smart-instanceof.cpp

if ($x instanceof A) 之后,$x 在 if 体内自动重命名为 instance_cast<A>($x)。核心创新在于在类型推断之前做变量拆分:

// PHP 源码
if ($x instanceof A) {
    $x->methodOfA();  // $x 自动变为 instance_cast<A>($x)
}

// 反向守卫模式
if (!($x instanceof A)) return;
// 此后 $x 全函数范围替换为 instance_cast<A>($x)

同时处理 catch (SomeClass $e) 中同名变量在不同 catch 块间的重命名,防止 assumption 混淆。

AOT 借鉴优先级: P0

当前 SsaTypeOptimizer 只做 int/float/string 基本类型收窄。可以增加对象类型收窄:

  1. 在 SSA builder 中识别 instanceof 守卫
  2. 在 then/else 分支中替换为目标子类类型
  3. 结合现有的 stableObjects 机制进行 devirtualization

2.3 流水线并行编译

文件: compiler/compiler.cpp

编译过程以函数为粒度,通过 operator>> 链接管道,多线程并行处理:

SchedulerConstructor{scheduler}
    >> PipeC<LoadFileF>{}
    >> PipeC<FileToTokensF>{}
    >> PipeC<ParseF>{}
    >> PassC<GenTreePostprocessPass>{}
    /* ... 60+ pipes */;

三种管道类型:

  • PipeC<T>: 通用转换,输入→输出
  • PassC<T>: 函数级变换,遍历所有 AST 顶点
  • SyncC<T>: 同步点,所有输入处理完毕才输出

不同函数可在不同阶段同时处理,全局存储使用线程安全或无锁结构。

AOT 借鉴优先级: P3

当前 PreprocessorCompilerBase 是串行执行。对于大型项目可引入函数级并行:

  • 类/函数粒度独立编译
  • SyncC 同步点用于合并全局符号表

2.4 Switch 拆分(状态机变换)

文件: compiler/pipes/split-switch.cpp

将 switch 的每个 case 分支提取为独立函数,用状态变量驱动:

// 每个 case 变成:
int case_state = 0;
auto case_res = switch_func_N(&case_state);
if (case_state == 1) return case_res;     // 正常返回
if (case_state == -1) break;              // break 语义

break Ncontinue N 被转换为状态变量 -1 设置 + return,与之前 AOT 实现的 _brk_flag / _cnt_flag 方案思路一致。

AOT 借鉴优先级: P2

大型 switch 可以拆分为独立函数,降低单函数复杂度,使 GCC 内联/优化更有空间。


2.5 常量不可变标记与 init-once

文件: compiler/pipes/collect-const-vars.cppruntime-common/core/memory-resource/

编译期常量数组/字符串使用特殊的 refcount 标记:

ExtraRefCnt::for_global_const   // 不可变,修改时触发 COW
ExtraRefCnt::for_instance_cache // 跨请求共享,不可修改

这些常量存放在 data section,服务器启动时初始化一次,后续请求只读使用。任何修改操作自动触发 COW。

AOT 借鉴优先级: P1

当前 AOT 已将常量数组提升为 static 变量,但可以引入更细粒度的不可变标记机制,减少不必要的 COW 拷贝(当编译期可证明变量从未被修改时)。


2.6 函数特化(多版本生成)

文件: compiler/pipes/early-optimization.cpp

在类型推断之前根据参数进行函数特化:

  • microtime()_microtime_float()_microtime_string()(根据参数 true/false)
  • list() + explode()_explode_tupleN()(精确 N 元组类型)
  • explode()[N]_explode_nth()(O(1) 直接访问第 N 个元素)
  • substr() 在函数参数位置 → _tmp_substr()(避免字符串拷贝)

关键是特殊化版本返回更精确的类型。例如 microtime() 返回 mixed,而 _microtime_float() 返回 float

AOT 借鉴优先级: P1

FuncCallOptimizer 目前只做常量折叠,可以扩展为多版本特化

// 当前
$result = strlen($s);  // 返回 mixed/int

// 优化后
$result = _strlen_string($s);  // 编译期确定返回 int

2.7 Class Assumptions:先验类型预测

文件: compiler/class-assumptions.cpp

解决类型推断和调用图构建的循环依赖

$obj->method()  需要 $obj 的类型才能绑定 method()
               但类型推断需要完整调用图
               → Assumption 打破循环

Assumption 来源:

  • @param ClassName $x — 参数类型
  • @return ClassName — 返回类型
  • @var ClassName — 局部变量
  • 构造函数调用 new ClassName() → 直接得到类型

Assumption 在类型推断之前进行,用于绑定调用图。类型推断之后进行校验——若不匹配则报错。

AOT 借鉴优先级: P2

对于 method call devirtualization:assumption 比纯 SSA 分析更早可用,可作为 devirtualization 的第一阶段(在 SSA 不可用时回退)。


2.8 虚拟方法自动生成

文件: compiler/pipes/generate-virtual-methods.cpp

当方法被子类覆盖时,基类方法自动成为分发器:

ReturnType f$Base$$method(instance_var, args...) {
    if (instance_var.ce() == Child1::ce)
        return f$Child1$$method(instance_cast<Child1>(instance_var), args...);
    if (instance_var.ce() == Child2::ce)
        return f$Child2$$method(instance_cast<Child2>(instance_var), args...);
    // ... fallback to self
    return f$Base$$method$$Base(instance_var, args...);
}

同时做 PHP 7.4+ 类型变体检查(参数逆变、返回协变)。

AOT 借鉴优先级: P2

当前 devirtualization plan 中的 "runtime exact-type guard" 与此思路一致。可借鉴其自动生成全部分发分支 + variance 检查。


2.9 性能检查注解(Performance Inspections)

文件: docs/kphp-language/best-practices/performance-inspections.md

编译期性能分析,通过注解激活:

/** @kphp-warn-performance implicit-array-cast */
function businessLogic() { ... }

支持的检查项:

  • implicit-array-cast — 检测 array<int>array<mixed> 隐式转换(昂贵拷贝)
  • array-merge-into — 检测可通过 array_merge_into 优化的合并
  • array-reserve — 检测可预分配大小的数组
  • constant-execution-in-loop — 检测循环中的常量表达式

注解通过调用链传播到所有可达函数。

AOT 借鉴优先级: P3

与函数着色类似,可作为一种编译期静态分析插件。implicit-array-cast 对类型化数组的性能影响极大,值得单独检测。


2.10 池式内存分配器

文件: runtime-common/core/memory-resource/unsynchronized_pool_resource.h

  • 预分配固定大小 buffer
  • 小块 (<16KB): slab 分配,按大小分级(free_chunks_[chunk_id]),O(1) 分配/释放
  • 大块 (≥16KB): 红黑树管理(huge_pieces_),支持碎片整理
  • 每个请求结束后硬重置hard_reset()),无需逐个释放
  • 支持 OOM handling memory 预留

AOT 借鉴优先级: P4

当前依赖 Zend MM。对于长时间运行的 CLI 模式,pool allocator 可显著降低碎片和分配开销。但需要替换整个内存管理层,工程量大。


三、工具链分析

3.1 测试基础设施

KPHP 有三层测试体系:

层级 目录 用途
PHPT 测试 tests/phpt/ (75+ 子目录) PHP 行为兼容性测试
C++ 单元测试 tests/cpp/compiler/ tests/cpp/runtime/ tests/cpp/server/ 编译器/运行时/服务器组件测试
Python 集成测试 tests/python/tests/ HTTP/RPC/多进程集成测试

测试运行器 tests/kphp_tester.py 支持:

  • 标签机制(@ok, @kphp_should_fail, @kphp_should_warn 等)
  • PHP 版本选择(@php7.4, @php8
  • 多进程并行执行(基于 ThreadPool)
  • TCP server 管理
  • k2 模式(组件编译)兼容
  • 增量编译支持(nocc 分布式编译)

AOT 借鉴:

  • 当前 AOT 只有 phpunit/ (PHPUnit) 和 tests/compiler/ (PHPT) 两层,缺少编译器内部单元测试和集成测试
  • 标签机制比纯 PHPT 更灵活——可以标记预期编译失败、预期警告等
  • Python 测试 runner 提供了更好的 CI 集成能力

3.2 基准测试框架

文件: tests/benchmarks/

使用 Go 编写的 ktest 工具做 KPHP vs PHP 性能对比:

$ KPHP_ROOT=/path/to/repo/kphp ./ktest bench-vs-php tests/benchmarks/

基准测试覆盖:

  • BenchmarkBasic.php — 基础操作
  • BenchmarkConcat.php — 字符串拼接
  • BenchmarkExplode.php — explode 性能
  • BenchmarkMultiSwitch.php — 大型 switch
  • BenchmarkTmpString.php — 临时字符串优化效果
  • BenchmarkJson.php / BenchmarkFFI.php — 特定功能

AOT 借鉴:

  • 可以建立类似的 AOT vs PHP 基准对比套件
  • 特别关注 AOT 编译器声称优化的场景(如 typed property access、devirtualized calls)

3.3 IDE 集成

KPHP 提供 kphpstorm IDE 插件(docs/kphp-language/kphpstorm-ide-plugin/),支持:

  • @kphp-* 注解语法高亮
  • 类型标注补全
  • KPHP 特有类型的提示

AOT 借鉴:

  • 当前 AOT 编译器使用 @phpstan-* 等注解,可考虑提供 VSCode/JetBrains 插件

3.4 增量编译

KPHP 仅重新编译变更的文件(基于 CRC64 哈希):

// 每个生成文件开头
//crc64      <content_hash>
//crc64_with_comments <hash_with_comments>

比较这些哈希与上次生成结果,确定需要重编译的文件,包括所有依赖此文件的上游文件。

AOT 借鉴:

  • 当前 build/ 目录全量重新生成,可引入类似的增量机制加速大型项目迭代

四、语法兼容性分析

4.1 KPHP 支持的 PHP 版本

KPHP 瞄准 PHP 7.4 语言级别,部分 8.0/8.1 特性正在添加。

4.2 不支持的特性(架构原因)

特性 原因
动态函数/方法调用 (call_user_func) 编译期无法解析符号
eval() 编译期不可知
动态类/函数声明 符号表必须在编译期完整
Reflection 需要运行时元数据
Mock (PHPUnit) 依赖 Reflection + 动态重定义
数组内部指针 (reset/current/next) 不符合引用语义
PHP 扩展互操作 自研运行时替代

4.3 不支持的特性(未实现)

特性 状态
嵌套 list() 未实现
生成器 (yield) 未实现
匿名类 未实现
Group use declarations 未实现
finally 未实现
func_get_args 未实现
引用(除 foreach by ref 和引用参数外) 部分支持
接口在父链中出现多次 不支持
insteadof / traits 重命名 不支持

4.4 KPHP 特有注解

// 函数注解
@kphp-inline                  // 强制内联(GCC inline)
@kphp-flatten                 // 激进内联所有 callee
@kphp-required                // 强制编译(用于字符串回调)
@kphp-sync                    // 禁止成为 resumable
@kphp-no-return               // 永不返回(优化 CFG)
@kphp-pure-function           // 纯函数(常量数组可调用)
@kphp-warn-unused-result      // 未使用返回值时报错
@kphp-should-not-throw        // 禁止抛异常
@kphp-throws {Class}          // 受检异常
@kphp-generic T1, T2          // 泛型函数
@kphp-color {color}           // 能力标注
@kphp-warn-performance {...}  // 性能检查
@kphp-disable-warnings {...}  // 抑制特定警告
@kphp-profile                 // 嵌入 profiler

// 类注解
@kphp-serializable            // 可序列化
@kphp-immutable-class         // 不可变类
@kphp-json {attr}={value}     // JSON 配置

4.5 与 AOT 编译器的语法差异

特性 KPHP AOT Compiler
基础 PHP 版本 7.4 8.2+
枚举 不支持 支持 (PHP 8.1 enum)
命名参数 不支持 支持
Match 表达式 不支持 支持
联合类型 部分支持 支持
Nullsafe ?-> 不支持 支持
属性提升 (constructor promotion) 不支持 支持
list() 解构 部分 完整支持
break N / continue N 部分支持 已支持
类型化数组 自定义语法 array<T> 无(使用 phpstan 标注)
泛型函数 @kphp-generic
元组 / Shapes 自定义语法
FFI 支持(自定义 FFI)

4.6 类型系统的关键差异

KPHP 的类型系统比 PHP 严格得多:

  • 不允许类型混用: f(42); f("string") 对同一个 $arg 是编译错误
  • 数组类型化: array<int> vs array<string> 是不同的类型,转换需要显式或隐式 cast
  • mixed 代价高昂: 16 字节 tagged union + switch-case 分发
  • 泛型函数: 通过 @kphp-generic 实现编译期特化,类似 C++ template
  • 变量分裂: 同一变量名可能在不同的 CFG 路径上分裂为不同名称(如 $x$x$v1

五、总结与优先级建议

优先级 技术 难度 收益 说明
P0 Rewrite Rules DSL 声明式优化规则,可扩展性极强
P0 Smart instanceof casts 直接提升对象类型收窄 + devirtualization
P1 函数特化(多版本) 更精确的返回类型,消除 mixed 污染
P1 不可变常量标记 减少 COW,已有常量提升基础
P2 Switch 拆分 已有多级 break 基础,特定场景优化
P2 Class Assumptions 需要 PHPDoc 解析基础设施
P2 虚拟方法自动生成 与 devirtualization plan 互补
P3 性能检查注解 辅助开发者发现隐藏性能问题
P3 流水线并行 大型项目编译提速
P3 函数着色 辅助安全/IO 审计
P4 增量编译 开发体验优化
P5 Resumable 状态机 需实际 async 需求
P5 Pool allocator 极高 需替换整个内存管理