Оптимізація PHP-скриптів: практичні поради

У цій статті мова піде про те, як оптимізувати якийсь PHP-скрипт, щоб він виконувався як можна швидше, витрачаючи якомога менше процесорного часу

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

Відразу скажу, що займатися передчасної оптимізацією не потрібно. Варто задуматися про переробку коду тільки в наступних випадках:
[+] Ваш скрипт підлягає виконується (наприклад, робить якісь займають тривалий час операції в циклі);
[+] Ваш скрипт дуже часто виконується (це був мій випадок - іноді в день тест проходили близько десяти тисяч чоловік - а це близько мільйона звернень до скрипту на добу);
[+] Від використовуваних скриптом ресурсів залежить те, скільки грошей ви платите. Якщо скрипт виконується на звичайному віртуальному хостингу і не перевищує ліміт навантаження на процесор, то, швидше за все, можна і не паритися;
[+] Ви збираєтеся масово поширювати скрипт, і його буде використовувати в кінцевому підсумку безліч людей, яким критично, скільки ресурсів скрипт споживає.

Для аналізу продуктивності PHP-скриптів ми будемо використовувати XDebug . Це розширення для Zend добре описано на багатьох расурсах в Інтернеті (в тому числі і російською мовою), тому не буду детально зупинятися на цьому. Коротко - це розширення дозволяє виконувати налагодження PHP-скриптів, відстежувати виняткові ситуації в них і профілювати їх за часом виконання.

Далі я коротко опишу, як встановлюється це розширення на Windows (і веб-сервер Apache), так як сам займався профилированием саме під Windows. Бажаючі зробити це під Linux без проблем зможуть знайти інформацію по установці в мережі. Отже, викачуємо відповідний бінарний дистрибутив XDebug з офіційного сайту . Кладемо завантажену DLL'ку в директорію, що містить всі екстеншени PHP (наприклад, C: \ Program Files \ Apache Software Foundation \ Apache2.2 \ php5 \ ext). Далі відкриваємо php.ini (він лежить в директорії Windows) і в секції налаштувань [PHP] (наприклад, в самий її кінець), дописуємо наступне:

А ім'я вашої DLL-ки, завантаженої з сайту XDebug

zend_extension = php_xdebug - 2.2.3 - 5.5 - vc11. dll

[XDebug]

xdebug. profiler_enable = 1

; Шлях, куди будуть зберігатися звіти Профілювальники

; Створіть його заздалегідь

xdebug. profiler_output_dir = "C: \ Program Files \ Apache Software Foundation \ Apache2.2 \ htdocs \ xdebug"

xdebug. profiler_output_name = "cachegrind.out.% t-% s"

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

Нарешті, перезапускаємо Web-сервер. Все готово для профілювання. Звертаємося до цікавого для нас скрипту. Після завершення виконання скрипта в каталозі, вказаному в налаштуванні xdebug.profiler_output_dir, створиться файл з ім'ям виду cachegrind.out.1381302243-C__Program_Files_Apache_Software_Foundation_Apache2.2_htdocs_index_php. Он-то нас і цікавить. Для того, щоб переглянути його вміст в читається, нам потрібна програма WinCacheGrind . Вона не вимагає установки. Запускаємо її і відкриваємо в ній вироблений XDebug'ом файл. Ось як виглядає інтерфейс цієї програми (на прикладі виконання головного файлу движка тесту старої версії 0.11):

Ми бачимо список викликів функцій, визначених в скрипті (і в скриптах, які підключаються до нього) і нативних функцій PHP. Програма покаже, скільки в середньому займає виклик кожної функції (крім хіба що зовсім швидких) включаючи всі внутрішні виклики (колонка Avg. Cum.) І з огляду на тільки код самої функції без внутрішніх викликів (Avg. Self), скільки викликів тієї чи іншої функції було скоєно (Calls), скільки в сумі часу зайняло виконання тієї чи іншої функції (Total Cum., Total Self). Нас більшою мірою будуть цікавити колонки Avg. Cum., Avg. Self і Calls. Крім статистики викликів, можна побачити список конкретних місць викликів зі стектрейсамі для всіх функцій.

Для початку відсортуємо всі виклики по колонці Total Self, отримавши в підсумку список функцій, які в сумі виконувалися найдовше. Їх-то код і слід переробляти.

Їх-то код і слід переробляти

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

Якщо та чи інша функція була викликана багато раз, ймовірно, варто зменшити кількість її викликів (так як на сам виклик теж витрачається час)

Далі я приведу кілька реальних прикладів викликів, які були оптимізовані в моєму движку.

З першого скріншота видно, що однією з функцій, які посіли найбільше часу, є TestPreferences :: scaleExists (). Ось так код цієї функції виглядав в перших версіях движка:

public function scaleExists ($ id)

{

foreach ($ this -> scales as $ s)

{

if ($ s -> getId () == $ id)

return true;

}

return false;

}

$ This-> scales - це масив, що містить елементи типу TestScale (характеристика). Відповідно, функція перевіряє, чи є в масиві характеристика з заданим ідентифікатором. Для оптимізації цієї функції я зробив ключем масиву $ this-> scales ідентифікатор зберігається у відповідному значенні характеристики, після чого функція TestPreferences :: scaleExists () стала виглядати так:

public function scaleExists ($ id)

{

return isset ($ this -> scales [$ id]);

}

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

Далі за кількістю витраченого часу йде TestPreferences :: processTestCondition (). Ця функція обробляла список підсумкових результатів тесту, і її виявилося можливим просто прибрати при завантаженні головної сторінки зі списком тестів, як і TestPreferences :: loadQuestion () (завантажує питання), TestPreferences :: loadAdditionalQuestion () (завантажує додаткове питання) і TestPreferences :: readScales () (завантажує характеристику) і багато інших допоміжні функції. В останній версії движка ці функції викликаються тільки при проходженні тесту.

