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.

Expresión analítica y geométrica del producto escalar

El producto escalar es una operación entre dos vectores que retorna un escalar, es decir, un número real. Existen dos definiciones de esta operación que darán el mismo resultado, aunque inicialmente no sea muy intuitivo que así sea: la analítica y la geométrica. Veamos la primera de ellas:

Dados dos vectores del espacio vectorial ℝn, u = (u1, u2, …, un) y v = (v1, v2, …, vn), se define el producto escalar de ambos, u · v como:

u·v = u1 · v1 + u2 · v2 + … + un · vn

En todo espacio vectorial euclídeo, y por lo tanto normado, podemos usar también la definición geométrica, esta nos dice que el producto escalar de dos vectores es el producto del módulo (o norma) de cada uno de ellos por el coseno del ángulo que forman:

Producto escalar

(2)

A continuación, veamos dos ejemplos sencillos en el plano cartesiano, ℝ2, para ver que ambas formas arrojan el mismo resultado. Ya nos advirtió Johan Cruyff: “Un palomo no hace verano”, por lo que estos dos ejemplos no pretenden demostrar nada sino ejemplificar este concepto poco intuitivo. El primer ejemplo consistirá en los vectores u = (2, 2) y v = (2, -2):

ejemplo 1El ángulo entre ambos es, obviamente, 90º, pues cada uno hace un ángulo de 45º con el eje de las abscisas. Como cos(90º) = 0, según la segunda definición el producto escalar también es 0, pues cualquier número multiplicado por 0 es 0. El mismo resultado obtenemos con la forma analítica:

u·v =2·2 + 2·(-2) = 4 – 4 = 0

El segundo ejemplo estará constituido por los vectores u = (1, 1) y v = (2, 0):

Segundo ejemploEn este caso, el ángulo entre ambos es de 45º. El coseno de 45º es √2 / 2, no es tan redondo como el anterior, no obstante, ambas definiciones deben coincidir:

u·v = 1·2 + 1·0 = 2

Para poder calcular según la segunda definición necesitamos saber, en primer lugar, el módulo o norma de cada vector:

modulo de umodulo de vEfectivamente, coinciden:

con cosenoFinalmente, destacar dos aspectos de la definición geométrica. El primero de ellos es que nos permite obtener el ángulo que forman dos vectores pues, obviamente, al aislar el coseno en la ecuación (2) obtenemos:

angulo de dos vectores

(3)

El segundo es que la interpretación geométrica del producto escalar de dos vectores es la proyección de uno sobre otro multiplicada por el módulo de este último. Con los vectores del último ejemplo, u = (1, 1) y v = (2, 0), la proyección de u sobre v sería:

proyeccion de u sobre v

w es la proyeccion de u sobre v

w es un vector cuyo modulo es la distancia desde el origen hasta la proyección de u sobre v; la podemos calcular mediante trigonometría básica:

resiltado de la proyeccion de u sobre v

Atención: el módulo de w no es el producto escalar entre u y v. Para que lo sea, es necesario multiplicar por el módulo del segundo (v):

Siendo esta exactamente la definición geométrica que vimos en la ecuación (2). En definitiva, podemos ver que:

Producto vectorial con proyección de vectores

Producto vectorial con proyección de vectores

Donde:

Proyección de u sobre v

Proyección de u sobre v

Esta fórmula no sale de la nada (aunque sí habrá salido en muchas chuletas), acabamos de ver que el vector w es la proyección del vector u sobre v, como sabemos que:

Por (3):

Para aislar el módulo de w, olvidémonos del coseno y centrémonos en la segunda y tercera igualdad: multiplicando por el módulo de u en ambos lados de la ecuación nos resulta la fórmula de la proyección de u sobre v:

A modo de anécdota, para saber cuál es el vector w, debemos calcular el vector unitario de v, pues ambos van en la misma dirección, y multiplicarlo por |w|.

Cómo simplificar una relación Many-To-Many consigo misma

En esta breve entrada expongo cómo el programador puede facilitarse un poco su actividad cuando en Doctrine establezca una relación de una entidad consigo misma. El código que se expone es de una entidad del framework Symfony, pero fácilmente se puede adaptar a cualquier entorno PHP donde, eso sí, se esté empleando este conocido ORM.

Según la documentación de Doctrine, el código para la relación Many-To-Many (de muchos a muchos) de una entidad que se referencia a si misma, por ejemplo un producto que puede tener otros productos relacionados, sería:

