# TypePHP 编译实现分析、提速方案与 mago 替代评估 > 生成日期:2026-08-03 > 适用范围:`swoole/typephp` v0.4.3(本仓库 `D:\git\php\aot-compiler`) > 说明:所有耗时数字均来自本仓库实际基准测试(`src/` 下 139 个 PHP 文件、约 1.65 MB 源码,PHP 8.4.16 CLI;端到端对比用 302 文件合成项目,Windows + MSVC) --- ## 0. 实施状态(2026-08-03 更新) **S1(合并两次解析)与 S2(writeFile 内容比对 + 生成 .cc 增量缓存)已实施并验证。** ### 已落地的改动 | 文件 | 改动 | |---|---| | `src/CompilerBase.php` | 新增 `parseCachedAst()` / `cloneAst()`(AST 内存缓存 + CloningVisitor 深拷贝);`writeFile()` 增加内容比对(内容相同不覆盖,返回 bool 保持 mtime 稳定) | | `src/Preprocessor.php` | `prepareFile()` 改用 `parseCachedAst()`,消除与 convert 的重复 parse | | `src/Translator.php` | `doConvert()` 改用 `parseCachedAst()`;新增 `hasGeneratedObjectFileCache()` / `getGeneratedObjectCacheKey()` / `getGeneratedHeaderDependencies()` / `isGeneratedSourceFile()`;`compileFile()` 对生成的 .cc 走对象缓存;`save()`/`genExtension()` 仅在实际写入时 format | ### 实测收益(302 文件合成项目,dry 模式,3 次取均值) | 场景 | 基线 | 改动后 | 提升 | |---|---|---|---| | 全量构建(冷) | 6011 ms | 5629 ms | **-6.4%** | | 增量构建(第 3 次,不清理 build) | — | 5023 ms | **-16.4%**(相对基线冷构建) | 说明:dry 模式不包含 C++ 编译,S2 对象缓存的更大收益(跳过 g++/cl 编译)需在完整 build 场景体现;dry 耗时中固定开销(CLI 启动、YAML、符号表)占比高,稀释了 S1 的解析节省。 ### 验证记录 - `parseCachedAst`:同一文件 prepare+convert 各调用一次,第二次命中缓存且返回深拷贝(不共享节点、无 resolvedName 残留污染)✓ - `writeFile` 内容比对:相同内容跳过写入,mtime 保持不变 ✓ - `isGeneratedSourceFile`:buildDir 前缀 + .cc 扩展名判断,与 `getCppFile` 路径一致 ✓ - PHPUnit:Entity 目录 85 测试、FileScanner 等 110 测试通过(1 个 Windows 路径分隔符既有失败与本次无关) - `typephp-think`(554 文件):补丁后 prepare 通过,convert 阶段因「仓库源码类型系统比 v1095 正式版严格」(`Cannot re-assign`)受阻,属源码版本差异,非本次改动问题 --- ## 1. 编译是如何实现的(管线剖析) ### 1.1 总体流程 入口为 `bin/tpc.php` → `src/compiler.php` 的 `main()` → `src/Translator.php`(继承 `Preprocessor` → `CompilerBase`)。编译是一条四阶段流水线: ``` ┌────────────────────────────────────────────────────────────────────────┐ │ ① prepare() 扫描+解析+建符号表+依赖拓扑排序 │ │ src/Build/SourcePipelineTrait.php::prepare() │ │ → 每文件: Preprocessor::prepareFile() │ │ parser->parse() ←── AST 解析 #1 │ │ NodeTraverser(NameResolver + Visitor + 常量校验 + AttributeLower) │ │ 收集 symbolDeclInFile / symbolCallInFile / classMap / funcMap │ │ → getSortedFiles(): marcj/topsort 按文件间符号依赖拓扑排序 │ ├────────────────────────────────────────────────────────────────────────┤ │ ② convert() 每文件: AST → C++ 源码 │ │ src/Build/SourcePipelineTrait.php::convert() │ │ → Translator::convertFile() → doConvert() │ │ loadFile() 读盘(第二次) │ │ parser->parse() ←── AST 解析 #2(同一文件解析两次!) │ │ NodeTraverser 再次遍历(与 prepare 相同的 4 个 visitor) │ │ 逐节点翻译生成 C++(CompilerBase 4000 行核心逻辑) │ │ → 生成 func_decl.h / data_decl.h / extension-.cc │ ├────────────────────────────────────────────────────────────────────────┤ │ ③ compile() C++ → .o(外部编译器) │ │ Translator::compile() │ │ → phpx misc 文件 + 生成的 .cc │ │ → PCH 预编译头(PrecompiledHeaderManager,内容指纹缓存) │ │ → Linux/macOS: pcntl fork 并行(默认 4 jobs,大文件优先调度) │ │ → Windows: 串行(supportsPcntlParallelCompile() = false) │ │ → phpx misc 有 .o 缓存(hasMiscObjectFileCache,命令+ABI 指纹) │ ├────────────────────────────────────────────────────────────────────────┤ │ ④ build() 链接为可执行文件 / 扩展 / 库 │ │ Translator::build() → NativeBuilder::link() │ └────────────────────────────────────────────────────────────────────────┘ ``` ### 1.2 关键实现位置 | 职责 | 文件 | |---|---| | 四阶段编排 | `src/Build/SourcePipelineTrait.php`(prepare/convert)、`src/Translator.php`(compile/build) | | PHP→C++ 逐节点翻译 | `src/CompilerBase.php`(4133 行,核心) | | 扫描/符号表/依赖排序 | `src/Preprocessor.php` | | 编译器抽象 | `src/Backend/`(CompilerBackend → GccLikeBackend → Gcc/Clang、Msvc) | | 平台抽象 | `src/Platform/`(Linux/Macos/Windows) | | 并行编译调度 | `src/Build/NativeBuilder.php::dispatchParallel()` + `SourceCompileQueue` | | PCH 管理 | `src/Build/PrecompiledHeaderManager.php` | | 生成物落盘 | `CompilerBase::writeFile()`(无内容比对,直接覆盖) | ### 1.3 当前已具备的优化 1. **PCH 预编译头**:`phpx.h` 等 8 个公共头打成 `.gch`,带 24 位 sha256 内容指纹缓存(`PrecompiledHeaderManager`)。 2. **phpx misc 对象缓存**:`phpx/src/misc/*.cc` 的 `.o` 用「编译命令 + PHP ABI」指纹缓存,避免每次全量重编 PHPX 运行时。 3. **编译并行**:Linux/macOS 用 pcntl fork 并行(`-j`,默认 4),且按「大文件优先占满槽位 + 小文件快速通道」调度。 4. **编译期常量展开、字面量字符串表**、`--dry` 只生成 C++ 等。 --- ## 2. 性能瓶颈实测 ### 2.1 基准方法 用 `nikic/php-parser 5.6.1`(与仓库 `composer.json` 一致)对 `src/` 全部 139 个 PHP 文件(约 1.65 MB)做多轮测量: ``` parse (nikic/php-parser) ×3 1455.5 ms → 单遍 ≈ 485 ms(3.18 MB/s) parse + NameResolver ×3 1797.0 ms → 单遍 ≈ 599 ms parse+resolve+serialize (1x) 701.1 ms unserialize ×3 779.8 ms → 单遍 ≈ 260 ms parse+resolve+prettyPrint ×3 2196.0 ms serialized AST 体积 47.6 MB(源码的 28.8 倍) ``` ### 2.2 结论性瓶颈清单(按影响排序) | # | 瓶颈 | 证据 | 影响 | |---|---|---|---| | B1 | **每个 PHP 文件被解析两次** | `prepareFile()`(Preprocessor.php:130)与 `doConvert()`(Translator.php:2325)各自 `parser->parse()` 一次,且各自跑一遍完整 NodeTraverser | PHP 侧最大浪费:139 文件规模下纯解析+resolve ≈ 2×599ms ≈ **1.2s 起步**,大项目线性放大 | | B2 | **生成的 .cc 无增量缓存** | `compileFile()` 只对 phpx misc 走 `hasMiscObjectFileCache`;用户代码生成的 `.cc` 每次全量重编 | 改 1 个 PHP 文件也要重编全部 .cc(C++ 编译是耗时的绝对大头) | | B3 | **生成文件无内容比对** | `CompilerBase::writeFile()` 直接 `file_put_contents` 覆盖,mtime 必然变化 | 即使内容相同也触发下游重新依赖判断/重编,破坏增量可能性 | | B4 | **prepare/convert 阶段完全串行** | 两阶段都是单进程 `foreach` | 多核只用于 compile;PHP 侧解析/翻译占用的核数为 1 | | B5 | **Windows 无并行编译** | `Windows::supportsPcntlParallelCompile() = false`,直接走 `compileSourceFile()` 串行 | Windows 大项目编译时间 = 串行全量 | | B6 | **PCH/缓存指纹每次全量计算** | `PrecompiledHeaderManager::buildFingerprint()` 每次构建都递归遍历 phpx 头文件并逐个 hash(实测 131 个头文件 ≈ 42ms,PHP 侧还有 misc 缓存同样的遍历) | 每次构建的固定开销,虽不大但在累积 | | B7 | 转换阶段反复 `getType()` 字符串比较、NodeFinder 多次全树扫描(如 `findSymbolUsing`、`canOptimizeMultiReturn`) | CompilerBase/Preprocessor 中多次独立 `findInstanceOf` | 每文件多次 O(N) 全树扫描,可合并为一次遍历 | ### 2.3 一个反直觉的事实 **AST 序列化缓存是划算的**:unserialize 单遍 260ms vs parse+resolve 599ms(快 2.3 倍),即使加上 serialize 成本(首轮 ~700ms/139 文件),二次构建纯 PHP 解析阶段也能从 ~1.2s 降到 ~260ms。但注意序列化体积膨胀 28.8 倍(47.6MB/1.65MB),对磁盘 I/O 有压力——更优做法是**合并两阶段解析**(见方案 S1)。 --- ## 3. 提速方案(按性价比排序) ### S1(P0)prepare/convert 合并为单次解析 —— 省一半 PHP 解析时间 - **问题**:同一文件 `prepare` 和 `convert` 各 parse+traverse 一次,逻辑完全重复(visitor 列表一致)。 - **做法**:让 `prepareFile()` 把 parse 好的 `$stmts`(NameResolver 等已跑完)挂到文件缓存(内存 `array>`,或按 `filemtime+内容hash` 落盘 `build/cache/ast/`),`doConvert()` 优先取缓存;取不到(如 `--force`、跨进程)才重新 parse。 - **收益**:139 文件规模 PHP 解析从 ~1.2s → ~0.6s;大项目直接减半。 - **风险**:低。`convert` 依赖的 `resolvedName` 等 attribute 由 NameResolver 在 traverse 时写入,缓存的是 traverse 之后的 AST,语义不变。需注意 `ConstantExpressionValidationVisitor` 的报错回调在 prepare 是 warning、convert 是 fatal,缓存后错误在 prepare 阶段就能暴露,行为只会更早更一致。 ### S2(P0)生成 .cc 的增量对象缓存 —— 编译期最大收益 - **做法**:把 `hasMiscObjectFileCache()` 的机制推广到所有生成的 `.cc`:缓存 key = 编译命令 + PHP ABI + **生成物内容 hash(或源 PHP hash + 编译器版本)**。内容未变 → 跳过 g++/clang。 - **配套**:`writeFile()` 加「内容比对,相同则不落盘」逻辑(解决 B3),保证 mtime 稳定。 - **收益**:改 1 个文件后二次构建,C++ 编译从全量变为仅差异文件;这是肉眼可感知的最大提升。 - **风险**:中。生成 .cc 的内容由 `genFunctionDeclarations`/`genDataDeclarations` 等公共头影响——若公共头变了,所有 .cc 的 `#include` 语义可能变,但 .cc 内容 hash 不会变。**必须把公共声明头也纳入 key**(hash func_decl.h/data_decl.h/extension 文件内容),或采用「.cc 内容未变 且 公共头未变 且 .o 存在 且 命令指纹未变」四条件跳过。 ### S3(P0)修复 PCH/缓存指纹的计算时机 - `buildFingerprint()` 每次构建全量扫描+hash 头文件(~42ms + misc 缓存同款遍历),应改为「mtime+size 列表指纹」先行判断,内容 hash 仅在前者变化时执行。 ### S4(P1)prepare/convert 并行化 - parse+resolve 是纯函数,可用 pcntl fork 分片解析(子进程只做 parse+serialize,父进程归并反序列化),类似已有 `dispatchParallel` 的模式。转换阶段因共享状态(literalStrings、classMap 等)风险高,可先只并行「解析」,翻译仍串行。 ### S5(P1)Windows 并行编译 - 两条路:(a) 给 `Msvc` 后端加 `/MP`(MSVC 原生多进程编译,单命令即可);(b) Windows 上用 `proc_open` 实现多进程调度替代 pcntl。收益对 Windows 用户显著。 ### S6(P2)工具链层 - `ccache`/`sccache` 兜底(对 C++ 编译做内容缓存,配合 S2 效果更稳);链接器换 `mold`/`lld`;`-pipe`、`-fno-asynchronous-unwind-tables` 等编译 flag 微调。 ### S7(P2)单次遍历替代多次 NodeFinder - `findSymbolUsing()` 等每文件多次 `findInstanceOf` 全树扫描,可合并为一次 visit 收集函数调用/类引用/常量引用三类符号,减少 O(N) 重复。 --- ## 4. AST 解析能否用 mago 替代?—— 结论:不建议(现阶段不可行) ### 4.1 mago 是什么 [mago](https://github.com/carthage-software/mago)(carthage-software/mago)是一个 **Rust 编写的 PHP 工具链**:lint、format、静态分析(对标 Clippy/OXC),v1.45.0(2026-07),MIT/Apache-2.0 双许可,开发活跃。其核心是自研高性能 PHP 解析器(crate:`mago-syntax` 词法/语法/遍历、`mago-names` 名称解析),官方基准:7M 行 WordPress 静态分析 1.46s(PHPStan 55.9s)、lint 0.88s、format 0.43s。**已支持 PHP 8.4 property hooks、asymmetric visibility(private(set) 等)。** ### 4.2 为什么不能直接替代 | 维度 | nikic/php-parser(现状) | mago | 兼容性判断 | |---|---|---|---| | 语言/进程 | PHP 类库,进程内对象 | Rust 二进制/crate,AST 是 Rust 结构 | 无法在 PHP 进程中直接消费 | | AST 模型 | AST(抽象语法树),含 attribute 机制(`resolvedName`/`namespacedName` 等存于节点属性) | CST(具体语法树,含 trivia),`mago ast --json` 是调试输出 | 结构不同,需要完整映射层 | | 下游依赖 | TypePHP 全库 296 处 `PhpParser\*` 引用、75 个文件;依赖 `NodeTraverser`/`NodeVisitor`/`NameResolver`/`NodeFinder`/`ConstExprEvaluator`/`PrettyPrinter`/`Modifiers` 等 | 无 PHP API | 替换 = 重写编译器前端 | | 名称解析 | `NameResolver` 写入节点 attribute,后续代码生成大量读取 | `mago-names` 输出到 Rust 侧 | 需要桥接层搬运 | | 代码生成 | `PrettyPrinter` 用于 stub 生成(`LibraryImportStubGenerator`、`gen_stub.php`) | 无等价物 | 缺失 | | 错误语义 | `PhpParser\Error` + 行列号,被 `SyntaxError` 包装 | 错误模型不同 | 需要适配 | **核心矛盾**:TypePHP 不是「解析完就丢」的 linter,而是「AST 必须留在 PHP 进程内、被 4000 行 CompilerBase 逐节点翻译」的编译器。走 mago 只有两条路,都不可行: 1. **mago CLI → JSON AST → PHP 重建对象**:JSON 序列化体积远大于源码(参考第 2 节:php-parser AST 序列化已膨胀 28.8 倍,mago CST 只多不少),反序列化 + 对象重建的 CPU/内存开销会**吃掉**解析省下的时间,且每文件一次进程启动(exec)成本极高;同时丢失 attribute 机制、注释/行列语义需重新映射。 2. **PHP FFI 绑定 mago-syntax crate**:需要为整个 Rust AST 写 FFI 边界 + 转 PHP 对象层,工程量接近重写前端,且 mago 的 CST 与 TypePHP 需要的「带 resolvedName 的 AST」不是一回事,仍要在 PHP 侧自建名称解析与代码生成数据模型。 ### 4.3 mago 在本项目中的合理定位(可选) - **前置快速语法校验**:编译前用 `mago`(或 `mago lint --quick`)做廉价的全项目语法/明显错误预检,给出比 php-parser 更快的反馈(适合 IDE/CI 场景,不进入编译流水线核心)。 - **性能参照物**:mago 证明了「Rust 原生解析」的量级(~百 MB/s vs php-parser 3 MB/s),可作为未来「把前端重写为原生编译器(如 Rust 侧做 parse+name-resolution,产物是自研序列化 AST)」的路线参考——但那是一个全新项目,而非对现有编译器的增量改造。 - **若仍想引入原生解析**:更现实的是保留 php-parser 语义、仅加速解析环节(如 `--php-parser` 的 `PhpParser\ParserFactory` 换用 tree-sitter 绑定并在 PHP 侧重建 php-parser 节点),但收益-成本比仍不如 S1/S2。 ### 4.4 结论 > **不建议用 mago 替换 nikic/php-parser**。mago 是「解析即弃」的工具链,TypePHP 是「解析后深度消费 AST」的编译器,二者 AST 模型与进程模型不兼容,桥接成本会抹平解析速度优势。 > 真正的提速路径在 **S1(两阶段合并单次解析)+ S2(生成 .cc 增量缓存 + writeFile 内容比对)+ S4/S5(并行化)**,全部基于现有架构增量改造,风险可控、收益立竿见影。 --- ## 5. 落地顺序建议 ``` 第一阶段(P0,改动小收益大,1-2 天) 1. writeFile 内容比对(S2 前置) 2. prepare/convert 共享 AST(S1) 3. 生成 .cc 增量对象缓存(S2,key 含公共头内容 hash) 第二阶段(P1,结构性收益) 4. PCH/misc 指纹轻量化(S3) 5. 解析阶段 pcntl 并行(S4 解析部分) 6. Windows 并行(/MP 或 proc_open)(S5) 第三阶段(P2,长期) 7. 单次遍历合并 NodeFinder(S7) 8. ccache / mold / lld(S6) ``` ## 6. 附:基准脚本 分析所用基准脚本:`.workbuddy/bench_ast.php`(可重复运行:`php .workbuddy/bench_ast.php`),复现本节所有数字。