Una vez tuvo sentido
En las décadas de los 70, 80 e incluso 90, las máquinas tenían muy poca memoria RAM (unos pocos kilobytes al principio) y una velocidad de CPU de pocos megahertz (8 MHz en el caso del Intel 8086). Un procesador Intel i7 de hoy en día supera los 2.66GHz. Se han superado las prestaciones en muchos órdenes de magnitud. Sin embargo hoy se sigue educando a los futuros programadores para que escriban un código óptimo en consumo de CPU y memoria, como si las máquinas tuvieran las prestaciones de hace 30 años.
Cuando las máquinas tenían pocas prestaciones en cuanto a hardware, los compiladores también eran muy limitados comparados con los de hoy en día. Los lenguajes de programación más usados eran de bajo nivel (sobre todo se usaba Ensamblador y después C) y los programadores tenían que gestionar la memoria y optimizar al máximo su código porque ninguna otra herramienta lo iba a hacer por ellos. En la década de los 80, los compiladores empezaban a implementar suficientes optimizaciones como para que el lenguaje Ensamblador fuese perdiendo popularidad. Cuando aparecieron máquinas virtuales de proceso como la JVM, los compiladores experimentaron una revolución, eran capaces de optimizar el código mejor que los propios programadores usuarios del lenguaje (Java, por ejemplo). Cualquier optimización que uno pretenda hacer en Java para ganar unos pocos ciclos de CPU es insignificante en términos de velocidad y a veces incluso contraproducente. El compilador lo hará mejor. Otra cosa es cambiar el diseño de los algoritmos para que en lugar de tener una complejidad cuadrática, pasen a ser logarítmicos, esto sí puede tener un impacto notable si estamos trabajando con cantidades de datos ingentes (si estamos hablando de buscar en 1000 registros en memoria, tampoco se va a notar nada).
Cuándo optimizar
¿Merece la pena intentar escribir código eficiente en consumo de CPU y memoria hoy en día? Hay algunos casos en los que sí, pero son muy pocos. Aquí algunos:
- Si estás escribiendo un programa para una máquina industrial con poca CPU y poca memoria, que además tiene un sistema operativo de tiempo real, seguramente lo tengas que hacer con Ensamblador o lenguaje C.
- Si estás programando para dispositivos móviles con pocas prestaciones para el volumen de datos/cálculos a computar al que van a ser sometidos, es importante diseñar el software para cumplir con la expectativa de los usuarios. Por ejemplo que no se congele el terminal o no colapse. Los videojuegos demandan mucha capacidad de cómputo, por eso en este nicho se buscan los especialistas, ya que el rendimiento es crítico. En una entrevista a David Brevik, el creador del Diablo, le oí decir que él escribió su propio motor de juego a bajo nivel porque necesitaba ganar en rendimiento para Diablo.
- Si la principal ventaja competitiva de tu producto es la velocidad, habrá que diseñar la arquitectura para que así sea, lo cual va más allá incluso de optimizar código. Un ejemplo reciente de esto fue el navegador Google Chrome. Cuando nació era increíblemente más rápido que los demás navegadores del momento, porque se creó para que así lo fuera y en pocos años se ha convertido en el líder del mercado.
Si no eres David Brevik ni estás trabajando en el siguiente browser más rápido del mercado, ¡olvídate de intentar optimizar tu código! Porque el código que “parece óptimo”, es menos legible, costará más trabajo entenderlo. Digo que “parece óptimo” porque ni siquiera medimos si hay ganancia. El primer paso para conseguir código óptimo es medir, hacer comparativas (benchmarks) para saber de forma empírica cuál es el que mejor rinde. Cuando diseñaron Chrome seguro que no dejaron el código que de primeras les parecía más rápido, sino que hicieron comparativas midiendo consumos. Si de verdad te preocupa el rendimiento, utiliza métricas, no ofusques el código solo por la intuición de que estás ganando en velocidad (ya que seguramente no es cierto).
El cuello de botella está en el acceso a datos
Recuerdo que esta frase se la escuché por primera vez al bueno de Esteban Manchado. El cuello de botella hoy en día está en la latencia de la red y del disco. Una petición de lectura mediante una API REST va a tardar más de 100 milisegundos, fácilmente serán 200, 300 o más. Imagina que el backend trae 10.000 registros de la base de datos y luego en código hacemos alguna operación que filtra, ordena o de alguna forma recorre esos registros, consumiendo CPU antes de devolverlos por la red. ¿Qué importancia tiene la optimización de nuestro código en esta operación en memoria?. He hecho aquí un pequeño experimento para ver lo que tardan en mi pc distintas operaciones. Recorrer un array de 10.000 elementos y leer una propiedad de cada elemento (lo que seria hacer un filtrado simple), tarda menos de 1 milisegundo. Ordenar el array con merge sort tarda unos 50 milisegundos y ordenarlo con bubble sort tarda unos 1000 milisegundos. ¿Tiene sentido que intentes optimizar el código que filtra datos en memoria? Es evidente que no. Sin embargo tiene sentido entender de complejidad de los algoritmos para darse cuenta de que operaciones cuadráticas como el bubble sort tienen un impacto notable, aunque de nuevo dependerá del volumen de datos que estamos moviendo. Si son 200 registros (y no se esperan que aumente el orden de magnitud), no te molestes.
A veces escribimos código oscuro en nombre de la optimización, por ejemplo para convertir un entero en un string:
let text = number + "";
Aun cuando es mucho más explícito lo siguiente:
let text = number.toString();
¿Cuántos miles o millones de veces tendría que ejecutarse esa conversión para que llegásemos a notar algún tipo de diferencia de rendimiento?, ¿lo notaríamos alguna vez?
El código es más fácil de entender cuanto mejor refleja la intención de quien lo escribió, para lo cual se requiere ser muy explícitos escribiéndolo.
Mejor código legible que óptimo
Es preferible programar para que el código sea legible que para que sea óptimo, aunque no esta de más conocer la complejidad de los algoritmos que podemos usar en librerías, para que escojamos el mejor. Por desgracia hay que escoger entre legible y óptimo, porque son características bastante excluyentes.
Cuando no estamos seguros del volumen de cómputo que a futuro podemos necesitar, yo prefiero quedarme con el código que es suficientemente bueno para hoy aunque mañana se quede corto. Si tengo la idea de que es muy probable que haya que mejorar el rendimiento en el futuro, prefiero anotarlo en el documento de gestión de deuda técnica y dejar el código simple. Prefiero un enfoque lean con buena planificación y estrategia, que la sobreingenieria prematura.