Вы когда-нибудь пытались загрузить в память 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 |
Примечание: когда генераторы действительно полезны
Если задача простая — один раз пройти CSV построчно, то хватит обычного while (fgetcsv(...))
.
Генераторы становятся уместны, когда:
нужен ленивый поток (читать/запрашивать ровно столько, сколько потребитель возьмёт);
важна возможность остановиться раньше (экономия запросов к API или БД);
требуется композиция операций (
filter
,map
,take
) поверх любого источника.
Пример ленивой пагинации с ранней остановкой:
function fetchCars(): Generator {
for ($page = 1; ; $page++) {
$data = apiRequest('cars', ['page' => $page]);
if (empty($data['items'])) break;
foreach ($data['items'] as $car) yield $car;
}
}
foreach (take(filter(fetchCars(), fn($c) => $c['price'] > 1_000_000), 100) as $car) {
process($car); // лишние страницы не запрашиваются
}
Реальный кейс из продакшена
Мы парсили 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 — для сложных.
Если вы ещё ими не пользовались — попробуйте в следующем проекте.
💬 А вы используете генераторы в продакшене? Поделитесь в комментариях своими кейсами!