Поліпшення продуктивності PHP 7



PHP — це програмне забезпечення, написане на мові С. Кодова база PHP містить близько 800 тисяч рядків коду і в сьомій версії була істотно перероблена.

У цій статті ми розглянемо, що змінилося в движку Zend сьомої версії порівняно з п'ятою, а також розберемося, як можна ефективно використовувати внутрішні оптимізації. В якості вихідної точки візьмемо PHP 5.6. Найчастіше багато що залежить від того, як ті чи інші речі написані і представлені движку. При написанні критично важливого коду необхідно приділяти увагу його продуктивності. Змінивши кілька дрібниць, ви можете сильно прискорити роботу движка, часто без шкоди для інших аспектів начебто читабельності коду або управління налагодженням. Свої міркування я доведу з допомогою профилировщика Blackfire.

Якщо ви хочете підвищити продуктивність PHP, мігрувавши на сьому версію, то вам потрібно буде:

  1. Мігрувати кодову базу без внесення змін до неї (або просто перетворити її в код, сумісний з PHP 7). Цього буде достатньо для підвищення швидкості роботи.
  2. Скористатися наведеними нижче порадами, щоб зрозуміти, як змінилися різні частини коду віртуальної машини PHP і як їх використовувати для ще більшого збільшення продуктивності.
Запаковані масиви
Запаковані масиви — перша з чудових оптимізацій в PHP 7. Вони споживають менше пам'яті і у багатьох випадках працює значно швидше традиційних масивів. Запаковані масиви повинні задовольняти критеріям:

  • Ключі — тільки цілочисельні значення;
  • Ключі вставляються в масив тільки за зростанням.
Приклад 1:

$a = ['foo', 'bar', 'baz'];

Приклад 2:

$a = [12 => 'baz', 42 => 'bar', 67 => [] ];

Ці масиви внутрішньо дуже добре оптимізовані. Але очевидно, що на трехъячеечном масиві ви не відчуєте різниці порівняно з PHP 5.

Приміром, у фреймворку Symfony запаковані масиви існують тільки в генераторі карти класів (class map generator), що створює код на зразок:

