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

3 lata temu Ze strony David Grudl  

PHP w wersji 8.0 zostało wydane. Jest ona pełna nowych funkcji, jak żadna inna wersja wcześniej. Ich wprowadzenie zasłużyło na cztery osobne artykuły. W ostatniej części przyjrzymy się nowym funkcjom i klasom oraz przedstawimy kompilator Just in Time.

Nowe funkcje

Standardowa biblioteka PHP zawiera setki funkcji, a w wersji 8.0 pojawiło się sześć nowych. Nie wydaje się to dużo, ale większość z nich naprawia słabe punkty języka. Jest to zgodne z ideą wersji 8.0, która zacieśnia i konsoliduje PHP jak żadna inna wersja. Przegląd wszystkich nowych funkcji i metod można znaleźć w przewodniku po migracji.

str_contains() str_starts_with() str_ends_with()

Funkcje pozwalające określić, czy łańcuch zaczyna się, kończy lub zawiera podłańcuch.

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

Wraz z pojawieniem się tej trójcy, PHP definiuje sposób obsługi pustego łańcucha podczas wyszukiwania, do czego stosują się wszystkie inne powiązane funkcje, a mianowicie pusty łańcuch znajduje się wszędzie:.

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

Dzięki temu zachowanie trójcy jest całkowicie identyczne z analogami 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; nie da się uniknąć niespójności i błędów. Ale jednocześnie jest to świadectwo danego języka. O dziwo, w 25-letnim PHP brakuje funkcji do tak podstawowych operacji, jak zwracanie pierwszego lub ostatniego elementu tablicy, ucieczka przed HTML bez przykrych niespodzianek (htmlspecialchars nie ucieka przed apostrofem), czy po prostu szukanie ciągu w ciągu. Nie trzyma się tego, że można go w jakiś sposób ominąć, ponieważ wynikiem jest nieczytelny i zrozumiały kod. To jest lekcja dla wszystkich autorów API. Kiedy widzisz, że znaczną część dokumentacji funkcji zajmują wyjaśnienia pułapek (takich jak wartości zwracane przez strpos), to jest to wyraźny znak, aby zmodyfikować bibliotekę i dodać str_contains.

get_debug_type()

Zastępuje przestarzały już get_type(). Zamiast długich typów jak integer, zwraca obecnie używany int, w przypadku obiektów zwraca bezpośrednio typ:

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)

Migracja zasobów do obiektów

Wartości typu resource pochodzą z czasów, gdy PHP nie posiadał jeszcze obiektów, a wręcz ich potrzebował. W ten sposób narodziły się zasoby. Dziś mamy obiekty i w porównaniu z zasobami znacznie lepiej współpracują z garbage collectorem, więc plan jest taki, aby stopniowo zastąpić je wszystkie obiektami.

Od PHP 8.0, zasoby images, curl joins, openssl, xml, etc. zostały zmienione na obiekty. W PHP 8.1, połączenia FTP, itp. będą podążać za nimi.

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

Te obiekty nie mają jeszcze żadnych metod, ani nie można ich bezpośrednio instancjonować. Jak na razie jest to tylko kwestia pozbycia się przestarzałych zasobów z PHP bez zmiany API. I dobrze, bo stworzenie dobrego API to osobne i wymagające zadanie. Nikt nie życzy sobie tworzenia nowych klas PHP takich jak SplFileObject z metodami o nazwach fgetc() lub fgets().

PhpToken

Tokenizer i funkcje wokół token_get_all również zostały zmigrowane do obiektów. Tym razem nie chodzi o pozbycie się zasobów, ale otrzymujemy pełnoprawny obiekt reprezentujący jeden token 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

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

Słabe mapy

Słabe mapy są związane z garbage collectorem, który zwalnia z pamięci wszystkie obiekty i wartości, które nie są już używane (tzn. nie ma zawierającej je zmiennej lub właściwości). Ponieważ wątki PHP są krótkotrwałe, a na naszych serwerach mamy dużo dostępnej pamięci, zazwyczaj w ogóle nie zajmujemy się kwestiami dotyczącymi efektywnego zwalniania pamięci. Jednak w przypadku dłużej działających skryptów są one niezbędne.

