Articulo de referencia

Optimización del programa

En informática , la optimización de programas , la optimización de código o la optimización de software es el proceso de modificar un sistema de software para que algún aspecto ...

En informática , la optimización de programas , la optimización de código o la optimización de software es el proceso de modificar un sistema de software para que algún aspecto del mismo funcione de manera más eficiente o utilice menos recursos. [ 1 ] En general, un programa informático puede optimizarse para que se ejecute más rápidamente, o para que sea capaz de operar con menos memoria u otros recursos, o para que consuma menos energía.

Descripción general

Aunque el término "optimización" deriva de "óptimo", [ 2 ] lograr un sistema verdaderamente óptimo es raro en la práctica, lo que se conoce como superoptimización . [ 3 ] La optimización generalmente se centra en mejorar un sistema con respecto a una métrica de calidad específica en lugar de hacerlo universalmente óptimo. Esto a menudo lleva a compensaciones, donde mejorar una métrica puede ser a expensas de otra. Un ejemplo citado frecuentemente es la compensación espacio-tiempo , donde reducir el tiempo de ejecución de un programa puede aumentar su consumo de memoria. Por el contrario, en escenarios donde la memoria es limitada, los ingenieros podrían priorizar un algoritmo más lento para ahorrar espacio. Rara vez hay un solo diseño que pueda sobresalir en todas las situaciones, lo que requiere que los programadores prioricen los atributos más relevantes para la aplicación en cuestión. Las métricas para software incluyen rendimiento, latencia , uso de memoria volátil , almacenamiento persistente , uso de internet , consumo de energía y desgaste del hardware . La métrica más común es la velocidad.

Además, lograr la optimización absoluta suele requerir un esfuerzo desproporcionado en relación con los beneficios obtenidos. Por consiguiente, los procesos de optimización generalmente se ralentizan una vez que se alcanzan mejoras suficientes. Afortunadamente, las ganancias significativas suelen producirse al inicio del proceso de optimización, lo que permite detenerlo antes de que comiencen los rendimientos decrecientes .

Niveles de optimización

La optimización puede ocurrir en varios niveles. Por lo general, los niveles superiores tienen mayor impacto y son más difíciles de modificar posteriormente en un proyecto, requiriendo cambios significativos o una reescritura completa si es necesario cambiarlos. Por lo tanto, la optimización suele proceder mediante el refinamiento de niveles superiores a inferiores, con ganancias iniciales mayores y logradas con menos esfuerzo, y ganancias posteriores menores y que requieren más esfuerzo. Sin embargo, en algunos casos, el rendimiento general depende del rendimiento de partes de muy bajo nivel de un programa, y ​​pequeños cambios en una etapa tardía o la consideración temprana de detalles de bajo nivel pueden tener un impacto desproporcionado. Por lo general, se presta cierta atención a la eficiencia a lo largo de un proyecto —aunque esto varía significativamente— , pero la optimización importante a menudo se considera un refinamiento que se realiza al final, si es que se realiza. En proyectos de larga duración, suelen existir ciclos de optimización, donde la mejora de un área revela limitaciones en otra, y estos ciclos suelen acortarse cuando el rendimiento es aceptable o las ganancias se vuelven demasiado pequeñas o costosas. Las mejores prácticas para la optimización durante los ciclos de desarrollo iterativos incluyen el monitoreo continuo de problemas de rendimiento junto con pruebas de rendimiento regulares. [ 4 ] [ 5 ]  

Dado que el rendimiento forma parte de las especificaciones de un programa ( un programa excesivamente lento no cumple su función: un videojuego a 60 Hz (fotogramas por segundo) podría ser aceptable, pero 6 fotogramas por segundo resulta inaceptablemente entrecortado ), el rendimiento se considera desde el principio para garantizar que el sistema ofrezca un rendimiento suficiente. Los primeros prototipos deben tener un rendimiento aceptable para que haya confianza en que el sistema final (con optimización) alcanzará un rendimiento aceptable. Esto a veces se omite, creyendo que la optimización siempre se puede realizar más adelante, lo que da como resultado sistemas prototipo demasiado lentos ( a menudo por un orden de magnitud o más ) y sistemas que, en última instancia, fracasan porque su arquitectura no les permite alcanzar sus objetivos de rendimiento, como el Intel 432 (1981); o sistemas que requieren años de trabajo para lograr un rendimiento aceptable, como Java (1995), que solo alcanzó un rendimiento comparable al del código nativo con HotSpot (1999). [ 6 ] El grado en que el rendimiento cambia entre el prototipo y el sistema de producción, y cuán susceptible es a la optimización, puede ser una fuente importante de incertidumbre y riesgo.     

