
前言PHP 8.0 最受关注的新特性就是 JITJust-In-Time即时编译。当时的宣传语给了很多人一个预期开启 JIT 就能显著提速。于是大量团队照着教程在php.ini里加了两行重启服务然后……什么也没感觉到接口耗时没有任何变化甚至因为内存占用上升机器上能起的 fpm 进程变少了opcache_get_status()里根本找不到 JIT 的信息在命令行下测试无论开不开 JIT脚本耗时几乎一样有人打开 JIT 后某些复杂脚本反而慢了一点。这些结果都不奇怪。JIT 是针对 CPU 密集CPU-bound代码的优化它解决的瓶颈是“PHP 虚拟机在解释 opcode 上花的时间”。而一个典型的 Web 请求时间主要花在数据库查询、Redis、HTTP 调用、磁盘 IO 上——这些是 IO 密集IO-bound的部分JIT 一点忙都帮不上。至于“开没开都一样”多半是根本没生效。本文讲清楚三件事JIT 到底优化了什么、为什么 Web 场景常常看不到收益、以及怎样用一份可以自己跑的基准代码在你的机器上得出属于你的结论。本文不会给出任何“提升百分之多少”的数字因为收益完全取决于代码形态、PHP 版本、CPU 与配置任何脱离实测的数字都是误导。一、JIT 优化的是哪一段先看一次 PHP 执行经历的阶段阶段做什么缓存/复用方式词法/语法分析源码 → 抽象语法树 → opcodeOPcache 把 opcode 缓存在共享内存解释执行ZEND 虚拟机逐条执行 opcode每次都走“取指—译码—执行”循环JIT 编译把热点 opcode 直接翻译成机器码机器码放进 JIT 缓冲区后续直接调用实际运算上面三阶段最终做的事算术、字符串、函数调用与前面无关关键结论JIT 只能减少“解释 opcode”的开销不能减少“业务运算”本身的开销。如果一段代码的时间主要消耗在系统调用读写数据库、网络上那么把解释器换成机器码只是让 CPU 更快地走到那个等待点然后继续等。JIT 必须建立在 OPcache 之上因为它是从 OPcache 缓存的 opcode 出发做编译的。因此有一个前置条件经常被忽略opcache.enable_cli默认是关的所以在命令行里跑脚本时如果没打开它OPcache 和 JIT 都不会生效。二、为什么 Web 请求常常看不到收益把原因列清楚就能自己判断该不该开第一时间分布决定了收益上限。如果一个请求 90% 的时间在等数据库返回那么即使 PHP 自身的执行时间被优化到零整体也只能快 10%。JIT 优化的正是这 10% 里的解释开销收益还要再打折扣。第二php-fpm 的进程模型不擅长“预热”。Tracing JIT 需要先观察哪些代码路径是热点达到阈值后才编译成机器码。短小的请求跑不了几轮就结束了编译成本还没摊平。OPcache 的 opcode 可以跨请求复用JIT 缓冲区也在共享内存里但触发编译的时机依然依赖实际运行。第三收益集中在特定代码形态。JIT 效果最好的是循环密集、算术密集、函数调用密集的纯 PHP 代码而框架里大量出现的反射、字符串拼接、数组操作、自动加载以及所有走 IO 的部分都不是 JIT 的强项。第四代价是内存。opcache.jit_buffer_size是从共享内存里预留的加上编译产生的元信息会占用额外内存。在按内存规划 fpm 进程数的机器上这可能是净负收益。所以结论很朴素IO 密集的 Web 应用不要指望 JIT 带来明显提升CPU 密集的脚本它才有价值。三、怎样正确地开启并确认生效在php.ini里配置数值请按机器内存调整; 前置条件OPcache 必须开启 opcache.enable1 ; CLI 下也需要开启否则命令行测试时 JIT 完全不生效 opcache.enable_cli1 opcache.memory_consumption128 ; JIT 模式tracing追踪热点路径或 function按函数编译 opcache.jittracing ; 为 0 时 JIT 不启用 opcache.jit_buffer_size64M配置要点配置项作用常见错误opcache.enableOPcache 总开关关着 OPcache 却去调 JITopcache.enable_cliCLI 下是否启用不打开于是命令行测不出差异opcache.jitJIT 模式tracing/function/ 数字组合只设了大小没设模式opcache.jit_buffer_sizeJIT 缓冲区大小保持默认 0等于关闭 JIT用下面这行命令确认到底生效了没有php -d opcache.enable_cli1 -d opcache.jittracing -d opcache.jit_buffer_size64M \ -r var_dump(opcache_get_status()[jit]);输出里应当能看到enabled、on、kind、buffer_size、buffer_free等字段。enabled为true而on为false说明 JIT 只是被配置了但还没开始编译代码这在只跑一次的短脚本上很常见——继续跑一会儿热点代码on就会变成truebuffer_free也会随之减少。另外注意修改opcache.jit_buffer_size这类共享内存相关配置后必须重启 php-fpm运行时调opcache_reset()不够。四、完整可运行示例自己跑一份对比与其相信别人的结论不如用下面这份 CPU 密集的基准脚本在同一台机器上跑两次对照。脚本刻意只做纯计算不碰任何 IO# 关闭 JIT php -d opcache.enable_cli0 bench.php # 打开 JIT php -d opcache.enable_cli1 -d opcache.jittracing -d opcache.jit_buffer_size64M bench.php?php declare(strict_types1); // 运行环境PHP 8.0用于对比 JIT 开关前后的差异 /** * CPU 密集任务一整数运算与循环 */ function sumOfSquares(int $n): int { $sum 0; for ($i 1; $i $n; $i) { $sum $i * $i; } return $sum; } /** * CPU 密集任务二纯 PHP 的字符串处理 */ function digest(string $seed, int $rounds): string { $value $seed; for ($i 0; $i $rounds; $i) { // 用内置哈希制造一个依赖上一轮的短字符串 $value substr(hash(sha256, $value . $i), 0, 16); } return $value; } /** * CPU 密集任务三数组与调用密集 */ function transform(array $rows): float { $total 0.0; foreach ($rows as $row) { $total sqrt($row[price] * $row[qty]); } return $total; } $rows []; for ($i 1; $i 2000; $i) { $rows[] [price $i * 1.5, qty $i % 17]; } /** * 跑 $rounds 次丢弃前 $warmup 次预热返回剩余轮次的中位数毫秒数 */ function measure(callable $task, int $rounds 9, int $warmup 3): float { $samples []; for ($i 0; $i $rounds; $i) { $t0 hrtime(true); $task(); $t1 hrtime(true); if ($i $warmup) { $samples[] ($t1 - $t0) / 1e6; } } sort($samples); return $samples[intdiv(count($samples), 2)]; } $status function_exists(opcache_get_status) ? opcache_get_status() : null; $jit is_array($status) ? ($status[jit] ?? null) : null; printf(PHP 版本 : %s\n, PHP_VERSION); printf(OPcache 启用 : %s\n, $status false || $status null ? 否 : 是); printf(JIT 状态 : %s\n, $jit null ? 无 JIT 信息未启用或未编译 : json_encode($jit, JSON_UNESCAPED_UNICODE)); printf(整数循环 : %.2f ms\n, measure(static fn () sumOfSquares(3000000))); printf(字符串哈希 : %.2f ms\n, measure(static fn () digest(seed, 2000))); printf(数组与函数调用: %.2f ms\n, measure(static fn () transform($rows)));这份脚本做了三件重要的事也是任何 JIT 对比测试都该做的纯计算不碰 IO这样测出来的差异才归因于 JIT先预热再统计丢弃前 3 轮避免把编译开销算进结果取多轮中位数而不是只跑一次避免被系统抖动误导。得到两组数字之后再请自己判断值不值得在生产开启如果纯计算的收益确实明显而你的业务里又有大量这类代码比如报表计算、图像处理、加解密、批量数据转换那 JIT 就是划算的如果你的业务是典型的“查库—拼 JSON—返回”那么这组数字多半不会有变化把精力花在 SQL 和缓存上更实际。常见坑点1. 在 CLI 下测试却没开opcache.enable_cli❌php bench.php比较两次耗时结论是“JIT 没用” ✅ 加上-d opcache.enable_cli1否则 JIT 与 OPcache 都不生效2.opcache.jit_buffer_size保持默认值❌ 只设了opcache.jittracing缓冲区仍是 0 ✅ JIT 缓冲区为 0 时不会启用必须显式分配3. 用 IO 密集的接口做基准❌ 拿一个查数据库的接口比较开关前后的 QPS ✅ 用纯计算脚本业务接口的差异会被 IO 噪声淹没4. 只跑一次就下结论❌time php script.php前后对比把冷启动和编译开销算进去 ✅ 预热后再统计取多轮结果的中位数5. 改了jit_buffer_size却不重启❌ 改完 ini 直接opcache_reset()就以为生效了 ✅ 缓冲区大小属于共享内存配置需要重启 php-fpm6. 只看文档里的数字❌ 相信“JIT 让性能提升数倍”的说法直接上线 ✅ 用本文的脚本在自己的机器与代码上实测7. 忽略内存代价❌ 在内存紧张的机器上分配很大的 JIT 缓冲区导致 fpm 可用的进程数减少 ✅ 评估“多花的内存”和“省下的 CPU”哪个更值钱8. 把 JIT 当成解决慢代码的手段❌ 用 JIT 掩盖糟糕的算法与 N1 查询 ✅ 先消除算法和 IO 层面的浪费再考虑 JIT总结判断维度适合开 JIT不适合开 JIT负载类型CPU 密集计算、编解码、加解密IO 密集数据库、网络、磁盘进程模型常驻进程、长任务、CLI worker生命周期极短的 Web 请求代码形态循环、算术、函数调用密集反射、自动加载、IO 等待为主资源情况CPU 是瓶颈内存有余量内存紧张需要更多 fpm 进程验证方式纯计算基准 预热 多轮中位数凭感觉或别人的数字JIT 是真实存在的优化但它的边界非常明确它加快的是“PHP 执行自己代码”的速度而不是“等待外部资源”的速度。所以在动手配置之前先问一句“我的瓶颈是 CPU 还是 IO”——如果答案是 IO那么优化 SQL、加缓存、减少下游调用次数收益会远大于打开 JIT。而如果你确实有大量 CPU 密集的 PHP 代码那就用本文的脚本实测一遍让数据替你决定。