Optimización de Código

Definición y Propiedades

Optimización de código es cualquier método de modificación de código para mejorar la calidad y eficiencia del código. Un programa puede optimizarse para que se convierta en un tamaño más pequeño, consuma menos memoria, ejecute más rápidamente o realice menos operaciones de entrada/salida.

Los requisitos básicos que deben cumplir los métodos de optimización son que un programa optimizado debe tener la misma salida y efectos secundarios que su versión no optimizada. Este requisito, sin embargo, puede ignorarse en el caso de que el beneficio de la optimización se estime más importante que las consecuencias probables de un cambio en el comportamiento del programa.

Tipos y niveles de optimización

La optimización puede ser realizada por optimizadores automáticos o programadores. Un optimizador es una herramienta de software especializada o una unidad incorporada de un compilador (el llamado compilador de optimización). Los procesadores modernos también pueden optimizar el orden de ejecución de las instrucciones de código.

Las optimizaciones se clasifican en optimizaciones de alto y bajo nivel. Las optimizaciones de alto nivel generalmente las realiza el programador, que maneja entidades abstractas (funciones, procedimientos, clases, etc.).) y tiene en cuenta el marco general de la tarea de optimizar el diseño de un sistema. Optimizaciones realizadas a nivel de bloques estructurales elementales de código fuente: bucles, ramas, etc. – también se suelen denominar optimizaciones de alto nivel, mientras que algunos autores las clasifican en un nivel separado (“medio”) (N. Wirth?). Las optimizaciones de bajo nivel se realizan en la etapa en la que el código fuente se compila en un conjunto de instrucciones de máquina, y es en esta etapa que generalmente se emplea la optimización automatizada. Sin embargo, los programadores ensambladores creen que ninguna máquina, por perfecta que sea, puede hacer esto mejor que un programador experto (sin embargo, todos están de acuerdo en que un programador pobre lo hará mucho peor que una computadora).

Qué optimizar

Con la optimización manual de código, uno se enfrenta a otro problema: no solo necesita saber cómo se debe hacer exactamente la optimización, sino también qué parte particular del programa se debe optimizar. Debido a varias razones (operaciones de entrada lentas, la diferencia en la velocidad de trabajo de un operador humano y una computadora, etc.), el 90% del tiempo de ejecución de un programa se gasta en ejecutar solo el 10% del código (esta afirmación es bastante especulativa, con el principio de Pareto como un argumento bastante dudoso, pero A. Tanenbaum lo hace sonar convincente). Dado que la optimización toma tiempo adicional, aparte del tiempo que ha dedicado a desarrollar el programa, es mejor que se centre en optimizar este 10% del código, que es crítico para el tiempo, en lugar de intentar optimizar todo el programa. Estos fragmentos de código se conocen como cuellos de botella y pueden ser detectados por utilidades especiales, los perfiladores, que pueden medir el tiempo que tardan varias partes del programa en ejecutarse.

En la práctica, sin embargo, la optimización generalmente se realiza después de la etapa de programación “caótica” (incluidos métodos como “Copiar y pegar”, “veremos más adelante”, “está bien de esta manera”), y por lo tanto es una mezcla de optimización como tal, refactorización y corrección de errores: simplificación de construcciones “queer” como strlen(path.c_str ()), condiciones lógicas como (a.x != 0 & & a. x != 0), y así sucesivamente. Los perfiladores son de poca ayuda con este tipo de optimización. Sin embargo, puede detectar estos problemas con herramientas de análisis estático, es decir, herramientas diseñadas para buscar errores semánticos, basándose en un análisis profundo del código fuente. Como puede ver en el ejemplo mencionado anteriormente con la condición extraña, el código ineficiente puede aparecer como resultado de errores (como un error de impresión en nuestro ejemplo, donde a.x != 0 & & a. y != 0 debería ser en su lugar). Un potente analizador estático detectará dichos fragmentos de código y llamará su atención sobre ellos mediante la producción de mensajes de advertencia.

Resultados buenos y malos de la optimización

En la programación, casi todo debe tratarse desde el punto de vista de la racionalidad: la optimización no es una excepción. Existe la creencia de que el código escrito por un programador ensamblador sin experiencia es 3-5 veces más lento que el código generado por el compilador (Zubkov). Es ampliamente conocida una frase de Knuth con respecto a las primeras optimizaciones de bajo nivel (como los intentos de ahorrar en operadores o variables): “La optimización prematura es la raíz de todo mal”.

La mayoría de los programadores no se quejan de las optimizaciones realizadas por el optimizador, algunas de las cuales son convencionales y obligatorias. Como, por ejemplo, la optimización de llamadas de cola en lenguajes funcionales (la llamada de cola es un caso especial de recursividad, que se puede representar como un bucle).

Sin embargo, uno debe entender que múltiples optimizaciones complejas a nivel de código máquina pueden causar una gran ralentización de la compilación. El beneficio que le permiten obtener puede ser demasiado insignificante, en comparación con las optimizaciones de diseño de sistemas generales (Wirth). También hay que tener en cuenta que los lenguajes modernos, con todos sus “adornos” sintácticos y semánticos, tienen muchos matices y sutilezas, por lo que un programador que no está familiarizado con ellos puede sorprenderse con un resultado de optimización.

Por ejemplo, tomemos C++ y la llamada Optimización de Valor de retorno, cuando el compilador evita copiar un objeto temporal devuelto por una función. Debido a que el compilador omite la copia, este método también se llama “Copiar elisión”. Así, el siguiente código:

#include <iostream> struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; }}; C f() { return C();} int main() { std::cout << "Hello World!\n"; C obj = f();}

puede tener varias salidas:

Hello World!A copy was made.A copy was made.Hello World!A copy was made.Hello World!

Por extraño que parezca, las tres versiones son válidas porque el estándar de lenguaje permite omitir llamadas de un constructor de copia en tales casos, incluso si el constructor tiene efectos secundarios (§12.8 Copying Class Objects, Párrafo 15).

Conclusión

Por lo tanto, siempre debemos considerar optimizar el código del programa utilizando utilidades especializadas siempre que sea posible, sin embargo, hacerlo con mucho cuidado y estar listos para la probabilidad de trucos inesperados del compilador a veces.

PVS-Studio

En el analizador estático PVS-Studio se implementa un conjunto de diagnósticos que le permiten encontrar algunas situaciones en las que se puede optimizar el código. Sin embargo, PVS-Studio, como cualquier otro analizador estático, no puede servir como reemplazo de las herramientas de perfilado. Solo los analizadores de programa dinámicos son capaces de identificar los cuellos de botella. Los analizadores estáticos no saben qué datos de entrada obtienen los programas y con qué frecuencia se ejecuta un código en particular. Es por eso que estamos diciendo que el analizador sugiere implementar algunas “micro-optimizaciones” de código, que no garantizan las ganancias de rendimiento.

A pesar de la desventaja considerada, PVS-Studio analyzer actúa como un buen complemento para las herramientas de perfilado. Además, cuando se trata de advertencias de PVS-Studio, relacionadas con la optimización, el código a menudo se vuelve más simple y más corto. Este efecto se considera con más detalle en el artículo “Explorando Microoptimizaciones Usando Código Tizen como ejemplo”.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.