Nivel de diseño

En el nivel más alto, el diseño puede optimizarse para aprovechar al máximo los recursos disponibles, dados los objetivos, las restricciones y el uso/carga esperados. El diseño arquitectónico de un sistema afecta enormemente su rendimiento. Por ejemplo, un sistema limitado por la latencia de red (donde la latencia de red es la principal restricción del rendimiento general) se optimizaría para minimizar los viajes de red, idealmente realizando una sola solicitud (o ninguna, como en un protocolo push ) en lugar de múltiples viajes de ida y vuelta. La elección del diseño depende de los objetivos: al diseñar un compilador , si la compilación rápida es la prioridad clave, un compilador de una pasada es más rápido que un compilador de múltiples pasadas (suponiendo el mismo trabajo), pero si el objetivo es la velocidad del código de salida, un compilador de múltiples pasadas más lento cumple mejor el objetivo, aunque tome más tiempo. La elección de la plataforma y el lenguaje de programación se realiza a este nivel, y cambiarlos con frecuencia requiere una reescritura completa, aunque un sistema modular puede permitir la reescritura de solo algún componente ; por ejemplo, para un programa en Python se pueden reescribir las secciones críticas para el rendimiento en C. En un sistema distribuido, la elección de la arquitectura ( cliente-servidor , punto a punto , etc.) se realiza a nivel de diseño y puede ser difícil de cambiar, particularmente si no se pueden reemplazar todos los componentes de forma sincronizada (por ejemplo, clientes antiguos). 

Algoritmos y estructuras de datos

Dado un diseño general, la elección de algoritmos y estructuras de datos eficientes , así como su implementación eficiente, son pasos fundamentales. Tras el diseño, la elección de algoritmos y estructuras de datos influye en la eficiencia más que cualquier otro aspecto del programa. Generalmente, las estructuras de datos son más difíciles de modificar que los algoritmos, ya que las suposiciones sobre su estructura y su rendimiento se utilizan a lo largo de todo el programa. Sin embargo, esto puede minimizarse mediante el uso de tipos de datos abstractos en las definiciones de funciones y limitando las definiciones concretas de la estructura de datos a unos pocos lugares. Los cambios en las estructuras de datos mapeadas a una base de datos pueden requerir la migración del esquema y otros cambios complejos de software o infraestructura. [ 7 ]

En el caso de los algoritmos, esto consiste principalmente en asegurar que tengan una complejidad constante O(1), logarítmica O(log n ), lineal O( n ) o, en algunos casos, logarítmica lineal O( n log n ) en la entrada (tanto en espacio como en tiempo). Los algoritmos con complejidad cuadrática O( ) no escalan bien, e incluso los algoritmos lineales causan problemas si se llaman repetidamente, por lo que suelen sustituirse por algoritmos constantes o logarítmicos siempre que sea posible .

Más allá del orden de crecimiento asintótico, los factores constantes son importantes: un algoritmo asintóticamente más lento puede ser más rápido o más pequeño (por ser más simple) que un algoritmo asintóticamente más rápido cuando ambos se enfrentan a una entrada pequeña, lo cual puede ocurrir en la realidad. A menudo, un algoritmo híbrido proporcionará el mejor rendimiento, debido a que esta compensación varía con el tamaño.

Una técnica general para mejorar el rendimiento consiste en evitar tareas innecesarias. Un buen ejemplo es el uso de una ruta rápida para casos comunes, lo que mejora el rendimiento al evitar trabajo superfluo. Por ejemplo, usar un algoritmo de maquetación simple para texto en latín y cambiar a un algoritmo complejo solo para escrituras complejas, como el devanagari . Otra técnica importante es el almacenamiento en caché, en particular la memorización , que evita cálculos redundantes. Debido a la importancia del almacenamiento en caché, a menudo existen muchos niveles de caché en un sistema, lo que puede causar problemas de uso de memoria y errores de corrección debido a cachés obsoletas.

Nivel de código fuente

