¿Un código siempre se puede optimizar o existe un límite que sería la manera perfecta de hacerlo?

Primero definamos el término “optimización”.
Merriam-Webster (en inglés)
Definition of “optimization”
An act, process, or methodology of making something (such as a design, system, or decision) as fully perfect, functional, or effective as possible.


En Español:
Un acto, proceso o metodología de hacer algo (como un diseño, sistema o decisión) tan perfecto, funcional o efectivo como sea posible.
Llevando dicha definición al terreno del software esto involucra varios aspectos y no únicamente relacionados al hardware, tiempo de ejecución (runtime) o similares.
Para mi existen varios tipos de optimizaciones desde el punto de vista de software, incluso algunos de ellos pueden derivar a otros o estos pueden estar muy relacionados entre si.

Aquí algunos más comunes:

  • Optimización de algoritmos y estructuras de datos
  • Optimización del diseño de software
  • Optimización del rendimiento de software
  • Optimización de velocidad (carga, ejecución, procesos, etc)
  • Optimización del tamaño de la aplicación (espacio en disco, optimización de dependencias, static binaries and stripping, etc)
  • Refactorización y simplificación de código (que podría considerarse una forma de optimización)
  • Optimización de recursos (orientado a hardware por lo general)
Recalcar también que estos tipos de optimización pueden perfectamente agruparse como “program/software optimizations” (optimizaciones de programa o software) y hardware optimizations (optimizaciones de hardware) este último relacionado a componentes internos de un ordenador (escritorio/móvil/servidor). Por ejemplo optimizaciones de software para arquitectura de CPUs en específico, firmware, entre otros.
Si me preguntas de forma muy general:
¿Un código siempre se puede optimizar?
Por lo general sí, recuerda que una pieza de software tiende a consolidarse con el tiempo y la manera de hacerlo es a través del mantenimiento del mismo durante su ciclo de vida, lo cual implica nuevas características, correcciones de errores y optimizaciones. Optimizaciones que por lo general pueden llevarse a cabo por diferentes razones ya sea feedback de sus consumidores, debido a correcciones de errores o vulnerabilidades, lentitud en ejecución, bajo rendimiento, ineficiencia procesando ciertos tipos de datos, alto consumo de recursos (CPU, RAM, disco, red), entre otros.
Como vez no es fácil de abordar. Ya que para llevar a cabo alguna optimización hay que tener en cuenta que aspecto(s) del software se quiere optimizar y porque razón(es). Además esto demanda un costo (tiempo, dinero y recursos humanos).
Un ejemplo de optimización de algoritmos
Dicho lo anterior, ahora un ejemplo clásico de optimización sobre el terreno del software.
La idea es entender sobre recursión. Es decir, cuando puede ser conveniente y cuando no, así como aprender a optimizar un árbol recursivo.
Aquí el clásico ejemplo en C en su versión no optimizada que puede expresarse como un árbol recursivo cuya complejidad es O(2^n).
Si compilamos y ejecutamos el código utilizando una complejidad de 20 conseguimos casi instantáneamente como resultado 6765.
Si incrementamos el valor a 30 conseguimos 832040 con una ligera lentitud de respuesta. Pero si por ejemplo incrementamos aún más el valor a, digamos 50, luego la respuesta será 12586269025, sin embargo esta vez la aplicación tardará casi un minuto en mostrar el resultado.
Esto sucede porque si el valor de entrada se incrementa también lo hace la complejidad que el algoritmo en cuestión tiene que manejar para calcular el valor resultante, ya que la función se llama de forma recursiva dos veces y cada llamada multiplicada por su valor de entrada. Imagina si incrementamos el valor aún más.
Optimizando el algoritmo
Hay varias formas de optimizar el algoritmo. Como la preocupación aquí es la ineficiencia debido al número exponencial de cálculos repetidos (abundante redundancia), podríamos por ejemplo utilizar memos (o memoization  )aprovechando estructuras de datos como Arrays o Tablas Hash (o también llamados diccionarios de datos) para memorizar valores y sus estados, y así evitar cálculos repetitivos en conjunto con la recursividad de funciones utilizada.
Sin embargo, aquí hay una alternativa de solución utilizando iteración o (bucles).

Si bien no es necesario tener una tabla dinámica larga (Tabla Hash por ejemplo) para almacenar todos estos estados, sólo sería necesario almacenar los dos estados anteriores. Por lo tanto, se puede optimizar aún más para reducir la complejidad de O(2^n) a O(1).

Aquí la alternativa de solución utilizando iteración.
Compilamos y ejecutamos el programa y obtendremos casi inmediatamente 12586269025 para una complejidad de 50. Podemos hacer lo mismo calculando 80 lo cual resulta en un valor de 23416728348467685 pero esta vez la ejecución es casi inmediata.
El método de optimización utilizado se basa en la siguiente ecuación llamada “State Transfer Equation” (Ecuación de transferencia de estado) que muestra la forma matemática para describir la estructura del problema:
Aquí nosotros queremos que “f” de “n” sea un estado “n”, y ese estado “n” se transfiera de la suma de los estados “n” menos 1 y “n” menos 2. A eso le llamamos “transferencia de estado”.

Por lo tanto, podrás ver que según la ecuación de “transición de estado de la secuencia de Fibonacci”, el estado actual sólo está relacionado con los dos estados anteriores. Es por eso que aquí no es necesario tener una larga Tabla DP (Dynamic Programming Tabla) como se ha mencionado anteriormente.

Conclusión
La optimización de código no sucede porque si, hay implicancias que lo potencian, como se ha mencionado anteriormente. Y por lo general tampoco se empieza a optimizar código desde un principio (a menos que tengas la completa libertad, tiempo y/o dinero suficiente para hacerlo o porque la situación lo requiera), esto tiende a suceder a posteriori.
Sin embargo, un código de rápida ejecución y eficiencia no necesariamente significa (por lo general sí, ver Rust por ejemplo) que consumirá la menor cantidad de recursos (ver Java JVM). O un código rápido y optimizado al consumir la menor cantidad de recursos no necesariamente significa que sea seguro en memoria (ver C/C++).

Deja un comentario