Optimización del programa - Program optimization

En ciencias de la computación , la optimización de programas , optimización de código, o la optimización de software es el proceso de modificar un sistema de software para hacer algún aspecto de que funcione más eficientemente o utilizar menos recursos. En general, un programa de computadora puede optimizarse para que se ejecute más rápidamente, o para hacerlo capaz de operar con menos memoria de almacenamiento u otros recursos, o para consumir menos energía.

General

Aunque la palabra "optimización" comparte la misma raíz que "óptimo", es raro que el proceso de optimización produzca un sistema verdaderamente óptimo. Por lo general, un sistema se puede optimizar no en términos absolutos, sino solo con respecto a una métrica de calidad determinada, que puede contrastar con otras métricas posibles. Como resultado, el sistema optimizado normalmente solo será óptimo en una aplicación o para una audiencia. Se podría reducir la cantidad de tiempo que tarda un programa en realizar alguna tarea al precio de hacer que consuma más memoria. En una aplicación donde el espacio de memoria es escaso, uno podría elegir deliberadamente un algoritmo más lento para usar menos memoria. A menudo, no existe un diseño de "talla única" que funcione bien en todos los casos, por lo que los ingenieros hacen concesiones para optimizar los atributos de mayor interés. Además, el esfuerzo requerido para hacer que un software sea completamente óptimo - incapaz de mejorar más - es casi siempre más de lo que es razonable para los beneficios que se acumularían; por lo que el proceso de optimización puede detenerse antes de que se haya alcanzado una solución completamente óptima. Afortunadamente, a menudo ocurre que las mejoras más importantes se producen al principio del proceso.

Incluso para una métrica de calidad determinada (como la velocidad de ejecución), la mayoría de los métodos de optimización solo mejoran el resultado; no tienen ninguna pretensión de producir resultados óptimos. La superoptimización es el proceso de encontrar resultados verdaderamente óptimos.

Niveles de optimización

La optimización puede ocurrir en varios niveles. Por lo general, los niveles más altos tienen un mayor impacto y son más difíciles de cambiar más adelante en un proyecto, lo que requiere cambios significativos o una reescritura completa si es necesario cambiarlos. Por lo tanto, la optimización puede proceder típicamente a través del refinamiento de mayor a menor, siendo las ganancias iniciales mayores y logradas con menos trabajo, y las ganancias posteriores son menores y requieren más trabajo. Sin embargo, en algunos casos, el desempeño general depende del desempeño de partes de un programa de muy bajo nivel, y los pequeños cambios en una etapa tardía o la consideración temprana de detalles de bajo nivel pueden tener un impacto descomunal. Por lo general, se considera 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 debe realizar tarde, si es que se hace alguna vez. En proyectos de ejecución más larga, normalmente hay ciclos de optimización, en los que la mejora de un área revela limitaciones en otra, y estas suelen reducirse cuando el rendimiento es aceptable o las ganancias se vuelven demasiado pequeñas o costosas.