Más allá de los algoritmos generales y su implementación en una máquina abstracta, las decisiones concretas a nivel de código fuente pueden marcar una diferencia significativa. Por ejemplo, en los primeros compiladores de C, era más lento que para un bucle incondicional, porque evaluaba y luego tenía un salto condicional que comprobaba si era verdadero, mientras que tenía un salto incondicional. Algunas optimizaciones (como esta) pueden realizarse hoy en día mediante compiladores optimizadores . Esto depende del lenguaje fuente, el lenguaje máquina de destino y el compilador, y puede ser difícil de entender o predecir y cambia con el tiempo; este es un punto clave donde la comprensión de los compiladores y el código máquina puede mejorar el rendimiento. El movimiento de código invariante de bucle y la optimización del valor de retorno son ejemplos de optimizaciones que reducen la necesidad de variables auxiliares e incluso pueden resultar en un rendimiento más rápido al evitar optimizaciones indirectas.while(true)for(;;)while(true)truefor(;;)

Nivel de construcción

Entre el nivel de código fuente y el de compilación, se pueden usar directivas y opciones de compilación para ajustar el rendimiento en el código fuente y el compilador, respectivamente. Por ejemplo, se pueden usar definiciones de preprocesador para deshabilitar funciones de software innecesarias, optimizar para modelos de procesador o capacidades de hardware específicos, o predecir bifurcaciones . Los sistemas de distribución de software basados ​​en código fuente, como Ports de BSD y Portage de Gentoo , pueden aprovechar esta forma de optimización.

Nivel de compilación

El uso de un compilador optimizador con las optimizaciones habilitadas suele garantizar que el programa ejecutable se optimice al menos hasta donde el compilador pueda hacerlo razonablemente. Consulte la sección «Compilador optimizador» para obtener más detalles.

Nivel de ensamblaje

En el nivel más básico, escribir código en lenguaje ensamblador , diseñado para una plataforma de hardware específica, puede generar el código más eficiente y compacto si el programador aprovecha todo el repertorio de instrucciones de máquina . Muchos sistemas operativos utilizados en sistemas embebidos se han escrito tradicionalmente en código ensamblador por esta razón. Los programas (excepto los muy pequeños) rara vez se escriben de principio a fin en ensamblador debido al tiempo y el costo que implican. La mayoría se compilan desde un lenguaje de alto nivel a ensamblador y se optimizan manualmente a partir de ahí. Cuando la eficiencia y el tamaño son menos importantes, las partes grandes pueden escribirse en un lenguaje de alto nivel.

Con los compiladores optimizadores más modernos y la mayor complejidad de las CPU recientes , es más difícil escribir código más eficiente que el que genera el compilador, y pocos proyectos necesitan este paso de optimización "definitivo".

Gran parte del código escrito hoy en día está diseñado para ejecutarse en la mayor cantidad de máquinas posible. En consecuencia, los programadores y compiladores no siempre aprovechan las instrucciones más eficientes que ofrecen las CPU más recientes ni las particularidades de los modelos antiguos. Además, un código ensamblador optimizado para un procesador específico sin utilizar dichas instrucciones podría tener un rendimiento subóptimo en otro procesador, que requeriría una optimización diferente.

Actualmente, en lugar de escribir en lenguaje ensamblador, los programadores suelen utilizar un desensamblador para analizar la salida de un compilador y modificar el código fuente de alto nivel para que pueda compilarse de forma más eficiente, o para comprender por qué es ineficiente.

Tiempo de ejecución

Los compiladores justo a tiempo pueden generar código máquina personalizado a partir de datos de ejecución, aunque esto conlleva una sobrecarga de compilación. Esta técnica se remonta a los primeros motores de expresiones regulares y se ha generalizado con Java HotSpot y V8 para JavaScript. En algunos casos, la optimización adaptativa puede realizar optimizaciones en tiempo de ejecución que superan la capacidad de los compiladores estáticos, ajustando dinámicamente los parámetros según la entrada real u otros factores.

La optimización guiada por perfiles es una técnica de optimización de compilación anticipada (AOT, por sus siglas en inglés) basada en perfiles de tiempo de ejecución, y es similar a un análogo estático de "caso promedio" de la técnica dinámica de optimización adaptativa.

El código automodificable puede alterarse a sí mismo en respuesta a las condiciones de ejecución para optimizar el código; esto era más común en los programas en lenguaje ensamblador.

Algunos diseños de CPU permiten realizar optimizaciones en tiempo de ejecución. Algunos ejemplos incluyen la ejecución fuera de orden , la ejecución especulativa , las tuberías de instrucciones y los predictores de bifurcación . Los compiladores pueden ayudar al programa a aprovechar estas características de la CPU, por ejemplo, mediante la planificación de instrucciones .

