Junior: питання на співбесіді з теми «Пам'ять і продуктивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Генератор - функція, яка віддає значення по одному через yield і «засинає» між ними. Вона повертає об'єкт Generator, який можна обходити foreach, але значення обчислюються лише тоді, коли їх просять.
function readLines(string $path): Generator
{
$handle = fopen($path, 'r');
try {
while (($line = fgets($handle)) !== false) {
yield $line;
}
} finally {
fclose($handle);
}
}
foreach (readLines('access.log') as $line) {
// у пам'яті лише один рядок, хай файл і 10 ГБ
}
Навіщо:
- Пам'ять. Масив з мільйона рядків займає сотні мегабайтів; генератор - стільки, скільки один елемент.
- Ліниве обчислення. Якщо цикл зупиниться на сотому елементі, решта не обчислюватиметься взагалі.
Обмеження: генератор можна пройти лише один раз, у нього немає count() і довільного доступу за індексом.
У Laravel на генераторах побудовані LazyCollection і cursor() / lazy() в Eloquent.
Перед виконанням PHP компілює кожен файл у байт-код (опкоди). Без кешу це відбувається на кожному запиті для кожного підключеного файлу - а в Laravel-застосунку їх сотні.
OPcache зберігає скомпільований байт-код у спільній пам'яті. Наступні запити беруть готовий код, не читаючи й не розбираючи файли знову. Це дає кратне прискорення без жодних змін у коді.
Що він НЕ кешує: дані, результати запитів до БД, HTML. Для цього - кеш застосунку (Redis тощо).
Налаштування на проді, на які дивляться:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
validate_timestamps=0 вимикає перевірку, чи змінився файл, - ще трохи швидше, але тоді після деплою потрібно скинути кеш (перезапустити PHP-FPM чи викликати opcache_reset()), інакше працюватиме старий код.
Типова помилка: max_accelerated_files менший за кількість файлів у vendor/. Кеш переповнюється, і частина файлів компілюється щоразу.
Fatal error: Allowed memory size of 134217728 bytes exhausted означає, що скрипт спробував використати більше пам'яті, ніж дозволяє налаштування memory_limit (тут 128 МБ). PHP зупиняє виконання, щоб один запит не з'їв пам'ять усього сервера.
Типові причини:
- Усе завантажено одразу:
User::all()на мільйоні рядків,file()для великого файлу,json_decodeвеликої відповіді API. - Накопичення в циклі: масив, у який дописують результати обробки сотень тисяч записів.
- Нескінченна рекурсія чи цикл, що безкінечно додає елементи.
- Витік у довгому процесі: воркер черги, що накопичує дані між завданнями.
Як виправляти - не піднімати ліміт, а змінити підхід:
// Замість User::all()
User::query()->chunkById(1000, function ($users) {
foreach ($users as $user) { /* ... */ }
});
// Або ліниво, по одному
foreach (User::query()->lazy() as $user) { /* ... */ }
// Великий файл - рядок за рядком
$file = new SplFileObject('huge.csv');
foreach ($file as $line) { /* ... */ }
Коли збільшити ліміт доречно: справді важка, але обмежена задача - генерація великого звіту в команді, обробка зображення. Тоді точково: ini_set('memory_limit', '512M') у конкретній команді, а не глобально для веб-запитів.
Поруч - max_execution_time: ліміт часу виконання (у CLI за замовчуванням вимкнений, для веб - зазвичай 30 с). Помилка Maximum execution time exceeded - сигнал перенести роботу в чергу.
Як знайти винуватця: memory_get_peak_usage(true) між етапами або профайлер (Xdebug, Blackfire, SPX).
Посилання - друге ім'я для тієї самої змінної. Зміна через одне ім'я видна через інше.
$a = 1;
$b = &$a;
$b = 2;
echo $a; // 2
Де трапляються посилання:
- Передача в функцію за посиланням - функція змінює змінну викликача:
function addTax(array &$order): void
{
$order['total'] *= 1.2;
}
foreachза посиланням - зміна елементів масиву на місці:
foreach ($prices as &$price) {
$price = round($price, 2);
}
unset($price); // обов'язково!
Пастка з foreach: після циклу $price лишається посиланням на останній елемент. Наступний foreach ($prices as $price) (вже без &) запише в нього кожне значення по черзі - і останній елемент масиву стане копією передостаннього. Тому після циклу за посиланням - unset().
Чого посилання НЕ дають - економії пам'яті. Масиви й рядки в PHP копіюються ліниво (copy-on-write): передача у функцію за значенням не копіює дані, поки їх не змінять. «Передати за посиланням, щоб було швидше» нічого не прискорює, а інколи навпаки змушує PHP розділити масив.
Об'єкти й так передаються «за ідентифікатором»: функція, що отримала об'єкт, змінює той самий об'єкт. & для об'єктів потрібен лише щоб замінити саму змінну на інший об'єкт.
Порада: у сучасному коді посилання - рідкість. Зрозуміліше повернути нове значення ($order = addTax($order)) чи використати array_map, ніж змінювати аргументи непомітно для читача.