Consejos para optimizar código PHP.

Optimizar el código es fundamental porque cuanto menos uso hagamos del procesador del servidor más libre estará para aceptar nuevas peticiones y además la web cargará más rápido.

Fuente: https://icme.hpc.msstate.edu/mediawiki/index.php/SRCLID:Performance_optimized_codes_for_multiscale_modeling
Fuente: https://icme.hpc.msstate.edu/mediawiki/index.php/SRCLID:Performance_optimized_codes_for_multiscale_modeling

Cuando se crea un producto a nivel informático se puede obtener el mismo resultado según se programe y su eficiente implementación se diferenciará en función del uso de cpu que requiera y la velocidad que requiera para hacer su función.

En AGENCIA LA NAVE nuestra prioridad es programar de forma eficiente para que su proyecto web funciona bien ahora y en el futuro aunque crezca en visitas diarias. ¿No es mejor prepararse para el futuro?

Fundamentos de arquitectura de computadores:

Antes de explicar algunos conceptos para optimizar el código de las programaciones de PHP se debe tener en cuenta que todo lenguaje interpretado o no debe convertirse al lenguaje de la maquina o ensamblador.

Si se entiende como funciona la arquitectura de un computador será más fácil deducir porque si programamos de forma eficiente podemos acelerar una rutina en cualquier lenguaje incluido en PHP.

Optimizar el código a nivel de lenguaje maquina (ensamblador) requería conocer bien la arquitectura de un procesador y conocer el número de ciclos que tarda en ejecutarse una cierta operación del procesador (OPCODES).  Por ejemplo una operación NOP (No operar consume 1 ciclo de procesador sin hacer nada), mientras que INC BX (incrementar un registro un 1 ciclo) y una DIV (división consume mucho más ciclos en función del procesador).

Pero en el mundo que vivimos los procesadores permiten la ejecución de tareas en paralelo (threads + hilos ejecución) y disponen de HIPER THREADING por lo que a modo de ejemplo un procesador como el i7 posee 8 núcleos en paralelo: En ese momento se complica la cosa y para optimizar se debe pensar si se pueden ejecutar las tareas en paralelo para ejecutar más rápido un código.

Pongamos un ejemplo con el siguiente código:

mov ax,5 => ax=5 
inc ax => ax= ax+1 
mov bx,6 => bx=6
inc bx => bx=bx+1

Sera más lento que ejecutar el siguiente código porque ambas instrucciones pueden ejecutarse en paralelo en el segundo código mientras que en el primero deberemos esperar al primero.

mov ax,5 => ax=5 
mov bx,6 => bx=6
inc ax => ax= ax+1 
inc bx => bx=bx+1

Esta claro que no podemos en un articulo tan reducido explicar como optimizar el código a nivel de maquina aunque os recomendamos leer este brillante articulo de uno de nuestro ídolos Agner Fog que os podrá dar ciertas pistas.

Es importante conocer algunos criterios fundamentales: Los procesadores tienen registros internos y siempre será más rápido acceder a ellos que cualquier acceso a memoria. Los comandos (opcodes) aritméticos modifican unos FLAGS (registros de 1 solo bit)  lo cual puede parecer carente de importancia pero no lo es porque optimizamos el código de forma clara pensando en ensamblador.

Imaginemos que queremos repetir 10000 veces un código, ¿Que será más eficiente? Esta variable parte de inicializar el registro a 0 y va ejecutando comparando si llega al limite. Es equivalente a un bucle for($i=0;$i<10000; $i++) tarea().

mov ax,0 => ax=0
repetir: ... => tarea a repetir
inc ax => ax++ => ax=ax+1
cmp ax,10000
jne repetir

Quizás este código parezca el mismo pero no lo es y es mucho más rápido. La diferencia es que partimos del número final y usamos el hecho que decrementar ocupa menos ciclos y evita comparar un valor (lo cual es equivalente a una resta)  (equivalente a un código php como $i=10000; for(;$i;$i–) … tarea();)

mov ax,10000 
repetir: ... => tarea a repetir 
dec ax
jne repetir

Por suerte en programación en alto nivel tanto en C (lenguaje compilado) como en PHP (lenguaje interpretado) los “compiladores” o “interpretadores” intentan optimizar el código ellos mismos pero nunca son tan inteligentes como nosotros por lo que nunca esta de más saber optimizar nuestro código PHP para crear webs más eficientes.

