Enmilocalfunciona

Thoughts, stories and ideas.

Como mejorar el rendimiento de scripts en PHP

Publicado por Javier Torrón Díaz el

phprendimientoprofiling

El tema del rendimiento es controvertido y algo a lo que no solemos dar demasiada importancia, pero a veces con pequeños cambios (otros con un buen estudio) podemos solucionar nuestros cuellos de botella. En este post vamos a ver diferentes técnicas que podemos abordar para mejorar el rendimiento de nuestro código PHP.

Menos código

Parece obvio, pero es algo que solemos olvidar. Un if se puede cambiar por un operador ternario en una sola línea, e incluso (si cabe y estamos con PHP 7.X) el operador coalescente nulo (??). Siempre debemos pensar en cómo simplificar el código pero siempre sin perder legibilidad ni granularidad.

La elección del framework

Siempre que podamos elegir con qué framework vamos a trabajar, si el cliente no nos lo impone, o el propio proyecto, la elección del framework será un concepto clave en el rendimiento de la aplicación. Está claro que la mejor opción es usar PHP puro siempre, no hay nada más eficiente, sin embargo todos sabemos que es inviable en nuestros proyectos. Por lo general solemos lanzarnos a utilizar Laravel o Symfony pues son los conocidos y los famosos, pero ¿es esta nuestra mejor opción?

En la gran parte de los casos, no, no es la mejor opción. En la web PHP Benchmarks podemos ver comparativas de rendimiento de diferentes frameworks dependiendo del uso que vayamos a darle. Por ejemplo Symlex(predecesor de Sylex) es el más rápido en el renderizado de las plantillas y el HTML, pero Ubiquity 2 es el más rápido en resolver peticiones. Teniendo en cuenta para qué necesitamos el framework, una pequeña investigación previa arrojará algo de luz sobre cuál es el framework más idóneo a utilizar.

Siguiendo con los ejemplos si vamos a desarrollar un backend para en front en JS podríamos usar Ubiquity o incluso Lumen (microframework hijo de Laravel), o ir a un framework intermedio un poco más lento que los anteriores en pos de un desarrollo más cómodo y rápido.

El recolector de basura de PHP (Garbage collector)

En PHP 5.3 se implementa el recolector de basura (en adelante GC), que muchos conoceréis  de otros sistemas como Java o .NET. Uno de los errores de los programadores modernos es confiar ciegamente en este mecanismo de limpieza de memoria. El GC se ejecuta cada diez mil ciclos de recolección (si queréis entender como funciona el algoritmo ciclos podéis estudiarlo aquí), lo cual nos puede dar una idea de que realmente, además de nuestro código hay algo ejecutándose en el procesador que consume tiempo de ejecución y libera memoria.

Hasta aquí todo suena muy idílico y cómodo para el desarrollador, sin embargo hagamos un análisis más en profundidad: en cada ciclo de recolección, el GC incrementa o decrementa el contador de referencias a un zval (un contenedor de variables de PHP) según se ha usado la variable, si no se ha usado se decrementa, y si llega a 0 se elimina del zval liberando memoria. Pongamos el siguiente caso:

Nuestro algoritmo tiene un bucle en el que se itera un array de 40.000 elementos, cada cual con 50 elementos y que se realiza una operación con los elementos para, posteriormente hacer un push a otro array con el item modificado. Este array requiere un gran espacio en memoria, y dado que lo estamos iterando, su refcount (contador de referencias) no se va a decrementar, como mínimo hasta que no termine el bucle. Asimismo, el segundo array tendrá una reserva de memoria muy grande, por lo cual parece el escenario perfecto para el GC, y sin embargo es lo contrario. Hay que darse cuenta de que cada ciclo de recolección consume tiempo de ejecución en el procesador, por lo cual el tiempo que dure nuestro bucle tendremos un GC consumiendo tiempo de proceso pero que no va a liberar nada de memoria.

Como vemos, en el escenario anterior, el GC tan solo ralentizaría la ejecución del script en cuestión, por lo cual no nos sirve de nada. ¿Qué podemos hacer?

La librería GC

Son una serie de funciones (todas comienzan por gc_... ) que nos dan unas potentes herramientas para controlarlo:

Referencias en GC

Siempre que realicemos una referencia a una variable se establece un refcount permanente que no se decrementa hasta que liberemos la variable de referencia, por lo cual el GC nunca va a liberar esa memoria. Además, las variables de referencia tienen un ámbito global, por lo cual, aunque las realicemos dentro de un bloque, al salir del bloque, la referencia continuará.

Iteración por referencia

El operador de asignación = en PHP realiza una asignación de valor, es decir, el valor de una variable (o de un literal) se asigna a otra variable. ¿Qué significa esto? Aunque pueda parece lo contrario, el funcionamiento real es que este operador copia el valor que asigna. Esto se hace así porque en realidad el valor de una variable está contenido en una zona de la memoria, al asignar el valor de la variable b a la variable c, en el espacio de memoria reservado para la variable c se escribe lo mismo que hay en el espacio de memoria de la variable b.

Cuando realizamos una asignación por referencia en vez de por valor, la variable contendrá una referencia al valor de otra variable, es decir, ambas variables tienen el mismo contenido en la misma zona de memoria. Los usos de las referencias en PHP son pocos, pero nos vamos a centrar en lo que trata este artículo, mejorar el rendimiento de nuestro código.

Cuando realizamos un bucle foreach($array as $item), cada iteración del bucle crea un espacio en memoria con el contenido del elemento que se está iterando en ese momento, con lo cual primero tiene que copiar el valor (lo que significa doble espacio de memoria ocupado) y después realizar la iteración (más lenta puesto que ha consumido tiempo de ejecución en la copia del valor). Este comportamiento se evidencia notablemente cuando cada elemento del array tiene un peso considerable (por ejemplo imágenes blob o en base64, tablas de bases de datos con muchos campos y registros, etc.). Además, nada nos garantiza que el GC libere la memoria justo al terminar la iteración.

En este caso la utilización de referencias en el array nos ahorra el tiempo de copia de los valores así como el consumo de la misma: foreach ($array as &$item).

Unset y null

Aunque confiamos excesivamente en el GC, debemos de ser responsables y realizar la limpieza de memoria a mano, como mínimo cuando trabajemos con instancias pesadas u objetos que contengan conexiones externas. Una conexión MySQL no liberada puede resultar en un colapso en la base de datos dependiendo de nuestros usuarios concurrentes si al GC no le ha dado tiempo a liberar el objeto de conexión. Tenemos dos opciones para hacer esto:

  • Igualación a null. Conocida por todos, al igualar a null una variable se elimina su contenido, pero no elimina la variable del zval, por lo que su asignación de memoria sigue reservada en cierto modo. Sin embargo a veces necesitamos reutilizar la variable pero no el valor: este es el caso en el que debemos de aplicarlo.
  • unset($variable). Este método elimina la variable de su zval, dejando el espacio de memoria que ocupaba completamente liberado. Hay que tener cuidado con este método, pues podrías eliminar una propiedad de una instancia de clase. Ver ejemplo a continuación:
class Car  
{
    protected $connection;

    public function getProperties()
    {
        return $this->connection->gerCarProperties();
    }

    public function clean()
    {
        unset($connection);
    }
}

En el ejemplo anterior si llamamos al método clean(), cuando llamemos al método getProperties() lanzará una excepción porque la instancia no tiene una variable llamada connection.

Profiling

El profiling consiste en analizar la ejecución de un código para conocer todos los detalles del mismo, tales como el tiempo de ejecución, partes del código que ejecuta, etc. Hay muchas herramientas comerciales para realizar profiling, pero tenemos una básica y gratuita que es un poco complicada de configurar: XDebug. Os dejo un pequeño tutorial de como utilizarla para hacer profiling.

Getters y Setters

Es muy habitual, especialmente si has trabajado en otros lenguajes, la utilización de getters y setters para el acceso a las propiedades de una clase. En general hay dos razones por las que se desaconseja su utilización, sin embargo no podemos ser inflexibles, veamos todos los ámbitos:

  • Rendimiento. Para el tiempo de proceso es mucho más rápido el acceso a una variable (simplemente es acceder a una posición de memoria y leer o modificar) que ejecutar un método.
  • Falta de control. Si la ejecución de un método depende del valor de una propiedad de la clase, en caso de no haber invocado a su setter, el método podría lanzar una Excepción o tener comportamientos caóticos e inesperados.
  • Líneas de código. Estos métodos hinchan mucho el número de líneas de código que nuestros scripts poseen, aumentando el tiempo de ejecución y dificultando la comprensión del mismo.

Sin embargo, la utilización de propiedades de ámbito público mejora el rendimiento, pero sigue fallando en el segundo y tercer punto. Entonces, ¿qué hacemos? Insertarlas en el constructor de la clase. De esta manera nos aseguramos de que la clase tendrá los valores que necesita y no podremos ejecutar el script en caso de no facilitar estos valores. Esto mejorará el rendimiento, la legibilidad y el control sobre la clase, manteniendo un número de líneas de código lógico (buenas prácticas en PHP).

Métodos mágicos

El propio Zend desaconseja su utilización pues son más lentos que utilizar cualquier otro tipo de acceso a variables. Echad un vistazo a este benchmark en el que se estudia el tiempo de acceso. Para quien no conozca los métodos mágicos, puede estudiarlos aquí.

Conclusión

La mayor parte de las veces en las que el código PHP que desarrollamos en los proyectos en los que trabajamos no tiene un rendimiento adecuado es porque no le damos la importancia suficiente a revisar, hacer profiling o seguir buenas prácticas. Muchos de los embudos a los que nos enfrentamos se resuelven fácilmente teniendo en cuenta estos sencillos tips, de forma que podamos conseguir aplicaciones más rápidas y eficientes con poco esfuerzo y costumbre.

Si te ha gustado el post, ¡síguenos en Twitter para estar al día de nuevas entregas!