Estamos trabajando en una base de código C ++ de tamaño moderado (10Mloc) que a través de nuestros esfuerzos de optimización se está volviendo uniformemente lenta .
Esta base de código es un conjunto de bibliotecas que combinamos para ponerlas a trabajar. Cuando se desarrolló el marco general de cómo se comunican estas bibliotecas, hubo un cierto énfasis en el rendimiento y más tarde, cuando se agregaron más partes, el marco general no cambió mucho. La optimización se realizó cuando fue necesario y a medida que nuestro hardware evolucionó. Esto hizo que la decisión temprana y costosa fuera evidente solo mucho más tarde. Ahora estamos en un punto donde las optimizaciones adicionales son mucho más caras ya que requerirían reescribir grandes partes de la base del código. Nos encontramos acercándonos a un mínimo local indeseable ya que sabemos que, en principio, el código debería poder ejecutarse mucho más rápido.
¿Existen metodologías exitosas que ayuden a decidir qué turnos para hacerse cargo de la evolución de una base de código hacia una solución global de rendimiento óptimo que no se confunde fácilmente con las oportunidades de optimización fácil?
EDITAR
Para responder a la pregunta de cómo nos perfilamos actualmente:
Realmente solo tenemos 2 escenarios diferentes de cómo se puede usar este código, ambos vergonzosamente paralelos. La creación de perfiles se realiza tanto con el tiempo promedio del reloj de pared sobre una gran muestra de entradas como con ejecuciones más detalladas (costos de instrucción, predicciones erróneas de rama y problemas de almacenamiento en caché). Esto funciona bien ya que corremos exclusivamente en nuestras máquinas extremadamente homogéneas (un grupo de miles de máquinas idénticas). Dado que generalmente mantenemos ocupadas todas nuestras máquinas la mayor parte del tiempo funcionando más rápido, podemos ver cosas nuevas adicionales. El problema es, por supuesto, que cuando aparezcan nuevas variaciones de entrada, podrían recibir una penalización tardía, ya que eliminamos las microeficiencias más obvias para los otros casos de uso, lo que posiblemente reduzca el número de escenarios de "ejecución óptima".
fuente
sloc
. Lo llamé "de tamaño moderado" porque no tengo idea de lo que se considera "grande" aquí.Respuestas:
No conozco un enfoque de propósito general para este problema, pero dos enfoques algo relacionados me funcionaron bien en el pasado: por falta de mejores términos, los llamé agrupamiento y optimización horizontal .
El enfoque de agrupamiento es un intento de reemplazar un gran número de operaciones cortas y rápidas con una operación única, altamente lenta y altamente especializada que finalmente produce el mismo resultado.
Ejemplo: Después de perfilar una operación particularmente lenta de nuestro editor de reglas visuales, no descubrimos ninguna "fruta baja": no hubo una sola operación que tomara más del 2% del tiempo de ejecución, pero la operación en su conjunto se sintió lenta. Sin embargo, descubrimos que el editor estaba enviando una gran cantidad de pequeñas solicitudes al servidor. Aunque el editor estaba procesando rápidamente las respuestas individuales, el número de interacciones de solicitud / respuesta tuvo un efecto multiplicador, por lo que el tiempo total que tomó la operación fue de varios segundos. Después de catalogar cuidadosamente las interacciones del editor durante esa operación de larga duración, agregamos un nuevo comando a la interfaz del servidor. El comando adicional era más especializado, ya que aceptaba los datos necesarios para realizar un subconjunto de operaciones cortas, exploró las dependencias de datos para determinar el conjunto final de datos a devolver, y proporcionó una respuesta que contenía la información necesaria para completar todas las pequeñas operaciones individuales en un solo viaje al servidor. Esto no redujo el tiempo de procesamiento en nuestro código, pero redujo una cantidad muy significativa de latencia debido a la eliminación de múltiples costosos viajes de ida y vuelta cliente-servidor.
La optimización horizontal es una técnica relacionada cuando elimina la "lentitud" que se distribuye de manera delgada entre múltiples componentes de su sistema utilizando una característica particular de su entorno de ejecución.
Ejemplo: Después de perfilar una operación de larga duración, descubrimos que realizamos muchas llamadas a través del límite del dominio de la aplicación (esto es muy específico de .NET). No pudimos eliminar ninguna de las llamadas, y no pudimos agruparlas: venían en diferentes momentos de secciones muy diferentes de nuestro sistema, y las cosas que solicitaron dependían de los resultados devueltos de solicitudes anteriores. Cada llamada requería la serialización y deserialización de una cantidad relativamente pequeña de datos. Nuevamente, las llamadas individuales fueron de corta duración, pero muy grandes en número. Terminamos diseñando un esquema que evitaba la serialización casi por completo, reemplazándolo con pasar un puntero a través del límite del dominio de la aplicación. Esta fue una gran victoria, porque muchas solicitudes de clases completamente no relacionadas se volvieron instantáneamente mucho más rápidas como resultado de aplicar una solasolución horizontal
fuente
Cuando comienzas esta reescritura tienes que hacer varias cosas de manera diferente.
Primero. Y lo más importante. Deja de "optimizar". La "optimización" no importa mucho. Como has visto, solo importa la reescritura al por mayor.
Por lo tanto.
Segundo. Comprenda las implicaciones de cada estructura de datos y elección de algoritmo.
Tercero. Haga que la elección real de la estructura de datos y el algoritmo sea una cuestión de "vinculación tardía". Diseñe interfaces que puedan tener cualquiera de varias implementaciones utilizadas detrás de la interfaz.
Lo que está haciendo ahora (reescribir) debería ser mucho, mucho menos doloroso si tiene un conjunto de interfaces definidas que le permite realizar un cambio general en la estructura de datos o el algoritmo.
fuente
++
y+=1
será irrelevante y casi inconmensurable. Es lo que debes durar .Un buen truco práctico es utilizar su conjunto de pruebas unitarias como conjunto de pruebas de rendimiento .
El siguiente enfoque funcionó bien en mis bases de código:
Si sigue haciendo todo esto, con el tiempo el rendimiento promedio de su base de código debería mejorar orgánicamente.
También sería posible rastrear tiempos de prueba históricos y dibujar gráficos de rendimiento y regresiones puntuales a lo largo del tiempo en el rendimiento promedio. Nunca me molesté en esto principalmente porque es un poco complicado asegurarse de que comparas cosas similares a medida que cambias y agregas nuevas pruebas, pero podría ser un ejercicio interesante si el rendimiento es lo suficientemente importante para ti.
fuente
La respuesta de @dasblinkenlight señala un problema muy común, especialmente con bases de código grandes (en mi experiencia). Puede haber serios problemas de rendimiento, pero no están localizados . Si ejecuta un generador de perfiles, no hay rutinas que tomen suficiente tiempo, por sí mismas, para llamar la atención. (Suponiendo que observe el porcentaje de tiempo inclusivo que incluye callees. Ni siquiera se moleste con el "tiempo propio").
De hecho, en ese caso, el problema real no se encontró mediante la elaboración de perfiles, sino por una visión afortunada.
Tengo un estudio de caso, para el cual hay una breve presentación de diapositivas en PDF , que ilustra este problema en detalle y cómo manejarlo. El punto básico es que, dado que el código es mucho más lento de lo que podría ser, eso significa (por definición) que se dedica el tiempo sobrante a hacer algo que podría eliminarse.
Si mirara algunas muestras de tiempo aleatorio del estado del programa, simplemente lo vería haciendo la actividad extraíble, debido al porcentaje de tiempo que lleva. Es muy posible que la actividad extraíble no se limite a una función, o incluso a muchas funciones. No se localiza de esa manera.
No es un "punto caliente".
Es una descripción que haces de lo que ves, que resulta ser cierto un gran porcentaje de tiempo. Eso lo hace fácil de descubrir, pero si es fácil de arreglar depende de la cantidad de reescritura que requiera.
(A menudo se hace una crítica de este enfoque, que el número de muestras es demasiado pequeño para la validez estadística. Eso se responde en la diapositiva 13 del PDF. Brevemente: sí, existe una gran incertidumbre en la "medición" de los ahorros potenciales, pero 1) el valor esperado de ese ahorro potencial no se ve afectado esencialmente, y 2) cuando el ahorro potencial $ x $ se traduce en una tasa de aceleración de $ 1 / (1-x) $, se sesga fuertemente hacia factores altos (beneficiosos).
fuente