Como el rendimiento es parte de la especificación de un programa, un programa que es inusualmente lento no es adecuado para su propósito: un videojuego con 60 Hz (cuadros por segundo) es aceptable, pero 6 cuadros por segundo es inaceptablemente entrecortado. El rendimiento es una consideración desde el principio, para garantizar que el sistema pueda ofrecer un rendimiento suficiente, y los primeros prototipos deben tener un rendimiento aproximadamente aceptable para que haya confianza en que el sistema final (con optimización) logrará un rendimiento aceptable. Esto a veces se omite en la creencia de que la optimización siempre se puede hacer más tarde, lo que da como resultado sistemas prototipo que son demasiado lentos, a menudo en un orden de magnitud o más, y sistemas que en última instancia son fallas porque arquitectónicamente no pueden lograr sus objetivos de rendimiento, por ejemplo. como Intel 432 (1981); o aquellos que requieren años de trabajo para lograr un rendimiento aceptable, como Java (1995), que solo logró un rendimiento aceptable con HotSpot (1999). El grado en que cambia el rendimiento entre el prototipo y el sistema de producción, y qué tan susceptible es 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 hacer el mejor uso de los recursos disponibles, los objetivos dados, las limitaciones y el uso / carga esperados. El diseño arquitectónico de un sistema afecta de manera abrumadora su desempeño. Por ejemplo, un sistema que está vinculado a la latencia de la red (donde la latencia de la red es la principal limitación del rendimiento general) se optimizaría para minimizar los viajes de red, idealmente haciendo una sola solicitud (o ninguna solicitud, 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 (asumiendo el mismo trabajo), pero si la velocidad del código de salida es la meta, un compilador de múltiples pasadas más lento cumple mejor el objetivo, aunque tarde más. La elección de la plataforma y el lenguaje de programación se produce en este nivel, y cambiarlos con frecuencia requiere una reescritura completa, aunque un sistema modular puede permitir la reescritura de solo algunos componentes; por ejemplo, un programa Python puede reescribir secciones críticas para el rendimiento en C. sistema, la elección de la arquitectura ( cliente-servidor , peer-to-peer , etc.) ocurre en el nivel de diseño y puede ser difícil de cambiar, particularmente si todos los componentes no se pueden reemplazar en sincronía (por ejemplo, clientes antiguos).

Algoritmos y estructuras de datos

Dado un diseño general, viene a continuación una buena selección de algoritmos y estructuras de datos eficientes, y la implementación eficiente de estos algoritmos y estructuras de datos. Después del diseño, la elección de algoritmos y estructuras de datos afecta la eficiencia más que cualquier otro aspecto del programa. En general, las estructuras de datos son más difíciles de cambiar que los algoritmos, ya que un supuesto de estructura de datos y sus supuestos de rendimiento se utilizan en todo el programa, aunque esto puede minimizarse mediante el uso de tipos de datos abstractos en las definiciones de funciones y manteniendo restringidas las definiciones de estructuras de datos concretas. a algunos lugares.

Para los algoritmos, esto consiste principalmente en asegurar que los algoritmos sean O constante (1), O logarítmico (log n ), O lineal ( n ), o en algunos casos O log-lineal ( n log n ) en la entrada (ambos en el espacio y tiempo). Los algoritmos con complejidad cuadrática O ( n 2 ) no se escalan, e incluso los algoritmos lineales causan problemas si se llaman repetidamente, y generalmente se reemplazan con constantes o logarítmicos si es posible.

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

Una técnica general para mejorar el rendimiento es evitar el trabajo. Un buen ejemplo es el uso de una ruta rápida para casos comunes, mejorando el rendimiento al evitar trabajos innecesarios. Por ejemplo, usar un algoritmo de diseño de texto simple para texto latino, solo cambiar a un algoritmo de diseño complejo para scripts complejos, como 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 hay muchos niveles de almacenamiento en caché en un sistema, lo que puede causar problemas por el uso de la memoria y problemas de corrección por cachés obsoletos.

Nivel de código fuente

Más allá de los algoritmos generales y su implementación en una máquina abstracta, las elecciones concretas del nivel de código fuente pueden marcar una diferencia significativa. Por ejemplo, en los primeros compiladores de C, while(1)era más lento que for(;;)para un bucle incondicional, porque while(1)evaluó 1 y luego tuvo un salto condicional que probó si era cierto, mientras que for (;;)tuvo un salto incondicional. Algunas optimizaciones (como esta) hoy en día se pueden realizar optimizando compiladores . Esto depende del idioma de origen, el idioma de la máquina de destino y el compilador, y puede ser difícil de entender o predecir y cambia con el tiempo; este es un lugar clave donde la comprensión de los compiladores y el código de la máquina puede mejorar el rendimiento. El movimiento de código invariante en bucle y la optimización del valor de retorno son ejemplos de optimizaciones que reducen la necesidad de variables auxiliares e incluso pueden dar como resultado un rendimiento más rápido al evitar optimizaciones indirectas.

Nivel de construcción

Entre el nivel de origen y de compilación, las directivas y los indicadores de compilación se pueden usar para ajustar las opciones de rendimiento en el código fuente y el compilador respectivamente, como usar definiciones de preprocesador para deshabilitar funciones de software innecesarias, optimizar para modelos de procesador específicos o capacidades de hardware, o predecir la ramificación , por ejemplo. Sistemas de distribución de software basada en fuentes como BSD 's Puertos y Gentoo ' s Portage pueden tomar ventaja de esta forma de optimización.

Nivel de compilación

El uso de un compilador optimizador tiende a asegurar que el programa ejecutable esté optimizado al menos tanto como el compilador puede predecir.

Nivel de montaje

En el nivel más bajo, escribir código usando un lenguaje ensamblador , diseñado para una plataforma de hardware en particular, puede producir el código más eficiente y compacto si el programador aprovecha el repertorio completo de instrucciones de la máquina . Muchos sistemas operativos usados ​​en sistemas embebidos se han escrito tradicionalmente en código ensamblador por esta razón. Los programas (que no sean programas muy pequeños) rara vez se escriben de principio a fin en ensamblaje debido al tiempo y al costo involucrados. La mayoría se compilan desde un lenguaje de alto nivel hasta el ensamblaje y se optimizan manualmente desde allí. Cuando la eficiencia y el tamaño son menos importantes, las partes grandes pueden escribirse en un lenguaje de alto nivel.

Con compiladores de optimización 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 está destinado a ejecutarse en tantas máquinas como sea posible. Como consecuencia, los programadores y compiladores no siempre aprovechan las instrucciones más eficientes proporcionadas por las CPU más nuevas o las peculiaridades de los modelos más antiguos. Además, el código ensamblador sintonizado para un procesador en particular sin usar tales instrucciones aún podría ser subóptimo en un procesador diferente, esperando un ajuste diferente del código.

Por lo general, hoy en día, en lugar de escribir en lenguaje ensamblador, los programadores usarán un desensamblador para analizar la salida de un compilador y cambiar el código fuente de alto nivel para que pueda compilarse de manera más eficiente o comprender por qué es ineficiente.

Tiempo de ejecución

Los compiladores justo a tiempo pueden producir código de máquina personalizado basado en datos en tiempo de ejecución, a costa de los gastos generales de compilación. Esta técnica data de los primeros motores de expresión regular y se ha generalizado con Java HotSpot y V8 para JavaScript. En algunos casos, la optimización adaptativa puede realizar una optimización del tiempo de ejecución que exceda la capacidad de los compiladores estáticos ajustando dinámicamente los parámetros de acuerdo con la entrada real u otros factores.

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

El código que se modifica automáticamente puede modificarse a sí mismo en respuesta a las condiciones de tiempo de ejecución para optimizar el código; esto era más común en los programas de lenguaje ensamblador.

Algunos diseños de CPU pueden realizar algunas optimizaciones en tiempo de ejecución. Algunos ejemplos incluyen ejecución fuera de orden , ejecución especulativa , canalizaciones de instrucciones y predictores de rama . Los compiladores pueden ayudar al programa a aprovechar estas características de la CPU, por ejemplo, mediante la programación de instrucciones .

Optimizaciones dependientes e independientes de la plataforma

Código de optimización se puede también clasificarse ampliamente como plataforma dependiente y técnicas independientes de la plataforma. Si bien las ú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 única o incluso del procesador único. 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 desenrollado de bucles , reducción de llamadas a funciones, rutinas eficientes de memoria, reducción de condiciones, etc.) que impactan en la mayoría de las arquitecturas de CPU de forma similar. manera. Se ha mostrado un gran ejemplo de optimización independiente de la plataforma con un 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. Generalmente, estos sirven para reducir la longitud total de la ruta de instrucción requerida 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 programación de instrucciones, paralelismo a nivel de instrucción, paralelismo a nivel de datos, técnicas de optimización de caché (es decir, parámetros que difieren entre varias plataformas) y la programación de instrucciones óptima puede ser diferente incluso en diferentes procesadores del sistema. misma arquitectura.