Ще один цікавий випадок - функція TestArgs :: isNumber (). Вона перевіряє, чи є аргумент, переданий їй, числом. Ось її початковий код:

static public function isNumber ($ arg)

{

return preg_match ( '/ ^ -? \ d + $ /', $ arg);

}

А ось - оптимізований варіант:

static public function isNumber ($ arg)

{

return filter_var ($ arg, FILTER_VALIDATE_INT)! == false;

}

Про цю оптимізації я докладніше розповім далі. А поки що - ось скріншот аналізу нової версії движка тесту (все той же звернення до головної сторінки):

А поки що - ось скріншот аналізу нової версії движка тесту (все той же звернення до головної сторінки):

Разом отримуємо: стара версія при відображенні головної сторінки витрачає 1066 мілісекунд (зрозуміло, час перебільшено, так як скрипт запускався під XDebug, але порядок зрозумілий), а нова - всього 53 мілісекунди! Стара версія при цьому робить 23608 викликів функцій, а нова - всього 499. Зрозуміло, порівняння проведено за однакового набору плагінів і тестів.

Тепер подивимося на скріншоти аналізу старої і нової версії движка, коли людина натискає кнопку "Почати тест":

Як видно, результат покращився приблизно в два рази: 1003 мілісекунди проти 1784 х і 23096 викликів проти 41073. Був сильно перероблений механізм роботи з плагінами (функція TestPluginManager :: NotifyPlugins () зайняла 578 мілісекунд за майже 500 викликів в старій версії, а в новій - 250 викликів і 31 мілісекунду). Ця функція призначена для оповіщення завантажених плагінів про різні події, що відбуваються в двигуні (наприклад, "тест завантажений", "користувач передав відповіді" і т.д.) Відповідно, вона викликає по черзі різні функції в кожному плагін в залежності від типу події. Є кілька типів функцій (нічого не повертає, що повертає значення і фільтрує). У перших версіях движка в функції TestPluginManager :: NotifyPlugins () був об'ємний switch-case за типом функції, а далі в кожному case йшов виклик потрібної функції для кожного плагіна. У новій версії весь код, який здійснює виклик, був перенесений всередину класів, що представляють функції. В результаті switch-case був прибраний, і деякі блоки коду стали виконуватися один раз, хоча раніше викликалися в циклі для кожного плагіна (наприклад, формування аргументів і імені викликається функції).

Де було можливо, додалося кешування викликів, тому плагіни стали смикатися набагато рідше. Частина функціоналу була перенесена з функції TestPluginManager :: NotifyPlugins () в TestPluginFunction :: callPlugins () (раніше ця функція називалася TestPluginFunction :: call ()).

Крім того, була змінена внутрішня структура зберігання питань і відповідей користувача, в результаті чого вийшло прибрати підлягає виконувалася функцію TestSession :: findCurrentQuestionRecursively (), а розмір файлу сесії при цьому зменшився в кілька разів.

До речі, можна помітити, що деякі функції стали виконуватися трохи довше, але це пов'язано з тим, що в новій версії движка з'явився різний новий функціонал.

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

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

1. У самих високонавантажених скриптах є сенс відмовитися від використання ООП. Це сильно уповільнює виконання скриптів. Іноді можна замість класів застосовувати масиви - це швидше. Виклик методу класу завжди довше, ніж виклик статичного методу, що в свою чергу довше, ніж виклик звичайної функції. Звернення до члена класу завжди довше, ніж звернення до статичного члену класу, що, в свою чергу, довше, ніж звернення до звичайної змінної. Я від ООП не відмовився (бо це досить зручно), але подекуди обмежив його застосування.

2. Якщо ООП використовується, то є сенс відмовитися від геттеров і сеттерів в найпростіших випадках. Звернення до членів класу безпосередньо завжди швидше, ніж виклик методів.

3. Слід всюди, де можливо, використовувати строгі порівняння (=== та! ==) замість нестрогих (== і! =). Якщо порівнюються дві змінні, типи яких повинні бути однакові, слід використовувати суворе порівняння. Це дозволить уникнути маси непотрібних привидів типів.

4. Уникайте регулярних виразів, які перевіряють ті чи інші дані, в тих місцях, де можна обійтися і без них. Такий приклад я навів вище (оптимізація методу TestArgs :: isNumber ()). Як швидшої альтернативи регулярними виразами можна привести функції filter_var , ctype_digit , checkdate і т.д.

5. Не використовуйте type hinting у функціях, які часто викликаються. Перевірка типів переданих аргументів уповільнює їх виклик.

6. Застосовуйте конкатенацію і рядки в одинарних лапках замість інтерполяції. $ A = "test $ b" набагато повільніше, ніж $ a = 'test'. $ B.

7. Виносите функції з циклів, щоб вони не виконувалися кожну ітерацію. Дуже часто бачу подібний код:

for ($ i = 0; $ i <count ($ array); ++ $ i)

// ...

Який краще було б написати так:

for ($ i = 0, $ c = count ($ array); $ i <$ c; ++ $ i)

// ...

8. Використання попередніх функції PHP для виконання будь-яких завдань, і не пишіть власні. Наприклад, для зчитування файлу функція file_get_contents () або file () буде набагато швидше, ніж власна функція з fopen () + fread ().

9. Викликайте unset () для великих обсягів даних відразу після того, як закінчили їх використовувати. PHP, звичайно, і сам їх видалить з часом, але це може статися далеко не відразу, а економія пам'яті - в ваших інтересах.

10. Використовуйте всюди преінкремент, а то й потрібно постинкрементом.

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

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