Optimizaciones dependientes e independientes de la plataforma

La optimización de código también puede clasificarse en técnicas dependientes e independientes de la plataforma . Mientras que estas últimas son efectivas en la mayoría o en todas las plataformas, las técnicas dependientes de la plataforma utilizan propiedades específicas de una plataforma o se basan en parámetros que dependen de la plataforma o incluso del procesador. Por lo tanto, podría ser necesario escribir o producir diferentes versiones del mismo código para diferentes procesadores. Por ejemplo, en el caso de la optimización a nivel de compilación, las técnicas independientes de la plataforma son técnicas genéricas (como el desenrollado de bucles , la reducción de llamadas a funciones, las rutinas eficientes en memoria, la reducción de condiciones, etc.) que impactan en la mayoría de las arquitecturas de CPU de manera similar. Un gran ejemplo de optimización independiente de la plataforma se ha mostrado con el bucle `for` interno, donde se observó que un bucle con un bucle `for` interno realiza más cálculos por unidad de tiempo que un bucle sin él o uno con un bucle `while` interno. [ 8 ] Generalmente, estas sirven para reducir la longitud total de la ruta de instrucciones necesaria para completar el programa y/o reducir el uso total de memoria durante el proceso. Por otro lado, las técnicas dependientes de la plataforma implican la planificación de instrucciones, el paralelismo a nivel de instrucciones , el paralelismo a nivel de datos , las técnicas de optimización de caché (es decir, parámetros que difieren entre varias plataformas) y la planificación óptima de instrucciones puede ser diferente incluso en diferentes procesadores de la misma arquitectura.

Reducción de fuerza

Las tareas computacionales pueden realizarse de varias maneras diferentes con distinta eficiencia. Una versión más eficiente con funcionalidad equivalente se conoce como reducción de potencia . Por ejemplo, considere el siguiente fragmento de código C cuya intención es obtener la suma de todos los números enteros del 1 al N :

int suma = 0 ; para ( int i = 1 ; i <= N ; ++ i ) { suma += i ; } printf ( "suma: %d \n " , suma );

Este código puede (suponiendo que no haya desbordamiento aritmético ) reescribirse utilizando una fórmula matemática como:

int suma = N * ( 1 + N ) / 2 ; printf ( "suma: %d \n " , suma );

La optimización, a veces realizada automáticamente por un compilador optimizador, consiste en seleccionar un método ( algoritmo ) que sea más eficiente computacionalmente, manteniendo la misma funcionalidad. Consulte la sección sobre eficiencia algorítmica para obtener más información sobre algunas de estas técnicas. Sin embargo, a menudo se puede lograr una mejora significativa en el rendimiento eliminando funcionalidades superfluas.

La optimización no siempre es un proceso obvio o intuitivo. En el ejemplo anterior, la versión "optimizada" podría ser más lenta que la versión original si N fuera suficientemente pequeño y el hardware en cuestión fuera mucho más rápido realizando operaciones de suma y bucle que de multiplicación y división.

Compensaciones

En algunos casos, sin embargo, la optimización se basa en el uso de algoritmos más elaborados, el aprovechamiento de casos especiales, técnicas específicas y la realización de complejas compensaciones. Un programa totalmente optimizado puede ser más difícil de comprender y, por lo tanto, puede contener más errores que las versiones no optimizadas. Además de eliminar patrones de diseño anti-optimización evidentes, algunas optimizaciones a nivel de código disminuyen la mantenibilidad.

La optimización generalmente se centra en mejorar uno o dos aspectos del rendimiento: tiempo de ejecución, uso de memoria, espacio en disco, ancho de banda, consumo de energía u otro recurso. Esto suele implicar una compensación , donde un factor se optimiza a expensas de otros. Por ejemplo, aumentar el tamaño de la caché mejora el rendimiento en tiempo de ejecución, pero también incrementa el consumo de memoria. Otras compensaciones comunes incluyen la claridad y la concisión del código. 

Hay casos en los que el programador que realiza la optimización debe decidir mejorar el software para ciertas operaciones, pero a costa de que otras sean menos eficientes. Estas compensaciones a veces pueden ser de naturaleza no técnica , como cuando un competidor publica un resultado de referencia que debe superarse para mejorar el éxito comercial, pero que quizás implique que el uso normal del software sea menos eficiente. Estos cambios a veces se denominan, en tono de broma, pesimismos . 

cuellos de botella