Reducción de fuerza

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

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

Este código puede (asumiendo que no hay desbordamiento aritmético ) ser reescrito usando una fórmula matemática como:

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

La optimización, a veces realizada automáticamente por un compilador de optimización, consiste en seleccionar un método ( algoritmo ) que sea más eficiente desde el punto de vista computacional, conservando la misma funcionalidad. Ver eficiencia algorítmica para una discusión de algunas de estas técnicas. Sin embargo, a menudo se puede lograr una mejora significativa en el rendimiento eliminando funciones extrañas.

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 lo suficientemente pequeño y el hardware particular resulta ser mucho más rápido en realizar operaciones de suma y bucle que multiplicación y división.

Compensaciones

En algunos casos, sin embargo, la optimización se basa en el uso de algoritmos más elaborados, haciendo uso de "casos especiales" y "trucos" especiales y realizando complejas compensaciones. Un programa "completamente optimizado" puede ser más difícil de comprender y, por lo tanto, puede contener más fallas que las versiones no optimizadas. Más allá de eliminar antipatrones obvios, algunas optimizaciones a nivel de código disminuyen la capacidad de mantenimiento.

La optimización generalmente se enfocará en mejorar solo uno o dos aspectos del rendimiento: tiempo de ejecución, uso de memoria, espacio en disco, ancho de banda, consumo de energía o algún otro recurso. Por lo general, esto requerirá una compensación, donde un factor se optimiza a expensas de otros. Por ejemplo, aumentar el tamaño de la caché mejora el rendimiento del tiempo de ejecución, pero también aumenta 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 algunas operaciones, pero a costa de hacer que otras operaciones sean menos eficientes. Estas compensaciones a veces pueden ser de naturaleza no técnica, como cuando un competidor ha publicado un resultado de referencia que debe superarse para mejorar el éxito comercial, pero quizás conlleva la carga de hacer que el uso normal del software sea menos eficiente. A veces, estos cambios se denominan en broma pesimizaciones .

