performance optimization language-agnostic

performance - Estrategias de optimización de rendimiento de último recurso



optimization language-agnostic (30)

Ya hay muchas preguntas de rendimiento en este sitio, pero se me ocurre que casi todas son muy específicas de problemas y bastante estrechas. Y casi todos repiten el consejo para evitar una optimización prematura.

Asumamos:

  • El código ya está funcionando correctamente.
  • Los algoritmos elegidos ya son óptimos para las circunstancias del problema.
  • El código ha sido medido y las rutinas ofensivas han sido aisladas.
  • todos los intentos de optimización también se medirán para garantizar que no empeoren las cosas

Lo que estoy buscando aquí es estrategias y trucos para exprimir hasta el último porcentaje en un algoritmo crítico cuando no queda nada más que hacer sino lo que sea necesario.

Lo ideal es tratar de hacer que las respuestas al lenguaje sean agnósticas e indicar cualquier inconveniente de las estrategias sugeridas cuando corresponda.

Agregaré una respuesta con mis propias sugerencias iniciales, y espero con ansias cualquier otra cosa que la comunidad de desbordamiento de pila pueda imaginar.


¡Lanza más hardware!


Aunque me gusta la respuesta de Mike Dunlavey, de hecho, es una gran respuesta con un ejemplo de apoyo, creo que podría expresarse de manera muy simple:

Averigüe qué requiere la mayor cantidad de tiempo primero y entienda por qué.

Es el proceso de identificación de los cerdos del tiempo que le ayuda a comprender dónde debe perfeccionar su algoritmo. Esta es la única respuesta agnóstica del lenguaje que abarca todo lo que puedo encontrar para un problema que ya se supone que está totalmente optimizado. También suponiendo que quieres ser independiente de la arquitectura en tu búsqueda de velocidad.

Entonces, mientras que el algoritmo puede ser optimizado, la implementación del mismo puede no serlo. La identificación le permite saber qué parte es cuál: algoritmo o implementación. Así que, cualquiera que sea el momento, es tu principal candidato para la revisión. Pero dado que dice que desea exprimir el último%, puede que también desee examinar las partes menores, las partes que no ha examinado de cerca al principio.

Por último, un poco de prueba y error con cifras de rendimiento en diferentes formas de implementar la misma solución, o algoritmos potencialmente diferentes, puede aportar ideas que ayudan a identificar pérdidas de tiempo y ahorradores de tiempo.

HPH, como en movimiento


Cuando ya no pueda mejorar el rendimiento, vea si puede mejorar el rendimiento percibido .

Es posible que no pueda hacer que su algoritmo fooCalc sea más rápido, pero a menudo hay formas de hacer que su aplicación parezca más receptiva para el usuario.

Algunos ejemplos:

  • anticipando lo que el usuario va a solicitar y comenzar a trabajar en eso antes de esa fecha
  • mostrando los resultados a medida que entran, en lugar de todos a la vez al final
  • Medidor de progreso preciso

Esto no hará que su programa sea más rápido, pero podría hacer que sus usuarios estén más contentos con la velocidad que tiene.


Dado que muchos de los problemas de rendimiento implican problemas con la base de datos, le daré algunas cosas específicas que debe tener en cuenta al ajustar las consultas y los procedimientos almacenados.

Evite los cursores en la mayoría de las bases de datos. Evita hacer bucles también. La mayoría de las veces, el acceso a los datos debe estar basado en el establecimiento, no en el registro de procesamiento. Esto incluye no reutilizar un solo procedimiento almacenado de registro cuando desee insertar 1,000,000 registros a la vez.

Nunca use select *, solo devuelva los campos que realmente necesita. Esto es especialmente cierto si hay combinaciones ya que los campos de combinación se repetirán y, por lo tanto, causarán una carga innecesaria tanto en el servidor como en la red.

Evite el uso de subconsultas correlacionadas. Use combinaciones (incluidas combinaciones a tablas derivadas cuando sea posible) (sé que esto es cierto para Microsoft SQL Server, pero pruebe los consejos cuando use un backend diferente).

Índice, índice, índice. Y consiga esas estadísticas actualizadas si es aplicable a su base de datos.

Realiza la consulta sargable . El significado evita cosas que hacen que sea imposible usar los índices, como el uso de un comodín en el primer carácter de una cláusula similar o una función en la combinación o como la parte izquierda de una instrucción where.

Utilice los tipos de datos correctos. Es más rápido realizar cálculos de fecha en un campo de fecha que tener que intentar convertir un tipo de datos de cadena a un tipo de datos de fecha, luego realizar el cálculo.

¡Nunca ponga un bucle de ningún tipo en un gatillo!

La mayoría de las bases de datos tienen una forma de verificar cómo se realizará la ejecución de la consulta. En Microsoft SQL Server esto se llama un plan de ejecución. Revise los primeros para ver dónde se encuentran las áreas problemáticas.

Tenga en cuenta la frecuencia con la que se ejecuta la consulta, así como el tiempo que tarda en ejecutarse para determinar qué debe optimizarse. A veces, puede obtener más rendimiento de un pequeño ajuste a una consulta que se ejecuta millones de veces al día que de borrar una consulta de ejecución larga que solo se ejecuta una vez al mes.

