PHP 8.0: Nowe funkcje, klasy i JIT (4/4)

4 lata temu przez David Grudl  

Ukazała się wersja PHP 8.0. Jest tak naładowana nowościami, jak żadna wersja wcześniej. Ich przedstawienie wymagało aż czterech oddzielnych artykułów. W tym ostatnim przyjrzymy się nowym funkcjom i klasom oraz przedstawimy Just in Time Compiler.

Nowe funkcje

Standardowa biblioteka PHP dysponuje setkami funkcji, a w wersji 8.0 pojawiło się sześć nowych. Wygląda to na mało, ale większość z nich łata słabe punkty języka. Co ładnie koresponduje z wydźwiękiem całej wersji 8.0, która dopracowuje i konsoliduje PHP jak żadna wersja wcześniej. Przegląd wszystkich nowych funkcji i metod znajdziesz w przewodniku migracji.

str_contains() str_starts_with() str_ends_with()

Funkcje do sprawdzania, czy ciąg znaków zaczyna się, kończy lub zawiera podciąg.

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

Wraz z pojawieniem się tej trójki PHP definiuje, jak postępować z pustym ciągiem znaków podczas wyszukiwania, według czego kierują się również wszystkie inne powiązane funkcje, a mianowicie tak, że pusty ciąg znaków znajduje się wszędzie:

str_contains('Nette', '')     // true
str_starts_with('Nette', '')  // true
strpos('Nette', '')           // 0 (wcześniej false)

Dzięki temu zachowanie trójki jest całkowicie identyczne z odpowiednikami w Nette:

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

Dlaczego te funkcje są tak ważne? Standardowe biblioteki wszystkich języków są zawsze obciążone historycznym rozwojem i nie da się uniknąć powstawania niespójności i potknięć. Ale jednocześnie jest to wizytówka danego języka. Dziwne jest, gdy w 25-letnim PHP brakuje funkcji do tak podstawowych operacji, jak zwracanie pierwszego czy ostatniego elementu z tablicy, escapowanie HTML bez pułapek (htmlspecialchars nie escapuje apostrofu), czy właśnie wyszukiwanie ciągu znaków w ciągu znaków. To, że da się to jakoś obejść, nie wytrzymuje krytyki, ponieważ wynikiem nie jest czytelny i zrozumiały kod. Jest to nauczka dla wszystkich autorów API. Kiedy widzisz, że znaczną część dokumentacji funkcji zajmuje wyjaśnienie pułapek (jak na przykład wartości zwracane przez strpos), jest to jasny sygnał do modyfikacji biblioteki i dodania właśnie str_contains.

get_debug_type()

Zastępuje już przestarzałe get_type(). Zamiast długich typów jak integer zwraca dzisiaj używane int, w przypadku obiektów zwraca od razu typ:

Wartość 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)

Obiektywizacja zasobów

Wartości typu resource pochodzą z czasów, gdy PHP jeszcze nie miało obiektów, ale właściwie ich potrzebowało. Tak powstały zasoby. Dzisiaj mamy obiekty i w porównaniu do zasobów działają znacznie lepiej z garbage collectorem, więc w planach jest stopniowe zastąpienie ich wszystkich obiektami.

Od PHP 8.0 na obiekty zmieniają się zasoby obrazów, połączeń curl, openssl, xml, itp.. W PHP 8.1 przyjdzie kolej na połączenia FTP itd.

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

Te obiekty na razie nie mają żadnych metod, ani nie można bezpośrednio tworzyć ich instancji. Chodzi na razie rzeczywiście tylko o to, aby pozbyć się z PHP przestarzałych zasobów bez zmiany API. I to dobrze, ponieważ stworzenie dobrego API jest oddzielnym i wymagającym zadaniem. Nikt nie chce, aby w PHP powstały kolejne klasy jak SplFileObject z metodami nazwanymi fgetc() lub fgets().

PhpToken

Do obiektów przenosi się również tokenizer, a więc funkcje wokół token_get_all. Tym razem nie chodzi o pozbywanie się zasobów, ale otrzymujemy pełnoprawny obiekt reprezentujący jeden token PHP.

<?php

$tokens = PhpToken::tokenize('<?php $a = 10;');
$token = $tokens[0];         // instancja 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

Metoda isIgnorable() zwraca true dla tokenów T_WHITESPACE, T_COMMENT, T_DOC_COMMENT i T_OPEN_TAG.

Weak maps

