performance - recomendaciones - Optimizando el código
porque se dañan las tablas en mysql (12)
Además del perfilado, como todos mencionan, las dos soluciones a las que siempre me dirijo primero (después de la creación de perfiles) son Memoization y Lazy Lazy, ambas son fáciles de implementar y normalmente hacen una gran diferencia.
Le dan un montón de código en su idioma favorito que se combina para formar una aplicación bastante complicada. Se ejecuta con bastante lentitud, y su jefe le ha pedido que lo optimice. ¿Cuáles son los pasos que sigue para optimizar el código de manera más eficiente?
¿Qué estrategias ha encontrado que no tuvieron éxito al optimizar el código?
Reescribe : ¿En qué momento decide dejar de optimizar y decir "Esto es lo más rápido posible sin una nueva escritura completa". ¿En qué casos recomendaría una simple reescritura completa de todos modos? ¿Cómo harías para diseñarlo?
Asegúrese de tener suficiente prueba de unidad para asegurarse de que cualquier optimización que haga no rompa nada
Asegúrese de calificar su entorno de ejecución . En algún momento, un simple cambio en las opciones de ejecución puede recorrer un largo camino.
Entonces, y solo entonces, comienza a analizar el código.
La decisión de reescritura (para un código que ya funciona ) solo debe considerarse si hay suficientes evoluciones futuras que pueden no ser compatibles con la arquitectura actual.
Si correcciones simples pueden acelerar un código que no debería evolucionar mucho, no debería ser necesaria una reescritura completa.
El criterio para detener se suele determinar en colaboración con el usuario final (el cliente), pero sugeriría un documento formal que fije los objetivos que se deben alcanzar con este proceso de optimización.
El lenguaje en el que hice más optimización para computación numérica en C y C ++ en Linux. Descubrí que la creación de perfiles, si bien es útil, puede sesgar los resultados de tiempo de ejecución de manera que las operaciones más baratas y frecuentes (como incrementos de iterador c ++). Así que toma esos con un grano de sal. En términos de estrategias reales que produjeron una buena aceleración son:
- Use matrices numéricas en lugar de matrices de objetos. Por ejemplo, c ++ tiene un tipo de datos "complejo". Las operaciones en una matriz de estas fueron mucho más lentas que una operación similar en dos matrices de flotadores. Esto se puede generalizar para "usar los tipos de máquina" para cualquier código de rendimiento crítico.
- Escriba el código para permitir que el compilador sea más efectivo en sus optimizaciones. Por ejemplo, si tiene una matriz de tamaño fijo, use índices de matriz para que funcione la vectorización automática (una característica del compilador de Intel).
- Las instrucciones SIMD pueden proporcionar una buena aceleración si su problema se ajusta al tipo de dominio para el que están diseñadas (multiplicar / dividir flotantes o ints, todo al mismo tiempo). Esto es algo como MMX, SSE, SSE2, etc.
- Usar tablas de búsqueda con interpolación para funciones costosas donde los valores exactos no son importantes. Esto no siempre es bueno, ya que buscar los datos en la memoria puede ser caro por derecho propio.
¡Espero que eso te inspire un poco!
En primer lugar, no olvide estos:
- La optimización prematura es la raíz de todos los males
- El rendimiento proviene del diseño
En segundo lugar;
No supongas que intentas y ves
Creo que esta es la regla fundamental de la optimización, pruébala, a menos que la pruebes y demuestres que funciona, no lo sabrías.
En su caso, lo que haría es, en primer lugar, refactorizar el código , comprenderlo.
Si tiene pruebas unitarias, tiene suerte simplemente vaya función por función y específicamente vaya y revise el código llamado con mayor frecuencia (use perfiles para observar las llamadas y dónde están los cuellos de botella). Si no tiene pruebas, escriba algunas pruebas básicas que confirmen la salida general en algunas condiciones para que pueda asegurarse de que no está rompiendo nada y libre para experimentar .
Ni siquiera te molestes en probar nada sin algún tipo de perfil, ¡no puedo enfatizar esto lo suficiente! Desafortunadamente, todos los perfiladores que sé chupan o son caros (¡por los costos de negocio!), Así que dejaré que los demás hagan recomendaciones allí :).
Sabes que necesitas una reescritura cuando tus datos te dicen que necesitas una nueva escritura, que suena recursiva, pero en realidad solo significa que el costo de tu arquitectura actual o la pila de software es suficiente por sí solo para empujarte sobre el acantilado de rendimiento , por lo tanto, nada de lo que haga en los cambios locales puede solucionar el problema general. Sin embargo, antes de salir del comando Archivo-> Nuevo ..., haga un plan, construya un prototipo y pruebe que el prototipo funciona mejor que el sistema actual para la misma tarea: ¡es sorprendente la frecuencia con la que no hay diferencia notoria!
Pasos:
- Medida
- Analizar
- Decidir
- Implementar
- Repetir
Primero obtenga un generador de perfiles para medir el código. No caiga en la trampa de suponer que sabe dónde están los cuellos de botella. Incluso después, si sus suposiciones demuestran ser correctas, no piense que puede omitir el paso de medición la próxima vez que tenga una tarea similar.
Luego analiza tus hallazgos. Mire el código, identifique los cuellos de botella sobre los que puede hacer algo que tendrán un efecto. Intente estimar cuánto de esto será una mejora.
Decide si quieres seguir este camino, según tu análisis. ¿Valdrán las ganancias? Es una reescritura garantizada? Tal vez descubras que, aunque funciona lento, es tan bueno como lo que se obtendrá o que estás cerca de la parte superior de la curva de rendimiento, donde el trabajo necesario para buscar una mejora minúscula no está garantizado.
Implementa tus cambios, haz la reescritura si es necesario o refactoriza el código si ese es el camino que has bajado. Hazlo en pequeños pasos para que sea fácil medir si tu cambio dio lo que querías o si necesitas retroceder un paso y probar una ruta diferente.
Luego regrese al inicio y mida, analice, decida, implemente, etc.
Además, en la nota del código de refactorización. Lo primero que debe cambiar son los grandes enfoques a nivel de algoritmo. Cosas como reemplazar un algoritmo de clasificación por otro que funciona mejor, etc. No comience con optimizaciones de nivel de línea, por ejemplo, cómo obtener una línea que incremente un valor para ir un poco más rápido. Esas son optimizaciones de último nivel y generalmente no valen la pena, a menos que se ejecute en condiciones de rendimiento extremo.
Primero decida cuál es su objetivo de optimización: establezca un objetivo para el momento de las operaciones particulares en una plataforma de hardware determinada. Mida el rendimiento con precisión (asegúrese de que sus resultados sean repetibles) y en un entorno similar a la producción (sin máquinas virtuales, etc. a menos que eso sea lo que usa en producción).
Entonces, si decides que ya es lo suficientemente rápido, puedes detenerte allí.
Si aún no es lo suficientemente bueno, entonces se necesitará algo de trabajo adicional, que es donde entra en juego la creación de perfiles. Es posible que no pueda usar un generador de perfiles muy bien (por ejemplo, si afecta demasiado el comportamiento), en cuyo caso la instrumentación debería usarse en su lugar.
Suponiendo que el código necesita optimización, entonces usaría: 1.) Optimizar la forma en que se manejan los almacenamientos intermedios - Optimización de caché. Las latencias de caché son una gran área que absorbe los ciclos de CPU como malos. aproximadamente en el rango de 5-10% de sobrecarga. Utilice los almacenamientos intermedios de datos en caché de manera eficiente.
2.) Los bucles críticos y las funciones intensivas de MCPS pueden codificarse en lenguaje ensamblador o utilizando los intrínsecos de bajo nivel proporcionados por el compilador para ese objetivo h / w.
3.) Las lecturas / escrituras de la memoria externa son costosas. minimizar el acceso a la memoria externa tanto como sea posible. O si tiene que acceder, hágalo de una manera eficiente (datos útiles, acceso DMA PArallel, etc.)
En general, si obtiene alrededor del 20% de optimización (el mejor de los casos) siguiendo las técnicas de optimización, diría que es lo suficientemente bueno y lo suficientemente bueno sin una reestructuración de código importante, el rediseño del algoritmo. Después de lo cual se convierte en truco, complicado y tedioso.
-ANUNCIO
Como muchos otros ya han declarado, perfilar es su primer paso.
Añadiría que centrarse en estructuras de datos y algoritmos como primer paso es generalmente más rentable que sumergirse directamente en micro-optimizaciones.
Por un lado, el compilador normalmente realizará muchos de los trucos de optimización clásicos para usted de todos modos (y, a menudo mejor que usted). Esto es especialmente cierto en lenguajes más modernos como C # que en lenguajes C más antiguos, menos restringidos, ya que el compilador tiene más conocimiento de la estructura del programa; peor aún, confundir el código "optimizando" puede hacer que al compilador le resulte más difícil aplicar sus propias optimizaciones más eficientes.
Sin embargo, en su mayoría, hay mucho más margen para mejorar las cosas cuando comienzas a mejorar el gran oh de tus operaciones. Por ejemplo, buscar en una lista vinculada es O (n); lo que significa que el tiempo necesario para buscarlo aumenta a la misma velocidad que el tamaño de los datos almacenados en él. La búsqueda de una tabla hash es solo O (1), por lo que puede agregar más datos sin aumentar el tiempo de búsqueda (hay otros factores cuando abandonamos el mundo teórico, por lo que esto no es del todo cierto, pero es cierto que la mayoría de los hora).
Enredando con sus bucles para que corran de mayor a menor, entonces el código generado puede ahorrar un par de ciclos de reloj con un JumpIfZero en lugar de un JumpIfLessThan ¡probablemente no tendrá el mismo grado de impacto!
Todas las buenas respuestas.
Yo refinaría la parte de "medida" del consejo. Realizo mediciones con el fin de cuantificar cualquier mejora que pueda hacer. Sin embargo, para encontrar lo que se debe corregir (y ese es un propósito diferente), obtengo varias muestras de la pila de llamadas, de forma manual .
Supongamos, por simplicidad, que el programa necesita 20 giga-ciclos para ejecutarse, cuando debería tomar 10. Si lo pausa 10 veces al azar, entonces en 5 de esas ocasiones, más o menos, estará en uno de esos ciclos innecesarios . Puedo ver si el ciclo es necesario mirando cada capa de la pila de llamadas. Si se puede eliminar cualquier instrucción de llamada en cualquier nivel de la pila, entonces el ciclo es innecesario. Si tal instrucción aparece en varias pilas, eliminarla acelerará el programa, en una cantidad que es aproximadamente el porcentaje de muestras de la pila en la que se encuentra.
Cualquier instrucción que aparezca en varias pilas es sospechosa: mientras más pilas haya, más sospechoso. Ahora, call _main
probablemente no sea uno que pueda eliminar, pero
foo.cpp:96 call std::vector::iterator:++
si aparece en varias pilas, definitivamente es un foco de atención.
Uno puede, por razones de estilo, no querer reemplazarlo, pero uno sabrá aproximadamente qué precio en rendimiento se le paga por esa elección.
Por lo tanto, la optimización consiste en identificar a los sospechosos y encontrar la manera de reemplazarlos o eliminarlos.
La parte difícil (y lo he hecho muchas veces) es entender qué es necesario y qué no. Para eso, vas a absorber una comprensión de cómo y por qué se hacen las cosas en ese programa, por lo que si alguna actividad es un ciclo-cerdo, puedes saber cómo reemplazarlo de manera segura.
Algunos cerdos de ciclo pueden ser fáciles de corregir, pero se encontrará rápidamente con aquellos que no sabe cómo reemplazar de manera segura. Para eso, necesitas estar más familiarizado con el código.
Te ayudará si puedes elegir el cerebro de alguien que ya trabajó en el programa.
De lo contrario (suponiendo que el código es ~ 10 ^ 6 líneas, como he trabajado) puede obtener cierta aceleración con bastante facilidad, pero para ir más allá, puede tomar meses llegar a donde se siente cómodo haciendo cambios significativos.
Buenas estrategias
Además de las mencionadas leyes básicas de optimización (medir, no optimizar si no es necesario), y aunque la pregunta explícitamente pedía eficiencia , siempre refactorizo dicho código durante mi inspección.
Normalmente, el código con un mal rendimiento también está mal documentado. Así que con la refactorización me aseguro de que el código esté mejor documentado por sí mismo y sea más fácil de entender. Esa es la base para asegurarme de que sé lo que estoy optimizando (ya que en la mayoría de los casos, los requisitos para esa pieza de código tampoco estarán completamente disponibles).
Cuándo parar
Con una aplicación de muy mal rendimiento, normalmente tendrá un pico en el tiempo de ejecución que se muestra para un único método (o conjunto de métodos relacionados) en su generador de perfiles, que muestra un error de programación o falla de diseño. Así que, por lo general, me detengo si el tiempo de ejecución de los métodos perfilados se distribuye en la mayoría de los casos (o si la mayoría de los métodos de cuello de botella que se muestran son de plataforma, como los métodos de Sun Java). Si sus clientes exigen mayor optimización, tendrá que rediseñar partes más grandes de la aplicación en lugar de optimizar el código existente.
Perfil antes de intentar cualquier optimización.
9 de cada 10 veces, el tiempo no se consumirá donde pueda adivinarlo.
La estrategia usual sin éxito es la micro-optimización, cuando lo que realmente se requiere es el algoritmo apropiado.
Cita Obligatoria de Donald Knuth:
"Deberíamos olvidarnos de las pequeñas eficiencias, digamos el 97% del tiempo: la optimización prematura es la raíz de todo mal"