Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 62 · Практика

Производительность: измерения, память и медленные подходы

Ищем узкое место по данным: SQL, сеть, алгоритмы, буферы и кеширование.

Сначала понимаем объём работы, затем измеряем время

Страница может тормозить из-за повторного поиска, сотни SQL-запросов или ожидания сети. Короткая запись функции не означает, что внутри мало работы. Начнём с подсчёта действий без ненадёжных обещаний миллисекунд.

Что уже пригодится. ключи массива, foreach

Диагностика
  1. Воспроизводим задачу на заданном объёме
  2. Находим число операций и внешних запросов
  3. Измеряем узкое место
  4. Меняем один подход
  5. Проверяем корректность и измеряем снова

Вложенный перебор сравнивает каждую нужную позицию со всем каталогом. Индекс по ключу позволяет сначала подготовить данные, затем обращаться к ним напрямую. Подготовка тоже стоит времени и памяти.

Один раз строим индекс и читаем известный ключСамостоятельный пример
<?php
declare(strict_types=1);

$products = [
    ['sku' => 'A', 'stock' => 7],
    ['sku' => 'B', 'stock' => 0],
];
$bySku = [];
foreach ($products as $product) {
    $bySku[$product['sku']] = $product;
}
echo $bySku['B']['stock'], "\n";
Ожидаемый результат
0

В учебном наборе ключи уникальны и B существует. Это условия примера, не гарантия внешнего API: реальные дубликаты и отсутствующие ключи проверяют. Выигрыш зависит от количества последующих обращений; для одного чтения подготовка индекса может не окупиться.

Следующая ступень. Теперь измеряем hrtime и память, разбираем SQL N+1, кеш и потоковую обработку. Сначала сохраняем правильный результат, затем оцениваем ускорение.

Быстро для одного запроса и быстро под нагрузкой — разные вещи

Наглядная модель

Почему индекс по ID убирает повторный поиск

Нужно найти товары для 100 позиций заказа в каталоге из 1000 товаров. Сравним число обходов, а не выдуманные миллисекунды.

  • Заново искать в списке

    Для каждой позиции повторяется проход по каталогу.

    • Взять одну позицию

      Заказ просит товар с заданным ID.

    • Перебирать каталог

      Проверять ID до совпадения или конца: до 1000 сравнений на одну позицию.

    • Повторить 100 раз

      В худшем случае получится до 100000 сравнений. Повторная работа растёт с обоими списками.

  • Один раз построить индекс

    Каталог с уникальными ID превращается в таблицу быстрых обращений.

    • Пройти 1000 товаров один раз

      Сохранить каждый товар под его ID. Договориться, что делать при дубле ID.

    • Обратиться по ID

      Для каждой из 100 позиций найти запись по ключу; отсутствующий товар обработать отдельно.

    • Измерить цену решения

      Меньше повторных обходов, но больше памяти под индекс. Проверить итог и время на своих данных.

Эта схема объясняет рост работы. Она не обещает одинаковую скорость на любой машине и не отменяет измерения SQL, сети и памяти.

Страница каталога может отвечать за 200 мс одному посетителю и за пять секунд сотне посетителей. Причина не обязательно в медленной функции PHP: заканчиваются воркеры, база ждёт блокировки, внешний API отвечает дольше или каждый запрос повторяет одинаковую работу. Начинайте с измерения полного пути и числа операций. Среднее значение скрывает хвосты, поэтому смотрите p95/p99 и долю ошибок вместе с нагрузкой.

Для длительности внутри процесса используйте hrtime(true), для памяти — memory_get_usage и memory_get_peak_usage. Сравнивайте одинаковые входы, прогретое окружение, несколько повторов и отдельно холодный запуск. Не публикуйте вывод «array_map всегда быстрее foreach» на основании одного микротеста: размер данных, callback, версия PHP и остальной код меняют результат.

Стоимость алгоритма объясняет рост лучше, чем случайные миллисекунды. Вложенный поиск товара в списке для каждой позиции заказа даёт порядка n×m сравнений. Индекс по ID требует дополнительной памяти, но позволяет один раз пройти справочник и затем делать прямые обращения. Для десятка элементов обе версии могут быть достаточны; для тысяч разница становится архитектурной.