La optimización puede incluir la identificación de un cuello de botella en un sistema : un componente que limita el rendimiento. En términos de código, esto suele ser un punto crítico ( una parte esencial del código que consume la mayor parte del recurso necesario ), aunque también puede deberse a otros factores, como la latencia de entrada/salida o el ancho de banda de la red.   

En informática, el consumo de recursos suele seguir una distribución de ley de potencias , y el principio de Pareto se puede aplicar a la optimización de recursos al observar que el 80 % de los recursos se utilizan normalmente en el 20 % de las operaciones. [ 9 ] En ingeniería de software, suele ser una mejor aproximación que el 90 % del tiempo de ejecución de un programa informático se dedique a ejecutar el 10 % del código (conocida como la ley 90/10 en este contexto).

Los algoritmos y estructuras de datos más complejos funcionan bien con muchos elementos, mientras que los algoritmos simples son más adecuados para pequeñas cantidades de datos. El tiempo de configuración, inicialización y los factores constantes del algoritmo más complejo pueden contrarrestar el beneficio, por lo que un algoritmo híbrido o adaptativo puede ser más rápido que cualquier algoritmo individual. Se puede utilizar un analizador de rendimiento para acotar las decisiones sobre qué funcionalidad se ajusta a cada condición. [ 10 ]

Por lo tanto, el análisis del rendimiento proporciona no solo la detección de cuellos de botella, sino también una variedad de métodos para guiar la optimización. La algoritmia empírica es la práctica de utilizar métodos empíricos, típicamente el análisis del rendimiento, para estudiar el comportamiento de los algoritmos, para que los desarrolladores lo comprendan y puedan realizar optimizaciones planificadas por humanos. La optimización guiada por perfiles es el uso automatizado de datos de perfilado como entrada para un compilador o intérprete optimizador. Algunos lenguajes de programación están asociados con herramientas para la optimización guiada por perfiles. [ 11 ] Algunos métodos de análisis del rendimiento enfatizan las mejoras basadas en la utilización de la caché . [ 12 ] Otros beneficios del análisis del rendimiento pueden incluir una mejor gestión de los recursos y una mejor experiencia de usuario. [ 13 ]

En algunos casos, aumentar la memoria puede acelerar un programa. Por ejemplo, un programa de filtrado suele leer cada línea, filtrarla y mostrarla inmediatamente. Esto solo consume memoria para una línea, pero el rendimiento suele ser deficiente debido a la latencia de cada lectura de disco. Almacenar el resultado en caché es igualmente efectivo, aunque requiere un mayor consumo de memoria.

Cuándo optimizar

Por lo general, la optimización implica elegir los mejores algoritmos y estructuras de datos en general. [ 14 ] Con frecuencia, las mejoras algorítmicas pueden generar mejoras de rendimiento de varios órdenes de magnitud, en lugar de microoptimizaciones, que rara vez mejoran el rendimiento en más de un pequeño porcentaje. [ 15 ] Si se espera a optimizar hasta el final del ciclo de desarrollo, entonces cambiar el algoritmo podría requerir una reescritura significativa.

Con frecuencia, la microoptimización puede reducir la legibilidad y complicar los programas o sistemas. Esto puede dificultar el mantenimiento y la depuración de los programas.

Donald Knuth hizo las dos siguientes afirmaciones sobre optimización:

"Deberíamos olvidarnos de las pequeñas eficiencias, digamos que en el 97% de los casos: la optimización prematura es la raíz de todos los males. Sin embargo, no debemos desaprovechar nuestras oportunidades en ese 3% crítico" [ 16 ].

(También atribuyó la cita a Tony Hoare varios años después, [ 17 ] aunque esto podría haber sido un error, ya que Hoare niega haber acuñado la frase. [ 18 ] )

"En las disciplinas de ingeniería establecidas, una mejora del 12%, fácilmente obtenible, nunca se considera marginal y creo que el mismo punto de vista debería prevalecer en la ingeniería de software" [ 16 ].

La "optimización prematura" se usa a menudo como un grito de guerra contra toda optimización en todas las situaciones y para todos los propósitos. [ 19 ] [ 20 ] [ 21 ] [ 22 ] Con frecuencia, el Código Limpio hace que el código sea más complicado que un código más simple y eficiente. [ 23 ]

A la hora de decidir qué optimizar, se debe utilizar la Ley de Amdahl para priorizar las partes en función del tiempo real empleado en una parte determinada, lo cual no siempre resulta evidente al examinar el código sin un análisis de rendimiento .

