Optimizaciones inútiles del código en PHP

En las aplicaciones donde la velocidad no es determinante – categoría a la que pertenecen muchas aplicaciones web – el código, antes que optimizado, debe estar estructurado. Como los requerimientos están cambiando frecuentemente, el tiempo del programador es mucho más importante (y costoso) que el tiempo de CPU. Además, lo que puede parecernos una optimización, a veces no lo es. Veamos un ejemplo con los desplazadores de bits.

La teoría nos dice que tanto la operación de multiplicación como la de división requieren cierto tiempo de computación, especialmente esta última. Ahora bien, si esta operación se hace con una potencia de 2, se convierte en algo trivial para la CPU gracias a los desplazadores, unos sencillos bloques combinacionales formados por unas pocas puertas lógicas.

Los desplazadores tienen una señal de entrada y una de salida de n bits ambas. La señas de salida se obtiene desplazando los bits de entrada m veces hacia la derecha o hacia la izquierda El desplazamiento a la izquierda equivale a multiplicar por 2m mientras que a la derecha realiza la división entera por 2m. En el caso del desplazamiento a la derecha o división, en función del valor que se asigne a los «nuevos» bits, existen dos tipos de desplazadores:

  • Lógicos: Se ponen a 0.
  • Aritméticos: Se ponen al mismo valor que el bit de más peso de la entrada. Sirven para que los números codificados en complemento a dos conserven el signo.
Desplazador aritmético y lógico de 4 bits