Убираем повторный линейный поиск

Индекс каталога для позиций заказаСамостоятельный пример
<?php
declare(strict_types=1);

$products = [['id'=>10,'name'=>'Tea'],['id'=>20,'name'=>'Coffee']];
$lines = [['productId'=>20,'quantity'=>2],['productId'=>10,'quantity'=>1]];
$byId = [];
foreach ($products as $product) $byId[$product['id']] = $product;
foreach ($lines as $line) {
    $product = $byId[$line['productId']] ?? null;
    if ($product === null) throw new DomainException('Unknown product');
    echo $product['name'].':'.$line['quantity']."
";
}
Ожидаемый результат
Coffee:2
Tea:1

Построение индекса — один проход по справочнику, затем обращения по ключу. При дубликатах ID последнее значение перезапишет предыдущее: если дубликаты запрещены, выявляйте их при построении индекса. Дополнительный массив потребляет память.

Не материализуем всё, если нужен один проход

Потоковый расчёт без массива из миллиона чиселСамостоятельный пример
<?php
declare(strict_types=1);

function quantities(int $count): Generator {
    for ($i=0;$i<$count;$i++) yield ($i % 3)+1;
}
$sum = 0;
foreach (quantities(100000) as $quantity) $sum += $quantity;
echo $sum."
";
Ожидаемый результат
199999

Генератор выдаёт элементы по одному. Выигрыш касается хранения последовательности, а не магического ускорения CPU. Если затем вызвать iterator_to_array, весь результат снова окажется в памяти. В реальном импорте источник тоже должен читаться потоково.

fetchAll, file_get_contents и ob_start удобны для ограниченного объёма, но материализуют данные целиком. Большой CSV обрабатывают строками, экспорт — страницами по ключу, JSON огромного размера требует подходящего потокового парсера либо изменения API. json_decode не становится потоковым от того, что результат потом обходится генератором: исходная строка и структура уже загружены.

Повторная конкатенация и копирование больших массивов могут увеличить пиковую память. PHP использует copy-on-write, поэтому присваивание массива не всегда немедленно копирует всё, но изменение общей структуры может потребовать копию. Не делайте вывод о памяти только из числа переменных. Измеряйте пик на реальном размере данных и учитывайте буферы расширений, которые не всегда полностью видны счётчикам PHP.

Подходы, которые особенно часто становятся дорогими

ПодходПочему дорогоЧто проверить / альтернатива
SQL в цикле N+1Много сетевых обменов и плановJOIN, пакетная выборка, число запросов
LIKE с ведущим %Обычный индекс может не помочьПлан запроса, полнотекстовый поиск
password_hash при входе для фиктивного пользователяНамеренно дорогой расчёт повторяетсяЗаранее подготовленный фиктивный хеш и rate limit
sleep в HTTPЗанятый воркер не обслуживает другого клиентаОчередь и available_at
Неограниченный regex на внешнем текстеВозможен дорогой backtrackingЛимит длины, простой парсер, проверка PCRE-ошибки
Сборка большого HTML/CSV в строкуПиковая память и задержка первого байтаПотоковая обработка при подходящей инфраструктуре
Сериализация всего объекта в каждом логеCPU, память и утечка данныхСтруктурированные безопасные поля

Кеш сокращает повторную работу, но добавляет вопросы актуальности, ключа и сброса. Кеш цены должен учитывать валюту, сегмент покупателя и версию прайс-листа. Ошибка ключа может показать чужую скидку. При одновременном истечении популярного ключа множество запросов начнут один дорогой расчёт; нужны координация обновления, случайное распределение TTL или выдача допустимого устаревшего значения.

OPcache хранит скомпилированный код и обычно полезен для веб-приложений. Он не кеширует результат SQL и не исправляет N+1. JIT не является универсальным ускорителем веб-сайта, где значительная часть времени уходит в I/O. Настройки выбирают измерением на целевой нагрузке, а не копированием максимальных чисел из статьи.