Cuellos de botella

La optimización puede incluir encontrar un cuello de botella en un sistema, un componente que es el factor limitante del rendimiento. En términos de código, esto a menudo será un punto de acceso  , una parte crítica del código que es el consumidor principal del recurso necesario, aunque puede ser otro factor, como la latencia de E / S o el ancho de banda de la red.

En informática, el consumo de recursos a menudo sigue una forma de distribución de la ley de potencia , 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. En ingeniería de software, a menudo es una mejor aproximación que el 90% del tiempo de ejecución de un programa de computadora 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: la configuración, el tiempo de inicialización y los factores constantes del algoritmo más complejo pueden superar el beneficio y, por lo tanto, un algoritmo híbrido o adaptativo El algoritmo puede ser más rápido que cualquier algoritmo. Se puede utilizar un generador de perfiles de rendimiento para limitar las decisiones sobre qué funcionalidad se ajusta a qué condiciones.

En algunos casos, agregar más memoria puede ayudar a que un programa se ejecute más rápido. Por ejemplo, un programa de filtrado leerá comúnmente cada línea y filtrará y generará esa línea inmediatamente. Esto solo usa suficiente memoria para una línea, pero el rendimiento suele ser deficiente debido a la latencia de cada lectura de disco. El almacenamiento en caché del resultado es igualmente efectivo, aunque también requiere un mayor uso de memoria.

Cuando optimizar

La optimización puede reducir la legibilidad y agregar código que se usa solo para mejorar el rendimiento . Esto puede complicar los programas o sistemas, haciéndolos más difíciles de mantener y depurar. Como resultado, la optimización o el ajuste del rendimiento a menudo se realiza al final de la etapa de desarrollo .

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

"Debemos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todos los males. Sin embargo, no debemos dejar pasar nuestras oportunidades en ese crítico 3%".

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

"En las disciplinas de ingeniería establecidas, una mejora del 12%, de fácil obtención, nunca se considera marginal y creo que el mismo punto de vista debería prevalecer en la ingeniería de software".