class Product
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ManyToMany(targetEntity="Product", mappedBy="myProducts")
     */
    private $relatedWithMe;

    /**
     * @ManyToMany(targetEntity="Product", inversedBy="relatedWithMe", fetch="EAGER")
     * @JoinTable(name="related_products",
     *      joinColumns={@JoinColumn(name="product_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="related_product_id", referencedColumnName="id")}
     *      )
     */
    private $myProducts;

    public function __construct()
    {
        $this->relatedWithMe = new ArrayCollection();
        $this->myProducts = new ArrayCollection();
    }

    public function addRelatedWithMe(Product $relatedWithMe): self
    {
        if (!$this->relatedWithMe->contains($relatedWithMe)) {
            $this->relatedWithMe[] = $relatedWithMe;
            $relatedWithMe->addMyProduct($this);
        }

        return $this;
    }

    public function removeRelatedWithMe(Product $relatedWithMe): self
    {
        if ($this->relatedWithMe->contains($relatedWithMe)) {
            $this->relatedWithMe->removeElement($relatedWithMe);
            $relatedWithMe->removeMyProduct($this);
        }

        return $this;
    }

    /**
     * @return Collection|Product[]
     */
    public function getMyProducts(): ?Collection
    {
        return $this->myProducts;
    }

    public function addMyProduct(Product $myProduct): self
    {
        if (!$this->myProducts->contains($myProduct)) {
            $this->myProducts[] = $myProduct;
        }

        return $this;
    }

    public function removeMyProduct(Product $myProduct): self
    {
        if ($this->myProducts->contains($myProduct)) {
            $this->myProducts->removeElement($myProduct);
        }

        return $this;
    }

Esta estructura con dos propiedades presenta un inconveniente: las consultas a la base de datos para buscar todos los productos relacionados pueden ser algo más complejas pues si, por ejemplo, el producto 1 está relacionado con el 2, el ser bidireccional implica que el 2 también lo está con el 1, pero en la tabla pivote related_products sólo tendremos el registro (1, 2) o el (2, 1).

Una solución que nos permite mantener las consultas a la base de datos sencillas y un código PHP nítido, es crear una relación unidireccional con un “truco” o hack de tan sólo dos líneas:

class Product
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ManyToMany(targetEntity="Product")
     * @JoinTable(name="related_products",
     *     joinColumns={@JoinColumn(name="product_id", referencedColumnName="id")},
     *     inverseJoinColumns={@JoinColumn(name="related_product_id", referencedColumnName="id")}
     *      )
     */
    private $relatedProducts;

    public function __construct()
    {
        $this->relatedProducts = new ArrayCollection();
        $this->stamps = new ArrayCollection();
    }

    /**
     * @return array
     */
    public function getRelatedProducts()
    {
        return $this->relatedProducts->toArray();
    }

    /**
     * @param  Product $product
     * @return void
     */
    public function addRelatedProduct(Product $product)
    {
        if (!$this->relatedProducts->contains($product)) {
            $this->relatedProducts->add($product); (1)
            $product->addRelatedProduct($this);
        }
    }

    /**
     * @param  Product $product
     * @return void
     */
    public function removeRelatedProduct(Product $product)
    {
        if ($this->relatedProducts->contains($product)) {
            $this->relatedProducts->removeElement($product);
            $product->removeRelatedProduct($this);
        }
    }

La primera línea marcada en negrita es la que inserta el segundo registro en la base de datos; de manera que para consultar los productos relacionados de un producto no tendremos que consultar dos propiedades. El precio a pagar será que en la tabla tendremos datos que podrían considerarse duplicados, pero hará la programación más sencilla y, de todas formas, con un ORM no se puede ser muy escrupuloso con las formas normales.

Esta idea también podría aplicarse al ejemplo inicial, pero si añadimos ambos registros para simplificar las consultas, entonces nos encontramos con que se añade complejidad a la clase pues ambas propiedades, $myProducts y $relatedWithMe, se deberían mantener sincronizadas y actualizadas.

Por lo tanto, esta relación unidireccional de una entidad consigo misma, aunque no aparece en la documentación oficial, es una solución que nos permite crear un código más claro y más fácil de mantener.


(1) Los métodos contains() y add() son propios de la clase ArrayCollection que implementa Doctrine.

Preferencia de las operaciones según Casio

