PHP OPcache JIT 调试信息如何定位未编译的函数
来源:17golang原创
时间:2026-10-09 20:47:55 295浏览 收藏
定位 PHP OPcache JIT 没有编译某个函数,最有效的办法不是先看耗时,而是建立两类证据:先确认当前 SAPI 中 JIT 确实处于 enabled/on 状态,再用 opcache.jit_debug 的 ASM 或 tracing 位观察“哪些函数或 trace 已产生编译输出”。目标函数没有出现时,再结合触发模式、热度阈值、trace 终止与黑名单信息继续排查。
官方配置说明:https://www.php.net/manual/en/opcache.configuration.php
这里有一个容易踩的版本差异:PHP 8.4 起 opcache.jit 的默认值是 disable。即使 opcache.jit_buffer_size 有非零默认值,也不能据此认定 JIT 已经运行。排查必须从实际加载的配置开始。
先排除 JIT 根本没有运行
问题现场通常是“这个计算函数跑了很多次,性能却和关闭 JIT 差不多”。这个现象只能提出怀疑,不能证明函数未编译。我们先确认 CLI 用的是哪份配置、OPcache 是否加载,以及 JIT 的全局状态。
# 确认 PHP 版本、配置文件和 OPcache 扩展实际来源 php -v php --ini php --ri "Zend OPcache" # 只读取全局 JIT 状态,不展开脚本缓存清单 php -d opcache.enable_cli=1 -r ' // 输出 enabled、on、kind、buffer_size 与 buffer_free var_export((opcache_get_status(false) ?: [])["jit"] ?? null); '
读结果时先看四个条件:opcache.enable_cli=1、jit.enabled=true、jit.on=true、buffer_size>0。其中任何一个不满足,都应该先修正加载配置,而不是分析某个函数为什么没有出现在调试信息里。
opcache_get_status(false) 适合证明 JIT 的全局状态,但它不是逐函数编译清单。buffer_free 很低可以提示容量压力,却不能直接证明具体函数未编译。

用最小脚本制造可观察目标
复杂框架会把自动加载、代理类、异常处理和业务分支混在一起,函数名也可能被闭包或命名空间包装。先准备一个函数名稳定、调用次数可控的最小脚本,能够把“没有运行到”与“运行了但没有编译”分开。
这段脚本的目的不是跑分,而是让函数名、调用路径和触发次数都可控。排查真实项目时,也要把同样的三个要素记下来:准确函数名、真实 SAPI、能稳定到达该函数的输入。
先用 ASM 输出建立已编译名单
官方 JIT RFC 与当前 zend_jit.h 都把 bit 0 定义为生成汇编输出,也就是 opcache.jit_debug=1。为了先排除“热度还没达到”这个变量,可以在隔离 CLI 中临时使用 function 模式。该模式对应 CRTO 1205,触发位 T=0,会在脚本加载阶段尝试编译函数。
# 在隔离 CLI 中启用 function JIT,并把高噪声调试信息单独保存 php -d opcache.enable_cli=1 \ -d opcache.jit_buffer_size=64M \ -d opcache.jit=function \ -d opcache.jit_debug=1 \ jit-target.php 2>jit-asm.log # 只检索目标函数名,建立“出现过”的正向证据 grep -E 'sumEven|formatLabel' jit-asm.log
若日志中能找到某个函数对应的 JIT 汇编段,它就是明确的已编译证据。若一个函数没有出现,结论只能是“当前复现条件下尚未看到编译输出”,还不能立刻写成“JIT 不支持它”。下一步要检查它是否真的被加载、名称是否被命名空间改变,以及当前 PHP 分支使用的 debug 位是否一致。
不要把 opcache.opt_debug_level 和 opcache.jit_debug 混在一起。前者打印优化前后的 opcode,适合观察 OPcache 优化阶段;后者控制 JIT 的汇编、SSA、trace 等调试输出。本文要找 JIT 编译证据,主入口是后者。
再看 tracing JIT 的编译与终止信息
生产常用的 tracing 模式对应 CRTO 1254。它按运行中的热路径形成 trace,不保证“整个函数”以一个完整单元出现。目标函数没出现在 ASM 清单中,可能只是路径未变热,也可能 trace 在形成过程中中止。
按当前 PHP 源码,TRACE_COMPILED 是 1,TRACE_ABORT 是 1,TRACE_BLACKLIST 是 1。组合值是 0x34000。这些内部调试位可能随版本变化,实际使用前应查看与安装版本匹配的 zend_jit.h,不要永远照搬 master 分支。
# 观察已编译 trace、终止与黑名单事件;位值应先和当前 PHP 源码分支核对 php -d opcache.enable_cli=1 \ -d opcache.jit_buffer_size=64M \ -d opcache.jit=tracing \ -d opcache.jit_debug=0x34000 \ jit-target.php 2>jit-trace.log # 分别搜索函数名和 trace 相关事件,避免被大量输出淹没 grep -E 'sumEven|formatLabel|TRACE' jit-trace.log
解释结果时可以这样推进:
- 能看到目标路径的 compiled 信息:说明该热路径已经形成 JIT 代码。
- 只看到 abort:先看终止位置和代码形态,不要马上提高热度阈值。
- 反复出现 blacklist:说明运行时已停止继续尝试某类 root/side trace,需要结合终止原因和对应限制排查。
- 完全没有目标相关信息:先确认请求或脚本确实执行到目标路径,再检查 hot 计数与 profiling 触发条件。

