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. Тому вмикають його після вимірювань на своєму навантаженні, а не «про всяк випадок».
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 майже нічого не додає.
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.
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 - лише об'єкти; значення утримуються сильно (якщо значення посилається на ключ, запис не звільниться). Момент звільнення залежить від лічильника посилань і збирача циклів, тож логіку коректності на ньому не будують - лише оптимізації.