PHP 8.0: Нові функції, класи та JIT (4/4)

3 рік тому Від David Grudl  

Вийшла версія PHP 8.0. У ній з'явилося стільки нових функцій, скільки не було в жодній іншій версії. Їх представлення заслуговує на чотири окремі статті. В останній частині ми розглянемо нові функції та класи, а також познайомимося з компілятором Just in Time.

Нові функції

Стандартна бібліотека PHP містить сотні функцій, а у версії 8.0 з'явилося шість нових. Здається, що це небагато, але більшість з них виправляють слабкі місця мови. Що добре узгоджується з усією концепцією версії 8.0, яка посилює і консолідує PHP, як жодна інша версія до цього. Огляд всіх нових функцій і методів можна знайти в посібнику з міграції.

str_contains() str_starts_with() str_ends_with()

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

if (str_contains('Nette', 'te')) {
	...
}

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

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (previously false)

Завдяки цьому поведінка трійці повністю ідентична аналогам Nette:

str_contains()      # Nette\Utils\String::contains()
str_starts_with()   # Nette\Utils\String::startsWith()
str_ends_with()     # Nette\Utils\String::endsWith()

Чому ці функції так важливі? Стандартні бібліотеки всіх мов завжди обтяжені історичним розвитком; невідповідностей і помилок не уникнути. Але в той же час це є свідченням розвитку мови. Дивно, але 25-річній PHP бракує функцій для таких базових операцій, як повернення першого або останнього елемента масиву, екранування HTML без неприємних сюрпризів (htmlspecialchars не екранує апостроф) або просто пошук рядка в рядку. Це не означає, що це можна якось обійти, тому що в результаті ми отримуємо не читабельний і не зрозумілий код. Це урок для всіх авторів API. Коли ви бачите, що значну частину документації до функції займають пояснення підводних каменів (наприклад, значення, що повертаються strpos), це явний знак, що потрібно модифікувати бібліотеку і додати str_contains.

get_debug_type()

Замінює застарілий get_type(). Замість довгих типів на кшталт integer повертає актуальний на сьогодні int, у випадку об'єктів повертає безпосередньо тип:

Value gettype() get_debug_type()
'abc' string string
[1, 2] array array
231 integer int
3.14 double float
true boolean bool
null NULL null
new stdClass object stdClass
new Foo\Bar object Foo\Bar
function() {} object Closure
new class {} object class@anonymous
new class extends Foo {} object Foo@anonymous
curl_init() resource resource (curl)
curl_close($ch) resource (closed) resource (closed)

Міграція ресурсів до об'єктів

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

Починаючи з PHP 8.0, ресурси зображення, з'єднання curl, openssl, xml і т.д. замінюються на об'єкти. У PHP 8.1 будуть замінені FTP-з'єднання тощо.

$res = imagecreatefromjpeg('image.jpg');
$res instanceof GdImage  // true
is_resource($res)        // false - BC break

Ці об'єкти ще не мають методів, і ви не можете створити їх безпосередньо. Поки що мова йде лише про те, щоб позбутися застарілих ресурсів з PHP без зміни API. І це добре, тому що створення хорошого API – це окрема і складна задача. Ніхто не бажає створення нових класів PHP, таких як SplFileObject з методами з іменами fgetc() або fgets().

PhpToken

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

<?php
$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // instance PhpToken

echo $token->id;             // T_OPEN_TAG
echo $token->text;           // '<?php'
echo $token->line;           // 1
echo $token->getTokenName(); // 'T_OPEN_TAG'
echo $token->is(T_STRING);   // false
echo $token->isIgnorable();  // true

Метод isIgnorable() повертає true для токенів T_WHITESPACE, T_COMMENT, T_DOC_COMMENT і T_OPEN_TAG.

Слабкі карти

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

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

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'data for $obj';

dump(count($map));  // 1
unset($obj);
dump(count($map));  // 0

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

class Comments
{
	private WeakMap $cache;

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

	public function getComments(Article $article): ?array
	{
		$this->cache[$article] ??= $this->loadComments($article);
		return $this->cache[$article]
	}

	...
}

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

PHP JIT (компілятор Just in Time)

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

Таким чином, JIT призначений для прискорення роботи PHP.

Спроба впровадити JIT в PHP почалася в 2011 році і підтримується Дмитром Стоговим. З того часу він спробував 3 різні реалізації, але жодна з них не потрапила до фінального релізу PHP з трьох причин: результатом ніколи не було значне збільшення продуктивності для типових веб-додатків; ускладнює супровід PHP (тобто ніхто, крім Дмитра, не розуміє його 😉); існували інші способи підвищити продуктивність без використання JIT.

Стрибкоподібне збільшення продуктивності, що спостерігалося в PHP версії 7, було побічним продуктом роботи над JIT, хоча, як це не парадоксально, він не був розгорнутий. Це відбувається тільки зараз в PHP 8. Але я буду стримувати перебільшені очікування: ви, швидше за все, не побачите ніякого прискорення.

Так чому ж JIT входить в PHP? По-перше, інші способи підвищення продуктивності поступово вичерпуються, і JIT – це просто наступний крок. У звичайних веб-додатках він не приносить ніяких поліпшень швидкості, але значно прискорює, наприклад, математичні обчислення. Це відкриває можливість почати писати такі речі на PHP. Фактично, можна буде реалізувати деякі функції безпосередньо в PHP, які раніше вимагали прямої реалізації на C через швидкість.

JIT є частиною розширення opcache і вмикається разом з ним у php.ini (прочитайте документацію про ці чотири цифри):

zend_extension=php_opcache.dll
opcache.jit=1205              ; configuration using four digits OTRC
opcache.enable_cli=1          ; in order to work in the CLI as well
opcache.jit_buffer_size=128M  ; dedicated memory for compiled code

Ви можете перевірити, чи працює JIT, наприклад, в інформаційній панелі Tracy Bar.

JIT працює дуже добре, якщо всі змінні мають чітко визначені типи і не можуть змінюватися навіть при багаторазовому виклику одного і того ж коду. Тому мені цікаво, чи будемо ми колись також оголошувати типи в PHP для змінних: string $s = 'Bye, this is the end of the series';