array (
0 => 'Symfony\\Bundle\\FrameworkBundle\\EventListener\\SessionListener',
1 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage',
2 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage',
3 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler',
4 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy',
5 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy',
6 => 'Symfony\\Component\\HttpFoundation\\Session\\Session',
/* ... */

Якщо виконати цей код на PHP 5 та PHP 7 і проаналізувати за допомогою Blackfire, то отримаємо:



Як бачите, загальна тривалість компілювання і виконання цього оголошення масиву зменшилася приблизно на 72 %. В PHP 7 такі масиви стають схожими на NOP'и, а в PHP 5 помітне час витрачається на компілювання і на завантаження в ході runtime.

Давайте візьмемо масив побільше, на 10 000 комірок:

for ($i=0; $i < 10000; $i++) {
$a[] = $i;
}

Те ж саме порівняння:



Тривалість використання процесора знизилася приблизно в 10 разів, а споживання пам'яті зменшилася з 3 Мб до 0,5 Мб.

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

Споживання пам'яті масивами в PHP 7 набагато нижче, ніж в PHP 5. А при використанні упакованих масивів економія ще вище.

Не забувайте:

  • Якщо вам потрібен список, то не використовуйте рядка в ключах (це не дасть застосовувати оптимізацію упакованих масивів);
  • Якщо ключі в списку тільки цілочисельні, постарайтеся розподілити їх за зростанням (в іншому випадку оптимізація теж не спрацює).
Такі списки можна використовувати в різних частинах вашого додатка: наприклад для перерахування (як карта класів у Symphony) або для отримання результатів з бази в певному порядку і з числовими даними в колонці. Це часто буває потрібно в веб-додатках (
$pdo->query("SELECT * FROM table LIMIT 10000")->fetchAll(PDO::FETCH_NUM)
).

Цілочисельні значення з плаваючою комою в PHP 7 безкоштовні
В PHP 7 абсолютно інший спосіб розміщення змінних в пам'яті. Замість купи вони тепер зберігаються в пулах стекової пам'яті. У цього є побічний ефект: ви можете безкоштовно повторно використовувати контейнери змінних (variable containers), пам'ять не виділяється. В PHP 5 таке неможливо, там для кожного створення/присвоєння змінної потрібно виділити трохи пам'яті (що погіршує продуктивність).

Погляньте на цей код:

for ($i=0; $i < 10000; $i++) {
$$i = 'foo';
}

Тут створюється 10 000 змінних з іменами від $0 до $10000 з 'foo' в якості значення рядка. Звичайно, при первинному створенні контейнера змінної (в нашому прикладі) споживається якась пам'ять. Але що буде, якщо тепер ми повторно використовуємо ці змінні для зберігання цілочисельних значень?

/* ... продовження ... */
for ($i=0; $i < 10000; $i++) {
$$i = 42;
}

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

Подивимося, що скаже Blackfire:



В PHP 7 відмова від додаткового звернення до пам'яті при зміні змінної призводить до економії процесорних циклів у другому циклі
for
. В результаті використання процесора зменшується на 50 %. А присвоювання цілочисельного значення ще більше знижує споживання пам'яті в порівнянні з PHP 5. У п'ятій версії на розміщення в пам'яті 10 000 цілочисельних значень витрачається 80 000 байтів (на платформі LP64), а також купа додаткової пам'яті на аллокатор. В PHP 7 цих витрат немає.

Оптимізація з допомогою encapsed-рядків
Encapsed-рядки — це значення, в яких виконується внутрішнє сканування на наявність змінних. Вони оголошуються за допомогою подвійних лапок, або Heredoc-синтаксису. Алгоритм аналізує значення та відокремлює змінні від рядків. Наприклад:

$a = 'foo';
$b = 'bar';
$c = "Мені подобається $a і $b";

При аналізі рядка $c движок повинен отримати рядок: «Мені подобається foo та bar». Цей процес в PHP 7 також був оптимізований.

Ось що робить PHP 5:

  • Виділяє буфер для «Мені подобається»;
  • Виділяє буфер для «Мені подобається foo»;
  • Додає (копіює в пам'яті) в останній буфер «Мені подобається» і «foo», повертає його тимчасове вміст;
  • Виділяє новий буфер для «Мені подобається foo та»;
  • Додає (копіює в пам'яті) « Мені подобається foo» та «і» в цей останній буфер і повертає його тимчасове вміст;
  • Виділяє новий буфер для «Мені подобається foo bar і»;
  • Додає (копіює в пам'яті) «Мені подобається foo та» і «bar» в цей останній буфер і повертає його вміст;
  • Звільняє всі проміжні використані буфери;
  • Повертає значення останнього буфера.
Багато роботи, вірно? Такий алгоритм в PHP 5 аналогічний тому, що використовується при роботі з рядками в С. Але справа в тому, що він погано масштабується. Цей алгоритм не є оптимальним при роботі з дуже довгими encapsed-рядками, що включають в себе велику кількість змінних. А адже encapsed-рядки часто використовуються в PHP.

В PHP 7 все працює інакше:

  • Створюється стек;
  • У нього поміщаються всі елементи, які потрібно додати;
  • Коли алгоритм доходить до кінця encapsed-рядки, одноразово виділяється пам'ять необхідного розміру, в яку переміщуються всі частини даних, в потрібні місця.
Рухи з пам'яттю залишилися, однак ніякі проміжні буфери, як в PHP 5, вже не використовуються. В PHP 7 лише один раз виділяється пам'ять для фінальної рядка, незалежно від кількості частин рядка і змінних.

Код та результат:

$w = md5(rand());
$x = md5(rand());
$y = md5(rand());
$z = md5(rand());

$a = str_repeat('a', 1024);
$b = str_repeat('a', 1024);

for ($i=0; $i < 1000; $i++) {
$$i = "У цьому рядку багато $a, а також багато $b, виглядає рандомно: $w $x $y $z";
}

Ми створили 1000 encapsed-рядків, в яких знаходимо статичні рядкові частини і шість змінних, дві з яких важать по 1 Кб.



Як бачите, в PHP 7 використання процесора знизилася в 10 разів порівняно з PHP 5. Зверніть увагу, що за допомогою Blackfire Probe API (у прикладі не показано) ми профилировали лише цикл, а не весь скрипт.

В PHP 7:

$bar = 'bar';
/* використовуйте це */
$a = "foo і $bar";
/* замість цього */
$a = "foo і " . $bar;

Операція конкатенації не оптимізована. Якщо ви використовуєте конкатенацию рядків, то в результаті будете робити ті ж дивні речі, що і в PHP 5. А encapsed-рядки допоможуть скористатися перевагами нового алгоритму аналізу, що виконує оціночну конкатенацию (evaluated concatenation) з допомогою структури “Світ“.

Reference mismatch
Reference mismatch виникає тоді, коли в якості аргументу, що передається по посиланню (passed-by-ref), ви передаєте функції нессылочную змінну (non-ref variable), або навпаки. Наприклад, так:

function foo(&$arg) { }
$var = 'str';
foo($var);

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

Причина в тому, як в PHP 5 побудована робота зі змінними і посиланнями. Переходячи до тіла функції, движок ще не знає, чи зміните ви значення аргументу. Якщо зміните, то передача аргументів за посиланням повинна призвести до відбиття зовні зробленого зміни при передаванні посилальної змінної (reference variable).

А якщо ви не зміните значення аргументу (як у нашому прикладі)? Тоді движок повинен створити посилання з нессылочной (non-reference) змінної, яку ви передаєте в ході виклику функції. В PHP 5 движок повністю дуплицирует вміст змінної (при дуже невеликій кількості покажчиків багато разів викликаючи
memcpy()
, що призводить до безлічі повільних звернень до пам'яті).

Коли в PHP 7 движок хоче створити посилання з нессылочной змінної, він просто обертає її заново створену колишню посилання (former). І ніяких копіювань в пам'ять. Вся справа в тому, що в PHP 7 робота зі змінними побудована зовсім інакше, а посилання суттєво перероблені.

Погляньте на цей код:

function bar(&$a) { $f = $a; }
$var = range(1,1024);
for ($i=0; $i < 1000; $i++) {
bar($var);
}

Тут подвійне розбіжність. При виклику
bar()
ви змушуєте движок створювати посилання
$var
на
$a
, як говорить
&$a
сигнатура. Оскільки
$a
тепер є частиною посилального набору (reference set) (
$var-$a
) в тілі
bar()
, ви можете впливати на нього за допомогою значення
$f
: це інша розбіжність. Посилання не впливає на
$f
, тому що
$a-$f$var
не можна з'єднувати один з одним. Однак
$var-$a
з'єднані в одну частину, а
$f
знаходиться в самоті у другій частині, поки ви не змусите движок створити копії. В PHP 7 можна досить легко створювати змінні з посилань і перетворювати їх на посилання, тільки в результаті може відбуватися копіювання при записі (copy-on-write).



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

Ми бачили, що PHP 7 знову дозволяє економити чимало ресурсів порівняно з PHP 5. Але в наших прикладах ми не стосувалися випадків копіювання при записі. Тут все було б інакше, і PHP довелося б робити дамп пам'яті під нашу змінну. Однак використання PHP 7 полегшує ситуації, коли розбіжності навмисне можуть не виконуватися, наприклад, якщо викликати
count($array)
, коли частиною посилання
$array
. У такому разі в PHP 7 не буде додаткових витрат, зате в PHP 5 процесор розжариться (при достатньо великому масиві, наприклад при зборі даних з SQL-запитів).

Незмінні масиви
Концепція незмінних масивів з'явилася в PHP 7, вони є частиною розширення OPCache. Незмінним називається масив, який заповнений незмінними елементами, чиї значення не вимагають обчислень і стають відомі під час компілювання: рядкові значення, цілочисельні значення з плаваючою комою або містять все перераховане масиви. Коротше, жодних динамічних елементів та змінних:

$ar = [ 'foo', 42, 'bar', [1, 2, 3], 9.87 ];

Невідмінювані масиви були оптимізовані PHP 7. В PHP 5 не робиться різниці між масивами, що містять динамічні елементи (
$vars
), і статичними при компіляції. В PHP 7 незмінні масиви не копіюються, не дуплицируются, а залишаються доступними тільки для читання.

Приклад:

for ($i = 0; $i < 1000; $i++) {
$var[] = [
0 => 'Symfony\\Bundle\\FrameworkBundle\\EventListener\\SessionListener',
1 => 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage',
/* ... go to many immutable items here */
];
}

Цей код тисячу разів створює один і той же масив (обрізаний, його можна представити у вигляді масиву з сотні або тисячі осередків).

В PHP 5 такий масив дуплицируется в пам'яті тисячу разів. Якщо він важить кілька сотень кілобайт або навіть мегабайти, то при тисячократному дублювання ми займемо великий обсяг пам'яті.

В PHP 7 OPCache позначає ці масиви як незмінні. Масив створюється один раз, а де необхідно, використовується покажчик на його пам'ять, що призводить до величезної економії пам'яті, особливо якщо масив великий, як у наведеному прикладі (взято з фреймворку Symfony 3).
Подивимось, як змінюється продуктивність:



Знову величезна різниця між PHP 5 та PHP 7. PHP 5 потрібно створювати масив 1000 разів, що займає 27 Мб пам'яті. В PHP 7 з OPCache задіюється лише 36 Кб!

Якщо вам зручно використовувати незмінні масиви, то не соромтеся. Тільки не треба порушувати незмінюваність, видаючи такий код:

$a = 'value';

/* Не робіть так */
$ar = ['foo', 'bar', 42, $a];

/* Краще так: */
$ar = ['foo', 'bar', 42, 'value'];

Інші міркування
Ми розглянули кілька внутрішніх хитрощів, що пояснюють, чому PHP 7 працює набагато швидше PHP 5. Але для багатьох робочих навантажень це микрооптимизации. Щоб отримати від них помітний ефект, потрібно використовувати величезні обсяги даних або численні цикли. Таке найчастіше трапляється, коли у фоновому режимі замість обробки запитів HTTP запускаються робочі PHP-процеси (workers).

У таких випадках ви можете дуже сильно зменшити споживання ресурсів (процесора і пам'яті), лише по-іншому написавши код.

Але не вірте своїм передчуттям щодо можливих оптимізації продуктивності. Не треба сліпо патчити код, використовуйте профилировщики, щоб підтвердити або спростувати свої гіпотези і перевірити реальну продуктивність коду.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.