Измерение производительности PHP: руководство по таймингу кода

Раздел: Производительность PHP -> Оптимизация производительности PHP

Основные методы измерения времени в PHP

Наиболее эффективный способ - использование функции hrtime(true) (доступна с PHP 7.3). Она возвращает монотонное время в наносекундах, не зависит от системных часов. Для замеров коротких операций (менее 1 мс) это лучший выбор.
<?
$start = hrtime(true);
// измеряемый код
$end = hrtime(true);
$seconds = ($end - $start) / 1_000_000_000;
echo $seconds;
?>

Timing php (тайминг в php)

Пояснения: hrtime(true) возвращает int наносекунд. Разность делится на 1e9 для секунд, на 1e6 для миллисекунд. Для PHP 7.2 и ниже применяется microtime(true) (возвращает float секунд с микросекундами).

Проблема: hrtime может быть недоступен на старых версиях PHP или сборках без поддержки. В таких случаях применяется microtime, но погрешность при коротких замерах выше. Также сам вызов hrtime занимает около 20–50 нс, что для сверхкоротких операций может быть существенно. Рекомендуется делать несколько замеров и усреднять.

Как замерить время нескольких участков кода с помощью class?

class Timing {
    private array $points = [];
    private float $start;
    public function __construct() {
        $this->start = hrtime(true);
    }
    public function mark(string $name): void {
        $this->points[$name] = hrtime(true);
    }
    public function report(): void {
        $prev = $this->start;
        foreach ($this->points as $name => $time) {
            $elapsed = ($time - $prev) / 1e6; // ms
            echo "$name: $elapsed ms\n";
            $prev = $time;
        }
    }
}
$t = new Timing();
usleep(10000);
$t->mark('step1');
usleep(20000);
$t->mark('step2');
$t->report();

Цель: выявить узкие места в последовательности операций.

Ошибка: если точки не вызваны, отчет может быть неполным. Также не учитывается накладные расходы на вызов mark. Для миллиона точек такой подход не подходит.

Как одновременно контролировать затраты времени и памяти?

function benchmark(callable $fn, int $iterations = 1): array {
    $memBefore = memory_get_usage();
    $timeBefore = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    $timeAfter = hrtime(true);
    $memAfter = memory_get_usage();
    return [
        'time' => ($timeAfter - $timeBefore) / 1e9 / $iterations,
        'memory' => ($memAfter - $memBefore) / $iterations
    ];
}
$result = benchmark(fn() => array_fill(0, 100, rand()), 10);
print_r($result);

Цель: оценить средние затраты на операцию.

Проблема: memory_get_usage не учитывает память, выделенную и освобождённую внутри цикла. Для точного пикового потребления применяется memory_get_peak_usage. Также PHP может не сразу освобождать память, результаты могут быть искажены.
Как логировать время выполнения в production?
$logger = new \Monolog\Logger('app');
$logger->pushHandler(new \Monolog\Handler\StreamHandler('/var/log/app.log'));
$stopwatch = new \Symfony\Component\Stopwatch\Stopwatch();
$stopwatch->start('db_query');
// вызов БД
$event = $stopwatch->stop('db_query');
$logger->info('DB query', ['duration' => $event->getDuration() . ' ms']);

Цель: мониторинг без остановки приложения.

Требует установки библиотек. Добавляет накладные расходы, но для production при низком уровне логирования это приемлемо.

Как получить детальный профиль выполнения каждого вызова функции?

// Включить в php.ini или через ini_set
xdebug_start_trace('/tmp/trace.xt');
// код
xdebug_stop_trace();
// Затем открыть trace.xt в KCacheGrind или QCacheGrind

Цель: найти bottleneck в сложных приложениях.

Xdebug снижает производительность, не подходит для production. Создаёт большие файлы. Рекомендуется использовать только на staging или локально.
Как получить замер каждого оператора (для экстремального профилирования)?
declare(ticks=1);
$start = microtime(true);
$tickCount = 0;
register_tick_function(function() use (&$tickCount, $start) {
    $tickCount++;
});
// код
unregister_tick_function('...');
echo "Выполнено $tickCount тиков за " . (microtime(true)-$start) . ' сек';
Чрезвычайно медленно. Применяется только для отладки микро-участков. Нельзя применять на production.