Utilice algún tipo de herramienta de perfilador para averiguar qué se está enviando realmente a la base de datos y desde ella. Puedo recordar una vez en el pasado que no pudimos entender por qué la página tardó en cargarse tan rápido cuando el procedimiento almacenado fue rápido y descubrimos a través del perfil que la página web pedía la consulta muchas veces en lugar de una sola vez.

El perfilador también te ayudará a encontrar quién está bloqueando a quién. Algunas consultas que se ejecutan rápidamente mientras se ejecutan solas pueden llegar a ser realmente lentas debido a los bloqueos de otras consultas.


De acuerdo, está definiendo el problema en el que parece que no hay mucho margen de mejora. Eso es bastante raro, en mi experiencia. Traté de explicarlo en un artículo del Dr. Dobbs en noviembre de 1993, comenzando desde un programa no trivial convencionalmente bien diseñado, sin desperdicio obvio, y analizándolo en una serie de optimizaciones hasta que se redujo su tiempo de reloj de pared de 48 segundos a 1.1 segundos, y el tamaño del código fuente se redujo en un factor de 4. Mi herramienta de diagnóstico fue esta . La secuencia de cambios fue esta:

  • El primer problema encontrado fue el uso de grupos de listas (ahora llamados "iteradores" y "clases de contenedor") que representan más de la mitad del tiempo. Estos fueron reemplazados por un código bastante simple, lo que reduce el tiempo a 20 segundos.

  • Ahora el tomador de tiempo más grande es más construcción de lista. Como porcentaje, antes no era tan grande, pero ahora se debe a que se eliminó el problema mayor. Encuentro una forma de acelerarlo, y el tiempo se reduce a 17 segundos.

  • Ahora es más difícil encontrar culpables obvios, pero hay algunos más pequeños sobre los que puedo hacer algo, y el tiempo se reduce a 13 segundos.

Ahora parece que he golpeado una pared. Las muestras me dicen exactamente lo que está haciendo, pero parece que no puedo encontrar nada que pueda mejorar. Luego reflexiono sobre el diseño básico del programa, sobre su estructura orientada a las transacciones, y pregunto si todas las búsquedas en la lista que está realizando en realidad están obligadas por los requisitos del problema.

Luego me topé con un nuevo diseño, donde el código del programa se genera realmente (a través de macros del preprocesador) a partir de un conjunto más pequeño de fuentes, y en el que el programa no está descubriendo constantemente cosas que el programador sabe que son bastante predecibles. En otras palabras, no "interpretes" la secuencia de cosas que debes hacer, "compílalas".

  • Ese rediseño se realiza, reduciendo el código fuente en un factor de 4, y el tiempo se reduce a 10 segundos.

Ahora, debido a que se está haciendo tan rápido, es difícil de muestrear, por lo que le doy 10 veces más trabajo por hacer, pero los siguientes tiempos se basan en la carga de trabajo original.

  • Más diagnósticos revelan que está gastando tiempo en la gestión de colas. En el interior estos reducen el tiempo a 7 segundos.

  • Ahora, un gran tomador de tiempo es la impresión de diagnóstico que había estado haciendo. Enjuague eso - 4 segundos.

  • Ahora los que más tiempo toman son llamadas a malloc y gratis . Reciclar objetos - 2.6 segundos.

  • Siguiendo con la muestra, todavía encuentro operaciones que no son estrictamente necesarias - 1.1 segundos.

Factor de aceleración total: 43.6

Ahora no hay dos programas iguales, pero en software no para juguetes siempre he visto una progresión como esta. Primero obtienes lo fácil, y luego lo más difícil, hasta que llegas a un punto de rendimientos decrecientes. Entonces, la información que adquiera puede llevar a un rediseño, comenzando una nueva ronda de aceleraciones, hasta que vuelva a alcanzar rendimientos decrecientes. Ahora, este es el punto en el que podría tener sentido preguntarse si ++i o i++ o for(;;) o while(1) son más rápidos: los tipos de preguntas que veo con tanta frecuencia en SO.

PS Puede preguntarse por qué no usé un perfilador. La respuesta es que casi cada uno de estos "problemas" era un sitio de llamada de función, que muestra muestras de manera precisa. Los perfiladores, incluso hoy en día, apenas están llegando a la idea de que las declaraciones y las instrucciones de llamada son más importantes de localizar y más fáciles de arreglar que las funciones completas. De hecho, construí un generador de perfiles para hacer esto, pero para una verdadera intimidad con lo que hace el código, no hay sustituto para poner sus dedos directamente en él. No es un problema que el número de muestras sea pequeño, ya que ninguno de los problemas que se encuentran es tan pequeño que se pueden pasar por alto fácilmente.

AÑADIDO: jerryjvl pidió algunos ejemplos. Aquí está el primer problema. Consiste en un pequeño número de líneas de código separadas, que toman más de la mitad del tiempo:

/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */ if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){ . . . /* FOR EACH OPERATION REQUEST */ for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){ . . . /* GET CURRENT TASK */ ptask = ILST_NTH(ptop->tasklist, ptop->current_task)

Estos usaban el clúster de lista ILST (similar a una clase de lista). Se implementan de la forma habitual, con "ocultación de información", lo que significa que los usuarios de la clase no deberían tener que preocuparse por cómo se implementaron. Cuando se escribieron estas líneas (de aproximadamente 800 líneas de código), no se le dio a la idea de que podrían ser un "cuello de botella" (odio esa palabra). Son simplemente la forma recomendada de hacer las cosas. Es fácil decir, en retrospectiva, que deberían haberse evitado, pero en mi experiencia, todos los problemas de rendimiento son así. En general, es bueno tratar de evitar crear problemas de rendimiento. Incluso es mejor encontrar y reparar los que se crean, aunque "deberían haberse evitado" (en retrospectiva). Espero que le dé un poco de sabor.

Aquí está el segundo problema, en dos líneas separadas:

/* ADD TASK TO TASK LIST */ ILST_APPEND(ptop->tasklist, ptask) . . . /* ADD TRANSACTION TO TRANSACTION QUEUE */ ILST_APPEND(trnque, ptrn)

Estas son listas de construcción mediante la adición de elementos a sus extremos. (La solución fue recolectar los elementos en arreglos y construir las listas a la vez). Lo interesante es que estas declaraciones solo cuestan (es decir, estaban en la pila de llamadas) 3/48 del tiempo original, por lo que no estaban en hecho un gran problema al principio . Sin embargo, después de eliminar el primer problema, costaban 3/20 del tiempo y ahora eran un "pez más grande". En general, así es como va.

Podría agregar que este proyecto se extrajo de un proyecto real en el que ayudé. En ese proyecto, los problemas de rendimiento fueron mucho más dramáticos (como lo fueron las aceleraciones), como llamar a una rutina de acceso a la base de datos dentro de un bucle interno para ver si se completó una tarea.

REFERENCIA AGREGADA: El código fuente, original y rediseñado, se puede encontrar en www.ddj.com , para 1993, en el archivo 9311.zip, archivos slug.asc y slug.zip.

EDICIÓN 2011/11/26: Ahora hay un proyecto de sourceforge que contiene código fuente en Visual C ++ y una descripción paso a paso de cómo fue sintonizado. Solo pasa por la primera mitad del escenario descrito anteriormente, y no sigue exactamente la misma secuencia, pero sigue obteniendo una aceleración de 2-3 órdenes de magnitud.


El factor limitante más importante hoy en día es el ancho de banda limitado de memoria . Los multicores simplemente empeoran esto, ya que el ancho de banda se comparte entre los núcleos. Además, el área de chip limitada dedicada a la implementación de cachés también se divide entre los núcleos y los hilos, lo que empeora aún más este problema. Finalmente, la señalización entre chips necesaria para mantener coherentes los diferentes cachés también aumenta con un mayor número de núcleos. Esto también añade una penalización.

Estos son los efectos que necesitas manejar. A veces, a través de la micro gestión de su código, pero a veces a través de una cuidadosa consideración y refactorización.

Muchos comentarios ya mencionan código amigable de caché. Hay al menos dos sabores distintos de esto:

  • Evite las latencias de recuperación de memoria.
  • Baja presión del bus de memoria (ancho de banda).

El primer problema específicamente tiene que ver con hacer que los patrones de acceso a los datos sean más regulares, permitiendo que el prefetcher de hardware funcione de manera eficiente. Evite la asignación de memoria dinámica que distribuye sus objetos de datos en la memoria. Utilice contenedores lineales en lugar de listas enlazadas, hashes y árboles.

El segundo problema tiene que ver con mejorar la reutilización de los datos. Modifique sus algoritmos para trabajar en subconjuntos de sus datos que se ajusten al caché disponible, y reutilice esos datos tanto como sea posible mientras aún esté en el caché.

Empaquetar los datos con más fuerza y ​​asegurarse de que utiliza todos los datos en las líneas de caché en los bucles activos, ayudará a evitar estos otros efectos y le permitirá incluir datos más útiles en el caché.


Mas sugerencias:

  • Evite E / S : cualquier E / S (disco, red, puertos, etc.) siempre será mucho más lenta que cualquier código que realice cálculos, así que deshágase de cualquier E / S que no necesite estrictamente.

  • Mueva la E / S por adelantado : cargue todos los datos que va a necesitar para un cálculo por adelantado, de modo que no tenga repetidas esperas de E / S dentro del núcleo de un algoritmo crítico (y tal vez como un resultado repetido las búsquedas en el disco, cuando se cargan todos los datos en un hit pueden evitar la búsqueda).

  • Retraso de E / S : No escriba sus resultados hasta que el cálculo haya terminado, guárdelos en una estructura de datos y luego vacíelos de una vez al final cuando haya terminado el trabajo.

  • E / S con hilos: para los que se atreven, combine ''I / O por adelantado'' o ''Delay I / O'' con el cálculo real moviendo la carga a un hilo paralelo, de modo que mientras carga más datos pueda trabajar en un cálculo de los datos que ya tiene, o mientras calcula el siguiente lote de datos, puede escribir simultáneamente los resultados del último lote.


Paso la mayor parte de mi vida en este lugar. Los trazos amplios son para ejecutar tu perfilador y hacer que se grabe:

  • Se pierde el caché . El caché de datos es la fuente principal de paradas en la mayoría de los programas. Mejore la tasa de aciertos de caché reorganizando las estructuras de datos ofensivas para tener una mejor ubicación; Empaque las estructuras y los tipos numéricos hacia abajo para eliminar los bytes desperdiciados (y, por lo tanto, las recuperaciones de caché perdidas); Prefetch datos siempre que sea posible para reducir los bloqueos.
  • Carga-golpe-tiendas . Las suposiciones del compilador sobre el aliasing de punteros, y los casos en que los datos se mueven entre conjuntos de registros desconectados a través de la memoria, pueden causar un cierto comportamiento patológico que hace que toda la tubería de la CPU se despeje en una operación de carga. Busque lugares donde se coloquen flotadores, vectores e ints y elimínelos. Utilice __restrict liberalmente para prometer al compilador sobre el aliasing.
  • Operaciones microcodificadas . La mayoría de los procesadores tienen algunas operaciones que no se pueden canalizar, sino que ejecutan una subrutina pequeña almacenada en la ROM. Los ejemplos en el PowerPC son multiplicar enteros, dividir y desplazar por cantidad variable. El problema es que toda la tubería se detiene mientras se ejecuta esta operación. Trate de eliminar el uso de estas operaciones o, al menos, divídalos en sus operaciones organizadas constituyentes para que pueda obtener el beneficio del envío superescala en lo que sea que esté haciendo el resto de su programa.
  • Rama mal predice . Estos también vacían la tubería. Encuentre casos en los que la CPU esté gastando mucho tiempo en rellenar la tubería después de una rama, y ​​utilice las sugerencias de rama, si están disponibles, para que prediga correctamente con más frecuencia. O mejor aún, reemplace las ramas con movimientos condicionales siempre que sea posible, especialmente después de las operaciones de punto flotante, ya que su tubería es generalmente más profunda y la lectura de las banderas de condición después de fcmp puede causar un bloqueo.
  • Operaciones de punto flotante secuenciales . Hacer estos SIMD.

Y una cosa más que me gusta hacer:

  • Configure su compilador para que genere listas de ensamblaje y observe lo que emite para las funciones de punto de acceso en su código. ¿Todas esas optimizaciones inteligentes que "un buen compilador debería poder hacer por ti automáticamente"? Es probable que su compilador real no los haga. He visto a GCC emitir realmente el código WTF.

Probablemente debería considerar la "perspectiva de Google", es decir, determinar cómo su aplicación puede ser paralela y paralela en gran medida, lo que inevitablemente también significará en algún momento considerar la distribución de su aplicación en diferentes máquinas y redes, de modo que la escala ideal sea casi lineal. con el hardware que le arrojas.

Por otro lado, los usuarios de Google también son conocidos por dedicar mucha mano de obra y recursos para resolver algunos de los problemas en proyectos, herramientas e infraestructura que están utilizando, como por ejemplo la optimización de todo el programa para gcc al tener un equipo dedicado de ingenieros. piratería interna de gcc para prepararlo para los escenarios de casos de uso típicos de Google.

De manera similar, perfilar una aplicación ya no significa simplemente perfilar el código del programa, sino también todos los sistemas e infraestructura que lo rodean (piense en redes, switches, servidores, matrices RAID) para identificar redundancias y potencial de optimización desde el punto de vista de un sistema.


Sugerencias:

  • Pre-calcule en lugar de volver a calcular : cualquier bucle o llamadas repetidas que contengan cálculos que tengan un rango de entradas relativamente limitado, considere realizar una búsqueda (matriz o diccionario) que contenga el resultado de ese cálculo para todos los valores en el rango válido de entradas A continuación, utilice una búsqueda simple dentro del algoritmo en su lugar.
    Aspectos negativos : si se utilizan realmente algunos de los valores precalculados, esto puede empeorar las cosas, y la búsqueda también puede llevar una memoria significativa.
  • No utilice métodos de biblioteca : la mayoría de las bibliotecas deben escribirse para funcionar correctamente en una amplia gama de escenarios y realizar comprobaciones nulas en los parámetros, etc. Al volver a implementar un método, es posible que pueda eliminar una gran cantidad de lógica que No se aplica en la circunstancia exacta en que lo está utilizando.
    Lados : escribir código adicional significa más área de superficie para errores.
  • Use métodos de biblioteca : para contradecirme, las bibliotecas de idiomas se escriben por personas que son mucho más inteligentes que usted o yo; Las probabilidades son que lo hicieron mejor y más rápido. No lo implemente usted mismo a menos que realmente pueda hacerlo más rápido (es decir: ¡siempre mida!)
  • Truco : en algunos casos, aunque puede que exista un cálculo exacto para su problema, es posible que no necesite "exacto", a veces una aproximación puede ser "lo suficientemente buena" y mucho más rápida en el trato. Pregúntate a ti mismo, ¿realmente importa si la respuesta es un 1%? 5%? incluso el 10%?
    Lados : Bueno ... la respuesta no será exacta.

Último pocos% es una cosa muy dependiente de la CPU y la aplicación ....

  • las arquitecturas de caché difieren, algunos chips tienen RAM en chip que puede mapear directamente, los ARM (a veces) tienen una unidad vectorial, SH4 es un código de operación de matriz útil. ¿Hay una GPU ? GPU vez un shader es el camino a seguir. TMS320 son muy sensibles a las ramas dentro de los bucles (por lo tanto, separe los bucles y mueva las condiciones afuera si es posible).

La lista continúa ... Pero este tipo de cosas son realmente el último recurso ...

Compile para x86 y ejecute Valgrind / Cachegrind contra el código para obtener un perfil de rendimiento adecuado. O CCStudio Texas Instruments tiene un perfilador dulce. Entonces realmente sabrás dónde enfocarte ...


Did you know that a CAT6 cable is capable of 10x better shielding off extrenal inteferences than a default Cat5e UTP cable?

Para cualquier proyecto que no esté fuera de línea, mientras tenga el mejor software y el mejor hardware, si su rendimiento total es débil, entonces esa línea delgada comprimirá los datos y le dará demoras, aunque en milisegundos ... pero si está hablando de las últimas caídas , eso es algunas caídas ganadas, 24/7 para cualquier paquete enviado o recibido.


Ajustar el sistema operativo y el marco.

Puede parecer una exageración, pero piénselo así: los sistemas operativos y los marcos están diseñados para hacer muchas cosas. Tu aplicación solo hace cosas muy específicas. Si pudiera hacer que el sistema operativo se adapte exactamente a las necesidades de su aplicación y que su aplicación comprenda cómo funciona el marco (php, .net, java), podría mejorar mucho su hardware.

Facebook, por ejemplo, cambió algunas cosas a nivel de kernel en Linux, cambió cómo funciona memcached (por ejemplo, escribieron un proxy de memcached y usaron udp en lugar de tcp ).

Otro ejemplo para esto es Window2008. Win2K8 tiene una versión en la que puede instalar solo el SO básico necesario para ejecutar aplicaciones X (por ejemplo, aplicaciones web, aplicaciones de servidor). Esto reduce gran parte de la sobrecarga que el sistema operativo tiene en los procesos en ejecución y le brinda un mejor rendimiento.

Por supuesto, siempre debes agregar más hardware como primer paso ...


Caching Una forma barata (en esfuerzo del programador) de hacer que cualquier cosa sea más rápida es agregar una capa de abstracción de almacenamiento en caché a cualquier área de movimiento de datos de su programa. Ya sea I / O o simplemente pasar / crear objetos o estructuras. A menudo es fácil agregar cachés a las clases de fábrica y lectores / escritores.

A veces, el caché no te hará ganar mucho, pero es un método fácil simplemente agregar el almacenamiento en caché y luego deshabilitarlo donde no ayuda. A menudo he encontrado que esto genera un gran rendimiento sin tener que micro-analizar el código.


Divide y conquistaras

Si el conjunto de datos que se está procesando es demasiado grande, haga un bucle sobre los fragmentos. Si has hecho bien tu código, la implementación debería ser fácil. Si tienes un programa monolítico, ahora lo sabes mejor.


Reducir tamaños variables (en sistemas embebidos)

Si su tamaño variable es mayor que el tamaño de palabra en una arquitectura específica, puede tener un efecto significativo tanto en el tamaño del código como en la velocidad. Por ejemplo, si tiene un sistema de 16 bits y utiliza una long intvariable con mucha frecuencia, y luego se da cuenta de que nunca puede salirse del rango (−32.768 ... 32.767), considere reducirlo ashort int.

Desde mi experiencia personal, si un programa está listo o casi listo, pero nos damos cuenta de que ocupa alrededor del 110% o 120% de la memoria del programa del hardware de destino, una rápida normalización de las variables generalmente resuelve el problema más a menudo que no.

En este momento, la optimización de los algoritmos o partes del código en sí puede volverse frustrantemente inútil:

  • reorganiza toda la estructura y el programa ya no funciona como es debido, o al menos introduces muchos errores.
  • haga algunos trucos inteligentes: por lo general pasa mucho tiempo optimizando algo, y descubre que no hay una disminución muy pequeña en el tamaño del código, ya que el compilador lo habría optimizado de todos modos.

Muchas personas cometen el error de tener variables que almacenan exactamente el valor numérico de una unidad para la que usan la variable: por ejemplo, su variable timealmacena el número exacto de milisegundos, incluso si solo son relevantes los pasos de tiempo de, por ejemplo, 50 ms. Tal vez si su variable representara 50 ms para cada incremento de uno, usted podría encajar en una variable más pequeña o igual al tamaño de la palabra. En un sistema de 8 bits, por ejemplo, incluso una simple adición de dos variables de 32 bits genera una buena cantidad de código, especialmente si tiene pocos registros, mientras que las adiciones de 8 bits son pequeñas y rápidas.


He pasado algo de tiempo trabajando en la optimización de sistemas empresariales cliente / servidor que operan en redes de bajo ancho de banda y de larga latencia (p. Ej., Satelital, remota, offshore) y he podido lograr algunas mejoras de rendimiento dramáticas con un proceso bastante repetible.

  • Medida : comience por comprender la capacidad subyacente y la topología de la red. Hablar con las personas de redes relevantes en el negocio y utilizar herramientas básicas como ping y traceroute para establecer (como mínimo) la latencia de la red desde cada ubicación del cliente, durante los períodos operativos típicos. A continuación, tome mediciones precisas de tiempo de funciones específicas del usuario final que muestren los síntomas problemáticos. Registre todas estas medidas, junto con sus ubicaciones, fechas y horas. Considere la posibilidad de incorporar la funcionalidad de "prueba de rendimiento de red" del usuario final en su aplicación cliente, permitiendo a sus usuarios avanzados participar en el proceso de mejora; empoderarlos así puede tener un gran impacto psicológico cuando trata con usuarios frustrados por un sistema de bajo rendimiento.

  • Analizar : Usar cualquiera y todos los métodos de registro disponibles para establecer exactamente qué datos se están transmitiendo y recibiendo durante la ejecución de las operaciones afectadas. Idealmente, su aplicación puede capturar datos transmitidos y recibidos tanto por el cliente como por el servidor. Si estos incluyen marcas de tiempo también, incluso mejor. Si no está disponible el registro suficiente (por ejemplo, un sistema cerrado, o la incapacidad de implementar modificaciones en un entorno de producción), use un rastreador de red y asegúrese de que realmente comprende lo que sucede a nivel de red.

  • Caché : busque casos en los que se transmitan datos estáticos o que se cambian con poca frecuencia de forma repetitiva y considere una estrategia de almacenamiento en caché adecuada. Los ejemplos típicos incluyen valores de "lista de selección" u otras "entidades de referencia", que pueden ser sorprendentemente grandes en algunas aplicaciones empresariales. En muchos casos, los usuarios pueden aceptar que deben reiniciar o actualizar la aplicación para actualizar los datos que se actualizan con poca frecuencia, especialmente si pueden ahorrar mucho tiempo en la visualización de los elementos de la interfaz de usuario más utilizados. Asegúrese de comprender el comportamiento real de los elementos de almacenamiento en caché ya implementados; muchos métodos comunes de almacenamiento en caché (por ejemplo, HTTP ETag) todavía requieren un recorrido de ida y vuelta de la red para garantizar la coherencia, y donde la latencia de la red es costosa, puede evitarlo por completo un enfoque de almacenamiento en caché diferente.

  • Paralelizar : busque transacciones secuenciales que no necesitan ser emitidas lógicamente de forma estrictamente secuencial, y vuelva a trabajar el sistema para emitirlas en paralelo. Traté un caso en el que una solicitud de extremo a extremo tenía un retardo de red inherente de ~ 2, lo que no era un problema para una sola transacción, pero cuando se requerían 6 viajes de ida y vuelta 2s secuenciales antes de que el usuario recuperara el control de la aplicación cliente , se convirtió en una gran fuente de frustración. Descubrir que estas transacciones eran de hecho independientes permitió que se ejecutaran en paralelo, reduciendo la demora del usuario final muy cerca del costo de un solo viaje de ida y vuelta.

  • Combinar : cuando las solicitudes secuenciales deben ejecutarse de forma secuencial, busque oportunidades para combinarlas en una sola solicitud más completa. Los ejemplos típicos incluyen la creación de nuevas entidades, seguidas de solicitudes para relacionar esas entidades con otras entidades existentes.

  • Comprimir : busque oportunidades para aprovechar la compresión de la carga útil, ya sea reemplazando una forma textual por una binaria o utilizando la tecnología de compresión real. Muchas pilas de tecnología moderna (es decir, dentro de una década) admiten esto de forma casi transparente, así que asegúrese de que esté configurado. A menudo me sorprendió el impacto significativo de la compresión, donde parecía claro que el problema era fundamentalmente latente en lugar de ancho de banda, y descubrí después que la transacción encajaba en un solo paquete o evitaba la pérdida de paquetes y, por lo tanto, tenía un gran tamaño impacto en el rendimiento.

  • Repita : regrese al principio y vuelva a medir sus operaciones (en las mismas ubicaciones y horarios) con las mejoras implementadas, registre e informe sus resultados. Al igual que con toda la optimización, algunos problemas pueden haberse resuelto al exponer otros que ahora dominan.

En los pasos anteriores, me enfoco en el proceso de optimización relacionado con la aplicación, pero, por supuesto, debe asegurarse de que la propia red subyacente esté configurada de la manera más eficiente para respaldar su aplicación también. Involucre a los especialistas en redes en el negocio y determine si son capaces de aplicar mejoras de capacidad, QoS, compresión de red u otras técnicas para abordar el problema. Por lo general, no entenderán las necesidades de su aplicación, por lo que es importante que esté equipado (después del paso Analizar) para analizarlo con ellos y también para justificar el costo comercial que va a pedirles que incurra . He encontrado casos en los que una configuración de red errónea causó que los datos de las aplicaciones se transmitieran a través de un enlace satelital lento en lugar de un enlace terrestre.simplemente porque estaba usando un puerto TCP que no era "bien conocido" por los especialistas en redes; Obviamente, la rectificación de un problema como este puede tener un impacto dramático en el rendimiento, sin necesidad de cambios en la configuración o el código de software.


La forma de Google es una opción "Almacenar en caché ... Siempre que sea posible, no toque el disco"


No es tan profundo ni complejo como las respuestas anteriores, pero aquí va: (estos son más nivel principiante / intermedio)

  • obvio: seco
  • ejecuta bucles hacia atrás para que siempre estés comparando con 0 en lugar de una variable
  • utilizar operadores bitwise siempre que pueda
  • romper código repetitivo en módulos / funciones
  • objetos de caché
  • Las variables locales tienen una ligera ventaja de rendimiento.
  • Limitar la manipulación de la cadena tanto como sea posible

pasar por referencia en lugar de por valor


A veces puede ayudar cambiar el diseño de sus datos. En C, puede cambiar de una matriz o estructuras a una estructura de matrices, o viceversa.


Aquí hay algunas técnicas de optimización rápidas y sucias que utilizo. Considero que esto es una optimización de "primer paso".

Conozca dónde se gasta el tiempo Averigüe exactamente qué se está demorando. ¿Es archivo IO? ¿Es tiempo de CPU? ¿Es la red? ¿Es la base de datos? Es inútil optimizar para IO si ese no es el cuello de botella.

Conozca su entorno Saber dónde optimizar normalmente depende del entorno de desarrollo. En VB6, por ejemplo, pasar por referencia es más lento que pasar por valor, pero en C y C ++, por referencia es mucho más rápido. En C, es razonable intentar algo y hacer algo diferente si un código de retorno indica una falla, mientras que en Dot Net, las excepciones de captura son mucho más lentas que verificar una condición válida antes de intentar.

Índices Cree índices en los campos de la base de datos consultados con frecuencia. Casi siempre se puede intercambiar espacio por velocidad.

Evite las búsquedas Dentro del bucle para ser optimizado, evito tener que hacer búsquedas. Encuentre el desplazamiento y / o el índice fuera del bucle y reutilice los datos que contiene.

Minimice la E / S intente diseñar de una manera que reduzca la cantidad de veces que tiene que leer o escribir, especialmente a través de una conexión en red

Reduzca las abstracciones. Cuantas más capas de abstracción tenga que trabajar el código, más lento será. Dentro del ciclo crítico, reduzca las abstracciones (p. Ej., Revele métodos de nivel inferior que evitan código adicional)

Genera subprocesos para proyectos con una interfaz de usuario, lo que genera un nuevo subproceso para realizar tareas más lentas hace que la aplicación se sienta más receptiva, aunque no lo es.

Proceso previo Por lo general, puede intercambiar espacio por velocidad. Si hay cálculos u otras operaciones intensas, vea si puede calcular previamente algo de la información antes de estar en el ciclo crítico.


Creo que esto ya se ha dicho de una manera diferente. Pero cuando se trata de un algoritmo de uso intensivo del procesador, debe simplificar todo dentro del bucle más interno a expensas de todo lo demás.

Esto puede parecer obvio para algunos, pero es algo en lo que trato de enfocarme independientemente del idioma con el que estoy trabajando. Por ejemplo, si está tratando con bucles anidados y encuentra la oportunidad de bajar un nivel de código, en algunos casos puede acelerar su código drásticamente. Como otro ejemplo, hay cosas pequeñas en las que pensar, como trabajar con enteros en lugar de variables de punto flotante siempre que pueda, y usar la multiplicación en lugar de la división siempre que pueda. De nuevo, estas son cosas que deben considerarse para su ciclo más interno.

A veces, puede encontrar el beneficio de realizar sus operaciones matemáticas en un número entero dentro del bucle interno y luego escalarlo a una variable de punto flotante con la que pueda trabajar después. Ese es un ejemplo de sacrificar la velocidad en una sección para mejorar la velocidad en otra, pero en algunos casos la recompensa puede valer la pena.


En primer lugar, como se mencionó en varias respuestas anteriores, aprenda qué es lo que afecta su rendimiento, ya sea memoria, procesador, red, base de datos o algo más. Dependiendo de eso ...

  • ... si es memoria: encuentre uno de los libros escritos hace mucho tiempo por Knuth, uno de la serie "El arte de la programación de computadoras". Lo más probable es que se trate de ordenación y búsqueda: si mi memoria está equivocada, tendrá que averiguar en qué habla sobre cómo manejar el almacenamiento lento de datos en cinta. Transforme mentalmente su par de memoria / cinta en su par de caché / memoria principal (o en par de caché L1 / L2) respectivamente. Estudie todos los trucos que describe: si no encuentra algo que resuelva su problema, contrate un científico informático profesional para realizar una investigación profesional. Si su problema de memoria es por casualidad con FFT (la memoria caché pierde en los índices de bit invertido al hacer mariposas radix-2), entonces no contrate a un científico, en su lugar, optimice manualmente los pases uno por uno hasta que ustedre ganar o llegar al callejón sin salida. Mencionasteexprimir hasta el último porcentaje ¿verdad? Si son pocos , seguramente ganarás.

  • ... si es procesador - cambia al lenguaje ensamblador. Estudio de la especificación del procesador: qué lleva a los ticks , VLIW, SIMD. Las llamadas a la función son, probablemente, comedores de garrapatas reemplazables. Aprender las transformaciones de bucle - canalización, desenrollar. Las multiplicidades y divisiones pueden ser reemplazables / interpoladas con desplazamientos de bits (los multiplicados por enteros pequeños pueden reemplazarse con adiciones). Pruebe trucos con datos más cortos: si tiene suerte, una instrucción con 64 bits puede resultar reemplazable con dos en 32 o incluso 4 en 16 u 8 en 8 bits. Prueba tambien por mas tiempodatos: por ejemplo, los cálculos de flotación pueden resultar más lentos que los dobles en un procesador particular. Si tienes cosas trigonométricas, lucha con tablas precalculadas; también tenga en cuenta que el seno de valor pequeño podría reemplazarse con ese valor si la pérdida de precisión está dentro de los límites permitidos.

  • ... si se trata de una red, piense en comprimir los datos que pasa por ella. Reemplazar la transferencia de XML con binario. Protocolos de estudio. Pruebe UDP en lugar de TCP si de alguna manera puede manejar la pérdida de datos.

  • ... si es una base de datos, bueno, vaya a cualquier foro de base de datos y pida consejo. Cuadrícula de datos en memoria, optimización del plan de consulta, etc., etc.

HTH :)


