Вы когда-нибудь пытались загрузить в память CSV-файл на миллион строк и увидели что-то вроде:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted
Даже если увеличить memory_limit, ощущение всё равно неприятное: мы держим в памяти весь массив данных, хотя работаем с ним построчно.
Решение? Ленивые вычисления — подход, при котором данные генерируются и обрабатываются только тогда, когда они реально нужны.
В PHP это можно сделать двумя способами: с помощью генераторов (yield) и через Iterator API. Сегодня разберём оба.
Что такое ленивые вычисления
Обычно, когда мы создаём массив, PHP загружает в память сразу все элементы:
function getNumbersArray(int $count): array { $result = []; for ($i = 1; $i <= $count; $i++) { $result[] = $i; } return $result; } foreach (getNumbersArray(5) as $number) { echo $number . PHP_EOL; }
Здесь в памяти хранится сразу весь массив [1, 2, 3, 4, 5].
А теперь попробуем ленивый подход:
function getNumbersGenerator(int $count): Generator { for ($i = 1; $i <= $count; $i++) { yield $i; } } foreach (getNumbersGenerator(5) as $number) { echo $number . PHP_EOL; }
📌 Разница: генератор не хранит всё — он отдаёт элемент только тогда, когда foreach его запросит.
Читаем огромный CSV без боли
Представим, что у нас есть файл data.csv на 2 ГБ. Обычный file() или fgetcsv в массиве — мгновенный Out of Memory.
С генератором — всё просто:
function readCsv(string $filename): Generator { $handle = fopen($filename, 'r'); if ($handle === false) { throw new RuntimeException("Не удалось открыть файл $filename"); } while (($row = fgetcsv($handle)) !== false) { yield $row; } fclose($handle); } foreach (readCsv('data.csv') as $row) { // Обрабатываем строку }
📊 Память: даже для 2 ГБ CSV этот код будет занимать несколько килобайт, потому что в памяти всегда только одна строка.
Бенчмарк: массив vs генератор
$startMemory = memory_get_usage(); $array = range(1, 1_000_000); // создаёт массив из миллиона чисел echo "Массив: " . (memory_get_usage() - $startMemory) / 1024 / 1024 . " MB\n"; unset($array); $startMemory = memory_get_usage(); function bigGenerator(): Generator { for ($i = 1; $i <= 1_000_000; $i++) { yield $i; } } foreach (bigGenerator() as $n) { // просто итерируем } echo "Генератор: " . (memory_get_usage() - $startMemory) / 1024 / 1024 . " MB\n";
Результат на моей машине:
Массив: 120 MB Генератор: 0.5 MB
Iterator API
Генераторы — это быстро и просто. Но иногда нужно больше контроля: хранить состояние, управлять ключами или даже динамически менять источник данных.
Тогда в бой идёт Iterator API.
Пример: собственный итератор
class RangeIterator implements Iterator { private int $start; private int $end; private int $current; public function __construct(int $start, int $end) { $this->start = $start; $this->end = $end; $this->current = $start; } public function current(): int { return $this->current; } public function key(): int { return $this->current; } public function next(): void { $this->current++; } public function rewind(): void { $this->current = $this->start; } public function valid(): bool { return $this->current <= $this->end; } } foreach (new RangeIterator(1, 5) as $num) { echo $num . PHP_EOL; }
Когда что использовать
|
Ситуация |
Что выбрать |
|---|---|
|
Нужно просто отдать данные по мере запроса |
Генератор |
|
Нужно хранить внутреннее состояние или сложную логику |
Iterator API |
|
Потоковая обработка из файла/БД |
Генератор |
|
Множественные обходы коллекции с сохранением состояния |
Iterator API |
Реальный кейс из продакшена
Мы парсили API автопродаж, которое возвращало сотни тысяч записей.
Раньше мы собирали всё в массив — скрипт ел по 1–2 ГБ памяти.
После перехода на генератор:
function fetchCars(): Generator { $page = 1; do { $data = apiRequest('cars', ['page' => $page]); foreach ($data['items'] as $car) { yield $car; } $page++; } while (!empty($data['items'])); }
📉 Память упала с 2 ГБ до 10 МБ, время выполнения осталось почти тем же.
🔍 Для «гиков»: как генераторы работают под капотом
1. Генератор — это объект
В PHP генератор — это объект класса Generator, реализующий Iterator и Traversable.
Он умеет:
-
хранить текущее состояние функции;
-
приостанавливать выполнение на
yield; -
возобновлять выполнение с того же места.
2. Жизненный цикл
-
Вызов функции-генератора не запускает её сразу — создаётся объект
Generator. -
При первой итерации выполнение идёт до
yield. -
yieldвозвращает значение и «замораживает» функцию. -
Следующая итерация продолжает выполнение с того же места.
-
Когда функция завершается — генератор помечается как завершённый.
3. На уровне Zend Engine
Если скомпилировать функцию с yield через VLD (Vulcan Logic Disassembler), мы увидим, что каждый yield — это инструкция, которая:
-
сохраняет стек вызовов;
-
запоминает переменные;
-
возвращает значение в вызывающий код.
4. Разница с массивами
-
Массив создаёт все элементы в памяти.
-
Генератор хранит один текущий элемент (
zval) и перезаписывает его. -
Поэтому можно обойти миллион элементов, используя пару сотен килобайт.
5. Пример бесконечного генератора
function counter(): Generator { $i = 0; while (true) { yield $i++; } } foreach (counter() as $num) { if ($num > 5) break; echo $num . PHP_EOL; }
С массивом это невозможно — память просто закончится.
Benchmark results (1,000,001 rows)
|
Method |
Time |
Memory used |
Peak diff |
Rows |
|---|---|---|---|---|
|
Array (eager) |
1.401s |
120 B |
395.92 MB |
1,000,001 |
|
Generator |
1.012s |
0 B |
0.00 MB |
1,000,001 |
Эти результаты показывают, что ленивые генераторы могут значительно сократить использование памяти при обработке больших наборов данных, таких как CSV.
Визуальные результаты
Пиковый расход памяти:

Время выполнения:

Исходный код
Вы можете попробовать всё сами — код, использованный в этой статье, имеет открытый исходный код:
👉 github.com/phpner/phpner-php-lazy-evaluation-demo
Включает в себя:
Тест CSV: массив (жадный) против генератора (ленивый)
Моделирование потока NDJSON
Профилирование памяти и времени
Инструменты с поддержкой CLI
Генератор примеров данных
Тесты PHPUnit
Вывод
-
Генераторы (
yield) и Iterator API — must-have для оптимизации. -
Они позволяют обрабатывать миллионы записей без перегрузки памяти.
-
Генераторы — для простых случаев, Iterator API — для сложных.
-
Если вы ещё ими не пользовались — попробуйте в следующем проекте.
💬 А вы используете генераторы в продакшене? Поделитесь в комментариях своими кейсами!
Источник
ссылка на оригинал статьи https://habr.com/ru/articles/939814/
Добавить комментарий