前言「PHP 8.0 性能翻倍是不是该赶紧升」这是升级讨论里最常出现的一句话也是最容易把人带偏的一句。真实情况是PHP 8.0 引入了 JITJust-In-Time 编译但 JIT 只对 CPU 密集型的代码有明显作用典型的 Web 请求瓶颈在数据库、缓存和外部 HTTP 调用上JIT 几乎帮不上忙。把 JIT 当作升级理由往往会在迁移完成后得到一个「好像没什么变化」的结论。但反过来说7.4 到 8.0 的差异确实很大只不过大在另一头——不是性能而是不兼容变更。字符串与数字的比较规则改了、一批老函数被删了、未定义常量从警告升级成了致命错误、连.和的优先级都变了。这些改动会让原本「跑得好好的」老代码在升级后出现行为差异而且是那种不报错、只出错结果的差异最难查。本文按三个层次讲清楚8.0 新增了什么、改了什么真正的迁移成本、以及按项目现状该怎么选。文中会给出可直接运行的对照脚本让读者在自己的机器上验证这些差异而不是只看结论。另外要说明一点PHP 7.4 与 PHP 8.0 都已结束官方支持。7.4 的安全支持在 2022 年 11 月结束8.0 在 2023 年 11 月结束。所以本文讨论的其实是「从哪个 EOL 版本迁到哪个受支持版本」的问题最终的落点基本都会是 8.2 及以上各个版本的支持截止时间请以 PHP 官方的 supported versions 说明为准。一、PHP 8.0 到底新增了什么先看能力面。下面这些是 8.0 引入、7.4 完全没有的语法与 API特性7.48.0说明命名参数不支持支持foo(name: x, age: 1)可跳过默认参数构造函数属性提升不支持支持在构造参数上直接写可见性即声明属性match表达式不支持支持严格比较且是表达式可赋值nullsafe 操作符?-不支持支持链式调用遇 null 短路返回 null联合类型 A\B不支持支持属性 Attributes#[...]不支持支持替代注释式注解throw作为表达式不支持支持可在??、箭头函数里 throw非捕获 catch不支持支持catch (Exception)省掉变量str_contains/str_starts_with/str_ends_with不支持支持替代strpos(...) ! falsefdiv()不支持支持除零返回 INF/NAN 而不报错get_debug_type()不支持支持比gettype()更精确JIT不支持支持需 opcache 且显式开启其中对日常代码影响最大的是str_contains及?-它们替代的是极易写错的strpos($h, $n) ! false和层层if ($a $a-b $a-b-c)。这两个改动能在重构时实打实减少 bug。二、真正的迁移成本在不兼容变更这一节是决定「要不要升、要花多少时间」的关键。以下变更都经过核实并且可以直接用代码复现。2.1 字符串与数字的比较规则变了?php // 同一份代码7.4 与 8.0 输出不同 var_dump(0 foo); // PHP 7.4: true PHP 8.0: false var_dump(1 01); // 两个版本都是 true var_dump(100 1e2); // 两个版本都是 truePHP 8.0 收紧了「字符串与数字比较」的规则只有当字符串是合法的数字字符串时才会按数值比较否则一律按字符串比较。这个改动修掉了很多历史怪相但也让依赖旧行为的代码尤其是权限判断、状态判断里的松散比较结果翻转。2.2 拼接运算符的优先级下调?php // 7.4: . 与 同级、左结合 (sum: . 1) 2 2 // 8.0: . 优先级低于 sum: . (1 2) sum: 3 echo sum: . 1 2;这是最难查的一类差异不报错、不警告只是输出变了。凡是把.和算术运算符混在一行、又没加括号的代码都要重点排查。2.3 未定义常量从警告变成致命错误?php // PHP 7.4: Warning: Use of undefined constant FOO // 并把 FOO 当作字符串使用程序继续跑 // PHP 8.0: Error: Undefined constant FOO直接中断 echo FOO;老代码里常有一处拼写错误的常量名被当成字符串用跑了很多年都没出事。升到 8.0 后它会变成 500 错误。2.4 一批函数被移除以下函数在 8.0 中已被移除调用会直接报未定义函数被移除替代方案create_function()用闭包function () {}each()用foreachmoney_format()用NumberFormatter或number_format()__autoload()用spl_autoload_register()get_magic_quotes_gpc()直接删掉该特性早已移除排查这些残留可以用一条命令注意\b这个词边界没有它foreach(也会被误报。rg -n -t php \b(each|create_function|money_format|__autoload|get_magic_quotes_gpc)\s*\( src/2.5 类型错误变得更严格8.0 起大量内部函数在参数类型不符时会抛TypeError而不是发个警告后继续例如把数组传给需要字符串参数的函数。8.1 又进一步把「向非 nullable 的内建函数参数传 null」标记为 deprecated。所以升级时除了看 8.0 的变更还要留意目标版本本身的 deprecation 输出。三、JIT 值不值得作为升级理由JIT 的原理是把 opcache 里反复执行的热点 opcode 直接编译成机器码省掉解释执行的开销。它的收益取决于两点——代码是否 CPU 密集以及请求是否能命中同一份热点路径。一个典型的 Web 请求大量时间花在等数据库、等缓存、等外部接口上CPU 大部分时间在休眠JIT 无从发挥。纯计算型任务图像处理、加密、大规模的字符串与数组运算、长时间运行的 CLI 批处理才可能看到明显差异。不要相信任何人给的百分比包括本文——自己测。下面这段脚本可以直接跑用hrtime()计时同一台机器上对比不同版本、不同 JIT 配置的结果?php declare(strict_types1); // jit_probe.php —— 纯计算任务PHP 7.4 与 8.x 都能运行 $n 3_000_000; $start hrtime(true); $sum 0; for ($i 0; $i $n; $i) { $sum ($i * $i) % 7; } $costMs (hrtime(true) - $start) / 1e6; printf(PHP 版本 : %s\n, PHP_VERSION); printf(JIT buffer 配置 : %s\n, (string) ini_get(opcache.jit_buffer_size) ?: (空)); printf(JIT 模式 : %s\n, (string) ini_get(opcache.jit) ?: (空)); printf(计算耗时 : %.2f ms (结果 %d)\n, $costMs, $sum);跑三组对照就有结论了7.4 一次、8.x 默认一次、8.x 打开opcache.jit_buffer_size64M再跑一次。CLI 下可以用-d临时改配置不必改 php.ini。php jit_probe.php php -d opcache.enable_cli1 -d opcache.jit_buffer_size64M -d opcache.jittracing jit_probe.php如果两组 8.x 的结果差别很小说明你的业务大概率不是 JIT 的受益者——那就更不该把 JIT 当作升级的核心理由。四、怎么选按项目现状分四种情况项目现状建议主要理由仍跑在 7.4 的生产项目直接规划迁到 8.2 及以上不要停在 8.07.4 与 8.0 都已 EOL停在 8.0 等于把迁移成本花在了没有安全支持的版本上已跑在 8.0 的项目继续升到 8.1 / 8.28.1 的never、枚举、readonly属性、交叉类型、Fiber 都是零迁移成本的能力增量全新项目直接用当前受支持的稳定版本新项目没有历史包袱选 EOL 版本没有任何好处依赖大量老旧扩展的项目先做依赖盘点再定版本扩展不支持是迁移的真正卡点语言层面的改动反而好办无论哪一种迁移都应遵循同一套流程先锁定基线。把当前composer.lock提交确保迁移出问题时能一键回退。静态排查。先用上面的rg命令扫移除函数再用工具做机械替换。rector/rector支持 PHP 版本升级规则组能把大部分语法层面的改动自动处理掉。打开全部告警跑一遍测试。把error_reporting设为E_ALL让 deprecation 全部显式输出而不是等上线后从用户那里知道。重点回归三类代码松散比较、含.与算术混用的字符串拼接、常量引用。灰度。新旧版本各留一组机器按流量比例切对比错误率与响应时间。常见坑点1. 用「能启动」当作迁移完成的判据❌ 页面能打开、接口返回 200就认为迁移成功。 ✅ 把error_reporting调到E_ALL并检查日志里的 deprecation 与 warning。8.0 的很多改动是行为改变不是报错。松散比较翻转、优先级改变都属于「页面正常但结果错了」的类型。2. 在 8.0 项目里继续写 7.4 风格的字符串判断❌if (strpos($haystack, $needle) ! false)✅if (str_contains($haystack, $needle))前者在$needle处于位置 0 时返回 0被误判为 false 是经典 bug。8.0 提供了正确的函数没有理由继续用老写法。3. 认为升级必须一次性全部改完❌ 一个几十万行的项目试图在一个分支里完成全部迁移。 ✅ 先升级依赖、修掉致命错误让程序能跑起来再分模块清理 deprecation。致命的未定义常量、被移除的函数必须先修行为差异的可以逐步排查。混在一起做分支会长期无法合入。4. 用--ignore-platform-req绕过依赖的 PHP 约束❌composer update --ignore-platform-reqphp强行装上只支持 8.1 的包。 ✅ 老老实实升级 PHP或者选支持当前版本的包。忽略平台检查只是让 Composer 闭嘴运行时该炸还是会炸而且报错位置通常在依赖内部极难定位。5. 把 JIT 当成免费性能❌ 为了「性能翻倍」而升级升完发现没变化于是怀疑升级没生效。 ✅ 先测你的热点代码是不是 CPU 密集再决定是否依赖 JIT。JIT 还需要 opcache 正确配置才生效CLI 与 FPM 的配置是分开的容易只配了一半。6. 忽略.与的优先级变化❌ 靠人眼扫一遍代码觉得「这种写法我们项目里应该没有」。 ✅ 用正则搜出同一行同时含.和/-再逐个确认。rg -n -t php [^]*\s*\.\s*[^;]*[-]7. 在 EOL 版本上做长期规划❌ 「先升到 8.0 顶一阵等以后有空再说」。 ✅ 一次规划到位直接落到仍在安全支持期的版本。从 7.4 升到 8.0 踩的坑绝大多数在升到更高版本时会再踩一遍8.1 的readonly与枚举、8.2 的动态属性废弃都是新的不兼容点。分两次做等于把成本付两遍。8. 忘记检查扩展兼容性❌ 只看composer.json能不能装。 ✅ 同时确认pecl扩展、SAPIFPM / CLI / Apache 模块和第三方二进制在新版本下有对应构建。扩展不跟进是升级里最常见的硬阻塞语言层面的改动反而好解决。总结对比维度PHP 7.4PHP 8.0支持状态已 EOL2022-11 结束安全支持已 EOL2023-11 结束安全支持语法能力类型化属性、箭头函数加上命名参数、属性提升、match、?-、联合类型、Attributes字符串与数字比较宽松0 foo为 true收紧0 foo为 false.与优先级同级.更低未定义常量警告当字符串用致命错误被移除的函数—create_function、each、money_format等JIT无有但仅对 CPU 密集场景有效迁移工作量版本本身无迁移概念主要花在行为差异排查上回答标题里的问题7.4 与 8.0 的差异在语言能力上很大在基准性能上对普通 Web 项目而言没有想象中那么大。所以选择时不应该问「哪个快」而应该问「我什么时候能离开 EOL 版本」。对还停在 7.4 的项目合理的路径不是升到 8.0而是直接跨到当前受支持的稳定版本对已经跑在 8.0 的项目往上小步走几乎只有收益没有需要担心的行为翻转。