En informática , 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" 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 puede ser óptimo no en términos absolutos, sino sólo con respecto a una métrica de calidad determinada, que puede contrastarse con otras métricas posibles. Como resultado, el sistema optimizado normalmente sólo 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, se podría elegir deliberadamente un algoritmo más lento para utilizar menos memoria. A menudo no existe un diseño único 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 mejorarlo más) 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 alcanzar una solución completamente óptima. Afortunadamente, suele ocurrir que las mayores mejoras se producen en las primeras etapas 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 sólo mejoran el resultado; no tienen ninguna 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 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 generalmente puede proceder mediante el refinamiento de mayor a menor, donde las ganancias iniciales son mayores y se logran 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 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 enorme. 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 debe realizarse tarde, si es que alguna vez se realiza. En los proyectos de mayor duración suele haber ciclos de optimización, en los que mejorar un área revela limitaciones en otra, y éstas suelen reducirse cuando el rendimiento es aceptable o las ganancias se vuelven demasiado pequeñas o costosas.
Como el rendimiento es parte de las especificaciones de un programa, un programa que es excesivamente 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 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. A veces esto se omite por 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, fracasan porque arquitectónicamente no pueden alcanzar sus objetivos de rendimiento, como como el Intel 432 (1981); o aquellos que requieren años de trabajo para lograr un rendimiento aceptable, como Java (1995), que sólo 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 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, teniendo en cuenta los objetivos, las limitaciones y el uso/carga esperado. 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 en el rendimiento general) se optimizaría para minimizar los viajes de la red, idealmente realizando 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 prioridad clave es la compilación rápida, un compilador de una sola pasada es más rápido que un compilador de varias 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 lleva más tiempo. La elección de plataforma y lenguaje de programación ocurre en este nivel, y cambiarlos con frecuencia requiere una reescritura completa, aunque un sistema modular puede permitir la reescritura sólo de algunos componentes; por ejemplo, un programa Python puede reescribir secciones críticas para el rendimiento en C. En un sistema distribuido En el sistema, 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, especialmente si todos los componentes no se pueden reemplazar de forma sincronizada (por ejemplo, clientes antiguos).
Dado un diseño general, lo siguiente es una buena elección de algoritmos y estructuras de datos eficientes y una 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. 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 restringidas las definiciones de estructuras de datos concretas. a algunos lugares.
Para los algoritmos, esto consiste principalmente en garantizar que los algoritmos sean constantes O (1), logarítmico O (log n ), lineal O ( n ) o, en algunos casos, log-lineal O ( n log n ) en la entrada (ambos en el espacio). y tiempo). Los algoritmos con complejidad cuadrática O ( n 2 ) no logran escalar, e incluso los algoritmos lineales causan problemas si se llaman repetidamente y, si es posible, generalmente se reemplazan por algoritmos constantes o logarítmicos.
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 entradas pequeñas, 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, mejorando el rendimiento evitando trabajos innecesarios. Por ejemplo, usar un algoritmo de diseño de texto simple para texto latino 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 ésta) hoy en día se pueden realizar optimizando los compiladores . Esto depende del lenguaje de origen, el lenguaje de máquina de destino y el compilador, y puede ser difícil de entender o predecir y cambiar con el tiempo; este es un lugar clave donde la comprensión de los compiladores y el código de máquina puede mejorar el rendimiento. El movimiento del 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.
Entre el nivel fuente y de compilación, se pueden usar directivas y indicadores de compilación 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 o capacidades de hardware específicos, o predecir ramificaciones . por ejemplo. 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 de optimización 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 la máquina . Por este motivo, muchos sistemas operativos utilizados en sistemas integrados se han escrito tradicionalmente en código ensamblador. Los programas (que no sean programas muy pequeños) rara vez se escriben de principio a fin en ensamblador debido al tiempo y al costo que implican. La mayoría se compilan desde un lenguaje de alto nivel hasta un lenguaje ensamblador y se optimizan manualmente desde allí. Cuando la eficiencia y el tamaño son menos importantes, las partes grandes se pueden escribir 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 "último".
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 ajustado para un procesador en particular sin utilizar dichas instrucciones podría seguir siendo subóptimo en un procesador diferente, lo que esperaría un ajuste diferente del código.
Normalmente, hoy en día, en lugar de escribir en lenguaje ensamblador, los programadores utilizan 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 justo a tiempo pueden producir código de máquina personalizado basado en datos en 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 realizar una optimización del tiempo de ejecución que excede 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 estático de "caso promedio" de la técnica dinámica de optimización adaptativa.
El código automodificable puede alterarse en respuesta a las condiciones de tiempo de ejecución para optimizar el código; esto era más común en 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 , canalizaciones de instrucciones y predictores de bifurcación . Los compiladores pueden ayudar al programa a aprovechar estas características de la CPU, por ejemplo mediante la programación de instrucciones .
La optimización del código también se puede clasificar en términos generales como técnicas dependientes de la plataforma e independientes de la plataforma. Si bien 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 una única plataforma o incluso de un único 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 desenrollado de bucles , reducción de llamadas a funciones, rutinas de eficiencia de memoria, reducción de condiciones, etc.), que impactan a la mayoría de las arquitecturas de CPU de manera similar. forma. Se ha mostrado un gran ejemplo de optimización independiente de la plataforma 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 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 instrucciones , 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 óptima de instrucciones puede ser diferente incluso en diferentes procesadores de la misma. misma arquitectura.
Las tareas computacionales se pueden realizar de varias maneras diferentes con diferente eficiencia. Una versión más eficiente con una 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 del 1 al N :
int yo , suma = 0 ; para ( i = 1 ; i <= N ; ++ i ) { suma += i ; } printf ( "suma: %d \n " , suma );
Este código se puede reescribir (suponiendo que no haya desbordamiento aritmético ) usando 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 de optimización, consiste en seleccionar un método ( algoritmo ) que sea más eficiente desde el punto de vista computacional, manteniendo al mismo tiempo la misma funcionalidad. Consulte eficiencia algorítmica para obtener una discusión sobre algunas de estas técnicas. Sin embargo, a menudo se puede lograr una mejora significativa en el rendimiento eliminando funciones superfluas.
La optimización no siempre es un proceso obvio o intuitivo. En el ejemplo anterior, la versión "optimizada" podría en realidad ser más lenta que la versión original si N fuera lo suficientemente pequeño y el hardware en particular resulta ser mucho más rápido al realizar operaciones de suma y bucle que la 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 compensaciones complejas. 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 centrará 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, en la que 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 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 que tal vez conlleva la carga de hacer menos eficiente el uso normal del software. A veces estos cambios se denominan en broma pesimizaciones .
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, este será a menudo un punto de acceso (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 suelen ser utilizados por el 20% de las operaciones. [3] En ingeniería de software, a menudo es una mejor aproximación decir que el 90% del tiempo de ejecución de un programa de computadora se dedica 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 individual. Se puede utilizar un generador de perfiles de rendimiento para limitar las decisiones sobre qué funcionalidad se ajusta 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á esa línea inmediatamente. Esto sólo 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 sólo 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 dos declaraciones siguientes sobre la optimización:
"Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% de las veces: 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%, que se obtiene fácilmente, 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 es tan limpio como podría haber sido o un código incorrecto, porque el código se complica por 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 en 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 desempeño .
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 elaboración de perfiles puede revelar problemas de rendimiento inesperados que no se habrían solucionado mediante una optimización prematura.
En la práctica, a menudo es necesario tener en cuenta los objetivos de rendimiento al diseñar 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 nuevamente se almacenan en caché a nivel del sistema operativo no produce mejoras en la ejecución. Aun así, es raro que el programador elimine las optimizaciones fallidas del código de producción. También es cierto que los avances en el hardware en la mayoría de los casos obviarán cualquier mejora potencial, pero el código oscurecido persistirá en el futuro mucho después de que se haya negado su propósito.
La optimización durante el desarrollo de código mediante macros adopta diferentes formas en diferentes idiomas.
En algunos lenguajes de procedimientos, 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 en muchos casos. En ambos casos, el cuerpo de la función incorporada 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 funcionales , las macros se implementan mediante la sustitución en tiempo de análisis de árboles de análisis/árboles de sintaxis abstracta, lo que se afirma que 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 sólo se realicen en el momento del análisis y, a veces, la única manera.
Lisp originó este estilo de macro, [ cita necesaria ] y estas macros a menudo se denominan "macros similares a Lisp". Se puede lograr un efecto similar mediante el uso de metaprogramación de plantillas en C++ .
En ambos casos, el trabajo se traslada al tiempo de compilación. La diferencia entre las macros C por un lado, y las macros tipo Lisp y la metaprogramación de plantillas C++ por el otro, es que estas últimas herramientas permiten realizar cálculos arbitrarios en tiempo de compilación/análisis, mientras que la expansión de las macros C no realiza ningún tipo de cálculo. cálculo y depende de la capacidad del optimizador para realizarlo. Además, las macros de C no admiten directamente la recursividad o la iteración , por lo que no están completas en 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.
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. Generalmente, la optimización más poderosa es encontrar un algoritmo superior .
La optimización de un sistema completo generalmente la realizan los programadores porque es demasiado compleja para los optimizadores automatizados. 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 costosa que las optimizaciones automatizadas. Dado que muchos parámetros influyen en el rendimiento del programa, el espacio de optimización del programa es grande. 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 la mayor cantidad de 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 suele ser errónea. [ cita necesaria ] La optimización de un fragmento de código sin importancia normalmente hará poco para mejorar el rendimiento general.
Cuando se localiza el cuello de botella, la optimización suele comenzar 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 concreto, lo que produce un mejor rendimiento que un algoritmo genérico. Por ejemplo, la tarea de ordenar una lista enorme 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 ordenados en algún orden 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 reducir la sobrecarga del bucle, aunque esto a menudo puede conducir a una menor velocidad si sobrecarga el 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 más que a 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 brinde 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 mayor velocidad. Los programas ya escritos en C pueden tener módulos escritos en ensamblador . Los programas escritos en D pueden utilizar el ensamblador en línea .
Reescribir secciones "merece la pena" en estas circunstancias debido a una " regla empírica " general conocida como la ley 90/10, que establece que el 90% del tiempo se dedica al 10% del código, y sólo el 10% del tiempo en el 90% restante del código. Por lo tanto, dedicar un esfuerzo intelectual a optimizar sólo 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 evaluarse su efecto 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 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 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 grid o computación distribuida tiene como objetivo optimizar todo el sistema, trasladando tareas de computadoras con alto uso a computadoras con tiempo de inactividad.
A veces, el tiempo necesario para realizar la optimización puede ser un problema.
La optimización del código existente generalmente no agrega nuevas funciones y, lo que es peor, puede 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 mantenimiento. La optimización tiene un precio y es importante estar seguro de que la inversión vale la pena.
Es posible que un optimizador automático (u compilador de optimización , un programa que realiza la optimización del código) deba optimizarse, ya sea para mejorar aún más la eficiencia de sus programas de destino o acelerar su propia operación. Una compilación realizada con la optimización "activada" suele tardar más, aunque esto suele ser sólo 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.
Hoare, sin embargo, no lo afirmó cuando le pregunté en enero de 2004.