Reflexiones performantes de instanciación de objetos en PHP

Publicado en WebPerf · 03 mayo 2019.
Instanciar objetos de manera óptima en PHP

Como programadores PHP nos gusta crear clases con el fin de tener nuestro código ordenado y simplificado. Sin embargo, PHP no es JAVA, por lo que cada vez que añadimos un new MyCLASS o un class_exists( sin el segundo argumento a false ) se lanza una serie de eventos a bajo nivel( siempre y cuando la clase no se haya cargado anteriormente bajo la misma petición HTTP ).

Cuando esto se hace con una gran cantidad de clases, interfaces o traits, que pueden tener a su vez otras clases, intefaces o traits el tiempo de respuesta de nuestra aplicación PHP se eleva considerablemente.

Lo que más tiempo se lleva es el acceso al archivo y el análisis de este. ¿ De cuanto tiempo estamos hablando ?, pues es difícil definirlo porque depende mucho del contexto( tamaño de la clase, algoritmo I/O scheduling escogido en el servidor, Opcache activo o no, … ). Aún así, hay un post en el que se ha intentado medir la carga de 4096 clases y estos son los resultados:

Cuanto se tarda en cargar clases en PHP

Quizás te parezca que es casi imposible alcanzar 4096 clases o si quiera una décima parte, pero si tienes en cuenta además de tus clases, las del framework/cms que usas, más la de tus otros vendors, ya no es algo tan descabellado. Puedes hacer un phploc a tu proyecto para saber cuantas clases serían. Para mi web me salen 3472 clases. Seguro que te estarás preguntando, si este tiempo es tanto, ¿ por qué no se ha hecho nada para reducirlo ? O incluso: ¿ hay alguna forma de cargar más rápido las clases PHP ?.

Seguramente sepas que antiguamente las clases se cargaban bajo demanda. Si necesitabas una clase le añadías un include o un require con la ruta dónde estaba su archivo y la cargabas. Uno de los problemas de esto es que los desarrolladores añadíamos estas directivas en la parte superior de cada archivo PHP al estilo JAVA por si daba la casualidad que se usara la clase. Cuando una clase variaba de lugar, había que ir buscando todas y cada una de estas directivas para cambiarla.

Además, podía suceder que se cargaran más de una vez determinadas clases con lo que la aplicación se rompía. Para ello, Zend sacó las directivas include_once y require_once que casi son lo mismo que sus hermana sin _once. La diferencia es que las _once mantienen una cache de todos los archivos que se han abierto de manera que no lo hagan dos veces. Esta diferencia hacía que en igualdad de condiciones, como es lógico, las _once son más lentas.

Con el pasar del tiempo, llegaron los autoloads, los namespaces y ...¡Composer! Para solucionar los problemas de los includes/requires. De todo esto hablaré más tarde. Lo importante aquí es darse cuenta que PHP al cargar una clase bajo demanda tenía que:

  1. Acceder a el archivo pedido vía sistema de archivo.
  2. Desmenuzar los archivos en trozos pequeños o tokens llamados Opcodes.
  3. Analizar esos Opcodes y ejecutarlos.

Luego, si se pedía otra vez bajo la misma petición, ya no se hacía lo mismo porque ya estaba en memoria.

Una máxima en la informática es “no te repitas”(DRY) y aquí, cuando las clases se pedían en distintas peticiones HTTP, el interprete de PHP se repetía. Como los desarrolladores del lenguaje PHP son muy avispados, crearon una caché en memoria llamada OPCache( normalmente lo leerás como APC por la librería que antiguamente contenía la funcionalidad ). Como nos cuenta la página de php.net: OPcache mejora el rendimiento de PHP almacenando el código de bytes de un script precompilado en la memoria compartida, eliminando así la necesidad de que PHP cargue y analice los script en cada petición.

Eso suena estupendo y lo es. Reduce considerablemente el tiempo de respuesta de nuestra aplicación web. Siempre que tengamos memoria suficiente para todas las clases de nuestro proyectos( o al menos las más importantes ).

Algunos desarrolladores de frameworks fueron un paso más allá y pensaron: “Si los css y js se unifican en un único archivo para optimizar su carga, ¿ por qué no hacerlo con PHP ?. Así que sus herramientas Combinan todos archivos de clases en uno solo en producción.