Dos desplazadores de 4 bits: El de arriba lógico y el de abajo aritmético. Ambos desplazan 01 (2(10) bits a la derecha, i.e., dividen por 2.

El ordenador, internamente, codifica los números enteros en complemento a dos, veamos entonces unos ejemplos de desplazamiento de bits con números positivos de 8 bits (el subíndice expresa la base):

  • Para dividir 5 entre 2 se corren los bits una posición a la derecha:
    00000101(Ca2 ⇒ >> 1A ⇒ 00000010 = 2(10
    Efectivamente, el resultado de la división entera es 2.
  • Para multiplicar 5 por 16 se desplazan los bits 4 posiciones a la izquierda,
    pues 16 = 24:
    00000101(Ca2 ⇒ >> 1A ⇒ 01010000 = 80(10
    Así es: el resultado de esta multiplicación es 80.

Por lo tanto, al disponer PHP, como muchos otros lenguajes, de desplazadores, si sabemos que vamos a multiplicar o a realizar la división de un número natural por una potencia de 2, aparentemente podríamos emplearlos para mejorar la velocidad. El siguiente código es una evaluación comparativa para comprobar hasta que punto los desplazadores superan en velocidad la multiplicación aritmética típica:

$start = 1;

$timestart = microtime(1);
for ($i = 0; $i < 10000000; $i++) {
    $result2 = $start << 2;
}
echo microtime(1) - $timestart;

$timestart = microtime(1);
for ($i = 0; $i < 10000000; $i++) {
    $result1 = $start * 4;
}
echo microtime(1) - $timestart;
echo "\n";

Al ejecutar este código en una máquina con las siguientes características:

  • PHP 7.0.32-0ubuntu0.16.04.1 (cli) ( NTS )
  • CPU: Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz

Obtenemos el siguiente resultado:

0.73733711242676
0.71091389656067

Sorprendente, ¿verdad? Los desplazadores de bits son más lentos que la multiplicación. Como podría tratarse de algo puntual, es conveniente ejecutarlo muchas veces, pero al hacerlo, se obtiene sistemáticamente el mismo resultado: la multiplicación es más rápida.

A continuación, probemos en un ordenador diferente con las siguientes características:

  • PHP 7.1.19 (cli) (built: Jun 20 2018 23:24:42) (ZTS MSVC14 (Visual C++ 2015) x64)
  • CPU (Intel(R) Core(TM) i5-4460S CPU @2.90GHz)

La CPU es muy similar, de la misma familia, Sandybridge, pero es más significativo el hecho de que se trata de un Windows ejecutando una versión posterior de PHP. Los resultados del test son:

0.24960112571716
0.28080010414124

En esta máquina sí se cumple la teoría. Además, lo hace de forma consistente en diferentes ejecuciones. Eso sí, por una diferencia mínima, pues se han producido en cada caso 10 millones de multiplicaciones; difícilmente semejante sucesión de multiplicaciones se producirá en una situación real.

La pregunta, entonces, sería: ¿Por qué sucede esto? Seguramente las razones son:

  1. PHP es un lenguaje interpretado y, por lo tanto, hay un intérprete por «encima» del código PHP sobre el que no tenemos ningún control y que consume una parte muy importante de los recursos.
  2. Probablemente, el intérprete consume tanto tiempo en mantener la variable contador $i como para hacer las multiplicaciones típicas. En el caso del desplazamiento de bits, tal vez incluso más recursos.
  3. Las multiplicaciones entre enteros no son lentas en las CPUs modernas.

Este caso de la falsa optimización con los desplazadores de bits es un buen ejemplo de como a veces los programadores olvidan que por encima de su código existen variables igual o incluso más determinantes. La situación empeora cuando dedican mucho tiempo a ganar la batalla de los microsegundos a costa de producir un código menos claro y, por lo tanto, más difícil de mantener, e incluso descuidan la de los mili segundos y hasta la de las centésimas.

Otro caso de optimización inútil, en este caso de memoria y no de tiempo de CPU, es cierto uso de la función unset(), con el que algunos programadores, incluso experimentados, creen que liberarán memoria pero lo único que van a conseguir, en la mayor parte de casos, es consumir innecesarios ciclos de CPU. Esto es debido al funcionamiento de PHP: sólo procederá a liberar el espacio consumido por las variables destruidas mediante unset() cuando el script alcance el máximo de memoria permitido (valor que se especifica en el fichero php.ini), priorizando así la velocidad de ejecución por encima del consumo de memoria. Por lo tanto, si el script no está dando problemas con su consumo de RAM, el empleo de esta función no tan sólo no aporta sino que resta.

Algunas de las optimizaciones típicas para ganar en las divisiones más pequeñas del segundo son:

  • En las comparaciones, usar === y !== en vez de == y !=, respectivamente, para evitar las conversiones de tipo.
  • Para las cadenas de texto, usar las comillas simples ‘ ‘ en vez de las dobles » « cuando el intérprete no tiene que «trabajar» la cadena, es decir, cuando esta contiene solo datos en vez de código y datos. Las dobles permiten hacer esto:
    $foo = "Hello, Mr. $bar";

    Mientras que para esta asignación:

    $bar = "Bean";

    Hubiera bastado con las simples.

Como se indicaba, no es infrecuente que un programador ponga enfásis en estos aspectos mientras pierde millones de ciclos de CPU en otros. El acceso a datos externos es la causa de muchos de estas pérdidas, veamos algunos ejemplos en las bases de datos relacionales y el SQL:

  1. Emplear SELECT * en vez de seleccionar sólo los campos necesarios.
  2. En general, las consultas que distan mucho de ser óptimas.
  3. Las bases de datos mal diseñadas que no respetan las formas normales.
  4. Relacionado con los dos puntos anteriores:
    • Las uniones entre tablas por campos que no tienen clave foránea o ni tan siquiera un índice.
    • Las falta de índice por un campo usado frecuentemente en las condiciones de las consultas. Por ejemplo, si una tabla dispone de un campo para indicar si un registro está activo o no, y casi en todas las consultas hacia esa tabla se considera si su valor es 0 o 1, sería buena idea añadirle un índice.
  5. El empleo de un ORM. Uno de sus principales inconvenientes es el gran número de consultas que lanza. Otro es que cuando las relaciones entre entidades tienen cierta complejidad, este sistema nos obliga a violar el punto 3 de este listado. En general, no es muy coherente usar un ORM y sufrir por cada milésima.

Sin duda esta lista podría tanto ampliarse como detallarse, lo que daría para, al menos, un artículo exclusivo, lo cual no es el objetivo del presente, sino aclarar cómo y dónde se producen las mayores ineficiencias y que no tiene mucho sentido dejarlas mientras la preocupación por las menos importantes consume mayor parte del tiempo del programador. Si además estas últimas desembocan en un código menos legible, y por lo tanto más difícil de mantener, el tiempo total del desarrollo que consume el proyecto a lo largo de su ciclo de vida aumentará aún más.

Deja un comentario

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.