"Optimización prematura" es una frase que se utiliza para describir una situación en la que un programador permite que las consideraciones de rendimiento afecten el diseño de un fragmento de código. Esto puede resultar en un diseño que no es tan limpio como podría haber sido o un código incorrecto, porque el código se complica con la optimización y el programador se distrae con la optimización.

Al decidir si optimizar una parte específica del programa, siempre se debe considerar la Ley de Amdahl : el impacto en el programa general depende en gran medida de cuánto tiempo se dedique realmente a esa parte específica, lo cual no siempre está claro al mirar el código. sin un análisis de rendimiento .

Por lo tanto, un mejor enfoque es diseñar primero, codificar a partir del diseño y luego perfilar / comparar el código resultante para ver qué partes deben optimizarse. Un diseño simple y elegante suele ser más fácil de optimizar en esta etapa, y la creación de perfiles puede revelar problemas de rendimiento inesperados que no se habrían abordado con una optimización prematura.

En la práctica, a menudo es necesario tener en cuenta los objetivos de rendimiento al diseñar el software por primera vez, pero el programador equilibra los objetivos de diseño y optimización.

Los compiladores y sistemas operativos modernos son tan eficientes que los aumentos de rendimiento previstos a menudo no se materializan. Por ejemplo, el almacenamiento en caché de datos en el nivel de la aplicación que nuevamente se almacenan en caché en el nivel del sistema operativo no produce mejoras en la ejecución. Aun así, es raro que el programador elimine optimizaciones fallidas del código de producción. También es cierto que los avances en el hardware a menudo evitarán cualquier mejora potencial, sin embargo, el código oscurecedor persistirá en el futuro mucho después de que su propósito haya sido negado.

Macros

La optimización durante el desarrollo de código utilizando macros adquiere diferentes formas en diferentes lenguajes.

En algunos lenguajes de procedimiento, como C y C ++ , las macros se implementan mediante la sustitución de tokens. Hoy en día, las funciones en línea se pueden utilizar como una alternativa segura de tipos 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, incluido el plegado constante , que puede mover 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 / árboles de sintaxis abstracta, lo que, según se afirma, los hace más seguros de usar. Dado que en muchos casos se utiliza la interpretación, esa es una forma de garantizar que dichos cálculos solo se realicen en el momento del análisis y, a veces, de la única forma.

Lisp originó este estilo de macro, y tales macros a menudo se denominan "macros tipo Lisp". Se puede lograr un efecto similar usando la metaprogramación de plantillas en C ++ .

En ambos casos, el trabajo se traslada al tiempo de compilación. La diferencia entre las macros de C en un lado y las macros de tipo Lisp y la metaprogramación de plantillas de C ++ en el otro lado, es que las últimas herramientas permiten realizar cálculos arbitrarios en tiempo de compilación / tiempo de análisis, mientras que la expansión de macros C no realiza ninguna cálculo, y se basa en la capacidad del optimizador para realizarlo. Además, las macros de C no admiten directamente la recursividad o la iteración , por lo que Turing no está completo .

Sin embargo, al igual que con cualquier optimización, a menudo es difícil predecir dónde tendrán el mayor impacto tales herramientas antes de que se complete un proyecto.

Optimización automatizada y manual

Ver también Categoría: Optimizaciones del compilador

La optimización puede ser automatizada por compiladores o realizada por programadores. Las ganancias suelen ser limitadas para la optimización local y mayores para las optimizaciones globales. Por lo general, la optimización más poderosa es encontrar un algoritmo superior .

Los programadores suelen llevar a cabo la optimización de un sistema completo porque es demasiado complejo para los optimizadores automáticos. En esta situación, los programadores o administradores del sistema cambian explícitamente el código para que el sistema en general funcione mejor. Aunque puede producir una mayor eficiencia, es mucho más caro que las optimizaciones automatizadas. Dado que muchos parámetros influyen en el rendimiento del programa, el espacio de optimización del programa es grande. La metaheurística y el aprendizaje automático se utilizan para abordar la complejidad de la optimización de programas.

