optimization - ¿Cuándo es la optimización prematura?
premature-optimization (20)
Como Knuth dijo,
Deberíamos olvidarnos de las pequeñas eficiencias, digamos aproximadamente el 97% del tiempo: la optimización prematura es la raíz de todo mal.
Esto es algo que a menudo surge en las respuestas de Stack Overflow a preguntas como "¿cuál es el mecanismo de bucle más eficiente?", "¿Técnicas de optimización de SQL?" ( y así sucesivamente ). La respuesta estándar a estas preguntas de optimización es crear un perfil de su código y ver si es un problema primero, y si no es así, entonces su nueva técnica es innecesaria.
Mi pregunta es, si una técnica en particular es diferente pero no particularmente oscura u ofuscada, ¿realmente se puede considerar una optimización prematura?
Aquí hay un artículo relacionado por Randall Hyde llamado The Falacy of Premature Optimization .
Mi pregunta es, si una técnica en particular es diferente pero no particularmente oscura u ofuscada, ¿realmente se puede considerar una optimización prematura?
Um ... Entonces tienes dos técnicas a mano, idénticas en costo (el mismo esfuerzo para usar, leer, modificar) y una es más eficiente. No, usar el más eficiente no sería, en ese caso, prematuro.
Interrumpir la escritura de códigos para buscar alternativas a las construcciones de programación / rutinas de biblioteca comunes con la posibilidad de que haya una versión más eficiente en algún lugar aunque, por lo que sabemos, la velocidad relativa de lo que está escribiendo nunca importará. .. Eso es prematuro.
A menos que descubra que necesita más rendimiento de su aplicación, ya sea por un usuario o una necesidad comercial, hay pocas razones para preocuparse por la optimización. Incluso entonces, no hagas nada hasta que hayas perfilado tu código. Luego ataca las partes que toman más tiempo.
Como publiqué en una pregunta similar, las reglas de optimización son:
1) No optimizar
2) (solo para expertos) Optimizar más tarde
¿Cuándo es la optimización prematura? Generalmente.
La excepción es quizás en su diseño, o en un código bien encapsulado que se usa mucho. En el pasado he trabajado en un código de tiempo crítico (una implementación de RSA) donde observar el ensamblador que produjo el compilador y eliminar una sola instrucción innecesaria en un ciclo interno dio un 30% de aceleración. Pero, la aceleración del uso de algoritmos más sofisticados fue de órdenes de magnitud más que eso.
Otra pregunta que debe hacerse al optimizar es "¿estoy haciendo lo mismo que optimizar un módem de 300 baudios aquí?" . En otras palabras, la ley de Moore hará que su optimización sea irrelevante antes de que pase demasiado tiempo. Muchos problemas de escalado se pueden resolver simplemente lanzando más hardware al problema.
Por último, es prematuro optimizar antes de que el programa vaya demasiado lento. Si está hablando de la aplicación web, puede ejecutarla bajo carga para ver dónde están los cuellos de botella, pero lo más probable es que tenga los mismos problemas de escalabilidad que la mayoría de los otros sitios, y se aplicarán las mismas soluciones.
editar: Por cierto, con respecto al artículo vinculado, cuestionaría muchas de las suposiciones hechas. En primer lugar, no es cierto que la ley de Moore dejó de funcionar en los años 90. En segundo lugar, no es obvio que el tiempo del usuario sea más valioso que el tiempo del programador. La mayoría de los usuarios son (por decir lo menos) no frenéticamente utilizando todos los ciclos de CPU disponibles de todos modos, probablemente estén esperando que la red haga algo. Además, hay un costo de oportunidad cuando el tiempo del programador se desvía de la implementación de otra cosa, para afeitarse unos pocos milisegundos de algo que el programa hace mientras el usuario está en el teléfono. Algo más que eso no suele ser la optimización, es la corrección de errores.
Cuando se programa, una cantidad de parámetros son vitales. Entre estos están:
- Legibilidad
- Mantenibilidad
- Complejidad
- Robustez
- Exactitud
- Actuación
- Tiempo de desarrollo
La optimización (yendo para el rendimiento) a menudo se produce a expensas de otros parámetros, y debe equilibrarse con la "pérdida" en estas áreas.
Cuando tiene la opción de elegir algoritmos bien conocidos que funcionan bien, el costo de "optimizar" por adelantado es a menudo aceptable.
De la forma en que lo veo, si optimizas algo sin saber cuánto rendimiento puedes obtener en diferentes escenarios ES una optimización prematura. El objetivo del código debería ser realmente más fácil de leer para los humanos.
Desde la perspectiva de una base de datos, no considerar el diseño óptimo en la etapa de diseño es temerario en el mejor de los casos. Las bases de datos no se refactorizan fácilmente. Una vez que están mal diseñados (esto es lo que un diseño que no considera la optimización no importa cómo puede tratar de esconderse detrás de las tonterías de la optimización prematura), casi nunca puede recuperarse de eso porque la base de datos es demasiado básica para el operación de todo el sistema Es mucho menos costoso diseñar correctamente teniendo en cuenta el código óptimo para la situación que espera que esperar hasta que haya un millón de usuarios y la gente grite porque utilizó los cursores en toda la aplicación. Otras optimizaciones como el uso de código sargeable, la selección de los mejores índices posibles, etc. solo tienen sentido en el momento del diseño. Hay una razón por la cual se llama rápido y sucio. Debido a que no puede funcionar bien nunca, no use la rapidez como sustituto del buen código. También, francamente, cuando comprenda la optimización del rendimiento en las bases de datos, puede escribir código que tenga más probabilidades de funcionar bien en el mismo tiempo o menos de lo que lleva escribir código que no funciona bien. No tomarse el tiempo para aprender qué es un buen diseño de la base de datos es la pereza del desarrollador, no la mejor práctica.
Don Knuth comenzó el movimiento de programación literaria porque creía que la función más importante del código de computadora es comunicar la intención del programador a un lector humano . Cualquier práctica de codificación que haga que su código sea más difícil de entender en nombre del rendimiento es una optimización prematura.
Ciertos modismos que se introdujeron en nombre de la optimización se han vuelto tan populares que todos los entienden y se han convertido en algo esperado , no prematuros. Ejemplos incluyen
Usar la aritmética de puntero en lugar de la notación de matriz en C, incluido el uso de modismos como
for (p = q; p < lim; p++)
Vuelva a enlazar variables globales a variables locales en Lua, como en
local table, io, string, math = table, io, string, math
Más allá de dichos modismos, tome atajos a su propio riesgo .
Toda optimización es prematura a menos que
Un programa es demasiado lento (muchas personas olvidan esta parte).
Tiene una medida (perfil o similar) que muestra que la optimización podría mejorar las cosas .
(También está permitido optimizar para la memoria).
Respuesta directa a la pregunta:
- Si su técnica "diferente" hace que el programa sea más difícil de entender , entonces es una optimización prematura .
EDITAR : En respuesta a los comentarios, usar quicksort en lugar de un algoritmo más simple como el ordenamiento por inserción es otro ejemplo de un idioma que todos entienden y esperan . (Aunque si escribe su propia rutina de clasificación en lugar de usar la rutina de clasificación de la biblioteca, se espera que tenga una muy buena razón).
El objetivo de la máxima es que, típicamente , la optimización es intrincada y compleja. Y, por lo general , usted el arquitecto / diseñador / programador / mantenedor necesita un código claro y conciso para comprender lo que está sucediendo.
Si una optimización particular es clara y concisa, siéntase libre de experimentar con ella (pero retroceda y verifique si esa optimización es efectiva). El objetivo es mantener el código claro y conciso durante todo el proceso de desarrollo, hasta que los beneficios del rendimiento superen los costos inducidos de escribir y mantener las optimizaciones.
En mi humilde opinión, el 90% de su optimización debe ocurrir en la etapa de diseño, en función de la corriente percibida, y más importante, los requisitos futuros. Si tiene que sacar un generador de perfiles porque su aplicación no escala a la carga requerida, lo ha dejado demasiado tarde, y la OMI perderá mucho tiempo y esfuerzo al no corregir el problema.
Por lo general, las únicas optimizaciones que valen la pena son aquellas que le permiten mejorar el orden de magnitud en términos de velocidad o un multiplicador en términos de almacenamiento o ancho de banda. Estos tipos de optimizaciones normalmente se relacionan con la selección del algoritmo y la estrategia de almacenamiento, y son extremadamente difíciles de revertir en el código existente. Pueden ir tan profundo como influir en la decisión sobre el idioma en el que implementa su sistema.
Así que mi consejo es que optimices pronto, en función de tus requisitos, no de tu código, y observa la posible duración prolongada de tu aplicación.
Este es el problema que veo con todo el concepto de evitar la optimización prematura.
Hay una desconexión entre decirlo y hacerlo.
He realizado muchos ajustes de rendimiento, extrayendo grandes factores de un código que, por lo demás, estaba bien diseñado, aparentemente sin una optimización prematura. Aquí hay un ejemplo.
En casi todos los casos, la razón del rendimiento subóptimo es lo que llamo generalización galopante , que es el uso de clases abstractas de múltiples capas y un diseño minucioso orientado a objetos, donde los conceptos simples serían menos elegantes pero totalmente suficientes.
Y en el material didáctico donde se enseñan estos conceptos abstractos de diseño, como la arquitectura impulsada por notificaciones y el ocultamiento de información donde simplemente establecer una propiedad booleana de un objeto puede tener un efecto dominó sin límites de las actividades, ¿cuál es el motivo? Eficiencia
Entonces, ¿fue esa optimización prematura o no?
Intento optimizar solo cuando se confirma un problema de rendimiento.
Mi definición de optimización prematura es "esfuerzo desperdiciado en el código que no se sabe que es un problema de rendimiento". Definitivamente hay un momento y lugar para la optimización. Sin embargo, el truco consiste en gastar el costo adicional solo en lo que respecta al rendimiento de la aplicación y en el que el costo adicional supera el rendimiento alcanzado.
Al escribir código (o una consulta DB) me esfuerzo por escribir código "eficiente" (es decir, código que realiza su función prevista, rápida y completamente con la lógica más simple razonable). Tenga en cuenta que el código "eficiente" no es necesariamente lo mismo que "optimizado" código. Las optimizaciones a menudo introducen una complejidad adicional en el código que aumenta tanto el costo de desarrollo como el de mantenimiento de ese código.
Mi consejo: solo pague el costo de la optimización cuando pueda cuantificar el beneficio.
La necesidad de usar un generador de perfiles debe dejarse para casos extremos. Los ingenieros del proyecto deben saber dónde están los cuellos de botella de rendimiento.
Creo que la "optimización prematura" es increíblemente subjetiva.
Si estoy escribiendo algún código y sé que debería usar un Hashtable, lo haré. No lo implementaré de manera defectuosa y luego esperaré a que el informe de error llegue un mes o un año después, cuando alguien tenga un problema con él.
El rediseño es más costoso que optimizar un diseño de manera evidente desde el principio.
Obviamente, algunas cosas pequeñas se perderán la primera vez, pero rara vez son decisiones clave de diseño.
Por lo tanto: NO optimizar un diseño es un olor a código en sí mismo.
La optimización puede ocurrir en diferentes niveles de granularidad, desde muy alto nivel a muy bajo nivel:
Comience con una buena arquitectura, acoplamiento flexible, modularidad, etc.
Elija las estructuras de datos y los algoritmos correctos para el problema.
Optimizar para memoria, tratando de encajar más código / datos en la memoria caché. El subsistema de memoria es de 10 a 100 veces más lento que la CPU, y si sus datos se localizan en el disco, es de 1000 a 10 000 veces más lento. Tener precaución con el consumo de memoria es más probable que proporcione ganancias mayores que la optimización de instrucciones individuales.
Dentro de cada función, haga un uso apropiado de las instrucciones de control de flujo. (Mueva las expresiones inmutables fuera del cuerpo del bucle. Ponga primero el valor más común en un interruptor / caja, etc.)
Dentro de cada enunciado, use las expresiones más eficientes que arrojen el resultado correcto. (Multiplicar vs. turno, etc.)
Nit-picking acerca de si usar una expresión de división o una expresión de cambio no es necesariamente una optimización prematura. Solo es prematuro si lo hace sin optimizar primero la arquitectura, las estructuras de datos, los algoritmos, la huella de memoria y el control de flujo.
Y, por supuesto, cualquier optimización es prematura si no define un umbral de rendimiento del objetivo.
En la mayoría de los casos, ya sea:
A) Puede alcanzar el umbral de rendimiento del objetivo realizando optimizaciones de alto nivel, por lo que no es necesario jugar con las expresiones.
o
B) Incluso después de realizar todas las optimizaciones posibles, no alcanzará el umbral de rendimiento de su objetivo, y las optimizaciones de bajo nivel no hacen suficiente diferencia en el rendimiento para justificar la pérdida de legibilidad.
En mi experiencia, la mayoría de los problemas de optimización se pueden resolver a nivel de arquitectura / diseño o de estructura de datos / algoritmo. La optimización de la huella de memoria a menudo (aunque no siempre) es necesaria. Pero rara vez es necesario optimizar la lógica de control de flujo y expresión. Y en los casos donde realmente es necesario, raramente es suficiente.
La respuesta de Norman es excelente. De alguna manera, rutinariamente se realiza una "optimización prematura" que, en realidad, son las mejores prácticas, ya que de lo contrario se sabe que es totalmente ineficiente.
Por ejemplo, para agregar a la lista de Norman:
- Usar la concatenación de StringBuilder en Java (o C #, etc.) en lugar de String + String (en un bucle);
- Evitando hacer bucles en C like:
for (i = 0; i < strlen(str); i++)
(porque strlen aquí es una llamada de función que recorre la cadena cada vez, llamada en cada ciclo); - Parece que en la mayoría de las implementaciones de JavaScript, es más rápido de hacer también
for (i = 0 l = str.length; i < l; i++)
y todavía es legible, así que está bien.
Y así. Pero tales micro-optimizaciones nunca deberían ser a costa de la legibilidad del código.
Lo que parece que está hablando es la optimización, como usar un contenedor de búsqueda basado en hash versus uno indexado, como una matriz, cuando se realizarán muchas búsquedas clave. Esto no es una optimización prematura, sino algo que debes decidir en la fase de diseño.
El tipo de optimización de la regla Knuth es minimizar la longitud de las rutas de códigos más comunes, optimizando el código que se ejecuta más, por ejemplo, reescribiendo en ensamblaje o simplificando el código, haciéndolo menos general. Pero hacer esto no sirve de nada hasta que esté seguro de qué partes del código necesitan este tipo de optimización y optimizar (¿podría?) Hacer que el código sea más difícil de comprender o mantener, por lo tanto, "la optimización prematura es la raíz de todo mal".
Knuth también dice que siempre es mejor, en lugar de optimizar, cambiar los algoritmos que usa su programa, el enfoque que lleva a un problema. Por ejemplo, mientras que un pequeño ajuste podría darle un 10% de aumento de velocidad con la optimización, cambiando fundamentalmente la forma en que funciona su programa podría hacerlo 10 veces más rápido.
En respuesta a muchos de los otros comentarios publicados sobre esta pregunta: ¡selección de algoritmo! = Optimización
No creo que las mejores prácticas reconocidas sean optimizaciones prematuras. Se trata más de grabar el tiempo en lo que tal si se trata de posibles problemas de rendimiento en función de los escenarios de uso. Un buen ejemplo: si quema una semana tratando de optimizar la reflexión sobre un objeto antes de tener pruebas de que se trata de un cuello de botella, está optimizando prematuramente.
Primero, haz que el código funcione. Segundo, verifique que el código sea correcto. Tercero, hazlo rápido.
Cualquier cambio de código que se realice antes de la etapa 3 es definitivamente prematuro. No estoy del todo seguro de cómo clasificar las elecciones de diseño anteriores (como usar estructuras de datos adecuadas), pero prefiero usar las abstracciones que son fáciles de programar en vez de las que tienen un buen rendimiento, hasta que estoy en una etapa donde puedo comenzar a usar perfiles y tener una implementación de referencia correcta (aunque frecuentemente lenta) para comparar resultados.
Si no ha perfilado, es prematuro.
Una optimización prematura para mí significa tratar de mejorar la eficiencia de su código antes de tener un sistema en funcionamiento, y antes de que realmente lo haya perfilado y sepa dónde está el cuello de botella. Incluso después de eso, la legibilidad y la mantenibilidad deben venir antes de la optimización en muchos casos.
Vale la pena señalar que la cita original de Knuth proviene de un documento que escribió promocionando el uso de goto
en áreas cuidadosamente seleccionadas y medidas como una forma de eliminar zonas activas. Su cita fue una advertencia que agregó para justificar su razón de ser de usar goto
para acelerar esos lazos críticos.
[...] nuevamente, este es un ahorro notable en la velocidad general de carrera, si, por ejemplo, el valor promedio de n es aproximadamente 20, y si la rutina de búsqueda se realiza aproximadamente un millón de veces en el programa. Tales optimizaciones de bucle [usando
gotos
] no son difíciles de aprender y, como ya he dicho, son apropiadas solo en una pequeña parte de un programa, pero a menudo producen ahorros sustanciales. [...]
Y continúa:
La sabiduría convencional compartida por muchos de los ingenieros de software de hoy llama a ignorar la eficiencia en lo pequeño; pero creo que esto es simplemente una reacción exagerada a los abusos que ven ser practicados por programadores pennywise-and-pound-tontos, que no pueden depurar ni mantener sus programas "optimizados". En disciplinas de ingeniería establecidas, una mejora del 12%, fácil de obtener, nunca se considera marginal; y creo que el mismo punto de vista debería prevalecer en la ingeniería de software. Por supuesto, no me molestaría en hacer tales optimizaciones en un trabajo único, pero cuando se trata de preparar programas de calidad, no quiero restringirme a herramientas que me nieguen tales eficiencias [es decir, declaraciones
goto
en este contexto].
Tenga en cuenta cómo utilizó "optimizado" entre comillas (el software probablemente no sea realmente eficiente). También tenga en cuenta que él no solo está criticando a estos programadores "pennywise-and-pound-tontos", sino también a las personas que reaccionan sugiriendo que siempre debe ignorar pequeñas ineficiencias. Finalmente, a la parte frecuentemente citada:
No hay duda de que el grial de la eficiencia conduce al abuso. Los programadores pierden una enorme cantidad de tiempo pensando o preocupándose por la velocidad de las partes no críticas de sus programas, y estos intentos de eficiencia en realidad tienen un fuerte impacto negativo cuando se consideran la depuración y el mantenimiento. Deberíamos olvidarnos de las pequeñas eficiencias, digamos el 97% del tiempo; La optimización prematura es la fuente de todos los males.
... y algo más sobre la importancia de las herramientas de creación de perfiles:
A menudo es un error hacer juicios a priori sobre qué partes de un programa son realmente críticas, ya que la experiencia universal de los programadores que han usado herramientas de medición ha sido que sus conjeturas intuitivas fallan. Después de trabajar con estas herramientas durante siete años, me he convencido de que todos los compiladores escritos de ahora en adelante deberían diseñarse para proporcionar a todos los programadores comentarios que indiquen qué partes de sus programas cuestan más; de hecho, esta retroalimentación debe ser proporcionada automáticamente a menos que haya sido específicamente desactivada.
La gente ha usado mal su cita por todas partes, a menudo sugiriendo que las micro-optimizaciones son prematuras cuando todo su trabajo abogaba por micro-optimizaciones. Uno de los grupos de personas a quienes criticaba que se hacen eco de esta "sabiduría convencional" ya que siempre ignora las eficiencias en los pequeños a menudo hace un uso indebido de su cita, que originalmente estaba dirigida, en parte, contra esos tipos que desalientan todas las formas de micro-optimización .
Sin embargo, fue una cita a favor de micro-optimizaciones aplicadas apropiadamente cuando una persona experimentada que tiene un perfilador la usa. El equivalente analógico de hoy podría ser: "La gente no debería apostar ciegamente por la optimización de su software, pero los asignadores de memoria personalizados pueden hacer una gran diferencia cuando se aplica en áreas clave para mejorar la referencia" o " Código SIMD manuscrito utilizando una El representante de SoA es realmente difícil de mantener y no debe usarlo en todas partes, pero puede consumir la memoria mucho más rápido si lo aplica con una mano experimentada y guiada " .
Cada vez que tratas de promocionar micro optimizaciones cuidadosamente aplicadas como Knuth promovido anteriormente, es bueno incluir un descargo de responsabilidad para desalentar a los principiantes de que se entusiasmen demasiado y apuñalen ciegamente la optimización, como reescribir todo el software para usar goto
. Eso es en parte lo que estaba haciendo. Su cita fue efectivamente parte de un gran descargo de responsabilidad, al igual que alguien haciendo un salto de motocicleta sobre un pozo de fuego llameante puede agregar una advertencia de que los aficionados no deben intentar esto en casa mientras critican a quienes intentan sin el conocimiento y equipo adecuados y se lastiman .
Lo que él consideró como "optimizaciones prematuras" fueron optimizaciones aplicadas por personas que efectivamente no sabían lo que estaban haciendo: no sabían si la optimización era realmente necesaria, no medían con las herramientas adecuadas, tal vez no entendían la naturaleza de su compilador o arquitectura de computadora, y sobre todo, eran "pennywise-and-pound-tontos", lo que significa que pasaron por alto las grandes oportunidades para optimizar (ahorrar millones de dólares) al intentar ahorrar centavos, y todo mientras crean código no pueden más eficazmente depurar y mantener.
Si no encajas en la categoría "pennywise-and-pound-tonto", entonces no estás optimizando prematuramente según los estándares de Knuth, incluso si estás usando un goto
para acelerar un ciclo crítico (algo que es es poco probable que ayude mucho contra los optimizadores de hoy en día, pero si lo hiciera, y en un área realmente crítica, entonces no estarías optimizando prematuramente). Si en realidad estás aplicando lo que sea que estés haciendo en áreas que realmente se necesitan y se benefician genuinamente de ello, entonces lo estás haciendo muy bien a los ojos de Knuth.