Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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/. Кеш переповнюється, і частина файлів компілюється щоразу.

Докладніше в документації: OPcache

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).

Докладніше в документації: memory_limit

Посилання - друге ім'я для тієї самої змінної. Зміна через одне ім'я видна через інше.

$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, ніж змінювати аргументи непомітно для читача.

Докладніше в документації: Посилання