En la práctica, a menudo es necesario tener en cuenta los objetivos de rendimiento al diseñar software, pero los programadores deben sopesar diversas ventajas y desventajas. El coste de desarrollo es significativo y el hardware es rápido.

Los compiladores modernos son tan eficientes que, en ocasiones, las mejoras de rendimiento previstas no se materializan. Dado que los compiladores realizan numerosas optimizaciones automáticas, algunas de ellas pueden generar un ejecutable idéntico. Además, a veces el hardware puede reducir el impacto de las microoptimizaciones. Por ejemplo, el hardware puede almacenar en caché datos que se almacenan en caché a nivel de software.

Macros

La optimización durante el desarrollo de código mediante macros adopta diferentes formas en diferentes lenguajes de programación.

En algunos lenguajes procedimentales, como C y C++ , las macros se implementan mediante sustitución de tokens. Actualmente, las funciones en línea se pueden usar como una alternativa segura en muchos casos. En ambos casos, el cuerpo de la función en línea puede someterse a optimizaciones adicionales en tiempo de compilación por parte del compilador, incluyendo la reducción de constantes , lo que puede trasladar algunos cálculos al tiempo de compilación.

En muchos lenguajes de programación funcional , las macros se implementan mediante la sustitución en tiempo de análisis de árboles de análisis sintáctico/árboles de sintaxis abstracta, lo que, según se afirma, las hace más seguras de usar. Dado que en muchos casos se utiliza la interpretación, esta es una forma de garantizar que dichos cálculos se realicen únicamente en tiempo de análisis sintáctico, y a veces la única.

Lisp fue el creador de este estilo de macro, y a menudo se las denomina "macros tipo Lisp". Un efecto similar se puede lograr utilizando metaprogramación con plantillas en C++ .

En ambos casos, el trabajo se traslada al tiempo de compilación. La diferencia entre las macros de C , por un lado, y las macros tipo Lisp y la metaprogramación con plantillas de C++ , por otro, radica en que estas últimas permiten realizar cálculos arbitrarios en tiempo de compilación/análisis, mientras que la expansión de macros de C no realiza ningún cálculo y depende de la capacidad del optimizador para llevarlo a cabo. Además, las macros de C no admiten directamente la recursión ni la iteración , por lo que no son Turing completas .

Sin embargo, como ocurre con cualquier optimización, a menudo resulta difícil predecir dónde tendrán mayor impacto estas herramientas antes de que finalice un proyecto.

Optimización automatizada y manual

Véase también Categoría:Optimizaciones del compilador

La optimización puede ser automatizada por compiladores o realizada por programadores. Las mejoras suelen ser limitadas para la optimización local y mayores para la optimización global. Generalmente, la optimización más eficaz consiste en encontrar un algoritmo superior .

La optimización de un sistema completo suele ser tarea de programadores, ya que resulta demasiado compleja para los optimizadores automáticos. En estos casos, los programadores o administradores de sistemas modifican explícitamente el código para mejorar el rendimiento general del sistema. Si bien esto puede aumentar la eficiencia, es mucho más costoso que las optimizaciones automáticas. Dado que muchos parámetros influyen en el rendimiento del programa, el espacio de optimización es amplio. Se utilizan metaheurísticas y aprendizaje automático para abordar la complejidad de la optimización de programas. [ 24 ]

Utilice un analizador de rendimiento para identificar las secciones del programa que consumen más recursos : el cuello de botella . A veces, los programadores creen saber dónde se encuentra el cuello de botella, pero la intuición suele ser errónea. Optimizar una parte del código que no es importante generalmente no mejora el rendimiento general. 

Cuando el cuello de botella está localizado, la optimización suele comenzar con una revisión del algoritmo utilizado en el programa. En la mayoría de los casos, un algoritmo específico puede adaptarse a un problema concreto, lo que resulta en un mejor rendimiento que un algoritmo genérico. Por ejemplo, la tarea de ordenar una lista enorme de elementos se suele realizar con el algoritmo Quicksort , uno de los algoritmos genéricos más eficientes. Sin embargo, si alguna característica de los elementos es aprovechable (por ejemplo, si ya están ordenados de una manera particular), se puede utilizar un método diferente, o incluso un algoritmo de ordenación personalizado.

