Estamos aprendiendo sobre conceptos importantes en el desarrollo de software de gran escala. Ya hemos hablado sobre el legacy code, la necesidad de refactorizar y qué es la deuda técnica. Para completar el tema, hablemos de la optimización.
¿Qué es la Optimización?
La optimización de código consiste en tomar código de un programa que ya está terminado y funcionando y tratar de mejorarlo para algún objetivo.
¿Qué se Intenta Mejorar?
- Velocidad de trabajo: lo rápido que se ejecuta el código.
- Velocidad de carga: cuánto tiempo tarda en cargarse el programa.
- Velocidad de respuesta del servidor: lo rápido que responde el servidor a las solicitudes.
- Estabilidad: lo estable que es el código.
- Volumen de código: la cantidad de código que se necesita para un programa.
Cada tipo de optimización se hace de forma diferente, por lo que usualmente se elige uno, el más importante: velocidad, seguridad o estabilidad. Luego, en iteraciones posteriores, se corrigen los demás.
A veces puede pasar que durante el proceso de optimización, se reescriba parte del código en otro lenguaje o se añada un nuevo framework. Con el tiempo, esto puede llevar a que el programa cambie completamente a otra biblioteca o a un lenguaje de programación diferente.
Optimización de la Velocidad de Trabajo
La optimización más frecuente es aumentar la velocidad de trabajo.
Por ejemplo, al desarrollar programas para edición de video, es importante que el usuario vea el resultado del trabajo de coloración inmediatamente. Si el programa tarda mucho en pensar cada vez, y hay otro software más rápido por ahí, con el tiempo la gente cambiará a ese otro programa y la empresa perderá dinero. Para evitar que esto suceda, se le pide al programador que acelere la función de procesamiento del filtro.
Ejemplos de cómo se puede lograr la aceleración:
- Escribir funciones que anticipen las acciones del usuario y calculen de antemano algunas situaciones.
- Enseñar al programa a tomar todos los recursos de la computadora por un tiempo. Esto es malo para las otras aplicaciones, pero la que está funcionando va rápido.
- Reemplazar comandos cortos pero complejos de un framework por comandos largos pero más simples para el compilador, que en conjunto se ejecutan más rápido.
- Crear tablas de búsqueda (look-up tables): por ejemplo, si un algoritmo necesita calcular el valor del seno $$sin(x)$$ cien mil veces con un paso conocido de antemano, se puede escribir una tabla con todos los valores necesarios. Entonces, el algoritmo no calcularía el seno desde cero, sino que buscaría el valor en la tabla y lo encontraría rápidamente.
- Insertar código en lenguaje ensamblador (assembler) para ejecutarlo directamente en el procesador, sin un compilador de alto nivel. Esto hace que el programa funcione mucho más rápido.
Optimización de la Velocidad de Carga
Esto es similar al punto anterior, pero con una diferencia: la mayoría de las veces se aumenta la velocidad de carga visible para el usuario, no la carga completa de todo el programa.
Apple utiliza este método con frecuencia: hacen sus programas para que el usuario vea la interfaz casi inmediatamente después de iniciarlos, pero en realidad se puede trabajar con el programa hasta después de 1-2 segundos. Esto se debe a que la prioridad es la velocidad de renderizado y carga de la interfaz, y los demás módulos del programa se cargan en segundo plano, lo que también lleva tiempo. Pero todo se ve como si el programa se hubiera iniciado al instante.
La otra situación es cuando realmente se aumenta la velocidad de carga de todo el programa. Para esto, los desarrolladores utilizan las capacidades de hardware del equipo y sacan algunas funciones a módulos separados. Si se necesitan, el programa los cargará, y si no, no hay necesidad de perder tiempo en eso al inicio.
Optimización de la Velocidad de Respuesta
Este tipo de optimización se realiza en sistemas con alta carga: bases de datos, servidores y sistemas de gestión. En ellos, es importante responder a las solicitudes lo más rápido posible, por lo que se optimizan las funciones con protocolos de red, formatos de almacenamiento para un acceso rápido a los datos y cálculos paralelos.
Al mismo tiempo, estos programas pueden tardar de 20 a 30 segundos, o incluso varios minutos, en cargarse. Nadie espera una carga instantánea de ellos, pero sí una reacción instantánea a las solicitudes.
Optimización para Estabilidad y Resistencia a Fallos
Si estamos escribiendo código para un hospital, donde se procesan en tiempo real los signos vitales de todos los pacientes, es importante que:
- El programa no se bloquee por datos ingresados incorrectamente.
- El programa funcione incluso si el servidor no responde o los sensores fallan.
- El programa no se caiga bajo carga.
En resumen, el programa debe ser muy difícil de romper.
Esto significa que necesitamos enfocarnos en el manejo de excepciones, la verificación de los valores ingresados y las solicitudes dobles para garantizar la recepción de datos precisos. Si un programa como este se cae cuando falla una de las bases de datos, definitivamente hay que optimizarlo.
Optimización para Reducir el Tamaño del Código
Los programas no solo funcionan en computadoras, también hay llaves, alarmas, hervidores inteligentes, sistemas de control de acceso, relojes, automóviles y mucho más. Poner un disco duro dentro de estos dispositivos es difícil, por lo que todos los programas se almacenan dentro de microcontroladores. La memoria allí es limitada.
O otra situación: tenemos el código de un programa para una versión de reloj inteligente, y necesitamos trasladar esas mismas funciones a otros relojes, donde el controlador es más sencillo y hay menos memoria.
En estos casos, los desarrolladores recurren a varios trucos: utilizan la compresión de código para que se descomprima en la memoria RAM en el momento de la ejecución; o simplifican el código, haciéndolo menos estable o resistente a fallos, pero más compacto.
También pueden utilizar funciones no documentadas del hardware para ampliar el espacio de almacenamiento del programa o sus componentes. Por ejemplo, en un teléfono antiguo, se puede colocar parte de los módulos del programa en el área de memoria donde se almacenan las melodías de llamada. Las melodías ocupan poco espacio, y queda parte libre. En este caso, el módulo se enmascara como melodía y se envía allí.
¿Qué Sigue?
Ahora estamos completamente preparados para trabajar en nuestro código antiguo: mantener el legado, refactorizar y optimizar diferentes funciones. Haremos todo en orden, pero otra vez.