Imposible decirlo. Depende de cómo se ve el código. Si podemos suponer que el código ya existe, entonces podemos simplemente mirarlo y averiguar a partir de eso, cómo optimizarlo.

Mejor localidad de caché, desenrollado de bucles, Intente eliminar largas cadenas de dependencia para obtener un mejor paralelismo a nivel de instrucción Prefiere movimientos condicionales sobre ramas cuando sea posible. Explotar las instrucciones SIMD cuando sea posible.

Comprenda lo que hace su código y entienda el hardware en el que se está ejecutando. Entonces, se vuelve bastante simple determinar qué debe hacer para mejorar el rendimiento de su código. Ese es realmente el único consejo verdaderamente general que se me ocurre.

Bien, eso, y "Muestre el código en SO y pida consejos de optimización para esa pieza específica de código".


Muy difícil dar una respuesta genérica a esta pregunta. Realmente depende de su problema de dominio y la implementación técnica. Una técnica general que es bastante neutral en términos de lenguaje: identificar puntos de acceso de código que no se pueden eliminar y optimizar manualmente el código del ensamblador.


Si un mejor hardware es una opción, entonces definitivamente vaya por eso. De otra manera

  • Comprueba que estás usando las mejores opciones de compilador y enlazador.
  • Si es una rutina de punto de acceso en una biblioteca diferente a la del llamante frecuente, considere moverlo o clonarlo al módulo de llamantes. Elimina parte de la sobrecarga de llamadas y puede mejorar los aciertos de caché (vea cómo AIX vincula estáticamente strcpy () de forma estática en objetos compartidos vinculados por separado). Por supuesto, esto también podría disminuir los aciertos de caché, por lo que una medida.
  • Vea si hay alguna posibilidad de usar una versión especializada de la rutina de punto de acceso. El inconveniente es más de una versión para mantener.
  • Mira el ensamblador. Si crees que podría ser mejor, considera por qué el compilador no resolvió esto y cómo podrías ayudar al compilador.
  • Considera: ¿estás realmente usando el mejor algoritmo? ¿Es el mejor algoritmo para su tamaño de entrada?

  • ¿En qué hardware estás ejecutando? ¿Se pueden usar optimizaciones específicas de la plataforma (como vectorización)?
  • ¿Se puede conseguir un mejor compilador? Por ejemplo, cambiar de GCC a Intel?
  • ¿Puedes hacer que tu algoritmo se ejecute en paralelo?
  • ¿Se pueden reducir los errores de caché reorganizando los datos?
  • ¿Se puede deshabilitar aserciones?
  • Micro-optimiza para tu compilador y plataforma. En el estilo de "en un if / else, coloque primero la declaración más común"

  • Cuando llega al punto en que está usando algoritmos eficientes, es una cuestión de qué necesita más velocidad o memoria . Utilice el almacenamiento en caché para "pagar" en la memoria para obtener más velocidad o utilice los cálculos para reducir la huella de memoria.
  • Si es posible (y más rentable) lanzar hardware al problema : una CPU más rápida, más memoria o HD podrían resolver el problema más rápido que intentar codificarlo.
  • Utilice la paralelización si es posible: ejecute parte del código en varios subprocesos.
  • Utilice la herramienta adecuada para el trabajo . algunos lenguajes de programación crean un código más eficiente, el uso de código administrado (es decir, Java / .NET) acelera el desarrollo, pero los lenguajes de programación nativos crean un código de ejecución más rápido.
  • Micro optimiza . Solo si era aplicable, puede utilizar el ensamblaje optimizado para acelerar pequeños fragmentos de código, el uso de optimizaciones de SSE / vector en los lugares correctos puede aumentar considerablemente el rendimiento.

  • Rutinas en línea (eliminar llamada / retorno y empuje de parámetros)
  • Intenta eliminar pruebas / interruptores con búsquedas en la tabla (si son más rápidas)
  • Desenrolle los bucles (el dispositivo de Duff) hasta el punto en que solo caben en el caché de la CPU
  • Localice el acceso a la memoria para no destruir su caché
  • Localice los cálculos relacionados si el optimizador no lo está haciendo.
  • Elimine las invariantes de bucle si el optimizador no lo está haciendo