把缺失证据映射到具体原因
| 观察到的证据 | 优先判断 | 下一步 |
|---|---|---|
| 所有 JIT 调试输出都为空 | 扩展、SAPI、JIT 开关或输出去向有误 | 复查 php --ini、OPcache 状态与 stderr |
| 其他函数有 ASM,目标函数没有 | 目标未加载、名称不同或代码形态未被编译 | 先用 function 模式和最小脚本复现 |
| function 模式出现,tracing 模式不出现 | 真实负载未达到热度或未走到该路径 | 核对调用次数、输入分支与 hot 参数 |
| 出现 TRACE_ABORT | trace 形成过程中被终止 | 按终止位置缩小代码形态和控制流 |
| 出现 TRACE_BLACKLIST | root/side trace 多次失败后不再尝试 | 结合 blacklist 阈值与先前 abort 信息 |
| buffer_free 接近耗尽 | JIT 缓冲区可能存在容量压力 | 在测试环境扩大缓冲并对照,但不把它当逐函数证据 |
opcache.jit_prof_threshold 只影响“首个请求采样后编译热点函数”的触发模式;opcache.jit_hot_func、opcache.jit_hot_loop 等则影响动态热度判断。排查时先认清 CRTO 的 T 位,再调整对应参数,否则会出现“改了阈值但行为不变”的假线索。
opcache.jit_bisect_limit 也不适合拿来列出未编译函数。它的用途是把 JIT 编译限制在前 N 个函数,帮助二分错误编译来源,而且官方说明它只在触发位为 0 或 1 时有效。正常定位缺失函数,优先使用正向编译输出和 trace 事件。
恢复配置并完成复查
调试位会产生大量底层信息,排查结束后应恢复 opcache.jit_debug=0,删除临时日志,并用与问题相同的 SAPI 和负载重新确认。不要把高噪声 ASM 或 IR 输出长期放在生产 worker 上。
最终复查至少回答四个问题:
- 目标函数在相同 PHP 版本、SAPI 和配置下是否稳定执行?
- JIT 全局状态是否同时满足 enabled、on 和非零 buffer?
- function 模式能否建立该函数的正向编译证据?
- tracing 模式缺席时,是否有热度不足、abort 或 blacklist 的对应证据?
只要按“全局开关—可重复目标—已编译名单—trace 事件—容量与阈值”的顺序推进,就能把模糊的“JIT 好像没生效”缩小成一个可验证原因。最重要的原则是:调试输出中的出现可以证明已编译,缺席却必须结合触发模式和运行路径解释。
常见问题
为什么 opcache_get_status 显示 JIT on,函数仍没有调试输出?
on 只说明 JIT 全局可用,不说明每个函数都满足触发条件。继续核对 SAPI、函数是否执行、CRTO 触发位、热度阈值和 debug 位。
opcache.jit_debug=1 能直接列出所有未编译函数吗?
不能。它输出已经生成的汇编,是正向证据。未出现的函数还需要与加载路径、调用次数、模式和 trace 事件交叉判断。
调试时应选 function 还是 tracing?
想先建立函数级正向证据,可在隔离环境用 function 模式;想解释真实 tracing 行为,则切回 tracing 并观察 compiled、abort 与 blacklist。两者回答的问题不同。
为什么不建议直接在生产打开全部 debug 位?
ASM、SSA、IR 和 trace 输出量可能很大,也会泄露内部函数与路径信息。应在可控复现环境逐个启用所需位,并在结束后恢复为 0。
-
371 收藏
-
347 收藏
-
112 收藏
-
387 收藏
-
142 收藏
-
377 收藏
-
208 收藏
-
223 收藏
-
376 收藏
-
文章 · php教程 | 14小时前 | php教程 · PHP 8.4 · php ReflectionClass newLazyGhost newLazyProxy lazy object 重量级服务202 收藏
-
文章 · php教程 | 16小时前 | 面向对象 · PHP · PHP 8.4 · PHP非对称属性可见性 private(set) protected(set) PHP 8.4属性 PHP对象封装216 收藏
-
227 收藏
-
272 收藏
-
178 收藏
-
文章 · php教程 | 1天前 | PHP · php-fpm · PHP OPcache opcache_reset validate_timestamps revalidate_freq opcache_invalidate382 收藏
-
117 收藏
-
167 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习