Por la preferencia de operadores, así resolvemos la siguiente operación:

O también:

Ahora bien, la Casio fx-83ES Plus opina diferente:

Casio Fx 82ES PLUS error por la preferencia de operaciones

En primer lugar calcula el paréntesis para, a continuación, multiplicarlo por el denominador, resultado que, finalmente, dividirá el numerador.

En cambio, curiosamente muestra el resultado correcto cuando expresamos la división como una fracción:

Si introducimos exactamente lo mismo que hemos visto en la primera imagen en la Casio fx-570SP X II, una calculadora de gama más alta, nos sigue mostrando el mismo resultado, 1, pero esta vez ella misma añade unos ilustrativos paréntesis que hacen el resultado correcto:

Casio fx 570SP X II

Si seguimos el camino que nos indica, es decir, si añadimos paréntesis, sí obtenemos el resultado esperado, en cualquiera de las dos calculadoras, por ejemplo:

En definitiva, el orden de los operadores conduce a la resolución que se ha descrito inicialmente. Ahora bien, en este tipo de operaciones, estas calculadoras no siguen la jerarquía matemáticamente correcta. De hecho, en el manual de la Casio fx-570SP II encontraremos el caso concreto del uso de paréntesis con omisión del signo de multiplicación, y en el de ambas la jerarquía de las operaciones.

Casio Fx 82ES PLUS operacion correcta

Aquí no hemos omitido el signo de multiplicación

Parece ser que, el acrónimo tan usado en informática: RTFM (Read The Fucking Manual) también aplica a ordenadores tan sencillos como una calculadora.

i elevado a i

Una serie de artículos acerca de la exponenciación con números complejos, que di por finalizada en su tercera entrega, y a los que el lector puede recurrir si no entiende algo de lo que en el presente artículo se explica, voy a ampliarla para el curioso caso de ii, y digo curioso pues el resultado es un número real.

Vimos que la generalización de la exponenciación, ab, sean a y b reales1 o complejos, es:

Exponenciación generalizada

En el presente caso, a = i, y si bien el logaritmo complejo existe, aplicando la fórmula de Euler, podremos obtener la forma exponencial para calcular fácilmente ii. En primer lugar, vamos a expresar i en forma polar para obtener dos datos que necesitaremos: el módulo y el argumento. El módulo, obviamente, será 1 y el argumento será π/2 radianes, pues i forma un ángulo recto con la parte real o eje de las ordenadas:

El número imaginario en el plano cartesiano complejo

Por lo tanto, i = 1π/2 en polar, mientras que en binómica es (0 +1i). De esta última, gracias a que conocemos tanto el módulo como el argumento, podemos pasar a la trigonométrica:

Expresión que se deduce por trigonometría:

forma trigonometrica del número complejo

Gracias a la fórmula de Euler donde x es el argumento:

Formula de EulerPodemos convertir, finalmente, la unidad imaginaria a la forma exponencial:

Por lo que:

Como i = √-1:

Y ya hemos calculado ii.

En rojo el número i y en azul r = ii en el plano complejo.


Si a ∈ ℝ, a > 0

Ecuación de la asíntota oblicua

Es frecuente que a los alumnos se les enseñe que, si una función tiene un asíntota oblicua que viene dada por y = ax + b, los términos a y b se calcularán a partir de dos fórmulas que deben memorizar:

término b

(1)

término a

(2)

Pero, ¿de dónde provienen? Si tenemos poca memoria y/o preferimos entender el porqué de las cosas, algo bastante conveniente en matemáticas, debemos tener en cuenta que, para una x infinitamente grande, por las propiedades de los límites, se cumplirá f(x) – y = 0:

ecuación asintota oblicua

(3)

Estas propiedades son debidas a que los límites cumplen con las condiciones de linealidad, vamos a aplicarlas paso a paso para ver cómo se obtiene el término b:

obtencion termino bAhora sólo resta aislarlo y obtendremos la fórmula inicial (1). En la ecuación (3), y dado que b no depende de x, podemos aislar a y obtendremos la fórmula (2).

 

Funciones

En azul la función f(x) = x²/(x+1) En rojo su asíntota oblicua y = x – 1

Cambiando un poco de tema, pero no demasiado, puede verse que si a = 0, entonces se trata de una asíntota horizontal, por lo que podemos ver las asíntotas horizontales como un caso particular de las oblicuas.