Entendiendo como va el lenguaje maquina se puede entender muchos de los consejos que os hacemos en este articulo: Un criterio fundamental “mejor acceder a un registro” que a variable en memoria. De hecho no se puede usar una comparación directa a memoria y siempre se debe pasar por un registro intermediario. (veamos su implicación)

“$numero = 1000;
for($i=0;$i<$numero;$i++) tarea();”

Por ejemplo en ensamblador:

mov ax, 0  => ax= 0 (usamos un registro de contador)
repetir: ... tarea en bucle 

mov bx, [1000] => obtiene un número de la posición de memoria 1000
inc ax => ax= ax+1 => incrementamos contador 
cmp ax,bx => comprobamos si ha llegado al valor tope
jne repetir

Como vemos se accede todo el rato a la memoria para ver si ha llegado al valor de la variable en memoria. Sin embargo si en nuestra programación ponemos fijo el datos reducimos el tiempo en  ejecución al evitar la lectura reiterada en memoria.

Lo podemos comparar con este código:

mov ax,0 => ax= 0 => inicializamos el contador 
mov bx, [1000] => Lee la memoria y guardamos en BX el valor de num

repetir: ... tarea 

inc ax  => ax=ax+1 incrementamos el contador sin ram
cmp ax,bx => comparamos 2 registros, no utilizamos memoria +rápidojne repetir

Y entender porque este equivalente en PHP es más rápido cuando se introduce el tope fuera de una variable evitando ciclos de ejecución absurdos. Este código queda claro que se puede mejorar en ciclos y veréis que el código maquina se hace más simple.

mov ax, [1000] => obtenemos de la memoria el numero (la variable)

repetir: ... tarea a repetir

dec ax => ax = ax-1 
jne repetir

¿Se os hace más fácil entender porque este PHP es más rápido?

$numero = 10000;

for(;$numero;$numero--) tarea();

Los accesos a memoria siempre son más lentos y optimizar el código ayuda a mejorar la velocidad de una programación sobre todo en partes que se empleen mucho: Hacer las cosas bien no cuesta tanto.

Para poder entender mejor como optimizar un código se debe saber como funciona una llamada a una función o método de una clase. En el fondo cada vez que se llama a una rutina en nuestra programación se guarda en una memoria especial llamada PILA el contenido de los registros y la posición de la siguiente linea de código para que cuando se realice la tarea saltando a otro lugar del código JMP se pueda restaurar el contenido. Volver equivale a un RET (el típico return de una función en PHP).

Con este criterio queda claro que este código se puede mejorar:

$lineas= file("fichero.txt");

for($i=0;$i<count($lineas); $i++) tarea();

Y que este código es mucho más eficiente al no ejecutar cada vez la función “count” que solo debería hacerse de la otra forma si la tarea en sí modifica el tamaño de $lineas. Revisar bien los bucles y asegurarse que las condiciones de fin son óptimas.