Время и память ограничивают намеренно

У каждого внешнего вызова должен быть timeout, у выборки — разумная граница, у загрузки — предел байтов, у рекурсии — ограничение глубины. Это одновременно надёжность и производительность. Больший memory_limit может временно скрыть утечку, но уменьшит число параллельных воркеров в доступной RAM. Рассчитывайте вместимость сервера по фактической памяти процессов с запасом, а не только по лимиту PHP.

Оптимизация завершена, когда измерения подтверждают улучшение без изменения результата и ухудшения важных сценариев. Сохраните вход, окружение и метод измерения. Сравнивайте не только скорость успешного ответа, но и поведение при ошибке API, конкуренции базы и пустом кеше. Именно эти режимы часто определяют устойчивость сайта в реальной работе.

Скорость не должна разрушать корректность

Быстрый кешированный ответ с чужими данными хуже медленного правильного. Перед оптимизацией зафиксируйте инварианты: права пользователя, актуальность цены, допустимую давность остатков и порядок операций. Кеш публичного описания товара и кеш доступного пользователю заказа имеют разные ключи и правила инвалидирования. Нельзя переносить одну стратегию на всё приложение.

Измеряйте стоимость промаха и одновременного обновления кеша, а не только идеальный hit. Проверяйте, что отказ кеша не превращается в лавину запросов к базе. Ограничение параллельности обновления и постепенное распределение TTL часто важнее ещё одной микросекунды сериализации. Любые числа скорости в документации без размера данных и окружения рассматривайте как пример методики, а не обещание результата.

Практика: проверьте решение, а не только успешный путь

Импорт 50 000 товаров использует foreach и на каждом шаге SELECT поставщика. С чего начать оптимизацию?

Разбор решения

Измерить количество и длительность запросов. Собрать нужные supplier_id и получить справочник пакетно либо использовать подходящий JOIN. Построить индекс по ID. Затем проверить память и размер пакета. Замена foreach на array_map не устраняет 50 000 сетевых обращений.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

hrtimeПолучить монотонную отметку для измерения длительности, а не календарную дату.

PHP 7.3+; macOS implementation changed in PHP 8.5 · standard

hrtime(bool $as_number = false): array|int|float|false

Получить монотонную отметку для измерения длительности, а не календарную дату.

Скобки после имени запускают вызов; значения внутри — аргументы. Знак = сохраняет возвращённый результат в переменную со знаком $. Функция может вернуть число, строку, массив, true/false либо завершить текущий сценарий. Сначала смотрите минимальный вызов и ожидаемый результат, затем переходите к условиям, функциям и обработке ошибок. Как устроен вызов, параметр и return.

Что произойдёт при вызове

Замер участка
  1. Снять start перед работой
  2. Выполнить ровно измеряемую работу
  3. Снять finish
  4. finish − start → длительность

Первый запуск и развитие задачи

Число записано в наносекундах, но это не обещание точности в одну наносекунду. Для тайм-аута используйте монотонный deadline; для даты события — DateTime/time.

Полный договор вызова

Часть вызоваПолный договор
as_number=falseМассив [секунды, наносекунды] из двух целых.
as_number=trueНаносекунды как int на 64-bit или float на 32-bit платформе.
Возврат falseПлатформа не предоставила отметку. Проверяйте строго === false.
ЭпохаПроизвольная и монотонная: значение годится для разности отметок, не для даты, ID или сравнения разных машин.

Ошибки и границы. Абсолютную отметку не сохраняют как деловую дату и не вычитают из microtime.

Версии и платформа. В PHP 8.5 macOS использует CLOCK_UPTIME_RAW; смысл остаётся монотонной длительностью.

Рядом: sleep, time_nanosleep.

Карта полного раздела Miscellaneous.

Без аргумента возвращает секунды/наносекунды, с true — числовое значение наносекунд. Часы предназначены для интервалов, а не календарной даты.

Измерить ограниченный расчётСамостоятельный пример
<?php
declare(strict_types=1);

