Блог

Прискорення роботи PHP

Юрій Мельников

OPCache та JIT. Це нам потрібно, якщо ми пишемо щось складніше за базові CRUD.

Вступ

Як відомо, існує два базових типи мов програмування: це компільовані та інтерпретовані. Як зрозуміло з їх назви, код компільованих мов перед виконанням має бути скомпільований у бінарний код, команди якого будуть зчитуватися безпосередньо процесором. На відміну від інтерпретованих мов, код яких зчитується та транслюється в операційний код (далі — опкод), який у свою чергу транслюється в бінарний код, що передається на виконання процесору.

Через те, що інтерпретатор повинен спочатку прочитати поданий на виконання файл, інтерпретувати його код, завантажити залежності, транслювати в опкоди, потім опкоди обробляє віртуальна машина Zend і лише потім передати на виконання безпосередньо процесору, інтерпретовані мови вважаються повільними через ці зайві операції.

Так і було до певного часу, коли люди зрозуміли, що на production-серверах вихідний код додатків змінюється досить рідко і кожного разу зчитувати та обробляти одні й ті ж файли — це зайва трата часу і можна один раз прочитаний файл тримати в пам'яті в скомпільованому вигляді, а не перекомпільовувати його при кожному запиті.

Так і з'явилося розширення OPCache.

Цикл життя скрипта на PHP

Якщо йти по кроках виконання скрипта з моменту його виклику в PHP, то це виглядає так: Парсинг > Компiляцiя > Виконання.

Але якщо ми додамо розширення OPCache, то в цикл життя скрипта, між компiляцiєю та виконанням, додасться ще два кроки: Оптимізація та Кешування.

Таким чином, з урахуванням OPCache, цикл життя скрипта на PHP виглядає так: Парсинг > Компiляцiя > Оптимізація > Кешування > Виконання

Парсинг

Це перший крок у життєвому циклі скрипта. На цьому етапі діють парсер і лексер, які розбивають код на токени і передають компілятору. Цей процес називається токенізацією і допомагає компілятору зрозуміти взаємозв'язок токенів.

Компiляцiя

На цьому кроці відбувається перетворення отриманих токенів в опкоди, в яких вже містяться інструкції для виконання додатка.

Оптимізація

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

Внутрішньофайлова оптимізація може оптимізувати заздалегідь відомі значення. Наприклад, у нас є файл з назвою script.php і він лежить у директорії /tmp. Наступний скрипт буде оптимізований:

if (__DIR__ == '/tmp') {
    echo 'yes';
} else {
    echo 'no';
}

Оскільки спочатку OPCache знає, де розташований файл, який він збирається оптимізувати, то немає сенсу перевіряти його розташування всередині коду ще раз і для себе компілятор скоротить цей запис до простого

// Если на этапе компиляции файл находился в директории /tmp
echo 'yes';

// В иных случаях
echo 'no';

Ось ще кілька прикладів:

if ($a) {
    echo 'foo';
} else {
   echo 'bar';
}

Тут компілятор не може заздалегідь передбачити, яке значення буде в змінній $a і залишить перевірку під час виконання

if function_exists('my_custom_function')) { }

У цьому випадку компілятор теж не зможе оптимізувати код, оскільки функція my_custom_function не входить у стандартний простір імен мови. Компiлятор не оптимізує перевірку на існування користувацьких функцій, оскільки вони можуть знаходитися в інших файлах.

if (function_exists('array_merge')) {
    echo 'yes';
}

Але перевірку на існування стандартних функцій компілятор оптимізує, оскільки її наявність він може перевірити у стандартному просторі імен і результат виконання цього коду відомий заздалегідь.

Так само справа йде і з константами. Оскільки константи ніколи не змінюються, то компілятор замість того, щоб кожного разу викликати звернення до них за вказівником у пам'яті, просто підставляє їх значення прямо в потрібні місця в коді.

Таким чином, наступний код

const SOME_STRING = 'foo';

echo SOME_STRING;

Перетвориться в прямий вивід значення:

echo 'foo';

Кешування

На цьому кроці OPCache виділяє загальну пам'ять для зберігання скриптів. При цьому важливо розуміти, що цей сегмент пам'яті буде виділений на постійній основі, тобто не буде ні змінений, ні видалений з пам'яті.

Розмір цього сегмента пам'яті вказується в налаштуванні opcache.memory_consumption. Звісно, універсальної рекомендації щодо налаштування розміру виділеної пам'яті не існує, оскільки у різних додатків різні запити. Але чим більший обсяг додатка ми маємо, тим більше пам'яті для нього потрібно виділити, що логічно. І не забуваємо, що файли фреймворків теж будуть скомпільовані і поміщені в загальну пам'ять, таким чином під загальний сегмент, краще виділяти стільки пам'яті, скільки не шкода.

Але це якщо говорити про великі проекти. А якщо говорити про невеликі, то цьому блогу, який ви зараз читаєте, достатньо 128MB, з яких зайнято лише близько 50MB.

Объём общей памяти OPCache, которая занята блогом mlnkv.dev
Объём общей памяти OPCache, которая занята блогом mlnkv.dev

Отже, що ж записує в цю пам'ять OPCache? В першу чергу, як не складно здогадатися, в загальну пам'ять записуються незмінні дані. Це можуть бути, наприклад, скомпільовані класи, а також функції та константи. Таким чином у пам'яті зберігаються всі потрібні дані і доступ до них відбувається через вказівник, без необхідності повторної компіляції або копіювання об'єктів.

Тобто OPCache завантажує скрипт, компілює його, обчислює точний розмір пам'яті, необхідний для зберігання скомпільованих даних і поміщає їх туди. Таким чином, пам'ять ніколи не звільняється і не фрагментується, одного разу поміщений об'єкт залишається доступним у пам'яті за своєю адресою завжди. Повертаючись до налаштування кількості виділеної пам'яті, не забувайте, що необхідна пам'ять — це не розмір файлу з кодом, а кількість об'єктів і змінних, які він створює.

JIT компіляція

Стоп! Але ж вище вже була компіляція, навіщо ще раз про неї говорити?

І так, і ні. JIT компіляція стоїть нижче звичайної компіляції файлів в опкоди, оскільки скомпільовані вище опкоди вона транслює вже безпосередньо в машинний код, в інструкції процесору, оминаючи віртуальну машину самого PHP.

Не кожен файл і навіть не кожен блок коду буде скомпільований за допомогою JIT. Цей тип компіляції може значно прискорити логічну частину коду. Це дозволить прискорити роботу з масивами, маніпуляції з байтами, обробку зображень і навіть відкриває доступ до розробки Machine Learning на PHP! Але компіляція JIT ніяк не прискорить функції вводу-виводу, оскільки швидкість дисків і мережі має свої обмеження.

JIT компіляцію, як і будь-який інший інструмент, потрібно застосовувати тоді, коли ви розумієте її необхідність. Вона не прискорить середньостатистичний блог, оскільки робота блогу — це переважно CRUD операції.

Підсумок

Отже, чи потрібні нам OPCache та JIT? Безсумнівно, потрібні! Якщо додаток не зав'язаний на операції вводу-виводу, а використовується, наприклад, для складних обчислень, то скомпільований код буде працювати значно швидше інтерпретованого.

З особистого досвіду, можу сказати, що пошук перетинаючихся областей після включення JIT запрацював значно швидше, оскільки це чисто логічна обробка даних.

Але, звісно, жоден компілятор не зможе прискорити написаний спочатку поганий код. Тому на оптимізатор надійся, а сам не плошай!