$lineas= file ("fichero.txt);
$todo= count($lineas); // esta tarea solo se realiza una vez
for(;$todo!=0;$todo--) $tarea();

Entendiendo la arquitectura de una computadora se puede entender porque una programación con recursividad siempre irá más lenta que una programada mediante pilas STACK virtuales (incluso en RAM): Evitar rutinas recursivas y convertirlas en iterativas.

¿Qué pasa con las variables globales o las locales? Queda claro que si usamos una variable de forma local está podrá ser almacenada en un registro del procesador para la ejecución del código mientras que si es una variable global muy probablemente requiera usar memoria: Usar solo globales cuando sea indispensable y así optimizarás.

Piensa además que utilizar variables que no se usan requiere de reservar memoria así que no declares variables que no se vayan a usar. Utiliza constantes para aquellos valores que serán fijos a lo largo de la ejecución.

Si estudiamos con más detenimiento como accede un procesador a la RAM podríamos optimizar mejor un recurso: Los accesos a la memoria se realizan a nivel procesador mediante descriptores de segmento CS (código), DS, ES, FS, GS para datos. Existen unos iteradores (SP,BP,BX etc)  que permiten acceder a los contenidos por ejemplo mov ax,ds:[bx] accederán a la posición de memoria ds+bx y obtendría un word (entero de 0 a 65535)  (si DS ya apunta a la zona de memoria). A nivel práctico, siempre conviene agrupar los contenidos en zonas de memoria conjuntas para hacer más óptimo el acceso.

La memoria no es ilimitada y cuando se reserva memoria se hace recomendable no presuponer que PHP va a liberar la memoria porque en todo caso lo hará al final de la ejecución con su Garbage Collector (liberador de memoria). Así que no esta de mas usar la función unset para eliminar variables y aumentar la memoria disponible, sobretodo cuando se usan arrays o variables extensas en servidores limitados.

Las clases puede venir genial pero nunca debemos olvidar para poder optimizar que aunque sean cómodas de usar una incorrecta optimización de los métodos puede crear procedimiento lentos y poco adecuados para hacer la web rápida.

¿Y qué pasa con las comparaciones? Los switch / case usan mas ciclos de procesador que los if / else, por lo tanto usa estos últimos siempre que puedas. Debemos utilizar a ser posible los comandos IF y combinarlos con ELSE para hacer más eficiente el código. No olvidemos usar bien los comandos “break” y “exit” para evitar lentitudes absurdas.

Entendiendo como funciona la arquitectura podemos deducir el motivo por el que un bucle for es mas lento que un while, que a su vez es mas lento que un do..while.  Cuanta más comprobación haya en el fondo se ejecutan más “códigos” adicionales. Debemos estudiar bien lo que necesitamos antes de ponernos a programar y tener en cuenta también que foreach es mas rápido que for.

Debemos diferenciar entre funciones (que requieren a nivel de maquina un salto JMP a otra dirección de memoria) de procedimientos integrados en el sistema. Por ejemplo isset no es una función sino una construcción del lenguaje. Esto significa que PHP no tendrá que hacer ninguna operación previa ni habrá sobrecarga. La función isset() (isSet) resulta de una utilidad tremenda. Nos sirve tanto para saber si una determinada variable ha sido inicializada como para comprobar que un índice existe e incluso para trabajar con longitudes de cadenas de un modo más eficiente:Si en un determinando momento necesitamos comprobar que una cadena tiene al menos una determinada longitud, por ejemplo que el nombre de usuario tenga más de 4 caracteres, lo más inmediato que se nos puede ocurrir es utilizar strlen().

if (strlen($username) < 5){ ... } // solo usarse si existe

Además la comprobación de existencia de una variable es mejor hacerla isset() antes que empty() o is_array(), ya que la primera es la mas eficiente. ¡A tener en cuenta!

Si seguimos optimizando debemos tener claro un criterio: Todo lo que se pueda hacer en HTML no lo hagas en PHP. Por ejemplo el código “echo ‘<h1>hola</h1>’;” mejor ?><h1>hola</h1><?php . Usa de forma prudente los comandos “echo” y “print”. Solo  en casos en que sea indispensable. Y por último debes saber que el comando “echo” es mucho más rápido que print. (una función más compleja con más opciones)

Respecto a las inclusiones: Aunque las funciones include_once() y require_once() son muy útiles para evitar cargar el mismo script repetidas veces, su costo es muy alto. Revisa tu código e intenta usar include() y require() en su lugar: ¿Son necesarios de verdad?

¿Y el azar en nuestras rutinas? Para crear números aleatorios usaremos las funciones de la familia mt_rand() que utiliza un algoritmo de Mersenne Twister mucho más rápido y eficiente que la típica rand().

En general cualquier tarea que requiera de cierto calculo debe uno plantearse utilizar la función que menos consuma según nuestras necesidades:  Expresiones regulares: Intenta usar regexp solo cuando sea necesario, ya que aunque ahorran trabajo, su procesado es mas lento. Separaciones: Es conveniente usar explode() antes que split(), ya que esta ultima permite el uso de expresiones regulares y la primera no. TODO depende de nuestro uso real.

Y este criterio aplicado a comparaciones puede emplearse:  Operador ===: Cuando compares dos variables del mismo tipo intenta evitar el operador ===, ya que a diferencia de == que solo compara valores, este además compara el tipo de variable. (ahorra ciclos de reloj) (no compares “1” con 1 porque requiere cambiar de tipo). Cambiar el tipo requiere de un proceso de calculo diferente.

¿Sabías que es mejor usar $i++; o ++$i;? Pre-incrementar (++$i) es en torno a un 10% más rápido que post-incrementar ($i++). La razón es que cuando hacemos post-incremento, PHP necesita crear una variable temporal en la que almacenar el valor a ser incrementado.

Las comillas simples y dobles no son lo mismo. Usa siempre que puedas las comillas simples ya que a diferencia de las dobles, que interpolan los valores de las variables, estas solo interpretan literales, con la consiguiente mejora de procesamiento. Además debes evitar el uso del símbolo del dolar sin escapar (\$) entre comillas dobles ya que ralentiza el código enormemente. Por ejemplo con comillas dobles “Hola $nombre que pasa” mientras con simples “Hola” . $nombre (al analizar con dobles es menos eficiente si no hay variables).

La re utilización de variables puede ayudar a ahorrar recursos y a optimizar. Los registros del procesador se pueden utilizar para acelerar procesos y evitar usar memoria así pues es recomendable que si usamos una variable en un punto y podemos utilizarla de nuevo en otro punto hacerlo antes que definir una nueva variable. (salvo en accesos locales)

i=0;  
while(i<500)  
{  
    echo 'Bienvenido a Agencia La Nave';  
    i++;  
}  

j=0;  
while(j<133)  
{  
    echo 'Adios! Muy mal no optimizas. ';  
    j++;  
}

Esta rutina utiliza otra variable local del proceso pero tratada como global lo cual requiere emplear más memoria. Por ello sería mucho más óptimo utilizar la misma variable para mejorar el código.

i=0;  
while(i<500)   {
       echo 'Hola Agencia La Nave';       
       i++;   
}   
while(i>133)  
{  
    echo 'Adios! Ahora + optimizado. ';  
    i--;  
}

Si vamos más lejos veremos que dicha programación usa variables globales en las funciones lo cual puede suponer en lenguaje maquina una mala conversión y no usar registros cuando se podría mejorar el proceso. En ese caso al ser las variables globales se podría implementar mediante registros internos en procesador.

for($i=0;$i<533;$i++) echo 'Hola mundo';
for(;$i>133;$i--) echo 'Adios!';

Como podemos ver la optimización no tiene limites: Todos los procesadores trabajan con bits 0 y 1 y cualquier operación que evite acceder a más memoria es buena para una rutina y se ejecutará en menos ciclos de procesador. A modo de ejemplo un comando del tipo “mov ax=3” que inicializa el registro ax a 3 consume de al menos 3 bytes en su condificación.

Utilizando la herramienta Defuse descubrimos que en espacio dicho código ocupa con su código hexadecimal.

66 b8 01 00             mov    ax,0x1
90                      nop
67 66 8b 07             mov    ax,WORD PTR [bx]
66 31 db                xor    bx,bx
26 67 66 8b 86 ab 20    mov    ax,WORD PTR es:[bp+0x20ab]

Queda claro el comando NOP (no hacer nada) solo ocupa 1 byte, el comando XOR del ejemplo únicamente ocupa 3 bytes, y las asignaciones con valor a los registros tanto por memoria como por valor ocupan 4 o incluso más bytes.

A nivel de optimización en PHP lo que intentamos transmitir es que conocer la arquitectura nos puede ayudar a mejorar el código: Así pues un $i=0; puede mejorarse con $i= $i ^ $i; Todo se realiza a nivel de bit y un proceso que haga operaciones como un OR exclusivo o una rotación de bit puede mejorar una inicialización o incluso una división.

El siguiente ejemplo es una buena técnica para comprobar que código es más eficiente y cual no si bien hacen lo mismo. El primero inicializando el valor a 0 tardo: 56 segundos mientras el otro tardo 45 segundos es decir un 20% más.

<?php

$inicio = time();

for($j=0;$j<200000000;$j++)
{
$i= 0;
$k= $i;

}

$fin= time();

$dif= $fin-$inicio;

echo "Tardo: " . $dif . "\n";

$inicio= time();

for($j=0;$j<200000000;$j++)
{
$i=0;
$k= $i^$i;
}

$fin= time();

$dif= $fin-$inicio;

echo "Tardo 2 metodo: " . $dif . "\n";

?>

Es un buen ejercicio retarse a uno mismo para hacer un buen código web pues siempre se puede optimizar el código. Pongamos ahora un proceso de dividir por 2 un número veremos la diferencia de usar una rutina de otra puede ser fundamental.

<?php

define(NUMERO,10000);

$inicio = time();

for($j=0;$j<1000000;$j++)
{

echo NUMERO/2;

}

echo "\n";

$fin= time();

$dif= $fin-$inicio;

echo "Tardo: " . $dif . "\n";

$inicio= time();

for($j=0;$j<1000000;$j++)
{
echo NUMERO >> 1;
}

$fin= time();

$dif= $fin-$inicio;

echo "\n";
echo "Tardo 2 metodo: " . $dif . "\n";

?>

Con este código demuestro que puedes dividir entre 2 el tiempo de ejecución de un código: El primero tardó 13 segundos frente a los 6 segundos de usar una rotación a la derecha a nivel de bit << 1. ¿Seguro que no vale la pena entender como funciona por dentro un procesador?

Otro punto interesante a saber es que en las comparaciones con valores constantes el orden importa así pues es mejor poner estos primero y después lo que quieres comparar, ejemplo: if(FALSE===$variable).

A nivel de MYSQL también se puede optimizar el código especialmente en las sentencias SELECT indicando los campos que realmente son necesarios y evitando siempre el uso de “*” u otros comodines:  No utilices “SELECT * FROM tabla” si no lo necesitas, utiliza “SELECT campo, campo FROM tabla”.”

Aunque no es la primera vez que lo hablamos optimizar el código con una cache del contenido es esencial para aumentar el rendimiento y también evitar ataques DDOS a la plataforma.  

Fuente: https://icme.hpc.msstate.edu/mediawiki/index.php/SRCLID:Performance_optimized_codes_for_multiscale_modeling
Fuente: https://icme.hpc.msstate.edu/mediawiki/index.php/SRCLID:Performance_optimized_codes_for_multiscale_modeling

Programar en hilos en PHP aunque no mucha gente se puede compilando modulos especiales y así tenemos habilitadas funciones como fork() para crear hilos e incluso manejar semáforos.

En AGENCIA LA NAVE no hacemos las cosas como los demás y contamos con profesionales que harán que su producto sea diferenciador, no deje su empresa en manos de cualquiera.

Si queremos ser radicales: Mejor usar la función original que un alias en PHP. Aquí tienes algunos alias(derecha) y su correspondiente función (izquierda):

  • die -> exit
  • diskfreespace -> disk_free_space
  • fputs -> fwrite
  • chop -> rtrim
  • ini_alter -> ini_set
  • strchr -> strstr
  • sizeof -> count
  • close -> closedir
  • is_writeable -> is_writable
  • join -> implode
  • pos -> current
  • dir -> getdir
  • rewind -> rewinddir

A menudo puede resultar útil realizar una tarea mediante un buffer de salida y en lugar de utilizar echo ‘texto que sea‘, utiliza ob_start() para empezar a almacenar el texto en el búffer de salida. De esa forma solo muestras el resultado al finalizar los datos y ocupas menos la salida de pantalla. Para terminar la captura tienes ob_get_contents() yob_get_clean(), o ob_end_clean().

No debemos olvidar para un código optimo que si bien con @ podemos silenciar errores eso consume más CPU y  ralentiza mucho tu script, corrige el error o desactiva los errores desde php.ini o con error_reporting(0).

Nos podrán decir que somos muy exigentes pero existe una realidad que todos debemos conocer: Los array son más rápidos y consumen menos memoria que instanciar un objeto por lo que no debemos usar clases sin necesidad pues si bien puede que hagamos más legible el código y para algunos más “protegido” y “claro” también lo hacemos más lento.

Si bien en SEO se utilizan mucho los .htaccess para crear direcciones no existentes en el directorio real de la web reconocemos que en ciertas webs no es necesario y se puede hacer de otra forma.  Para ello configura la directiva AllowOverride None donde sea posible.

De no hacer eso el propio servidor web APACHE deberá recorrer en busca de posibles .htaccess todas las carpetas con un incremento de los recursos de maquina y CPU. Imagina una estructura de directorios como la siguiente:media/imagenes/2014/04/mi_imagen.jpg. Si AllowOverride está en All, antes de que Apache pueda servir el recurso mi_imagen.jpg, debe recorrer todos los directorios desde la raíz hasta “/04″ en búsqueda los archivos .htaccess por si en alguno de ellos se especificase alguna directiva sobre el objeto de la petición.

Para finalizar indicar que hay una serie de comandos que pueden ayudarte a comparar un código de otro para comprobar si has mejorado o no tu rutina tanto en velocidad (tiempo empleado) como en consumo de memoria.  Para comprobar la velocidad y la memoria que consume tu código te serán útiles las funciones memory_get_usage() y microtime() o time().

Si necesita una web bien programada y preparada para crecer en el futuro no dude en contactar con AGENCIA LA NAVE y seguro que acertará en su desarrollo. Escuchamos a nuestros clientes como si fuera nuestro propio negocio.

Enlaces: Aprendizaje práctico lenguaje maquina compilador online x86

 

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *