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

Senior: питання на співбесіді з теми «Пам'ять і продуктивність»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

JIT (PHP 8.0) компілює гарячий байт-код у машинний код процесора. Звичайний PHP виконує опкоди віртуальною машиною; з JIT частина коду виконується напряму, без інтерпретації.

Де це дає багато: обчислювальні задачі, які довго крутяться в самому PHP - математика, обробка зображень на чистому PHP, парсери, симуляції. Там прискорення буває кратним.

Чому веб-застосунку - ні: типовий запит більшу частину часу чекає: на базу даних, Redis, зовнішній API, файлову систему. Сам PHP-код - це складання масивів і виклики функцій, які й без JIT виконуються швидко. Прискорити 10% часу запиту вдвічі - означає виграти 5%.

Що реально пришвидшує веб:

  • OPcache з достатнім розміром (це основа, без неї JIT і не працює).
  • Менше запитів до БД, індекси, кеш.
  • Кешування конфігурації, маршрутів і подань (php artisan optimize).
  • Довгоживучий процес (Octane, FrankenPHP), щоб не завантажувати фреймворк на кожному запиті.

Мінуси JIT: більше пам'яті, складніше налагоджувати, і в окремих версіях траплялися баги саме в JIT. Тому вмикають його після вимірювань на своєму навантаженні, а не «про всяк випадок».

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

Preloading (PHP 7.4) завантажує вказані файли в пам'ять один раз - під час старту PHP-FPM. Класи, функції й константи з них стають доступні всім запитам так, ніби вони вбудовані в PHP: без автозавантаження, без перевірки файлів, без зв'язування класів на кожному запиті.

opcache.preload=/var/www/preload.php
opcache.preload_user=www-data
// preload.php
require __DIR__ . '/vendor/autoload.php';

foreach ($classmap as $file) {
    opcache_compile_file($file);
}

Обмеження:

  • Зміна коду - лише через перезапуск PHP-FPM. Перевірка файлів для попередньо завантаженого коду не працює взагалі.
  • Один застосунок на пул. Завантажені класи глобальні для всього процесу, тож кілька застосунків з різними версіями однієї бібліотеки в одному пулі конфліктуватимуть.
  • Пам'ять витрачається на все завантажене, навіть на класи, які рідко потрібні. Завантажувати весь vendor - погана ідея; краще найчастіше використовувані класи.
  • Виграш скромний: зазвичай кілька відсотків, бо OPcache і так усуває компіляцію. Найбільше він помітний на фреймворках з тисячами класів.

Довгоживучі сервери (Octane, FrankenPHP worker mode) дають те саме й більше - фреймворк завантажується один раз на процес, тож з ними preloading майже нічого не додає.

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

Fiber (PHP 8.1) - функція з власним стеком, яку можна призупинити посередині й пізніше відновити з того самого місця. Це кооперативна багатозадачність: файбер сам вирішує, коли віддати керування.

$fiber = new Fiber(function (string $first): string {
    $second = Fiber::suspend("отримав: {$first}");   // пауза, повертаємо значення
    return "завершено з {$second}";
});

$value = $fiber->start('A');     // 'отримав: A'
$fiber->resume('B');             // продовжує після suspend
echo $fiber->getReturn();        // 'завершено з B'

Відмінність від потоків:

  • Потоки виконуються паралельно (на різних ядрах чи з перемиканням ОС у будь-який момент). Потрібна синхронізація доступу до спільних даних.
  • Файбери виконуються по черзі в одному потоці. Лише один файбер працює в кожен момент, перемикання відбувається тільки в suspend()/resume(). Гонок даних у класичному розумінні немає.

Тобто файбери не роблять обчислення швидшими й не використовують кілька ядер.

Навіщо вони: для асинхронного вводу-виводу. Поки один файбер чекає відповідь мережі, цикл подій (event loop) запускає інший. Головне - файбер дозволяє писати асинхронний код як звичайний синхронний, без колбеків і промісів: функція глибоко в стеку може призупинитися, не змінюючи сигнатур викликачів.

Хто використовує: Revolt (event loop), AMPHP v3, ReactPHP-адаптери. Прикладному коду рідко потрібно створювати файбери напряму - це будівельний блок для бібліотек.

Застереження: звичайні блокуючі функції (PDO, file_get_contents, sleep) блокують увесь процес разом з усіма файберами. Асинхронність працює лише з неблокуючими драйверами з екосистеми event loop.

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

PHP звільняє об'єкт, коли на нього не лишається посилань. Звичайний кеш, що тримає об'єкти як ключі чи значення, не дає їх звільнити - навіть коли решта програми про них уже забула. У довгоживучих процесах (воркери, Octane) це витік пам'яті.

WeakReference (PHP 7.4) - посилання, яке не заважає збирачу сміття:

$user = new User();
$ref = WeakReference::create($user);

$ref->get();   // об'єкт
unset($user);
$ref->get();   // null - об'єкт уже звільнено

WeakMap (PHP 8.0) - словник, де ключі - об'єкти, утримувані слабко. Коли ключ-об'єкт звільнено, запис зникає з мапи сам:

final class PriceCalculator
{
    private WeakMap $cache;

    public function __construct()
    {
        $this->cache = new WeakMap();
    }

    public function totalFor(Order $order): int
    {
        return $this->cache[$order] ??= $this->expensiveCalculation($order);
    }
}

Кеш живе рівно стільки, скільки живе кожне замовлення, і не росте безмежно.

Де застосовують:

  • Мемоізація за об'єктом-аргументом - як у прикладі.
  • Метадані до чужих об'єктів, які не можна змінити (сутності ORM, об'єкти бібліотек).
  • Реєстри й ідентичні мапи в ORM і контейнерах, що не мають утримувати сутності довше за запит.

Обмеження: ключі WeakMap - лише об'єкти; значення утримуються сильно (якщо значення посилається на ключ, запис не звільниться). Момент звільнення залежить від лічильника посилань і збирача циклів, тож логіку коректності на ньому не будують - лише оптимізації.

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