Skip to content

代码优化策略

概述

代码优化是在不改变程序功能的前提下,通过改进代码结构和算法,提升程序执行速度、降低内存占用的技术。PHP 代码优化涵盖了内存管理、字符串处理、数组操作、I/O 优化、自动加载优化等多个方面。合理运用这些优化策略,可以在不增加硬件成本的情况下显著提升应用性能。

前置知识

阅读本节前,建议先了解:XHProf 性能分析Xdebug 性能分析 来定位性能瓶颈。

基础概念

优化原则

  1. 先测量再优化:不要凭直觉优化,先用性能分析工具找到真正的瓶颈
  2. 优先算法优化:O(n) 比 O(n^2) 的任何微观优化都快
  3. 二八定律:80% 的性能问题通常集中在 20% 的代码中
  4. 可读性优先:除非有明确的性能需求,否则不要以牺牲可读性为代价进行优化

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);

注意事项

过早优化的陷阱

  1. 可读性 > 性能:微秒级的优化不值得牺牲代码可读性
  2. Profile 先于 Optimize:先找到真正的瓶颈
  3. 数据驱动:用实际数据说话,不要凭主观臆断

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;
    }
}

下一节

继续学习:信号处理

参考链接