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

4 года назад от David Grudl  

Вышла версия PHP 8.0. Она настолько насыщена новинками, как ни одна версия до этого. Их представление потребовало целых четыре отдельных статьи. В этой последней мы рассмотрим новые функции и классы и представим Just in Time Compiler.

Новые функции

Стандартная библиотека 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 (раньше 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, в случае объектов возвращает сразу тип:

Значение 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)

Объективизация ресурсов

Значения типа resource происходят из времен, когда в 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];         // экземпляр 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.

Weak maps

Weak maps связаны со сборщиком мусора, который освобождает из памяти все объекты и значения, которые больше не используются (т.е. нет используемой переменной или свойства, которые бы их содержали). Поскольку жизнь PHP-потока коротка, а памяти на серверах сегодня у нас достаточно, вопросы эффективного освобождения памяти, как правило, мы вообще не решаем. Но для длительно работающих скриптов они являются ключевыми.

Объект WeakMap похож на SplObjectStorage. В обоих в качестве ключей используются объекты, и они позволяют хранить под ними любые значения. Разница в том, что WeakMap не препятствует тому, чтобы объект был освобожден сборщиком мусора. Т.е. если единственным местом, где объект еще встречается, является ключ в weak map, он будет удален из карты и памяти.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'данные для $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 Compiler)

Возможно, вы знаете, что PHP компилируется в так называемый opcode, это низкоуровневые инструкции, которые можно посмотреть, например, здесь и которые выполняет виртуальная машина 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              ; конфигурация с помощью четверки OTRC
opcache.enable_cli=1          ; чтобы работало и в CLI
opcache.jit_buffer_size=128M  ; выделенная память для скомпилированного кода

О том, что JIT работает, вы узнаете, например, в информационной панели в Tracy Baru.

JIT очень хорошо работает тогда, когда все переменные имеют четко заданные типы и не могут изменяться при повторном вызове одного и того же кода. Поэтому мне интересно, будем ли мы когда-нибудь в PHP декларировать типы и для переменных: string $s = 'Привет, это заключение серии';