Una vez que el programador está razonablemente seguro de haber seleccionado el mejor algoritmo, puede comenzar la optimización del código. Se pueden desenrollar los bucles (para reducir la sobrecarga, aunque esto a menudo puede disminuir la velocidad si sobrecarga la caché de la CPU ), se pueden usar tipos de datos lo más pequeños posible, se puede usar aritmética de enteros en lugar de coma flotante, etc. (Consulte el artículo sobre eficiencia algorítmica para conocer estas y otras técnicas).

Los cuellos de botella en el rendimiento pueden deberse a limitaciones del lenguaje más que a los algoritmos o estructuras de datos utilizados en el programa. A veces, una parte crítica del programa puede reescribirse en un lenguaje de programación diferente que proporcione un acceso más directo a la máquina subyacente. Por ejemplo, es común que lenguajes de muy alto nivel como Python tengan módulos escritos en C para una mayor velocidad. Los programas ya escritos en C pueden tener módulos escritos en lenguaje ensamblador . Los programas escritos en D pueden usar el ensamblador en línea .

En estas circunstancias, reescribir secciones resulta beneficioso debido a una regla general conocida como la ley 90/10, que establece que el 90% del tiempo se dedica al 10% del código y solo el 10% al 90% restante. Por lo tanto, invertir tiempo y esfuerzo en optimizar una pequeña parte del programa puede tener un gran impacto en la velocidad general , siempre y cuando se identifiquen las partes correctas. 

La optimización manual a veces tiene como efecto secundario la dificultad para leer el código. Por lo tanto, las optimizaciones de código deben documentarse cuidadosamente (preferiblemente mediante comentarios en línea) y evaluarse su impacto en el desarrollo futuro.

El programa que realiza una optimización automatizada se llama optimizador . La mayoría de los optimizadores están integrados en los compiladores y operan durante la compilación. Los optimizadores suelen adaptar el código generado a procesadores específicos.

Actualmente, las optimizaciones automatizadas se limitan casi exclusivamente a la optimización del compilador . Sin embargo, dado que estas optimizaciones suelen limitarse a un conjunto fijo de optimizaciones bastante generales, existe una gran demanda de optimizadores que puedan aceptar descripciones de optimizaciones específicas del problema y del lenguaje, lo que permite a los ingenieros especificar optimizaciones personalizadas. Las herramientas que aceptan descripciones de optimizaciones se denominan sistemas de transformación de programas y están empezando a aplicarse a sistemas de software reales como C++.

Algunos lenguajes de alto nivel ( Eiffel , Esterel ) optimizan sus programas mediante el uso de un lenguaje intermedio .

La computación en malla o computación distribuida tiene como objetivo optimizar todo el sistema, trasladando las tareas de los ordenadores con mayor uso a los ordenadores con menor tiempo de inactividad.

Tiempo empleado para la optimización

En ocasiones, el tiempo necesario para llevar a cabo la optimización en sí misma puede ser un problema.

La optimización del código existente generalmente no añade nuevas funcionalidades y, lo que es peor, puede introducir nuevos errores en código que antes funcionaba correctamente (como cualquier cambio). Dado que el código optimizado manualmente a veces puede ser menos legible que el código sin optimizar, la optimización también puede afectar su mantenibilidad. La optimización tiene un coste y es importante asegurarse de que la inversión merezca la pena.

Un optimizador automático (o compilador optimizador , un programa que optimiza el código) puede necesitar ser optimizado, ya sea para mejorar aún más la eficiencia de los programas a los que se dirige o para acelerar su propio funcionamiento. Una compilación realizada con la optimización activada suele tardar más, aunque esto generalmente solo representa un problema cuando los programas son bastante grandes.

En particular, para los compiladores justo a tiempo, el rendimiento del componente de compilación en tiempo de ejecución , que se ejecuta junto con su código objetivo, es clave para mejorar la velocidad de ejecución general.

Optimización falsa

En ocasiones, las optimizaciones pueden perjudicar el rendimiento. La concurrencia y el paralelismo generan una sobrecarga significativa que incrementa el rendimiento y, posiblemente, el consumo de energía. Cabe destacar que el código C rara vez utiliza multiprocesamiento explícito, pero suele ser más rápido que cualquier otro lenguaje de programación. El almacenamiento en caché, la paginación y el intercambio de memoria en disco a menudo provocan un aumento considerable del consumo de energía y un mayor desgaste del hardware. Ejecutar procesos en segundo plano para mejorar el tiempo de inicio ralentiza todos los demás procesos.

Véase también

