PHP 8.0 引入 JIT 的时候,各种文章标题都很激动。我拖了很久没试,上个月终于在一个真实项目上做了对比测试,记录一下。
测试环境
- PHP 8.2.15(Opcache 开启)
- 项目是个中等规模的 Web 应用,Laravel 10
- 压测工具是
wrk,4 线程 100 连接,跑 60 秒 - 机器是 4 核 8G 的云主机,PHP-FPM 静态 20 个子进程
对比三组配置:
| 组 | Opcache | JIT |
|---|---|---|
| A | 开 | 关 |
| B | 开 | tracing,jit_buffer_size=64M |
| C | 开 | function,jit_buffer_size=64M |
结果
测了三个接口:一个纯计算的(生成报表数据)、一个典型 CRUD(查数据库返回 JSON)、一个渲染页面的。
| 接口类型 | A(无 JIT) | B(tracing) | C(function) |
|---|---|---|---|
| 纯计算 | 1250 req/s | 1380 req/s | 1290 req/s |
| CRUD | 3400 req/s | 3410 req/s | 3400 req/s |
| 页面渲染 | 890 req/s | 895 req/s | 890 req/s |
纯计算那个提升约 10%,其它两个基本没差别(在误差范围内)。
为什么和宣传的差这么多
看了一些资料,原因大致清楚:
JIT 只优化"热点"代码。 它需要同一段代码被执行足够多次才编译。而 Web 请求的特点是:一次请求执行几千行代码,每行只执行一次。跑到 JIT 阈值之前,请求已经结束了。
PHP 的瓶颈通常不在 CPU。 CRUD 接口的时间基本花在:网络往返、数据库查询、序列化。JIT 优化不到这些。
I/O 等待期间 CPU 是闲的。 一个请求等数据库 20ms,这 20ms 里 JIT 编译得再快也没用。
框架的开销不在计算上。 Laravel 启动、路由匹配、容器解析,这些是大量的函数调用和小对象创建,JIT 能帮一点但有限。
什么时候 JIT 有用
从我看到的资料和自己的测试,比较确定的场景:
1. 长时间运行的计算任务。 比如图像处理库、数学运算、模板编译。有个知名的例子是 php-cs-fixer 这类工具,开了 JIT 能快 20%~30%。
2. 常驻内存的运行模式。 比如 RoadRunner、Swoole 这种,进程不重启,热点代码能被充分 JIT。这种情况下收益明显得多。
3. 复杂正则和字符串处理。 有测试显示这类操作提升比较可观。
4. 单元测试跑得快一点。 这个是真能感觉到的,因为测试会反复执行同样的代码路径。
一个反直觉的发现
开 JIT 之后内存占用涨了。
A 组每个 FPM 进程大概 32MB,B 组涨到 40MB 左右,多了 jit_buffer_size 的一部分常驻。20 个进程就是多 160MB。
在我们那台 8G 机器上无所谓,但如果是在内存紧张的容器里,这个开销要算进去。
配置细节
如果要用,配置大概是:
; tracing 模式,最通用
opcache.jit=tracing
opcache.jit_buffer_size=64M
; function 模式,只做函数级优化,编译开销小
; opcache.jit=function
; opcache.jit_buffer_size=32M
opcache.jit 也可以写四位数组合,比如 1205,分别控制优化级别、寄存器分配、触发方式、优化策略。但说实话我没仔细研究过这些,默认的 tracing 和 function 够用。
几个注意点:
jit_buffer_size=0才是完全关闭,不写这行默认也是 0- CLI 和 FPM 的配置要分别设(
php.ini和php-fpm.conf里的php_admin_value) - 改完要重启 FPM,
reload不生效
我的结论
对大多数 Web 项目,JIT 的收益不值得专门去调。
我最后还是开着(tracing 模式),因为纯计算那个接口有 10% 提升,而且内存够用。但如果有人问我"要不要为 JIT 升级 PHP",我会说:升级 PHP 版本本身(8.0 到 8.2 的性能改进)比 JIT 重要得多。
PHP 8.0 到 8.2 之间的性能提升主要来自:
- 更高效的对象模型
readonly属性减少了防御性拷贝- 枚举比常量快
- 一些内部函数用 C 重写
这些是普适的提升,不挑场景。
如果真的要优化 PHP 性能
按收益排序,我的建议是:
1. 先看慢查询。 加个 slow log,八成能找到问题。这个收益通常是数倍,不是百分之几。
2. 检查 Opcache 配置对不对。 opcache.validate_timestamps=0 在生产环境应该打开(配合部署时重启 FPM),能省掉每次 stat 文件的开销。opcache.memory_consumption 要够大,不然缓存命中率低。这个经常被忽略。
3. 看有没有 N+1 查询。 Laravel 里 with() 忘了写,或者循环里查数据库,这个太常见了。
4. 考虑加缓存。 Redis 缓存热点数据,收益通常比 JIT 高一个数量级。
5. 最后才考虑 JIT。
顺序很重要。我见过花两周调 JIT 参数,结果发现有个接口本来应该 20ms 返回却要 800ms——因为少了个索引。