Con el paso a la época nueva de PHP y la llegada de Composer todo esto se olvidó y empeoró. Composer es principalmente un autoload( aunque tiene más usos ). Eso nos liberó de tener que poner los includes/requires en la parte superior de nuestro código( aunque ahora añadimos los use, ya ves tú ). Esto aumento el tiempo de carga de nuestra aplicación aún más, porque en vez de ejecutarse un evento por cada nueva clase, se ejeucutaban tres:

    1. Todos los autoload registrados se ejecutan uno detrás de otro, hasta que alguno cargue la clase pedida.
    2. Cada autoload se encarga de analizar el espacio de nombre( o namespace ) de la clase para tratar de localizar el archivo que la contiene. Un tratamiento muy común es cambiar las barras `\` por las `/`.
    3. Si se encuentra el archivo asociado este es leído, analizado y llevado a memoria. A no ser que se use Opcache como hemos visto.

Composer no es malo en sí, todo lo contrario, pero se sobreusa. Por ejemplo, haciendo que todas las clases del proyecto vayan por Composer. Esto es un error muy común lamentablemente.

Lo recomendable es que las clases que son base en tu proyecto( y sólo en tu proyecto, no hablo de clases de terceros o vendors ) se sigan cargando vía include/require( siempre con path absoluto y siempre bajo un front-controller ). OJO, no digo todas, sólo las que son base( validación de datos, acceso a base de datos, … ). Esto, hasta ahora, te libraba de que las clases que siempre usas pasaran una y otra vez por el autoload. Si lo haces así, a partir de PHP 7.4 además estas clases comunes podrán ser precargadas siempre en memoria. Con lo que el tiempo de carga cuando las uses será inmediato. Más información de precarga en PHP 7.4

Seguro que te estarás preguntando: ¿ y tanto tardan los autoloads de PHP en cargar una clase ?. Bueno, fíjate en la la respuesta de Rasmus Lerdorf. La explicación del comentario de Rasmus es bien sencilla:

Imagínate que tienes una clase Posts que hereda de Repository. Tú, como programador, haces un new Posts. OPcache guardaría los opcodes en memoria del archivo que contiene Posts pero sólo de él( no de Repository ). El autoload trabaja en tiempo de ejecución por lo que no sabe a que archivo/clase se refiere Repository o si lo vas a querer al vuelo según qué circunstancias. Lo más óptimo sería que todo lo necesario en Posts( incluído su clase base Repository, así que como interfaces que implementara o traits usados ) estuvieran OPCacheado junto, pero no es así porque el contenido de la clase base( así como las interfaces y traits ) depende de lo que el autoload decida. Distinto es si se los especificas mediante un include/require( fuera de un bloque ). Entonces, el sistema OPCache sabrá 100% seguro que clase ha de utilizar y podrá generar, optimizar y combinar los OPCodes.

Este modo singular de trabajar de PHP, luego trae consigo que, la gente te vea usar clases con métodos estáticos, equivocadamente se echen las manos a la cabeza. Bien porque vienen de JAVA o bien porque han leído sobre buenas prácticas de gente que venía de JAVA y lo asumen como una verdad absoluta. En JAVA, dónde una clase es siempre la misma clase en una ubicación concreta, este tipo de prácticas trae problemas para testing o simplemente solapamiento en el desarrollo. En PHP, con autoloads no hay estos problemas. Eso sí, cualquiera se los intenta explicar...

Por otra parte, Composer trae problemas propios,así que acuérdate de optimizarlo en tu servidor de producción.

Finalmente, para PHP 8.0 se espera el compilador JIT PHP. Sería algo parecido a lo que se hace con OPCache con la diferencia que, como no se hace al vuelo, se puede aprovechar para optimizar el código. Además, en vez de tener OPCodes podrá tener un código más cercano a lo que usa la CPU.

¿ Te acuerdas de los problemas de los autoloads que hemos hablado antes ?, también vienen heredados en JIT. Por lo que seguramente el rendimiento que consigas con JIT en web sea sólo algo superior al de OPCache. No sé si la implementación final han hecho algo para solucionarlo, pero creo que no y, de ser así, JIT no traería mejoras muy significativa de rendimiento para tus proyectos webs( siento defraudarte ).

¡ Compártelo !
Este sitio utiliza cookies propias y de terceros para mejorar tu experiencia con el sitio web. Al continuar con la navegación consideramos que acepta su uso.