Referencias

  1. Robert Sedgewick , Algoritmos , 1984, pág. 84.
  2. Antoniou, Andreas; Lu, Wu-Sheng (2021). Optimización práctica (PDF) . Textos en informática (2.ª  ed.). Springer . p.  1. doi : 10.1007/978-1-0716-0843-2 . ​​ISBN 978-1-0716-0841-8.
  3. "Superoptimización: Generación de código demostrablemente óptimo mediante programación de conjuntos de respuestas" . Universidad de Bath . Consultado el 11 de septiembre de 2024 .
  4. "Optimización del rendimiento en el desarrollo de software: acelerando tus aplicaciones" . Consultado el 12 de julio de 2025 .
  5. Agrawal, Amit. "Maximización de la eficiencia: Implementación de un sistema de monitoreo del desempeño" . Consultado el 12 de julio de 2025 .
  6. Düppe, Ingo. "Guía del autoestopista para el rendimiento de Java: el pasado, el presente y el futuro" . Consultado el 12 de julio de 2025 .
  7. Mullins, Craig S. "El impacto del cambio en las estructuras de bases de datos" . Consultado el 12 de julio de 2025 .
  8. Adewumi, Tosin P. (2018-08-01). "Inner loop program construct: A faster way for program execution" . Open Computer Science . 8 (1): 115– 122. doi : 10.1515/comp-2018-0004 .
  9. Wescott, Bob (2013). El libro de rendimiento de todos los ordenadores, Capítulo 3: Leyes útiles . CreateSpace . ISBN 978-1482657753.
  10. Krauss, Kirk J. "Perfilado de rendimiento con un enfoque" . Recuperado el 15 de agosto de 2017 .
  11. "Optimización guiada por perfiles" . Consultado el 12 de julio de 2025 .
  12. Los desarrolladores de Valgrind (2006). "5.2.2". Manual de usuario de Valgrind . Network Theory Ltd.
  13. Kodlekere, Ranjana. "Perfilado de rendimiento: explicado por etapas" . Consultado el 12 de julio de 2025 .
  14. "La falacia de la optimización prematura" .
  15. "La falacia de la optimización prematura" .
  16. 1 2 Knuth, Donald (diciembre de 1974). "Programación estructurada con instrucciones go to". ACM Computing Surveys . 6 (4): 268. CiteSeerX 10.1.1.103.6084 . doi : 10.1145/356635.356640 . S2CID 207630080 .  
  17. Los errores de TeX , en Software—Practice & Experience , Volumen 19, Número 7 (julio de 1989), págs. 607–685, reimpreso en su libro Literate Programming (pág. 276).
  18. "La optimización prematura es la raíz de todos los males" . hans.gerwitz.com . Consultado el 18 de diciembre de 2020. Sin embargo, Hoare no afirmó esto cuando le pregunté al respecto en enero de 2004 .
  19. "La falacia de la optimización prematura" .
  20. "No toda optimización es prematura" .
  21. "Cuando la optimización prematura no lo es" .
  22. ""Evitar la optimización prematura" no significa "escribir código basura"." .
  23. "Abstracciones prematuras" .
  24. Memeti, Suejb; Pllana, Sabri; Binotto, Alécio; Kołodziej, Joanna; Brandic, Ivona (26 de abril de 2018). "Uso de metaheurísticas y aprendizaje automático para la optimización de software de sistemas de computación paralela: una revisión sistemática de la literatura". Computing . 101 (8). Springer Vienna: 893– 936. arXiv : 1801.09444 . Bibcode : 2018arXiv180109444M . doi : 10.1007/s00607-018-0614-9 . S2CID 13868111 . 

Lecturas adicionales

  • Jon Bentley : Cómo escribir programas eficientes , ISBN 0-13-970251-2.
  • Donald Knuth : El arte de la programación informática
  • Cómo escribir código numérico rápido: una breve introducción
  • "Lo que todo programador debería saber sobre la memoria", de Ulrich Drepper , explica la estructura de los subsistemas de memoria modernos y sugiere cómo utilizarlos de manera eficiente. 
  • "Análisis y optimización del rendimiento multinúcleo de Linux en pocas palabras" , diapositivas de la presentación de Philip Mucci.
  • Optimización de la programación por Paul Hsieh
  • Cómo escribir programas eficientes ("Las reglas de Bentley") por Jon Bentley
  • "Antipatrones de rendimiento" de Bart Smaalders
Obtenido de " https://en.wikipedia.org/w/index.php?title=Program_optimization&oldid=1359475199 "