Weak mapy są związane z gargabe kolektorem, który zwalnia z pamięci wszystkie obiekty i wartości, które nie są już używane (tj. nie ma żadnej używanej zmiennej ani właściwości, która by je zawierała). Ponieważ życie wątku PHP jest efemeryczne, a pamięci mamy dzisiaj na serwerach pod dostatkiem, kwestie związane z efektywnym zwalnianiem pamięci zazwyczaj w ogóle nas nie dotyczą. Ale w przypadku długo działających skryptów są kluczowe.

Obiekt WeakMap jest podobny do SplObjectStorage. W obu jako klucze używane są obiekty i umożliwiają przechowywanie pod nimi dowolnych wartości. Różnica polega na tym, że WeakMap nie zapobiega zwolnieniu obiektu przez garbage kolektor. Tj. jeśli jedynym miejscem, gdzie obiekt jeszcze występuje, jest klucz w weak mapie, zostanie on usunięty z mapy i pamięci.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj]  = 'dane dla $obj';

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

Do czego to jest dobre? Na przykład do kešowania. Mamy metodę loadComments(), której przekazujemy artykuł na blogu, a ona zwraca wszystkie jego komentarze. Ponieważ metoda jest wywoływana z tym samym artykułem wielokrotnie, stworzymy sobie jeszcze getComments(), która będzie kešować wynik pierwszej metody:

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]
	}

	...
}

Sztuczka polega na tym, że w momencie, gdy obiekt $article zostanie zwolniony (np. aplikacja zacznie pracować z innym artykułem), zwolniona zostanie również jego pozycja z cache.

PHP JIT (Just in Time Compiler)

Być może wiesz, że PHP kompiluje się do tzw. opcode, czyli instrukcji niskiego poziomu, które można zobaczyć na przykład tutaj, a które wykonuje maszyna wirtualna PHP. A co to jest JIT? JIT potrafi transparentnie kompilować PHP bezpośrednio do kodu maszynowego, który wykonuje bezpośrednio procesor, dzięki czemu omija się wolniejsze wykonywanie przez maszynę wirtualną.

JIT ma więc przyspieszyć PHP.

Próby implementacji JIT w PHP sięgają aż 2011 roku i stoi za nimi Dmitry Stogov. Od tego czasu wypróbował 3 różne implementacje, ale żadna z nich nie trafiła do oficjalnego PHP z tych powodów: wynikiem nigdy nie był żaden znaczący wzrost wydajności dla typowych aplikacji webowych; komplikuje utrzymanie PHP (tj. nikt oprócz Dmitry'ego tego nie rozumie 😉); istniały inne sposoby na poprawę wydajności, bez konieczności używania JIT.

Skokowy wzrost wydajności PHP w wersji 7 był produktem ubocznym pracy nad JIT, chociaż paradoksalnie do jego wdrożenia nie doszło. Dochodzi do tego dopiero w PHP 8. Ale od razu będę hamować przesadne oczekiwania: prawdopodobnie żadnego przyspieszenia nie zauważysz.

Dlaczego więc JIT wchodzi do PHP? Po pierwsze, inne sposoby na poprawę wydajności powoli się wyczerpują, a JIT jest po prostu następny w kolejce. W zwykłych aplikacjach webowych wprawdzie przyspieszenia nie przynosi, ale zasadniczo przyspiesza na przykład obliczenia matematyczne. Otwiera się więc możliwość zaczęcia pisania tych rzeczy w PHP. A właściwie można by w PHP implementować funkcje, które dotychczas wymagały implementacji bezpośrednio w C ze względu na szybkość.

JIT jest częścią rozszerzenia opcache i włącza się go razem z nim w php.ini (przeczytaj dokumentację dotyczącą tej czwórki cyfr):

zend_extension=php_opcache.dll
opcache.jit=1205              ; konfiguracja za pomocą czwórki OTRC
opcache.enable_cli=1          ; aby działało również w CLI
opcache.jit_buffer_size=128M  ; zarezerwowana pamięć dla skompilowanego kodu

Że JIT działa, dowiesz się na przykład w panelu informacyjnym w Tracy Baru.

JIT bardzo dobrze działa wtedy, gdy wszystkie zmienne mają jasno określone typy i nie mogą się zmieniać przy wielokrotnym wywołaniu tego samego kodu. Jestem więc ciekaw, czy kiedyś będziemy w PHP deklarować typy również dla zmiennych: string $s = 'Witaj, to jest zakończenie serii';