公司动态

TypePHP开源:将PHP源码编译为原生机器码的AOT编译器

📅 2026/9/2 15:13:37
TypePHP开源:将PHP源码编译为原生机器码的AOT编译器
TypePHP 正式开源的消息最近在 PHP 圈子里讨论度不低。简单说这是一个试图把 PHP 源码直接编译成原生机器码的项目目标不是再套一层解释器而是让 PHP 脚本在编译之后能以本地可执行文件的形式运行。它和 PHP 8 内置的 JIT 思路不一样JIT 仍然在运行时边执行边翻译热路径代码TypePHP 走的是提前编译AOT路线更像 C/C、Rust 那种“先编译产物再运行产物”的模式。对长期依赖 php-fpm、OPcache 和解释器模式跑业务的开发者来说这个方向最大的想象空间在于命令行工具启动更快、CPU 密集型任务更可控、部署环境甚至可以不需要完整 PHP 运行时。这篇文章不准备只停留在“又一个开源项目”的介绍层面重点会放在为什么会出现这个方向、编译器内部大致做什么事、从源码构建到跑通最小案例时要注意什么以及哪些 PHP 代码目前还不适合直接拿去编译。如果你平时写 PHP 小工具、CLI 脚本、批量任务或者对编译器原理、语言运行时、AOT 编译这些话题本身感兴趣这篇文章可以帮你少走一些弯路。1. 为什么“PHP 原生编译器”这个方向值得关注1.1 PHP 一直在解决性能问题但传统思路偏向运行时说 PHP 性能差的说法并不准确。准确地说PHP 作为动态语言解释执行的模型决定了它在某些场景下有固定开销。传统 PHP 执行流程大致是这样Zend 引擎把 PHP 源码解析成字节码opcode然后把 opcode 交给虚拟机执行。OPcache 只是把“PHP 源码转 opcode”这一步的结果缓存下来减少重复解析但 opcode 仍然需要被逐条解释执行。PHP 8 加入 JIT 之后热点代码可以被编译成机器码但 JIT 的能力边界还是受限于动态类型、函数调用方式、内存模型这些脚本语言的固有特征。TypePHP 这类项目出现是想换一条路在运行之前就把 PHP 编译成机器码。有点像“我不用再背一个解释器到处跑”的思路。编译阶段把能确定的类型、函数调用、常量、静态逻辑尽量确定下来运行时只剩下真正需要动态处理的部分。这样做的直接收益是启动开销大幅降低冷启动阶段不再需要“解析脚本、编译 opcode、初始化运行时”整套流程拿到的是一个已经成型的可执行文件。1.2 AOT 编译和 JIT 到底有什么区别这里有一个常见误区JIT 也是编译器但它在运行时才介入。AOT 则是在构建阶段就把编译做完。两者的差别在工程上非常明显维度解释器 OPcachePHP 8 JITAOT 原生编译TypePHP 方向编译时机运行时解析成 opcode运行时热点代码编译构建阶段提前编译启动速度较慢需加载脚本和运行时第一次运行仍有开销优势明显直接执行机器码对动态特性支持最好随时可改较高但需要退路受限动态特性需要运行时兜底部署方式需要 PHP 环境需要 PHP 环境理想情况直接交付二进制文件适合场景传统 Web 应用CPU 密集的热点循环CLI 工具、小体积部署、固定逻辑任务如果你过去一直用 PHP 写 Web 应用你可能更习惯“写完代码上传服务器刷新页面”的循环。AOT 编译项目会改变这个循环它更像“写完代码执行编译产出可执行文件再部署可执行文件”。这种变化对部分团队是有价值的。1.3 真正适合用它的场景不是让大家直接拿框架做生产以我接触到的编译型 PHP 项目经验来看第一批受益者大概率不是 Laravel、ThinkPHP 这类复杂 Web 应用而是下面这几类CLI 工具比如内部同步脚本、数据处理工具、代码生成器。定时任务cron 里跑的脚本执行完就退出对启动速度很敏感。自动化 CI 步骤构建镜像时希望减少运行时依赖。对部署环境有严格要求的场景不想在容器里安装完整 PHP 环境但业务逻辑是 PHP 写的。这里要克制一点。TypePHP 作为新开源的编译器项目成熟度还在早期“能编译”和“能稳定支撑生产项目”是两回事。但它提供了一个很值得研究的起点PHP 到底有多少动态特性可以被静态化哪些代码能在编译期确定这类问题以前大家讨论得少因为 PHP 默认就跑在解释器上。2. 把 PHP 编译成机器码内部要经过哪几层处理很多人第一次接触编译器项目会以为它只是“用 C 语言写个程序把文本转成二进制”。实际上一个能处理 PHP 这种动态语言的编译器内部需要拆成多个阶段。理解这些阶段才能看懂 TypePHP 为什么值得关注也才能理解它为什么不可能一天之内兼容所有 PHP 语法。2.1 第一关语法解析先解决“读懂代码”的问题任何编译器的第一步都是词法分析和语法分析。词法分析把代码字符串拆成 token语法分析把 token 组合成抽象语法树AST。PHP 的语法有一个特点非常灵活约束少有大量动态表达方式。同一个功能可以用很多种写法实现而且历史上不同版本语法差异不小。这给解析器带来了压力。TypePHP 这类项目在解析阶段通常会选择成熟的解析器生成方案比如 tree-sitter或者借助已有的 PHP Parser 库。选择 tree-sitter 这类可增量解析工具的好处是对语法错误的容忍度更高解析速度更快也方便后续做工具链扩展。不过要注意解析器能读懂 PHP 语法不等于编译器能把所有语法都编译成功。语法树出来之后还要进入语义分析和代码生成阶段。2.2 第二关类型与语义分析动态语言最吃力的一步PHP 是动态类型语言变量可以随时改变类型函数参数可以传任何东西。AOT 编译器最难受的就是这一点。如果是 C 语言编译器可以从类型声明直接推断内存布局PHP 不行编译器需要在很多地方做“保守假设”也就是不确定类型的地方必须退回到动态调用路径。所以在 TypePHP 里你会看到类似这样的处理思路能确定类型的地方比如显式声明了int、string、array参数的函数生成更高效的机器码。无法确定类型的地方比如mixed参数、运行时动态拼接函数名就保留运行时类型检查走一条慢但不会错的路径。对函数调用、常量和类方法尽量在编译期解析并绑定实际地址减少运行时查找。这意味着你写的代码越“静态化”编译出来的效率越高也越容易通过编译。你写的代码越“动态化”编译器越难处理最终结果可能是编译失败或者运行时效率并没有比原始 PHP 好多少。2.3 第三关代码生成和链接产出真正的可执行文件解析完语义编译器会生成中间表示IR然后交给后端做代码生成。常见的后端选择包括 LLVM 或者直接生成对应平台的汇编代码。采用 LLVM 的好处是能直接利用 LLVM 做平台无关的优化比如寄存器分配、指令调度、死代码删除并且只需要实现一个 IR 生成逻辑就能支持多个 CPU 架构。但这里要注意一个工程现实生成的可执行文件很可能不是完全不依赖任何运行时二进制。编译器往往还需要把 PHP 运行时的基础能力打包比如字符串处理、数组结构、函数表、类注册、常量池。这些能力通常会被打成一个运行时库连接进最终产物。所以编译出来的二进制文件通常比原脚本本身大很多这是正常现象不用慌。2.4 第四关运行时文件与元数据处理二进制里住着一整套“小 PHP 内核”PHP 脚本执行时需要把函数、类、常量、命名空间、自动加载这些信息都准备好。AOT 编译时这些元数据不能只放在内存里它们要么被编码进二进制文件的静态数据区要么在程序初始化时从运行时库中恢复。类方法的调用、属性访问、魔术方法都需要对应的运行时结构支持。这也是为什么很多 PHP 编译器项目在早期版本里能跑通echo、for循环、普通函数但一遇到eval、ReflectionClass、call_user_func这类强动态功能就卡壳的原因。因为这些功能依赖运行时的解析和动态绑定能力和 AOT 的静态模型天然冲突。TypePHP 如果要逐步完善最大的工程量不在于支持更多语法而在于运行时把这些动态能力模拟得足够接近原版 PHP。3. 从源码构建 TypePHP 到跑通最小 PHP 脚本3.1 环境准备先看依赖再看命令新开源项目最怕什么不是功能不全而是 README 里少写了一行依赖导致构建失败新手直接劝退。TypePHP 这种编译器项目构建时通常需要准备这些git用来拉取源码。C/C 编译工具链比如 gcc、clang、make、cmake。如果项目用 tree-sitter 做解析可能需要拉取对应子模块。如果后端依赖 LLVM需要安装对应的 LLVM 开发库版本要和项目要求匹配。这一点很容易踩坑LLVM 版本不一致编译过程最容易报错。磁盘空间建议留出几个 GB。编译器和构建产物比较大尤其是带调试信息的版本。我一般会建议先别急着追最新代码找一个打了 tag 的稳定版本或者按仓库 README 标注的默认分支构建。源码编译器项目每天都在变API 变动很频繁用旧教程去编译新代码很容易遇到接口对不上。3.2 获取源码和编译步骤下面给出一段通用流程具体命令以 TypePHP 仓库 README 为准git clone --depth 1 https://github.com/typephp/typephp.git cd typephp # 如果使用子模块先初始化 git submodule update --init --recursive # 编译常见工具链流程 mkdir build cd build cmake .. make -j$(nproc)如果项目本身没有用 CMake而是用自定义脚本那就跟着 README 来。启动构建之后第一件事不是干等而是盯着日志里有没有“缺失依赖”“找不到头文件”“版本不匹配”这类提示。编译成功之后通常会在build/目录或者项目根目录生成typephp可执行文件这就是编译器本体。3.3 编译一个最小 PHP 脚本我建议第一次测试不要玩复杂的先创建一个最小文件?php echo hello, typephp\n;然后尝试编译。不同编译器的命令格式有差异但大概率会是这样./typephp hello.php -o hello这里的-o hello是指定输出文件名。如果没有指定有些编译器会默认生成和源文件同名的可执行文件。编译成功后直接运行./hello hello, typephp如果你的环境上输出正常说明编译器主体流程已经跑通。这一步值得认真确认因为后续所有高级功能测试都建立在这个基本流程之上。3.4 怎么判断编译结果是否真的可用除了看程序能不能运行还要确认几个点二进制文件是否真的存在且不是空文件。编译器可能只输出了中间文件需要你手动执行链接。程序运行后退出码是否为 0。可以执行echo $?查看。检查动态依赖。用ldd ./hello查看可执行文件到底链接了哪些库这决定了它能不能被拷贝到其他机器上运行。很多编译器项目为了省事默认生成的是动态链接版本这意味着其他机器上如果缺运行时库程序启动会直接报错。注意不要第一次编译就尝试编译整个 Web 框架。先跑通最小脚本再逐步添加函数、类、循环和数组操作。这样遇到问题时你能快速判断是哪个环节出了问题。4. 能跑通不等于全部支持先摸清能力边界4.1 更容易编译通过的代码长什么样AOT 编译喜欢“确定性强”的代码。下面这些特征是更容易被原生编译器接受的函数参数和返回值有明确类型声明或者编译器能通过常量推导出类型。没有eval、extract、compact、动态变量名$$var。函数调用是直接写函数名而不是通过call_user_func或者字符串拼接。类结构和方法调用在编译期可见不涉及反射。逻辑集中在循环、数值计算、字符串拼接、数组遍历这些基础操作上。如果你把 TypePHP 当实验品去编译一个计算密集型的小工具比如从数字列表生成哈希、批量处理文本这类代码的编译通过率通常会高一些。4.2 哪些地方最容易出问题我整理了几类比较典型的边界问题代码特征是否建议先用原因纯静态函数和类型声明强烈建议编译期可确定效率高闭包和匿名函数可以尝试涉及变量捕获时可能有限制eval动态执行代码不建议运行期字符串解析与 AOT 冲突ReflectionClass等反射 API不建议需要在运行时动态分析类结构call_user_func/call_user_func_array谨慎动态函数调用可能回退到慢路径include/require拼接动态路径谨慎编译期无法确定文件内容Composer 全自动加载复杂依赖不推荐现阶段尝试框架和工具包大量依赖运行期元数据为什么会这样其实很好理解。AOT 编译的核心优势在于“提前处理”所以凡是必须到运行期才能确定的操作就会成为一个外挂点。外挂点越多编译器和运行时之间的配合越复杂出问题的概率也越高。4.3 对 Composer 生态和 PHP 框架的兼容性这是目前 TypePHP 这类最容易被高估的地方。很多人看到“编译 PHP”第一反应是“那我是不是能把 Laravel 编译了部署时不用装 PHP”这个想法很自然但现阶段实现难度非常大。原因有几个方面现代 PHP 框架大量使用依赖注入容器很多类是在运行时才被实例化和解析的。自动加载机制本身就是一个动态绑定过程编译时很难穷举所有加载路径。框架里普遍使用魔术方法、方法拦截、动态调用中间件这些对 AOT 编译器很不友好。第三方扩展比如redis、pdo_mysql等需要编译器不仅能识别函数还要把扩展的 C 库一起打包这工程量直接翻倍。所以更稳妥的态度是现阶段把 TypePHP 当作“PHP 子集编译器”来看待而不是“第二个完整 PHP 运行时”。它能处理的是相对静态、自包含、依赖少的脚本。5. 性能提升看哪里别被“编译器”三个字带偏5.1 如何设计一套有意义的基准测试编译型工具最忌讳“拿一个 for 循环跑 10 万次就下结论”。真正判断这个方向有没有价值要看这几个维度编译时间把 PHP 编译成二进制这个过程本身要多久。启动时间连续运行一个空程序观察二进制和传统 PHP CLI 的启动差距。CPU 密集场景吞吐比如循环里做字符串处理、数组排序、数学计算。峰值内存二进制运行时内存占用和传统 PHP 进程的内存占用。可执行文件大小太大可能影响镜像分发。测试方式建议用多次运行取中位数不要手动掐秒表。Linux 下可以用time也可以用hyperfine这类工具hyperfine --warmup 3 ./hello php hello.php对比时除了看工具输出的平均值还要注意标准差。如果二进制程序的运行时间波动很大可能是运行时初始化开销没有完全消失也可能是系统资源有变动。5.2 预期优势在哪里哪里可能并不理想先说大概率有优势的地方启动速度。CLI 脚本场景传统 PHP 需要加载整个运行时编译后的二进制明显更快。冷启动部署。如果你在容器里跑 PHP CLI 脚本省略 PHP 环境后镜像体积和启动时间都会下降。逻辑相对固定、计算密集的小工具。编译期优化能去掉一部分类型检查开销。再说不一定占优的地方字符串和动态调度密集的代码。编译器对动态类型无能为力时只能退回运行时兜底这时性能跟传统 PHP 相比不见得有提升。Web 应用整体性能。PHP-FPM 之所以快是因为进程常驻、OPcache 预热好了、数据库连接池化。换 AOT 只是把“PHP 代码执行”这一层换了网络、数据库、缓存依然是主瓶颈。内存占用复杂的应用。整个运行时都要链接进二进制小脚本可能更重不一定比传统模式省内存。5.3 支持某个功能不代表所有格式都稳定“原生编译器正式开源”这句话容易给人很强的暗示好像项目已经很成熟。但“正式开源”更多是项目进度上的一个节点意味着代码公开了、协议明确了、开发者可以提 issue 和 PR。它并不代表所有语言特性已经全量支持也不代表性能已经达到某个可以写进宣传海报的数字。在实测时我强烈建议先确认当前版本的已知限制避免浪费几个小时折腾一个作者早就在文档里声明不支持的语法。多看 issue 区。很多坑别人已经踩过搜索关键词很可能直接命中。不要拿生产项目直接试。先建一个临时目录把需要编译的脚本复制进去避免编译器误改源码目录里的文件。注意如果程序在编译时没有报错但运行结果和原始 PHP 不一致先怀疑是不是某个动态特性被编译器用“简化方式”处理了。把代码不断删减直到找出最小复现片段再提交 issue。这对开源编译器项目的帮助非常大。6. 实际使用中容易踩的坑与排查顺序6.1 排查原则先输入再环境再特性很多人在编译器项目里遇到问题第一反应是“编译器有 bug”。但根据我的经验大多数问题其实是三个更基础的原因源码里用了编译器还不支持的语法。构建环境或依赖版本不对。可执行文件运行时的动态库路径不匹配。正确的排查顺序应该是第一步看报错信息。如果报错发生在解析阶段说明源码语法有问题如果发生在链接阶段说明构建环境有问题。第二步用最小脚本测试。把业务脚本逐步删减或者单独写一个重现问题的小文件可以快速定位问题范围。第三步查看编译器版本和 README 的限制说明。别用最新代码遇到报错然后去查一个月前的 issue版本之间改动可能很大。第四步检查可执行文件的依赖库。ldd输出里如果有找不到的库说明运行时环境不完整不是代码问题。6.2 常见问题和排查方向现象可能原因排查方向编译时报parse error语法解析器不认识当前语法确认 PHP 版本和语法特性查看项目支持范围编译报找不到头文件缺少系统依赖按文档补装编译工具库或 LLVM 相关组件编译过程卡住不动构建任务开启并发过高或磁盘空间不足查看 CPU 和内存占用清理临时空间降低-j并发数编译成功但运行报segmentation fault运行时对某个特性支持不完整最小化代码复现提交 issue生成的二进制换了机器运行失败动态链接了运行时库使用ldd查看依赖确认是否适合拷贝分发编译后的程序输出结果不对某个动态调用被错误静态化对比传统 PHP 运行结果定位具体函数或特性编译器构建失败但日志不明显步骤没按项目指定流程执行清理build目录严格按 README 重新构建6.3 推荐的测试顺序能节省大量时间我一般会把测试拆成四个阶段空脚本和 echo确认编译器本体能启动产物能运行。常量、变量、函数、循环确认基础语法和逻辑可以编译。数组、字符串函数、类与简单方法调用确认数据结构和面向对象基础可以运行。批量数据处理或真实工具脚本开始涉及输入输出、参数解析、多文件组织。不要一上来就怼一个 2000 行的业务脚本。编译器项目在早期版本中报错信息可能不友好甚至直接段错误。小步测试能让你快速判断责任在代码侧还是编译器侧也能积累一套自己的可用代码子集。6.4 关于运行时依赖和部署环境使用编译器最常见的收益想象是“编译完就不用装 PHP 了”。这句话在法律上没问题但实际上要看生成的二进制是否自包含。如果项目采用动态运行时链接那你的目标机器上还是需要提前准备对应版本的运行时库。最稳妥的方式是查看官方文档里关于“静态链接”或“self-contained”的说明。构建时增加静态链接选项通常会让二进制体积变大但换来的是更大的部署自由度。如果你是在容器里使用 TypePHP建议构建阶段和运行阶段分开。构建阶段安装全部编译工具运行阶段只拷贝最终二进制。这样即使二进制还依赖一些动态库也能通过镜像分层把依赖固定住。7. 现在适合什么人试用什么人更适合再等等7.1 现在就可以动手尝试的人第一类是喜欢研究编译原理的开发者。TypePHP 这类项目是极好的学习材料因为它要把一门动态语言改造成可静态编译的形态里面涉及解析、类型推断、中间表示、代码生成、运行时设计。你能从源码里看到作者对 PHP 动态特性做取舍的过程。第二类是维护基础 CLI 工具的人。如果你的 PHP 脚本只依赖少量内置函数没有复杂 Composer 依赖性能数值上对启动时间有要求那现在就可以在测试环境里尝试把产物接入 CI 或定时任务跑一段时间观察稳定性。第三类是容器化和部署链路研究人员。你需要关心的不是 PHP 语法而是编译产物如何打包进镜像、如何做多平台构建、如何减少运行环境体积。这类实验即使最后不落地也能帮你更清楚理解当前 PHP 部署模型和编译部署模型的差异。7.2 建议再等等的人如果你的项目是完整的 Web 业务比如 Laravel、ThinkPHP、Symfony 应用现阶段不建议贸然迁移。原因前面已经提到框架的自动加载、依赖注入、中间件调度、ORM 动态查询都是 AOT 编译器的重灾区。强行迁移大概率会得到一个“能编译但行为异常”的产物排查问题非常痛苦。如果你的项目依赖大量 PHP 扩展比如redis、mongodb、gd、imagick也要谨慎。扩展通常以 C 库形式存在编译器需要为每个扩展做适配和打包不是开箱即用的。如果你的代码里到处是eval、动态函数调用、反射和魔术方法AOT 编译器很可能把这些逻辑回退到慢速路径性能提升可能不明显甚至可能直接报不支持。这类代码更适合引入 PHP 8 JIT而不是 AOT。7.3 可以和 TypePHP 组合使用的其他方案TypePHP 的出现并不是要替代 OPcache、JIT 或 Swoole它更像是在另一个维度做补充。OPcache 仍然是传统 PHP 部署的首选适合动态语言特征明显的应用。PHP 8.0 之后的 JIT 对已经存在的 CPU 密集脚本有实际收益不需要改动代码就能试用。Swoole、RoadRunner 这类常驻内存方案解决的是“高并发长连接”问题和 CLI 编译工具是两条不同路线。如果你的目标是减少容器依赖也可以先把 PHP CLI 脚本用传统方式写好后再考虑编译产物替换。我不建议把 TypePHP 当成“PHP 的终点”更适合的心态是把编译器项目当作一个实验场理解这门语言哪些部分是可以用静态分析解决的哪些部分天然需要动态运行时。这种理解会直接影响你以后写 PHP 代码的风格。比如当你开始主动减少动态函数调用、写更明确的类型声明、尽量避免运行时反射时不仅未来更适合 AOT 编译就连现有 PHP 程序的性能和可读性也会提高。最后说点实际的。如果你准备今天就开始尝试我的建议是先 clone 一份源码看看 README 里的依赖和已知限制再创建一个几行代码的测试文件跑通最小编译然后跑一个你平时用 PHP 写的计算脚本看差异发现不支持的特性时不要急着骂编译器先看看 issue 里有没有人提交过相关案例。整个过程不需要太多时间但会让你对“PHP 原生编译器”这个方向有一个比新闻标题更实在的判断。