Utilice un generador de perfiles (o analizador de rendimiento ) para encontrar las secciones del programa que consumen más recursos: el cuello de botella . Los programadores a veces creen que tienen una idea clara de dónde está el cuello de botella, pero la intuición a menudo se equivoca. La optimización de un fragmento de código sin importancia normalmente ayudará poco al rendimiento general.

Cuando se localiza el cuello de botella, la optimización generalmente comienza con un replanteamiento del algoritmo utilizado en el programa. La mayoría de las veces, un algoritmo particular se puede adaptar específicamente a un problema particular, produciendo un mejor rendimiento que un algoritmo genérico. Por ejemplo, la tarea de ordenar una gran lista de elementos generalmente se realiza con una rutina de clasificación rápida , que es uno de los algoritmos genéricos más eficientes. Pero si alguna característica de los elementos es explotable (por ejemplo, ya están organizados en algún orden en particular), se puede utilizar un método diferente, o incluso una rutina de clasificación personalizada.

Una vez que el programador está razonablemente seguro de que se ha seleccionado el mejor algoritmo, puede comenzar la optimización del código. Los bucles se pueden desenrollar (para una sobrecarga de bucle más baja, aunque esto a menudo puede conducir a una velocidad más baja 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 punto 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 en lugar de a los algoritmos o estructuras de datos utilizados en el programa. A veces, una parte crítica del programa se puede reescribir en un lenguaje de programación diferente que brinda un acceso más directo a la máquina subyacente. Por ejemplo, es común que los lenguajes de muy alto nivel como Python tengan módulos escritos en C para mayor velocidad. Los programas ya escritos en C pueden tener módulos escritos en ensamblador . Los programas escritos en D pueden usar el ensamblador en línea .

Reescribir secciones "vale la pena" en estas circunstancias 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 al 10% del tiempo. en el 90% restante del código. Por lo tanto, poner un esfuerzo intelectual en optimizar solo una pequeña parte del programa puede tener un efecto enorme en la velocidad general, si se pueden ubicar las partes correctas.

La optimización manual a veces tiene el efecto secundario de socavar la legibilidad. Por lo tanto, las optimizaciones de código deben documentarse cuidadosamente (preferiblemente utilizando comentarios en línea) y evaluar su efecto en el desarrollo futuro.

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

Hoy en día, las optimizaciones automatizadas se limitan casi exclusivamente a la optimización del compilador . Sin embargo, debido a que las optimizaciones del compilador generalmente se limitan a un conjunto fijo de optimizaciones bastante generales, existe una demanda considerable de optimizadores que puedan aceptar descripciones de optimizaciones específicas del lenguaje y del problema, lo que permite a un ingeniero especificar optimizaciones personalizadas. Las herramientas que aceptan descripciones de optimizaciones se denominan sistemas de transformación de programas y están comenzando a aplicarse a sistemas de software reales como C ++.

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

La computación en cuadrícula o computación distribuida tiene como objetivo optimizar todo el sistema, al mover tareas de computadoras con un uso elevado a computadoras con tiempo de inactividad.

Tiempo necesario para la optimización

A veces, el tiempo necesario para llevar a cabo la optimización en el mismo puede ser un problema.

La optimización del código existente generalmente no agrega nuevas características y, lo que es peor, podría agregar nuevos errores en el código que funcionaba anteriormente (como podría ocurrir con cualquier cambio). Debido a que el código optimizado manualmente a veces puede tener menos "legibilidad" que el código no optimizado, la optimización también puede afectar su capacidad de mantenimiento. La optimización tiene un precio y es importante asegurarse de que la inversión valga la pena.

Un optimizador automático (o un compilador de optimización , un programa que realiza la optimización de código) puede tener que ser optimizado, ya sea para mejorar aún más la eficiencia de sus programas de destino o para acelerar su propia operación. Una compilación realizada con la optimización "activada" suele tardar más, aunque esto suele ser solo 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 de destino, es la clave para mejorar la velocidad de ejecución general.

Referencias

  • Jon Bentley : Redacción de programas eficientes , ISBN  0-13-970251-2 .
  • Donald Knuth : el arte de la programación informática

enlaces externos