Obiekt WeakMap jest podobny do SplObjectStorage Oba używają obiektów jako kluczy i pozwalają na przechowywanie pod nimi dowolnych wartości. Różnica polega na tym, że WeakMap nie zapobiega zwolnieniu obiektu przez garbage collector. Tzn. Jeśli jedynym miejscem, w którym obiekt obecnie istnieje, jest klucz w słabej mapie, zostanie on usunięty z mapy i pamięci.

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

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

Do czego to jest dobre? Na przykład do cachowania. Miejmy metodę loadComments(), której przekazujemy artykuł na blogu, a ona zwraca wszystkie jego komentarze. Ponieważ metoda jest wywoływana wielokrotnie dla tego samego artykułu, stworzymy kolejną getComments(), która będzie buforować 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]
	}

	...
}

Chodzi o to, że gdy obiekt $article zostanie zwolniony (na przykład aplikacja rozpocznie pracę z innym artykułem), jego wpis również zostanie zwolniony z pamięci podręcznej.

PHP JIT (Just in Time Compiler)

Być może wiesz, że PHP jest kompilowany do tzw. opcode, czyli niskopoziomowych instrukcji, które możesz zobaczyć na przykład tutaj, a które są wykonywane przez wirtualną maszynę PHP. A co to jest JIT? JIT może transparentnie skompilować PHP bezpośrednio do kodu maszynowego, który jest wykonywany bezpośrednio przez procesor, dzięki czemu wolniejsze wykonywanie przez maszynę wirtualną jest omijane.

JIT jest więc przeznaczony do przyspieszenia PHP.

Wysiłki mające na celu wdrożenie JIT do PHP sięgają 2011 roku i są wspierane przez Dmitry'ego Stogova. Od tego czasu wypróbował on 3 różne implementacje, ale żadna z nich nie trafiła do finalnego wydania PHP z trzech powodów: rezultatem nigdy nie był znaczący wzrost wydajności dla typowych aplikacji internetowych; komplikuje utrzymanie PHP (tzn. nikt poza Dmitrym tego nie rozumie 😉 ); istniały inne sposoby na poprawę wydajności bez konieczności używania JIT.

Skokowy wzrost wydajności obserwowany w PHP w wersji 7 był produktem ubocznym prac nad JIT, choć paradoksalnie nie został wdrożony. Dzieje się to dopiero teraz w PHP 8. Będę jednak powstrzymywał przesadne oczekiwania: prawdopodobnie nie zobaczysz żadnego przyspieszenia.

Dlaczego więc JIT wchodzi do PHP? Po pierwsze, inne sposoby na poprawę wydajności powoli się kończą, a JIT jest po prostu kolejnym krokiem. W zwykłych aplikacjach internetowych nie przynosi poprawy prędkości, ale znacznie przyspiesza np. obliczenia matematyczne. Otwiera to możliwość rozpoczęcia pisania takich rzeczy w PHP. W rzeczywistości możliwe byłoby wdrożenie niektórych funkcji bezpośrednio w PHP, które wcześniej wymagały bezpośredniej implementacji w C ze względu na szybkość.

JIT jest częścią rozszerzenia opcache i jest włączany razem z nim w php.ini (przeczytaj dokumentację o tych czterech cyfrach):

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

To, że JIT jest uruchomiony, można sprawdzić np. w panelu informacyjnym Tracy Bar.

JIT działa bardzo dobrze, jeśli wszystkie zmienne mają jasno określone typy i nie mogą się zmieniać nawet przy wielokrotnym wywoływaniu tego samego kodu. Dlatego zastanawiam się, czy kiedyś w PHP dla zmiennych też będziemy deklarować typy: string $s = 'Bye, this is the end of the series';