Заключение

Выбор метода тайминга зависит от требуемой точности, сложности приложения и условий эксплуатации. Для быстрых единичных замеров удобен hrtime, для многоточечного профилирования - самописные классы или компонент Stopwatch, для всестороннего анализа - Xdebug или Blackfire.

Примеры углубленного тайминга в PHP

1. Многоточечный таймер с автоматическим отчётом

Пример
class Profiler {
    private array $laps = [];
    private float $start;
    public function __construct() {
        $this->start = hrtime(true);
    }
    public function lap(string $label): void {
        $now = hrtime(true);
        $this->laps[] = ['label' => $label, 'time' => $now - $this->start];
    }
    public function results(): array {
        return $this->laps;
    }
    public function print(): void {
        $prev = 0;
        foreach ($this->laps as $i => $lap) {
            $delta = ($lap['time'] - $prev) / 1e6;
            printf("#%d %s: +%.3f ms (total %.3f ms)\n", $i+1, $lap['label'], $delta, $lap['time']/1e6);
            $prev = $lap['time'];
        }
    }
}
$p = new Profiler();
usleep(1000); $p->lap('sleep 1ms');
usleep(2000); $p->lap('sleep 2ms');
$p->print();
#1 sleep 1ms: +1.234 ms (total 1.234 ms)
#2 sleep 2ms: +2.456 ms (total 3.690 ms)

2. Бенчмарк с усреднением и отбрасыванием выбросов

Пример
function benchmarkAvg(callable $fn, int $times = 10): float {
    $durations = [];
    for ($i = 0; $i < $times; $i++) {
        $start = hrtime(true);
        $fn();
        $durations[] = (hrtime(true) - $start) / 1e9;
    }
    // удалить мин и макс
    sort($durations);
    array_shift($durations);
    array_pop($durations);
    return array_sum($durations) / count($durations);
}
$time = benchmarkAvg(fn() => array_sum(range(1, 10000)), 20);
echo "Среднее время: " . number_format($time, 6) . " сек";
Среднее время: 0.000245 сек

3. Сравнение производительности array_map и foreach

Пример
$data = range(1, 100000);
$start = microtime(true);
$result = array_map(fn($x) => $x * 2, $data);
$timeMap = microtime(true) - $start;

$result2 = [];
$start = microtime(true);
foreach ($data as $x) {
    $result2[] = $x * 2;
}
$timeForeach = microtime(true) - $start;

echo "array_map: " . number_format($timeMap, 6) . " сек\n";
echo "foreach:   " . number_format($timeForeach, 6) . " сек\n";
array_map: 0.012345 сек
foreach:   0.015678 сек

Пояснение: array_map обычно быстрее, но результат зависит от версии PHP и настроек.

4. Автоматический замер всего скрипта через register_shutdown_function

Пример
$start = hrtime(true);
register_shutdown_function(function() use ($start) {
    $elapsed = (hrtime(true) - $start) / 1e9;
    $peakMem = memory_get_peak_usage(true) / 1024 / 1024;
    echo "Скрипт выполнен за " . number_format($elapsed, 4) . " сек, пик памяти: " . number_format($peakMem, 2) . " MB\n";
});
// остальной код
Скрипт выполнен за 0.0234 сек, пик памяти: 1.45 MB

5. Профилирование рекурсивной функции (факториал) с замером каждого вызова

Пример
function factorial($n, &$calls = []) {
    $calls[] = microtime(true);
    if ($n <= 1) return 1;
    return $n * factorial($n-1, $calls);
}
$times = [];
$result = factorial(10, $times);
echo "Факториал 10 = $result\n";
echo "Количество вызовов: " . count($times) . "\n";
foreach ($times as $i => $t) {
    if ($i > 0) {
        $diff = ($t - $times[$i-1]) * 1000000;
        printf("Вызов #%d: +%.2f мкс\n", $i, $diff);
    }
}
Факториал 10 = 3628800
Количество вызовов: 11
Вызов #1: +2.34 мкс
Вызов #2: +1.89 мкс
...

Тайминг в PHP - comments

En
Timing php (php)