Содержание статьи
- 1. Утечка памяти в циклах обработки данных
- 2. Неожиданное поведение ссылок
- 3. Конкатенация больших строк
- 4. Циклические ссылки в объектах
- 5. Работа с большими массивами в функциях
- 6. Оптимизация временных переменных
- 7. Оптимизация ORM-запросов
- 8. Эффективная сериализация данных
1. Утечка памяти в циклах обработки данных
Проблемный код:
while ($row = getBigDataRow()) {
$processed[] = processRow($row);
}
Оптимизированное решение:
$batch = [];
while ($row = getBigDataRow()) {
$batch[] = processRow($row);
if (count($batch) >= 1000) {
saveBatch($batch);
$batch = [];
}
}
Почему это работает:
В оригинальном коде каждый новый элемент добавляется в массив $processed
, что приводит к:
- Постоянному увеличению
refcount
для zval массива - Многократному перевыделению памяти при расширении массива
- Накоплению всех zval-значений до конца выполнения цикла
Оптимизированный вариант сбрасывает ссылки каждые 1000 итераций, позволяя сборщику мусора своевременно освобождать память.
2. Неожиданное поведение ссылок
Проблемный код:
$data = ['key' => 'original'];
$ref = &$data['key'];
$copy = $data;
$copy['key'] = 'modified';
echo $data['key']; // Выведет 'modified'!
Правильный подход:
$data = ['key' => 'original'];
$copy = $data; // Копируем ДО создания ссылки
$ref = &$data['key'];
Механизм работы:
Когда создаётся ссылка (&
), PHP помечает zval флагом is_ref=1
. Последующие операции:
- При копировании массива с
is_ref=1
создаётся не независимая копия - Все переменные продолжают ссылаться на один zval
- Изменения через любую переменную влияют на все «копии»
Решение создаёт настоящую копию до установки ссылки.
3. Конкатенация больших строк
Проблемный код:
$report = '';
foreach ($records as $record) {
$report .= generateReportRow($record);
}
Оптимизированный вариант:
$rows = [];
foreach ($records as $record) {
$rows[] = generateReportRow($record);
}
$report = implode('', $rows);
Принцип работы:
Конкатенация через .=
в цикле вызывает:
- Создание нового zval-строка на каждой итерации
- Копирование всего предыдущего содержимого
- Сложность O(n²) по памяти
Вариант с implode
:
- Хранит части в отдельных zval
- Выделяет память для результата один раз
- Сложность O(n)
4. Циклические ссылки в объектах
Проблемный код:
class Node {
public $next;
}
$a = new Node();
$b = new Node();
$a->next = $b;
$b->next = $a; // Создана циклическая ссылка
Решение:
// Перед удалением:
unset($a->next, $b->next);
// Теперь GC сможет очистить память
unset($a, $b);
Как работает:
Циклические ссылки создают ситуацию, где:
- Каждый объект имеет
refcount=2
(оригинал + ссылка из другого объекта) - Сборщик мусора видит
refcount=1
после unset() переменных - Память не освобождается, так как объекты всё ещё ссылаются друг на друга
Явный разрыв ссылок перед удалением позволяет GC корректно очистить память.
5. Эффективная работа с большими массивами в функциях
Проблемный код:
function processArray($data) {
// Создаётся копия массива
foreach ($data as $k => $v) {
$data[$k] = $v * 2;
}
return $data;
}
$bigData = range(1, 100000);
processArray($bigData); // Двойное потребление памяти
Оптимизация:
function processArray(&$data) {
foreach ($data as $k => $v) {
$data[$k] = $v * 2;
}
}
$bigData = range(1, 100000);
processArray($bigData); // Работаем с оригиналом
Механизм:
При передаче массива в функцию:
- Без
&
создаётся новый zval сrefcount=1
- С
&
используется оригинальный zval с увеличениемrefcount
- Изменения применяются к исходному массиву
Важно: после работы со ссылками нужно unset()
временные переменные.
6. Оптимизация временных переменных
Проблемный код:
function calculate($x) {
$temp = $x * 2; // Лишний zval
return $temp + 1;
}
Улучшенный вариант:
function calculate($x) {
return ($x * 2) + 1; // Нет временных zval
}
Принцип работы:
Каждая переменная в PHP:
- Создаёт отдельный zval в текущей scope
- Увеличивает
refcount
для значений - Требует времени на аллокацию и освобождение
Инлайн-вычисления позволяют:
- Избежать создания промежуточных zval
- Снизить нагрузку на сборщик мусора
- Уменьшить пиковое потребление памяти
7. Оптимизация ORM-запросов
Проблемный код:
$users = User::all(); // Загружает все объекты
foreach ($users as $user) {
$user->updateStats(); // Все zval в памяти
}
Пакетная обработка:
User::chunk(1000, function($users) {
foreach ($users as $user) {
$user->updateStats();
}
}); // Освобождает память после каждой порции
Как это работает:
При загрузке объектов ORM:
- Каждый объект — отдельный zval
- Свойства объекта хранятся во внутреннем массиве (HashTable)
- Потребление памяти растёт линейно
Метод chunk()
:
- Загружает данные порциями
- После обработки порции zval освобождаются
- Снижает пиковое потребление памяти на 90%+
8. Эффективная сериализация
Проблемный код:
$bigData = getHugeDataset();
$serialized = serialize($bigData); // 2GB в памяти
Стриминг-решение:
$handle = fopen('cache.dat', 'w');
foreach (chunkData(getHugeDataset()) as $chunk) {
fwrite($handle, serialize($chunk));
}
fclose($handle);
Механизм работы:
При сериализации:
- PHP создаёт полную копию данных в памяти
- Формирует строковое представление
- Хранит всё в одном zval-строке
Пакетная сериализация:
- Обрабатывает данные частями
- Не превышает лимит memory_limit
- Позволяет работать с данными > доступной памяти
Добавить комментарий