En informática , la optimización de programas , optimización de código u optimización de software es el proceso de modificar un sistema de software para hacer que algún aspecto del mismo funcione de manera más eficiente o utilice menos recursos. [1] En general, un programa de computadora puede optimizarse para que se ejecute más rápidamente, o para que sea capaz de operar con menos almacenamiento de memoria u otros recursos, o consumir menos energía.
Aunque la palabra "optimización" tiene 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 hacer óptimo no en términos absolutos, sino solo con respecto a una métrica de calidad dada, que puede contrastar con otras métricas posibles. Como resultado, el sistema optimizado normalmente solo será óptimo en una aplicación o para un público. Se puede reducir la cantidad de tiempo que un programa tarda en realizar alguna tarea al precio de hacer que consuma más memoria. En una aplicación donde el espacio de memoria es un bien escaso, se puede elegir deliberadamente un algoritmo más lento para utilizar menos memoria. A menudo no existe un diseño "universal" 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 necesario para hacer que un software sea completamente óptimo (incapaz de cualquier mejora adicional) es casi siempre mayor de lo razonable para los beneficios que se obtendrí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 mayores mejoras se producen al principio del proceso.
Incluso para una determinada métrica de calidad (como la velocidad de ejecución), la mayoría de los métodos de optimización solo mejoran el resultado; no tienen la pretensión de producir un resultado óptimo. La superoptimización es el proceso de encontrar un resultado verdaderamente óptimo.
La optimización puede ocurrir en varios niveles. Normalmente, los niveles superiores tienen un mayor impacto y son más difíciles de cambiar más adelante en un proyecto, requiriendo 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, con ganancias iniciales mayores y logradas con menos trabajo, y ganancias posteriores menores y requiriendo más trabajo. Sin embargo, en algunos casos, el rendimiento general depende del rendimiento de partes de nivel muy bajo 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 descomunal. Normalmente se da cierta consideració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 debe hacer más tarde, si es que se hace. En proyectos de mayor duración, normalmente hay ciclos de optimización, donde la mejora de un área revela limitaciones en otra, y estos 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 sea inutilizablemente lento no es apto para el propósito: un videojuego con 60 Hz (fotogramas por segundo) es aceptable, pero 6 fotogramas por segundo es inaceptablemente entrecortado), el rendimiento es una consideración desde el principio, para garantizar que el sistema sea capaz de 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 por un orden de magnitud o más) y sistemas que en última instancia son fracasos porque arquitectónicamente no pueden lograr sus objetivos de rendimiento, como el 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 su grado de optimización, puede ser una fuente importante de incertidumbre y riesgo.
En el nivel más alto, el diseño puede optimizarse para hacer el mejor uso de los recursos disponibles, dadas las metas, las restricciones y el uso/carga esperados. El diseño arquitectónico de un sistema afecta abrumadoramente su rendimiento. Por ejemplo, un sistema que está limitado por la latencia de la red (donde la latencia de la red es la principal restricción en el 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 sola 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 el objetivo, un compilador de múltiples pasadas más lento cumple el objetivo mejor, aunque él mismo tome más tiempo. 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. En un sistema distribuido, la elección de la arquitectura ( cliente-servidor , peer-to-peer , etc.) se produce en el nivel de diseño y puede ser difícil de cambiar, en particular si no se pueden reemplazar todos los componentes en sincronía (por ejemplo, clientes antiguos).
Dado un diseño general, una buena elección de algoritmos y estructuras de datos eficientes , y una implementación eficiente de estos algoritmos y estructuras de datos viene a continuación. 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. Generalmente, las estructuras de datos son más difíciles de cambiar que los algoritmos, ya que una suposición de estructura de datos y sus suposiciones de rendimiento se utilizan en todo el programa, aunque esto se puede minimizar mediante el uso de tipos de datos abstractos en las definiciones de funciones y manteniendo las definiciones de estructura de datos concretas restringidas a unos pocos lugares.
En el caso de los algoritmos, esto consiste principalmente en garantizar que los algoritmos sean constantes O(1), logarítmicos O(log n ), lineales O( n ) o, en algunos casos, log-lineales O( n log n ) en la entrada (tanto en el espacio como en el tiempo). Los algoritmos con complejidad cuadrática O( n 2 ) no logran escalar, e incluso los algoritmos lineales causan problemas si se los llama repetidamente y, por lo general, se los reemplaza por 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 es más simple) que un algoritmo asintóticamente más rápido cuando ambos se enfrentan a una entrada pequeña, lo 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, lo que mejora el rendimiento al evitar trabajo innecesario. Por ejemplo, usar un algoritmo de diseño de texto simple para texto en latín y cambiar solo a un algoritmo de diseño complejo para escrituras complejas, 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 debido a cachés obsoletos.
Más allá de los algoritmos generales y su implementación en una máquina abstracta, las elecciones concretas a 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)
evaluaba 1 y luego tenía un salto condicional que probaba si era verdadero, mientras que for (;;)
tenía un salto incondicional. Algunas optimizaciones (como esta) se pueden realizar hoy en día optimizando compiladores . Esto depende del lenguaje de origen, el lenguaje 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 del código invariante de bucles 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.
Entre el nivel de código fuente y el de compilación, se pueden utilizar directivas y marcas de compilación para ajustar las opciones de rendimiento en el código fuente y el compilador respectivamente, como por ejemplo, utilizando definiciones de preprocesador para deshabilitar funciones de software innecesarias, optimizar para modelos de procesadores específicos o capacidades de hardware, o predecir ramificaciones . 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.
El uso de un compilador optimizador tiende a garantizar que el programa ejecutable esté optimizado al menos tanto como el compilador puede predecir.
En el nivel más bajo, escribir código utilizando un lenguaje ensamblador , diseñado para una plataforma de hardware particular, puede producir el código más eficiente y compacto si el programador aprovecha el repertorio completo de instrucciones de máquina . Muchos sistemas operativos utilizados en sistemas integrados se han escrito tradicionalmente en código ensamblador por esta razón. Los programas (excepto los programas muy pequeños) rara vez se escriben de principio a fin en ensamblador debido al tiempo y el costo involucrados. La mayoría se compilan desde un lenguaje de alto nivel a ensamblador y se optimizan manualmente a partir de 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 en día está pensado para ejecutarse en tantas máquinas como sea posible. Como consecuencia, los programadores y compiladores no siempre aprovechan las instrucciones más eficientes que ofrecen las CPU más nuevas o las peculiaridades de los modelos más antiguos. Además, el código ensamblador optimizado para un procesador en particular sin utilizar dichas instrucciones puede no ser óptimo en un procesador diferente, si se espera un ajuste diferente del código.
Hoy en día, en lugar de escribir en lenguaje ensamblador, los programadores suelen utilizar 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.
Los compiladores Just-in-time pueden producir código de máquina personalizado basado en datos de tiempo de ejecución, a costa de 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 ser capaz de realizar una optimización en tiempo de ejecución que supere la capacidad de los compiladores estáticos al ajustar 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 promedio" estático de la técnica dinámica de optimización adaptativa.
El código automodificable 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 en 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 , secuencias de instrucciones y predictores de bifurcaciones . Los compiladores pueden ayudar al programa a aprovechar estas características de la CPU, por ejemplo, a través de la programación de instrucciones .
La optimización de código también se puede categorizar ampliamente como técnicas dependientes de la plataforma e independientes de la plataforma. Mientras que 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, puede 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 en las llamadas de función, las rutinas eficientes en memoria, la reducción de las condiciones, etc.), que afectan a la mayoría de las arquitecturas de CPU de una 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. [2] Generalmente, estos sirven para reducir la longitud total de la ruta de instrucciones 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 involucran 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 podría ser diferente incluso en diferentes procesadores de la misma arquitectura.
Las tareas computacionales se pueden realizar de varias maneras diferentes con distinta eficiencia. Una versión más eficiente con funcionalidad equivalente se conoce como reducción de fuerza . 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 , suma = 0 ; para ( i = 1 ; i <= N ; ++ i ) { suma += i ; } printf ( "suma: %d \n " , suma );
Este código puede (asumiendo que no hay 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, que a veces se realiza automáticamente mediante un compilador optimizador, consiste en seleccionar un método ( algoritmo ) que sea más eficiente computacionalmente, pero que conserve la misma funcionalidad. Véase eficiencia algorítmica para obtener una explicación de algunas de estas técnicas. Sin embargo, a menudo se puede lograr una mejora significativa del 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 lo suficientemente pequeña y el hardware en particular fuera mucho más rápido al realizar operaciones de suma y bucle que de multiplicación y división.
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 concesiones complejas. Un programa "totalmente optimizado" puede ser más difícil de comprender y, por lo tanto, puede contener más fallas que las versiones no optimizadas. Además de eliminar los antipatrones obvios, algunas optimizaciones a nivel de código reducen la capacidad de mantenimiento.
La optimización generalmente se centra 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. Esto generalmente requiere un equilibrio, donde un factor se optimiza a expensas de otros. Por ejemplo, aumentar el tamaño de la memoria caché mejora el rendimiento en tiempo de ejecución, pero también aumenta el consumo de memoria. Otros equilibrios 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 pueden ser a veces de naturaleza no técnica, como cuando un competidor ha publicado un resultado de referencia que debe superarse para mejorar el éxito comercial, pero que tal vez conlleva la carga de hacer que el uso normal del software sea menos eficiente. A estos cambios a veces se los llama en broma pesimismos .
La optimización puede incluir la búsqueda de 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 crítico , una parte crítica del código que es el principal consumidor 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 ley de potencia , y el principio de Pareto se puede aplicar a la optimización de recursos observando que el 80% de los recursos se utilizan normalmente en el 20% de las operaciones. [3] 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 gasta en ejecutar el 10% del código (conocida como la ley 90/10 en este contexto).
Los algoritmos y las 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 puede ser más rápido que cualquier algoritmo único. Se puede utilizar un perfilador de rendimiento para limitar las decisiones sobre qué funcionalidad se adapta a qué condiciones. [4]
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 normalmente leerá cada línea y filtrará y generará la salida de esa línea inmediatamente. Esto solo utiliza suficiente memoria para una línea, pero el rendimiento suele ser deficiente debido a la latencia de cada lectura del disco. El almacenamiento en caché del resultado es igualmente eficaz, aunque también requiere un mayor uso de memoria.
La optimización puede reducir la legibilidad y agregar código que se utiliza 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 la optimización:
"Deberíamos 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 deberíamos dejar pasar nuestras oportunidades en ese crítico 3%" [5]
(También atribuyó la cita a Tony Hoare varios años después, [6] aunque esto podría haber sido un error ya que Hoare niega haber acuñado la frase. [7] )
"En las disciplinas de ingeniería establecidas, una mejora del 12%, fácilmente obtenida, nunca se considera marginal y creo que el mismo punto de vista debería prevalecer en la ingeniería de software" [5]
"Optimización prematura" es una frase que se utiliza para describir una situación en la que un programador permite que consideraciones de rendimiento afecten el diseño de un fragmento de código. Esto puede dar como resultado un diseño que no sea tan limpio como podría haber sido o un código incorrecto, porque el código se complica debido a la optimización y el programador se distrae con la optimización.
A la hora de decidir si optimizar una parte específica del programa, siempre se debe tener en cuenta la Ley de Amdahl : el impacto en el programa general depende en gran medida de cuánto tiempo se dedica realmente a esa parte específica, lo que no siempre queda claro al observar 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 se deben optimizar. Un diseño simple y elegante suele ser más fácil de optimizar en esta etapa, y el perfilado puede revelar problemas de rendimiento inesperados que no se habrían solucionado con una optimización prematura.
En la práctica, a menudo es necesario tener en mente los objetivos de rendimiento cuando se diseña 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, almacenar en caché datos a nivel de aplicación que a su vez se almacenan en caché a nivel de 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 hardware suelen obviar posibles mejoras, pero el código que oculta información persistirá en el futuro mucho después de que se haya anulado su propósito.
La optimización durante el desarrollo de código mediante macros adopta diferentes formas en distintos lenguajes.
En algunos lenguajes procedimentales, como C y C++ , las macros se implementan mediante sustitución de tokens. Hoy en día, las funciones en línea se pueden utilizar como una alternativa segura para los tipos en muchos casos. En ambos casos, el compilador puede realizar más optimizaciones en tiempo de compilación al cuerpo de la función en línea, incluido el plegado de constantes , 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/árboles de sintaxis abstracta, lo que, según se afirma, hace que su uso sea más seguro. Dado que en muchos casos se utiliza la interpretación, esa es una forma de garantizar que dichos cálculos solo se realicen en tiempo de análisis y, a veces, la única forma.
Lisp fue el creador de este estilo de macros, [ cita requerida ] y a estas macros se las suele llamar "macros similares a Lisp". Se puede lograr un efecto similar utilizando 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 por un lado, y las macros de tipo Lisp y la metaprogramación de plantillas de C++ por el otro, es que las últimas herramientas permiten realizar cálculos arbitrarios en tiempo de compilación/análisis, mientras que la expansión de las macros de C no realiza ningún cálculo y depende de la capacidad del optimizador para realizarlo. Además, las macros de C no admiten directamente la recursión o la iteración , por lo que no son completas según el método de Turing .
Sin embargo, como ocurre con cualquier optimización, a menudo es difícil predecir dónde dichas herramientas tendrán el mayor impacto antes de que se complete un proyecto.
Véase 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 consiste en encontrar un algoritmo superior .
La optimización de un sistema completo suele ser una tarea que llevan a cabo los programadores, ya que resulta demasiado compleja para los optimizadores automáticos. En esta situación, los programadores o administradores de sistemas modifican explícitamente el código para que el sistema en su conjunto funcione mejor. Aunque puede producir una mayor 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 del programa es amplio. Se utilizan metaheurísticas y aprendizaje automático para abordar la complejidad de la optimización del programa. [8]
Utilice un generador de perfiles (o analizador de rendimiento ) para encontrar las secciones del programa que consumen más recursos: el cuello de botella . A veces, los programadores creen que tienen una idea clara de dónde está el cuello de botella, pero la intuición suele estar equivocada. [ cita requerida ] Optimizar una parte poco importante del código normalmente no ayudará mucho al rendimiento general.
Cuando el cuello de botella está localizado, la optimización suele comenzar con un replanteamiento del algoritmo utilizado en el programa. En la mayoría de los casos, un algoritmo particular puede adaptarse específicamente a un problema particular, lo que produce un mejor rendimiento que un algoritmo genérico. Por ejemplo, la tarea de ordenar una enorme lista de elementos suele realizarse con una rutina de ordenación rápida , que es uno de los algoritmos genéricos más eficientes. Pero si alguna característica de los elementos es aprovechable (por ejemplo, ya están ordenados en un orden particular), se puede utilizar un método diferente, o incluso una rutina de ordenació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. Se pueden desenrollar los bucles (para reducir la sobrecarga de bucle, aunque esto puede conducir a una menor velocidad si sobrecarga la memoria caché de la CPU ), se pueden utilizar tipos de datos lo más pequeños posibles, se puede utilizar aritmética de números 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 las 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 brinde 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 una 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 .
En estas circunstancias, reescribir secciones "resulta rentable" 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 % del tiempo al 90 % restante del código. Por lo tanto, dedicar un esfuerzo intelectual a optimizar solo una pequeña parte del programa puede tener un efecto enorme en la velocidad general, siempre que se puedan encontrar 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 mediante comentarios en línea) y evaluar su efecto en el desarrollo futuro.
El programa que realiza una optimización automática se denomina optimizador . La mayoría de los optimizadores están integrados en los compiladores y funcionan durante la compilación. Los optimizadores suelen poder adaptar el código generado a procesadores específicos.
En la actualidad, las optimizaciones automatizadas se limitan casi exclusivamente a la optimización del compilador . Sin embargo, dado que las optimizaciones del compilador suelen limitarse a un conjunto fijo de optimizaciones bastante generales, existe una demanda considerable de optimizadores que puedan aceptar descripciones de problemas y optimizaciones específicas del lenguaje, 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, trasladando tareas desde computadoras con alto uso a computadoras con tiempo de inactividad.
A veces, el tiempo que lleva realizar la optimización en sí puede ser un problema.
La optimización del código existente no suele añadir nuevas funciones y, lo que es peor, puede añadir nuevos errores al código que funcionaba anteriormente (como 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 a su capacidad de mantenimiento. La optimización tiene un precio y es importante estar seguro de que la inversión valga la pena.
Es posible que un optimizador automático (o compilador optimizador , un programa que realiza la optimización del código) deba ser optimizado, ya sea para mejorar aún más la eficiencia de sus programas de destino o para acelerar su propio funcionamiento. Una compilación realizada con la optimización "activada" suele tardar más, aunque esto suele ser un problema solo cuando los programas son bastante grandes.
En particular, para los compiladores just-in-time, 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.
Hoare, sin embargo, no lo afirmó cuando lo consulté en enero de 2004