PHP 8.0: Nuevas funciones, clases y JIT (4/4)

hace 3 años por David Grudl  

La versión 8.0 de PHP ha sido liberada. Está llena de nuevas funciones como ninguna otra versión antes. Su introducción merecía cuatro artículos separados. En la última parte echaremos un vistazo a las nuevas funciones y clases e introduciremos el compilador Just in Time.

Nuevas funciones

La librería estándar de PHP tiene cientos de funciones y en la versión 8.0 han aparecido seis nuevas. No parece mucho, pero la mayoría de ellas remedian puntos débiles del lenguaje. Lo cual se alinea muy bien con el concepto de la versión 8.0, que refuerza y consolida PHP como ninguna versión antes. Una visión general de todas las nuevas funciones y métodos se puede encontrar en la guía de migración.

str_contains() str_starts_with() str_ends_with()

Funciones para determinar si una cadena comienza, termina o contiene una subcadena.

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

Con la llegada de esta trinidad, PHP define cómo manejar una cadena vacía mientras se busca, que es a lo que se adhieren todas las demás funciones relacionadas, y es que una cadena vacía se encuentra en todas partes:.

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

Gracias a esto, el comportamiento de la trinidad es completamente idéntico al de los análogos de Nette:

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

¿Por qué son tan importantes estas funciones? Las bibliotecas estándar de todos los lenguajes siempre están lastradas por el desarrollo histórico; no se pueden evitar las incoherencias y los pasos en falso. Pero al mismo tiempo es un testimonio del lenguaje respectivo. Sorprendentemente, el PHP de hace 25 años carece de funciones para operaciones tan básicas como devolver el primer o último elemento de un array, escapar HTML sin sorpresas desagradables (htmlspecialchars no escapa un apóstrofo), o simplemente buscar una cadena en una cadena. No se sostiene que pueda saltarse de alguna manera, porque el resultado no es un código legible y comprensible. Esta es una lección para todos los autores de API. Cuando ves que gran parte de la documentación de la función está ocupada por explicaciones de trampas (como los valores de retorno de strpos), es una clara señal para modificar la biblioteca y añadir str_contains.

get_debug_type()

Sustituye al ya obsoleto get_type(). En lugar de tipos largos como integer, devuelve el hoy utilizado int, en el caso de objetos devuelve directamente el tipo:

Valor 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)

Migración de recursos a objetos

Los valores de tipo resource provienen de una época en la que PHP aún no tenía objetos, pero en realidad los necesitaba. Así nacieron los recursos. Hoy tenemos objetos y, comparados con los recursos, funcionan mucho mejor con el recolector de basura, así que el plan es reemplazarlos gradualmente a todos con objetos.

A partir de PHP 8.0, los recursos images, curl joins, openssl, xml, etc. son cambiados a objetos. En PHP 8.1, seguirán las conexiones FTP, etc.

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

Estos objetos aún no tienen métodos, ni puedes instanciarlos directamente. Hasta ahora, es sólo cuestión de deshacerse de recursos obsoletos de PHP sin cambiar la API. Y eso es bueno, porque crear una buena API es una tarea aparte y desafiante. Nadie desea la creación de nuevas clases PHP como SplFileObject con métodos llamados fgetc() o fgets().

PhpToken

El tokenizador y las funciones alrededor de token_get_all también se migran a objetos. Esta vez no se trata de deshacerse de recursos, sino que obtenemos un objeto completo que representa un 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

El método isIgnorable() devuelve true para los tokens T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, y T_OPEN_TAG.

Mapas débiles

Los mapas débiles están relacionados con el recolector de basura, que libera de la memoria todos los objetos y valores que ya no se usan (es decir, no hay ninguna variable o propiedad que los contenga). Debido a que los hilos de PHP son de corta duración y tenemos mucha memoria disponible en nuestros servidores, usualmente no tratamos temas relacionados con la liberación efectiva de memoria. Pero para scripts de larga duración, son esenciales.

El objeto WeakMap es similar a SplObjectStorage Ambos utilizan objetos como claves y permiten almacenar valores arbitrarios bajo ellos. La diferencia es que WeakMap no impide que el objeto sea liberado por el recolector de basura. Es decir, si el único lugar, donde el objeto existe actualmente, es una clave en el mapa débil, será eliminado del mapa y de la memoria.

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

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

¿Para qué sirve? Por ejemplo, para cachear. Tengamos un método loadComments() al que le pasamos un artículo de un blog y nos devuelve todos sus comentarios. Como el método se llama repetidamente para el mismo artículo, crearemos otro getComments(), que almacenará en caché el resultado del primer método:

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

	...
}

La cuestión es que cuando el objeto $article se libera (por ejemplo, la aplicación empieza a trabajar con otro artículo), su entrada también se libera de la caché.

PHP JIT (Compilador Justo a Tiempo)

Puede que sepas que PHP se compila en los llamados opcode, que son instrucciones de bajo nivel que puedes ver aquí, por ejemplo y que son ejecutadas por una máquina virtual PHP. ¿Y qué es un JIT? JIT puede compilar PHP de forma transparente directamente en código máquina, que es ejecutado directamente por el procesador, de forma que se evita la ejecución más lenta por parte de la máquina virtual.

Por lo tanto, JIT está pensado para acelerar PHP.

El esfuerzo por implementar JIT en PHP se remonta a 2011 y está respaldado por Dmitry Stogov. Desde entonces, ha probado 3 implementaciones diferentes, pero ninguna de ellas llegó a una versión final de PHP por tres razones: el resultado nunca ha sido un aumento significativo del rendimiento para aplicaciones web típicas; complica el mantenimiento de PHP (es decir, nadie excepto Dmitry lo entiende 😉); había otras formas de mejorar el rendimiento sin tener que usar un JIT.

El salto en el aumento del rendimiento observado en la versión 7 de PHP fue un subproducto del trabajo sobre el JIT, aunque paradójicamente no se desplegó. Esto sólo está ocurriendo ahora en PHP 8. Pero voy a contener las expectativas exageradas: probablemente no verás ningún aumento de velocidad.

Entonces, ¿por qué está entrando JIT en PHP? Primero, otras formas de mejorar el rendimiento se están agotando lentamente, y JIT es simplemente el siguiente paso. En aplicaciones web comunes, no aporta ninguna mejora de velocidad, pero acelera significativamente, por ejemplo, los cálculos matemáticos. Esto abre la posibilidad de empezar a escribir estas cosas en PHP. De hecho, sería posible implementar algunas funciones directamente en PHP que antes requerían una implementación directa en C debido a la velocidad.

JIT es parte de la extensión opcache y se habilita junto con ella en php.ini (lea la documentación sobre esos cuatro dígitos):

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

Puede verificar que JIT está funcionando, por ejemplo, en el panel de información de Tracy Bar.

JIT funciona muy bien si todas las variables tienen tipos claramente definidos y no pueden cambiar aunque se llame al mismo código repetidamente. Por eso me pregunto si algún día también declararemos tipos en PHP para las variables: string $s = 'Bye, this is the end of the series';