免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

50个PHP程序性能优化的方法

50个PHP程序性能优化的方法 前言PHP 性能优化的 N 个方法是中文技术圈里被复制得最多、也最容易掺水的一类文章。 常见的毛病有三种给出没有来源的百分比提升 300%、把已被移除的老 API 当成技巧推荐、 以及把代码写得更短当成跑得更快。本文的写法不同先约定三条底线不编数字。任何某某比某某快几倍的说法都不写。要比较就给出你可以自己跑的测量代码。讲原理。PHP 的数组为什么isset()快它是有序哈希表、字符串拼接为什么有时不复制写时复制 引用计数、正则为什么可能突然变慢回溯型引擎的灾难性回溯、 Opcache 什么时候才会生效。原理能迁移结论不能。旧写法必须点名。mysql_*、ereg_*、split()在 PHP 7.0 已被移除each()、create_function()在 PHP 8.0移除mcrypt_*自 PHP 7.2 起废弃。 这些不是慢是跑不起来。下面按四个层面拆成 50 条每条只讲一个点。排序不代表优先级—— 真要优化先跳到第 50 条。一、语言与变量层面第 1~12 条升级到受支持的 PHP 8.x 版本。新版引擎在语言实现上持续改进PHP 8.0 引入的 JIT 收益主要集中在 CPU 密集的计算型脚本对以数据库和网络 I/O 为主的 Web 请求收益通常有限。用类型声明并考虑declare(strict_types1)。它不直接提速但能让引擎和静态分析工具尽早发现问题从而少写为了兼容松散类型的防御代码。不要用抑制错误。它不消除开销却会让真正的错误消失把性能问题藏起来。用?-PHP 8.0替代层层isset()判断。少写重复的属性链访问也少一次取值。用??给默认值而不是先isset()判断、再取值的两步走。用matchPHP 8.0替代长if/elseif。它用严格比较且无匹配又无default时抛UnhandledMatchError能更早暴露漏掉的分支。用enumPHP 8.1替代散落的字符串常量把合法取值这件事从运行时校验搬到声明处。用构造器属性提升PHP 8.0省掉重复的赋值样板PHP 7.4 只能用显式属性赋值替代。用readonlyPHP 8.1表达不可变减少为防止被改而写的拷贝与检查。用??PHP 7.4把判存在 初始化合并成一次操作例如写计数字典时先$hits[$key] ?? 0;再自增。用卫语句guard clause及早返回。把缓存命中判断、参数合法性判断放在最前面让慢路径根本不会被进入。彻底迁移掉已移除的旧 API。mysql_*、ereg_*、split()在 PHP 7.0 移除each()、create_function()在 PHP 8.0 移除mcrypt_*已废弃——用mysqli/PDO、preg_*、explode()、匿名函数、sodium_*或openssl_encrypt()替代。这一步带来的是能不能跑比任何微优化都重要。二、数组与字符串层面第 13~26 条记住 PHP 的数组是有序哈希表。按字符串键查找的平均代价是常数级所以isset($a[k])判断键是否存在非常快。in_array()是线性扫描。集合很大、判断次数很多时先用array_flip()把集合转成键之后用isset()查找。一次建索引换多次常数级查找。isset()和array_key_exists()对null的语义不同。键存在但值为null时isset()返回falsearray_key_exists()返回true。选错是正确性问题不是性能问题。遍历数组用foreach不要用for加下标。后者每次迭代都要做一次下标查找和一次边界比较。循环不变式外提。for ($i 0; $i count($list); $i)每次迭代都会调用一次count()count()对数组是 O(1)所以这不是复杂度问题而是每轮多一次函数调用——循环很大时才值得外提。批量转换用array_map/array_filter表达。语法更短、意图更清楚是否更快取决于场景别默认它一定更快。排序用内置函数sort、asort、usort底层实现经过长期优化不要手写排序。字符串是写时复制的值。$s . $x在$s引用计数为 1 时可以原地扩容如果这个字符串同时被别的变量或数组元素持有追加就会触发一次真正的复制。这就是循环里攒字符串有时快、有时不快的真正原因。拼接大量片段时先收集进数组再implode()也是常见做法。两种写法都建议用第 50 条的方式实测不要照搬别人的结论。用str_contains()、str_starts_with()、str_ends_with()PHP 8.0替代strpos()的比较写法顺带绕开返回值是 0 被当成 false这个经典坑。PHP 7.x 上仍要写strpos(...) ! false。正则之前先问一句非用不可吗。纯查找替换用explode()/str_replace()通常更简单也更快。必须用正则时控制回溯。避免(.*)*、(a)这类嵌套量词会引发灾难性回溯输入变长耗时可能指数上升把模式收紧或使用占有量词*、与原子组(?...)明确告诉引擎不要回退。用生成器处理大数据集。生成器惰性产出元素不需要一次性把整份数据放进内存数组。格式化输出用sprintf。它把拼出可读的日志/文本这件事从手工拼接里解放出来——可维护性也是成本。三、数据库、文件与网络层面第 27~38 条消灭 N1 查询。循环里逐条发 SQL每次都是一轮跨进程/跨网络的往返开销远大于任何 PHP 语法级的微调。只取需要的列。SELECT *会把用不到的字段也传回来浪费带宽和内存。分页取数。别一次把整表读进内存数组用LIMIT配合偏移或用游标式翻页。让排序和过滤发生在数据库里。排序列上有索引时排序代价由数据库承担且不用把全量数据搬到 PHP 侧再排。复用预处理语句。prepare()一次、execute()多次既避免重复解析也避免手工转义的错误。批量写入。多值INSERT或把一批操作包进一个事务能显著减少往返次数。判断存在与否用SELECT 1 ... LIMIT 1不要用COUNT(*)——后者要让数据库数完所有匹配行。小文件一次读完file_get_contents避免循环fgets大文件反过来用流式读取别一次性载入内存。写文件优先file_put_contents而不是循环fwrite多次。善用缓存层。结果集、远程接口响应、计算结果都可以缓存缓存命中时省掉的是整条调用链。复用连接。长驻进程队列消费者、常驻服务复用同一个连接对象持久连接要清楚它的代价——连接状态会跨请求残留事务和临时表要自己收拾干净。缓存前先算一笔账这个值多久变一次、重算一次多贵。变化频繁或重算很便宜的值缓存只会增加复杂度和一致性风险。实战把性能优化变成可测量的东西在写任何优化之前先建立能测这件事。下面这段可以直接跑 存成bench.php后执行php bench.php。?php // 适用于 PHP 8.0declare(strict_types1);// 第 39 条的现场检查当前进程到底有没有 Opcacheif (function_exists(opcache_get_status)) {$status opcache_get_status(false);$on is_array($status) ($status[opcache_enabled] ?? false);printf(Opcache enabled: %s\n, $on ? yes : no);} else {echo Opcache 扩展不可用常见于 CLI 未开启\n;}/*** 跑 $rounds 轮返回每轮平均纳秒数。* hrtime(true) 是整数纳秒PHP 7.3不受系统时钟调整影响。*/function timeIt(callable $fn, int $rounds 2000): float{$start hrtime(true);for ($i 0; $i $rounds; $i) {$fn();}return (hrtime(true) - $start) / $rounds;}$haystack range(1, 1000);$lookup array_flip($haystack);// 热身把编译与缓存填充的开销排除在测量之外timeIt(static fn () in_array(999, $haystack, true), 200);timeIt(static fn () isset($lookup[999]), 200);printf(in_array(): %8.0f ns/次\n, timeIt(static fn () in_array(999, $haystack, true)));printf(isset(): %8.0f ns/次\n, timeIt(static fn () isset($lookup[999])));// 字符串拼接写时复制的两种情形$shared str_repeat(x, 1000);printf(追加(独占): %8.0f ns/次\n, timeIt(static function (): void {$s start;$s . -more;}, 20000));printf(追加(共享): %8.0f ns/次\n, timeIt(static function () use ($shared): void {$s $shared; // 与 $shared 共享同一份字符串$s . -more; // 触发写时复制}, 20000));怎么用结果先热身再测。第一轮包含编译与缓存填充不热身会测出虚高的数字。只比量级。两种写法差在噪声范围内说明这个场景不值得优化选可读性好的那个。换真实规模。1000 个元素的结论不能直接搬到 10 个元素或 100 万个元素上。别信别人的数字。包括本文——这里一条具体数字都没给就是因为它们依赖于版本、扩展、硬件和你的数据。数据库那一侧的 N1本质用一段 SQL 就能说清-- 反例应用层循环执行 N 次N1SELECT id, name FROM orders WHERE user_id 1;SELECT * FROM order_items WHERE order_id 101;SELECT * FROM order_items WHERE order_id 102; -- ... 重复 N 次-- 改进一次把需要的数据取回来在应用层用哈希表关联SELECT id, name FROM orders WHERE user_id 1;SELECT order_id, sku, qty FROM order_items WHERE order_id IN (101, 102, /* ... */);四、Opcache、自动加载与部署层面第 39~50 条生产环境开启 Opcache。它把编译后的操作码放进共享内存省掉每个请求重复的编译步骤是收益最直接的部署级优化之一。生产环境考虑关闭opcache.validate_timestamps改为发布时重载 PHP 来更新代码开发环境保持开启否则改了代码不生效会让人怀疑人生。给 Opcache 足够的内存和文件数上限否则缓存频繁被淘汰等于白开。用 Composer 的优化自动加载。composer dump-autoload --optimize生成类映射生产可用--classmap-authoritative跳过文件系统探测。引入文件用__DIR__拼绝对路径避免在include_path里逐目录查找。调整realpath_cache_size与realpath_cache_ttl减少路径解析反复触发的系统调用。保持php.ini精简关掉用不到的扩展——每个扩展都会占用内存并参与启动流程。生产用 php-fpmphp -S只用于开发。内置服务器是单进程模型不面向并发。FPM 的进程数要与机器资源和并发量匹配。进程开得太少会排队开得太多会互相抢内存。跨请求重复的计算结果放进共享缓存APCu、Redis 等而不是每个请求重算一遍。静态资源交给 Web 服务器或 CDN不要让 PHP 去读文件再转发——那会把 I/O 和内存都压在 PHP 进程上。先测量再优化。用 profiler 或hrtime()找到真正的热点只优化它。凭直觉猜出来的瓶颈绝大多数时候是错的——这也是这份清单里唯一一条可以无条件执行的建议。常见坑点❌ 照抄网上的优化技巧不管 PHP 版本✅ 先确认写法的适用范围mysql_*/ereg_*/split()在 PHP 7.0 已移除each()/create_function()在 PHP 8.0 已移除❌ 相信某某写法快 N 倍这类没有前提的数字✅ 用自己的数据、自己的版本跑一次原理可以信数字不能❌ 用isset()判断一个可能为null的键是否存在✅ 值可能为null时用array_key_exists()两者语义不同❌ 用strpos($s, $needle) false判断包含✅ 必须用PHP 8.0 可直接用str_contains()❌ 在热路径里写(.*)*这类嵌套量词的正则✅ 收紧模式或用占有量词*、原子组atomic group限制回溯❌ 在循环里逐条发 SQL然后再去优化 PHP 的语法细节✅ 先把往返次数降下来批量查询、IN、JOIN、事务❌ 在开发机上开着opcache.validate_timestamps就断言Opcache 没效果✅ 生产与开发的 Opcache 配置不同两者的测量结果不能互推❌ 一边假设$s的追加是原地扩容一边把这个字符串到处赋值✅ 字符串是写时复制的被多处引用时追加必然触发复制总结层面核心原理典型手段优先级环境与版本引擎能力与已移除的旧 API升级 PHP 8.x、迁移mysql_*/ereg_*/each()最高部署与缓存操作码复用、减少系统调用Opcache、优化自动加载、realpath_cache高I/O 与数据库跨进程/网络往返最贵消灭 N1、批量写、事务、分页高数组与字符串哈希表 O(1)、写时复制键查找替代线性扫描、循环不变式外提中正则与语言特性回溯代价、声明代替样板收紧正则、match/enum/?-中测量数据优先于直觉profiler、hrtime()自测贯穿始终这 50 条里真正能带来量级差异的是环境、部署和 I/O这三类 剩下的语言级写法更多是别做蠢事而不是能快多少。 而所有优化里最重要的一条是最后一条先测再改。 没有测量的优化只是把代码改得更复杂而已。
返回列表