代码优化策略
概述
代码优化是在不改变程序功能的前提下,通过改进代码结构和算法,提升程序执行速度、降低内存占用的技术。PHP 代码优化涵盖了内存管理、字符串处理、数组操作、I/O 优化、自动加载优化等多个方面。合理运用这些优化策略,可以在不增加硬件成本的情况下显著提升应用性能。
前置知识
阅读本节前,建议先了解:XHProf 性能分析 或 Xdebug 性能分析 来定位性能瓶颈。
基础概念
优化原则
- 先测量再优化:不要凭直觉优化,先用性能分析工具找到真正的瓶颈
- 优先算法优化:O(n) 比 O(n^2) 的任何微观优化都快
- 二八定律:80% 的性能问题通常集中在 20% 的代码中
- 可读性优先:除非有明确的性能需求,否则不要以牺牲可读性为代价进行优化
PHP 执行模型
PHP 的请求-响应模型意味着每个请求都是独立的进程(或线程),这与其他语言的长进程模型有本质区别。因此,PHP 优化的重点在于减少单次请求中的计算量和 I/O 等待。
详细说明
内存管理
变量作用域与内存释放
php
<?php
declare(strict_types=1);
/**
* 内存管理优化示例
*/
class MemoryOptimizer
{
/**
* 处理大数据集时及时释放内存
*/
public function processLargeData(): void
{
// 不好的做法:一次性加载所有数据到内存
// $data = file('large_file.txt'); // 假设有 500MB
// 好的做法:使用生成器逐行处理
$this->processLineByLine('large_file.txt');
}
/**
* 逐行处理大文件
*/
private function processLineByLine(string $filePath): void
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new RuntimeException("无法打开文件: {$filePath}");
}
while (($line = fgets($handle)) !== false) {
$this->processLine($line);
// $line 在每次循环后自动释放
}
fclose($handle);
}
private function processLine(string $line): void
{
// 处理逻辑
trim($line);
}
/**
* 及时 unset 大变量
*/
public function processWithCleanup(): array
{
$largeArray = range(1, 1_000_000);
// 处理大数组
$result = array_filter($largeArray, fn(int $n) => $n % 2 === 0);
// 及时释放不再需要的大变量
unset($largeArray);
return $result;
}
/**
* 使用引用避免大数组复制
*/
public function processByReference(array &$data): void
{
foreach ($data as &$item) {
$item = strtoupper((string) $item);
}
unset($data); // 解除引用绑定
}
}内存使用监控
php
<?php
declare(strict_types=1);
/**
* 内存使用监控器
*/
class MemoryMonitor
{
private array $snapshots = [];
/**
* 记录当前内存快照
*/
public function snapshot(string $label): void
{
$this->snapshots[$label] = [
'usage' => memory_get_usage(),
'peak' => memory_get_peak_usage(),
'real' => memory_get_usage(true),
'real_peak' => memory_get_peak_usage(true),
];
}
/**
* 生成内存使用报告
*/
public function report(): string
{
$lines = [];
$lines[] = str_pad('快照点', 30) . str_pad('使用(KB)', 15)
. str_pad('峰值(KB)', 15) . str_pad('实际(KB)', 15);
$lines[] = str_repeat('-', 75);
foreach ($this->snapshots as $label => $data) {
$lines[] = str_pad($label, 30)
. str_pad(round($data['usage'] / 1024, 2) . '', 15)
. str_pad(round($data['peak'] / 1024, 2) . '', 15)
. str_pad(round($data['real'] / 1024, 2) . '', 15);
}
return implode("\n", $lines);
}
/**
* 计算两个快照之间的内存增量
*/
public function diff(string $start, string $end): array
{
if (!isset($this->snapshots[$start], $this->snapshots[$end])) {
throw new InvalidArgumentException("快照不存在");
}
return [
'usage_diff' => $this->snapshots[$end]['usage'] - $this->snapshots[$start]['usage'],
'peak_diff' => $this->snapshots[$end]['peak'] - $this->snapshots[$start]['peak'],
];
}
}
// 使用示例
$monitor = new MemoryMonitor();
$monitor->snapshot('开始');
$data = range(1, 500000);
$monitor->snapshot('创建数组后');
$result = array_map(fn($n) => $n * 2, $data);
$monitor->snapshot('map 处理后');
unset($data);
$monitor->snapshot('释放原数组后');
echo $monitor->report() . PHP_EOL;字符串拼接优化
不同拼接方式对比
php
<?php
declare(strict_types=1);
/**
* 字符串拼接性能对比
*/
class StringBenchmark
{
private const ITERATIONS = 10_000;
/**
* 方式1: .= 操作符拼接
*/
public function concatOperator(): string
{
$result = '';
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result .= "item_{$i},";
}
return $result;
}
/**
* 方式2: 数组 + implode(推荐)
*/
public function arrayImplode(): string
{
$parts = [];
for ($i = 0; $i < self::ITERATIONS; $i++) {
$parts[] = "item_{$i}";
}
return implode(',', $parts);
}
/**
* 方式3: sprintf 格式化
*/
public function sprintfFormat(): string
{
$result = '';
for ($i = 0; $i < self::ITERATIONS; $i++) {
$result .= sprintf('%s%d,', 'item_', $i);
}
return $result;
}
/**
* 方式4: 数组收集 + implode
*/
public function arrayCollect(): string
{
$result = array_map(
fn(int $i): string => "item_{$i}",
range(0, self::ITERATIONS - 1)
);
return implode(',', $result);
}
/**
* 运行基准测试
*/
public function run(): void
{
$methods = [
'concatOperator' => '.= 操作符',
'arrayImplode' => '数组 + implode',
'sprintfFormat' => 'sprintf',
'arrayCollect' => 'array_map + implode',
];
foreach ($methods as $method => $label) {
$start = hrtime(true);
$this->$method();
$time = (hrtime(true) - $start) / 1_000_000;
echo "{$label}: {$time}ms" . PHP_EOL;
}
}
}
$bench = new StringBenchmark();
$bench->run();建议
在循环中进行大量字符串拼接时,优先使用数组收集 + implode() 方式,性能显著优于 .= 操作符。对于少量拼接,.= 足够。
字符串函数优化
php
<?php
declare(strict_types=1);
/**
* 字符串操作优化技巧
*/
// 1. 使用 str_contains / str_starts_with / str_ends_with(PHP 8.0+)
// 替代 strpos !== false 的写法
// 不好:
if (strpos($haystack, $needle) !== false) { ... }
// 好:
if (str_contains($haystack, $needle)) { ... }
// 2. 使用 str_starts_with 代替 strncmp
if (str_starts_with($url, 'https://')) { ... }
// 3. 多次正则匹配 → 单次预编译
// 不好:
for ($i = 0; $i < 1000; $i++) {
preg_match('/pattern/', $text);
}
// 好:
$pattern = '/pattern/';
for ($i = 0; $i < 1000; $i++) {
preg_match($pattern, $text);
}
// 4. 简单替换优先用 str_replace 而非 preg_replace
// str_replace 比 preg_replace 快 3-5 倍
$text = str_replace('foo', 'bar', $text); // 快
// $text = preg_replace('/foo/', 'bar', $text); // 慢数组操作优化
数组操作性能指南
php
<?php
declare(strict_types=1);
/**
* 数组操作优化示例
*/
class ArrayOptimization
{
/**
* 使用 array_key_exists vs isset
* isset 更快,但不能检查 null 值
*/
public function keyCheck(array $data): mixed
{
// isset 更快(不触发 Notice,且字节码更简洁)
if (isset($data['key'])) {
return $data['key'];
}
// 仅当需要区分 null 和不存在时使用 array_key_exists
if (array_key_exists('key', $data)) {
return $data['key']; // 可能为 null
}
return null;
}
/**
* 使用 in_array vs isset + array_flip
* 大量查找时 array_flip 更快
*/
public function fastLookup(array $values, array $searchItems): array
{
// 不好:每次 in_array 都是 O(n)
// foreach ($searchItems as $item) {
// if (in_array($item, $values, true)) { ... }
// }
// 好:先 flip,然后 O(1) 查找
$flipped = array_flip($values);
$results = [];
foreach ($searchItems as $item) {
if (isset($flipped[$item])) {
$results[] = $item;
}
}
return $results;
}
/**
* 避免在循环中调用 count()
*/
public function iterateEfficiently(array $items): array
{
// 不好:每次迭代都调用 count()
// for ($i = 0; $i < count($items); $i++) { ... }
// 好:缓存 count 结果
$count = count($items);
for ($i = 0; $i < $count; $i++) {
// ...
}
// 更好:使用 foreach
$results = [];
foreach ($items as $item) {
$results[] = $item * 2;
}
return $results;
}
/**
* 使用引用避免大数组复制
*/
public function modifyInPlace(array &$data): void
{
foreach ($data as &$value) {
$value = trim((string) $value);
}
unset($data); // 解除引用
}
/**
* 使用 splat 运算符解构
*/
public function unpackArrays(): array
{
$part1 = ['a', 'b'];
$part2 = ['c', 'd'];
// PHP 8.1+ 使用 spread 运算符合并
return [...$part1, ...$part2];
// 比 array_merge 更高效(不重排数字键)
}
}数组函数性能对比
php
<?php
declare(strict_types=1);
/**
* 数组函数选择指南
*/
// 过滤:array_filter vs foreach + 手动
// array_filter 会创建新数组(内存开销)
// 大数据集时 foreach 手动过滤更节省内存
// 排序选择:
// sort() - O(n log n) 基本排序
// usort() - 自定义排序(回调开销较大)
// array_unique() - 内部也是排序实现
// 查找选择:
// array_search - O(n) 线性搜索
// in_array - O(n) 线性搜索
// isset (array_flip后) - O(1) 哈希查找
// array_intersect - 使用哈希表,接近 O(n)
// 去重:
// array_unique - O(n log n) 排序去重
// array_flip(array_flip($arr)) - O(n) 利用键唯一性去重(仅适用于字符串/整数)
/**
* 大数组去重优化
*/
function fastUnique(array $arr): array
{
// 当值是字符串或整数时,利用 array_flip 的键唯一性
return array_keys(array_flip($arr));
}
/**
* 安全的大数组去重(任意类型)
*/
function safeUnique(array $arr): array
{
$seen = [];
$result = [];
foreach ($arr as $item) {
$key = is_object($item) ? spl_object_hash($item) : (string) $item;
if (!isset($seen[$key])) {
$seen[$key] = true;
$result[] = $item;
}
}
return $result;
}I/O 优化
文件 I/O 优化
php
<?php
declare(strict_types=1);
/**
* I/O 操作优化
*/
class IoOptimizer
{
/**
* 批量文件读取 vs 逐个读取
*/
public function batchRead(array $files): array
{
// 不好:逐个文件读取
// $results = [];
// foreach ($files as $file) {
// $results[$file] = file_get_contents($file);
// }
// 好:使用 parallel 扩展并行读取(如可用)
// 或者使用生成器逐个处理
$results = [];
foreach ($files as $file) {
$content = file_get_contents($file);
if ($content !== false) {
$results[$file] = $content;
}
}
return $results;
}
/**
* 使用流操作处理大文件
*/
public function processLargeFile(string $inputPath, string $outputPath): void
{
$input = fopen($inputPath, 'r');
$output = fopen($outputPath, 'w');
if ($input === false || $output === false) {
throw new RuntimeException('无法打开文件');
}
// 设置缓冲区大小(默认 8KB,可增大)
stream_set_read_buffer($input, 65536); // 64KB
stream_set_write_buffer($output, 65536);
while (($line = fgets($input)) !== false) {
$processed = trim($line) . "\n";
fwrite($output, $processed);
}
fclose($input);
fclose($output);
}
/**
* HTTP 请求优化(连接复用)
*/
public function batchHttpRequests(array $urls): array
{
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_multi_add_handle($mh, $ch);
$handles[$i] = $ch;
}
// 执行所有请求
$active = null;
do {
$status = curl_multi_exec($mh, $active);
} while ($status === CURLM_CALL_MULTI_PERFORM && $active);
// 等待所有请求完成
while ($status === CURLM_OK && $active) {
curl_multi_select($mh);
do {
$status = curl_multi_exec($mh, $active);
} while ($status === CURLM_CALL_MULTI_PERFORM && $active);
}
// 收集结果
$results = [];
foreach ($handles as $i => $ch) {
$results[$i] = [
'content' => curl_multi_getcontent($ch),
'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
'time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
];
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
return $results;
}
}数据库 I/O 优化
php
<?php
declare(strict_types=1);
/**
* 数据库查询优化
*/
class DbQueryOptimizer
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 使用批量插入替代逐条插入
*/
public function batchInsert(array $records): int
{
// 不好:逐条插入
// foreach ($records as $record) {
// $stmt = $this->pdo->prepare("INSERT INTO ...");
// $stmt->execute($record);
// }
// 好:使用事务 + 批量插入
$this->pdo->beginTransaction();
$stmt = $this->pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
$count = 0;
foreach ($records as $record) {
$stmt->execute([$record['name'], $record['email']]);
$count++;
}
$this->pdo->commit();
return $count;
}
/**
* 预编译语句复用
*/
public function fetchUsers(array $ids): array
{
// 占位符绑定(最多 65535 个)
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $this->pdo->prepare(
"SELECT * FROM users WHERE id IN ({$placeholders})"
);
$stmt->execute(array_values($ids));
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 使用生成器处理大量查询结果
*/
public function iterateLargeResult(string $query): \Generator
{
$stmt = $this->pdo->query($query);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
$stmt->closeCursor();
}
}自动加载优化
Composer 自动加载优化
bash
# 生成优化的自动加载文件
# classmap:将所有类映射到文件路径
composer dump-autoload --optimize
# 或
composer dump-autoload -o
# 开发环境:使用 authoritative 模式
composer dump-autoload --classmap-authoritative
# 此模式不会在文件系统中搜索类,性能最优但新增类需要重新运行自动加载配置
json
{
"autoload": {
"psr-4": {
"App\\": "src/"
},
"classmap": [
"database/",
"routes/"
],
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true,
"apcu-autoloader": true
}
}APCu 自动加载缓存
在 composer.json 中设置 "apcu-autoloader": true 可以利用 APCu 缓存自动加载的类映射,避免每次请求都从磁盘读取 vendor/composer/autoload_classmap.php。
自定义优化加载器
php
<?php
declare(strict_types=1);
/**
* 带缓存的自动加载器
*/
class CachedAutoloader
{
private string $cacheFile;
private array $classMap = [];
public function __construct(string $cacheFile, string $baseDir)
{
$this->cacheFile = $cacheFile;
// 尝试加载缓存
if (file_exists($cacheFile) && is_readable($cacheFile)) {
$this->classMap = require $cacheFile;
return;
}
// 扫描目录生成类映射
$this->buildClassMap($baseDir);
$this->saveCache();
}
/**
* 注册自动加载器
*/
public function register(): void
{
spl_autoload_register(function (string $class): void {
if (isset($this->classMap[$class])) {
require $this->classMap[$class];
}
});
}
private function buildClassMap(string $dir): void
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
/** @var SplFileInfo $file */
if ($file->isFile() && $file->getExtension() === 'php') {
$tokens = token_get_all(file_get_contents($file->getPathname()));
foreach ($tokens as $i => $token) {
if (is_array($token) && $token[0] === T_NAMESPACE) {
// 解析命名空间和类名
$namespace = '';
for ($j = $i + 2; $j < count($tokens); $j++) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
$namespace .= $tokens[$j][1];
} elseif (is_string($tokens[$j]) && $tokens[$j] === ';' || $tokens[$j] === '{') {
break;
} elseif (is_string($tokens[$j]) && $tokens[$j] === '\\') {
$namespace .= '\\';
}
}
}
}
}
}
}
private function saveCache(): void
{
$dir = dirname($this->cacheFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents(
$this->cacheFile,
'<?php return ' . var_export($this->classMap, true) . ';'
);
}
}实战示例
综合优化实战
php
<?php
declare(strict_types=1);
/**
* CSV 文件处理优化
* 展示从低效到高效的优化过程
*/
/**
* 版本1:低效实现(内存溢出风险)
*/
function processCsvSlow(string $filePath): array
{
$lines = file($filePath); // 一次性加载所有行到内存
$results = [];
foreach ($lines as $line) {
$data = str_getcsv($line);
$data = array_map('trim', $data);
$data = array_map('strtolower', $data);
// 字符串拼接
$key = $data[0] . '_' . $data[1] . '_' . $data[2];
$results[$key] = $data;
}
return $results;
}
/**
* 版本2:优化实现
*/
function processCsvOptimized(string $filePath): array
{
$results = [];
$handle = fopen($filePath, 'r');
if ($handle === false) {
return [];
}
stream_set_read_buffer($handle, 65536);
while (($line = fgets($handle)) !== false) {
$data = str_getcsv($line);
// 使用数组收集替代字符串拼接
$key = implode('_', array_slice($data, 0, 3));
// 利用批量操作
$results[strtolower($key)] = $data;
}
fclose($handle);
return $results;
}
/**
* 版本3:最高效(生成器 + 流式处理)
*/
function processCsvStreaming(string $filePath): \Generator
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
return;
}
while (($line = fgets($handle)) !== false) {
yield str_getcsv($line);
}
fclose($handle);
}性能对比测试框架
php
<?php
declare(strict_types=1);
/**
* 通用性能对比测试框架
*/
class PerformanceComparison
{
/**
* 对比多个实现的性能
*/
public static function compare(
array $benchmarks,
int $warmup = 3,
int $iterations = 100
): void {
echo str_pad('实现', 30) . str_pad('平均(ms)', 12)
. str_pad('中位数(ms)', 12) . str_pad('最快(ms)', 12) . str_pad('最慢(ms)', 12) . PHP_EOL;
echo str_repeat('-', 78) . PHP_EOL;
foreach ($benchmarks as $name => $fn) {
// 预热
for ($i = 0; $i < $warmup; $i++) {
$fn();
}
// 正式测试
$times = [];
for ($i = 0; $i < $iterations; $i++) {
$start = hrtime(true);
$fn();
$times[] = (hrtime(true) - $start) / 1_000_000;
}
sort($times);
$count = count($times);
echo str_pad($name, 30)
. str_pad(round(array_sum($times) / $count, 4) . '', 12)
. str_pad(round($times[(int) ($count * 0.5)], 4) . '', 12)
. str_pad(round($times[0], 4) . '', 12)
. str_pad(round($times[$count - 1], 4) . '', 12)
. PHP_EOL;
}
}
}
// 使用示例
PerformanceComparison::compare([
'array_merge' => fn() => array_merge(range(1, 1000), range(1001, 2000)),
'spread operator' => fn() => [...range(1, 1000), ...range(1001, 2000)],
'array_push' => function () {
$a = range(1, 1000);
foreach (range(1001, 2000) as $v) {
$a[] = $v;
}
return $a;
},
], 5, 200);注意事项
过早优化的陷阱
- 可读性 > 性能:微秒级的优化不值得牺牲代码可读性
- Profile 先于 Optimize:先找到真正的瓶颈
- 数据驱动:用实际数据说话,不要凭主观臆断
PHP 8.x 内置优化
PHP 8.x 本身已经包含大量优化,许多旧版本的"技巧"已不再必要:
php
<?php
declare(strict_types=1);
// PHP 8.0+ 已优化的操作(不需要手动优化)
// 1. 命名参数
$result = some_function(name: 'value'); // 不再需要按位置传参
// 2. match 表达式(比 switch + break 更高效)
$result = match ($status) {
200 => 'OK',
404 => 'Not Found',
default => 'Unknown',
};
// 3. null 安全运算符
$country = $user?->getAddress()?->country; // 替代层层嵌套的 isset
// 4. Fibers(协程)替代 callback hell最佳实践
1. 选择正确的数据结构
php
<?php
declare(strict_types=1);
// 频繁查找 → 使用关联数组(哈希表 O(1) 查找)
$lookup = ['apple' => 1, 'banana' => 2, 'cherry' => 3];
isset($lookup['banana']); // O(1)
// 有序集合 → 使用 SplFixedArray(数字索引数组更快)
$fixed = new SplFixedArray(1000);
$fixed[0] = 'first'; // 比普通数组快 20-30%
// 高频操作 → 考虑 DS 扩展
// $set = new \Ds\Set([1, 2, 3]);
// $map = new \Ds\Map(['key' => 'value']);2. 缓存计算结果
php
<?php
declare(strict_types=1);
class CachedCalculator
{
private array $cache = [];
public function expensiveCalculation(int $n): int
{
// 检查缓存
if (isset($this->cache[$n])) {
return $this->cache[$n];
}
// 执行计算
$result = $this->compute($n);
// 缓存结果
$this->cache[$n] = $result;
return $result;
}
private function compute(int $n): int
{
// 模拟耗时计算
$sum = 0;
for ($i = 0; $i < $n; $i++) {
$sum += $i * $i;
}
return $sum;
}
}3. 使用生成器处理大数据
php
<?php
declare(strict_types=1);
// 不好:返回完整数组
function getAllUsers(PDO $pdo): array
{
return $pdo->query("SELECT * FROM users")->fetchAll();
}
// 好:返回生成器
function iterateUsers(PDO $pdo): \Generator
{
$stmt = $pdo->query("SELECT * FROM users");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
}下一节
继续学习:信号处理