- PVSM.RU - https://www.pvsm.ru -
Вы когда-нибудь пытались загрузить в память 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 его запросит.
Представим, что у нас есть файл 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 этот код будет занимать несколько килобайт, потому что в памяти всегда только одна строка.
$startMemory = memory_get_usage();
$array = range(1, 1_000_000); // создаёт массив из миллиона чисел
echo "Массив: " . (memory_get_usage() - $startMemory) / 1024 / 1024 . " MBn";
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 . " MBn";
Результат на моей машине:
Массив: 120 MB
Генератор: 0.5 MB
Генераторы — это быстро и просто. Но иногда нужно больше контроля: хранить состояние, управлять ключами или даже динамически менять источник данных.
Тогда в бой идёт 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 МБ, время выполнения осталось почти тем же.
В PHP генератор — это объект класса Generator, реализующий Iterator и Traversable.
Он умеет:
хранить текущее состояние функции;
приостанавливать выполнение на yield;
возобновлять выполнение с того же места.
Вызов функции-генератора не запускает её сразу — создаётся объект Generator.
При первой итерации выполнение идёт до yield.
yield возвращает значение и «замораживает» функцию.
Следующая итерация продолжает выполнение с того же места.
Когда функция завершается — генератор помечается как завершённый.
Если скомпилировать функцию с yield через VLD (Vulcan Logic Disassembler), мы увидим, что каждый yield — это инструкция, которая:
сохраняет стек вызовов;
запоминает переменные;
возвращает значение в вызывающий код.
Массив создаёт все элементы в памяти.
Генератор хранит один текущий элемент (zval) и перезаписывает его.
Поэтому можно обойти миллион элементов, используя пару сотен килобайт.
function counter(): Generator {
$i = 0;
while (true) {
yield $i++;
}
}
foreach (counter() as $num) {
if ($num > 5) break;
echo $num . PHP_EOL;
}
С массивом это невозможно — память просто закончится.
|
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 [1]
Включает в себя:
Тест CSV: массив (жадный) против генератора (ленивый)
Моделирование потока NDJSON
Профилирование памяти и времени
Инструменты с поддержкой CLI
Генератор примеров данных
Тесты PHPUnit
Генераторы (yield) и Iterator API — must-have для оптимизации.
Они позволяют обрабатывать миллионы записей без перегрузки памяти.
Генераторы — для простых случаев, Iterator API — для сложных.
Если вы ещё ими не пользовались — попробуйте в следующем проекте.
💬 А вы используете генераторы в продакшене? Поделитесь в комментариях своими кейсами!
Перевод оригинала на [2]dev.to [3]
Автор: phpner
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/428823
Ссылки в тексте:
[1] github.com/phpner/phpner-php-lazy-evaluation-demo: https://github.com/phpner/phpner-php-lazy-evaluation-demo
[2] Перевод оригинала на : https://dev.to/oleksandr_vasyliev/lazy-evaluation-in-php-how-generators-and-iterators-save-memory-and-speed-up-code-3529
[3] dev.to: http://dev.to
[4] Источник: https://habr.com/ru/articles/939814/?utm_source=habrahabr&utm_medium=rss&utm_campaign=939814
Нажмите здесь для печати.