$start=hrtime(true);
$sum=array_sum(range(1,1000));
$elapsed=hrtime(true)-$start;
echo $sum===500500 && $elapsed>=0 ? "measured\n" : "failed\n";
Ожидаемый результат
measured

Не печатаем нестабильное число; в профилировании сохраняйте elapsed/1e6 в миллисекундах.

Проверка дедлайна без ожиданияСамостоятельный пример
<?php
declare(strict_types=1);

$deadline=hrtime(true)+5_000_000_000;
echo hrtime(true)<$deadline ? "budget remains\n" : "expired\n";
Ожидаемый результат
budget remains

Дедлайн включает время реальной работы, в отличие от счётчика только запрошенных пауз.

Рядом по задаче. microtime(true) даёт wall clock; time/DateTime нужны для даты события.

memory_get_peak_usageПолучить максимум памяти с начала запроса или последнего сброса пика.

Есть в проверенном PHP 8.5.11 · standard

memory_get_peak_usage(bool $real_usage = false): int

Получить максимум памяти с начала запроса или последнего сброса пика.

Пиковая величина отвечает на вопрос «до какого уровня поднималась память», поэтому подходит для импорта, генерации отчёта и обработки больших изображений. После освобождения временного массива текущая память падает, а пик сохраняется. real_usage имеет тот же смысл режимов, что у memory_get_usage.

Первый запуск и развитие задачи

Сравнить текущий уровень с пикомСамостоятельный пример
<?php
declare(strict_types=1);

$chunk = str_repeat('catalog;', 20000);
$current = memory_get_usage();
$peak = memory_get_peak_usage();
echo $peak >= $current ? "пик не ниже текущего\n" : "неожиданный счётчик\n";
unset($chunk);
Ожидаемый результат
пик не ниже текущего

Пик сохраняет максимум с начала запроса, даже если временный буфер позже освобождён. Сравнивайте одинаковый режим real_usage.

Начать новый участок наблюдения пикаСамостоятельный пример
<?php
declare(strict_types=1);

$temporary = str_repeat('a', 200000);
unset($temporary);
memory_reset_peak_usage();
$current = memory_get_usage();
$peak = memory_get_peak_usage();
echo $peak >= $current ? "новый пик отсчитывается\n" : "ошибка\n";
Ожидаемый результат
новый пик отсчитывается

Сброс меняет только счётчик пика; уже выделенная память не освобождается. Для очистки сначала убирают владельцев данных и закрывают ресурсы.

Полный договор вызова

ПараметрЧто передаём и зачем
real_usagefalse — пик используемой памяти; true — пик выделенных движку блоков.
Часть договораЧто получает программа
Сигнатураmemory_get_peak_usage(bool $real_usage = false): int
Результатint байтов.
Ошибки и особые значенияПик процесса не показывает, какой объект был причиной. Сравнение разных real_usage или разных конфигураций вводит в заблуждение.
ВерсииЕсть в проверенном PHP 8.5.11
Официальный диапазон узла: (PHP 5 >= 5.2.0, PHP 7, PHP 8).
СредаВсе SAPI, если ниже не указано иное.

Рядом: memory_get_usage, memory_reset_peak_usage.

Вернуться к карте задач или полному дереву функций.

Возвращает байты пикового потребления. true учитывает выделенные системные блоки, false — используемую память в учёте PHP.

Пик после массиваСамостоятельный пример
<?php
declare(strict_types=1);

$items=range(1,10000);
$peak=memory_get_peak_usage();
echo $peak>0 ? "peak recorded\n" : "missing\n";
Ожидаемый результат
peak recorded

Точное число зависит от сборки и окружения; сравнение выполняют на одинаковых данных.

Освобождение не уменьшает исторический пикСамостоятельный пример
<?php
declare(strict_types=1);

$items=range(1,10000);
$before=memory_get_peak_usage();
unset($items);
echo memory_get_peak_usage()>=$before ? "peak retained\n" : "unexpected\n";
Ожидаемый результат
peak retained

Для текущего потребления используют другую функцию; peak хранит максимум.

Рядом по задаче. memory_get_usage показывает текущее значение; системный RSS дополняет измерение.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.