OK, estás definiendo el problema donde parece que no hay mucho margen de mejora. Eso es bastante raro, en mi experiencia. Traté de explicar esto 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 llevándolo a través de una serie de optimizaciones hasta que su tiempo de reloj de pared se redujo 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, reduciendo el tiempo a 20 segundos.
Ahora, el que más tiempo hace es construir más listas. Como porcentaje, antes no era tan grande, pero ahora se debe a que se eliminó el problema más grande. Encuentro una manera de acelerarlo, y el tiempo cae 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 impulsada por las transacciones, y pregunto si toda la búsqueda en la lista que está haciendo es obligatoria según los requisitos del problema.
Luego me topé con un rediseño, donde el código del programa se genera realmente (a través de macros de preprocesador) a partir de un conjunto de fuentes más pequeño, y en el que el programa no está constantemente descubriendo cosas que el programador sabe que son bastante predecibles. En otras palabras, no "interprete" la secuencia de cosas que hacer, "compílela".
- Ese rediseño se realiza, reduciendo el código fuente por un factor de 4, y el tiempo se reduce a 10 segundos.
Ahora, debido a que se está volviendo tan rápido, es difícil de probar, por lo que le doy 10 veces más trabajo, pero los siguientes tiempos se basan en la carga de trabajo original.
Más diagnóstico revela que está pasando tiempo en la gestión de colas. El forro interior reduce el tiempo a 7 segundos.
Ahora una gran toma 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 gratuitas . Reciclar objetos - 2.6 segundos.
Continuando 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 el software que no es de juguete siempre he visto una progresión como esta. Primero obtienes las cosas fáciles, y luego las más difíciles, hasta que llegas a un punto de rendimientos decrecientes. Entonces, la información que obtenga puede conducir a un rediseño, comenzando una nueva ronda de aceleraciones, hasta que vuelva a alcanzar rendimientos decrecientes. Ahora bien, este es el punto en el que podría tener sentido a preguntarse si ++i
o i++
o for(;;)
o while(1)
son más rápidos: el tipo de preguntas que veo tan a menudo en desbordamiento de pila.
PD: Me pregunto por qué no utilicé un generador de perfiles. La respuesta es que casi cada uno de estos "problemas" era un sitio de llamada de función, que apila las muestras con precisión. Los perfiladores, incluso hoy, apenas llegan a la idea de que las declaraciones y las instrucciones de llamada son más importantes de localizar y más fáciles de corregir que las funciones completas.
Realmente construí un generador de perfiles para hacer esto, pero para una verdadera intimidad con lo que está haciendo el código, no hay sustituto para meter los dedos en él. No es un problema que el número de muestras sea pequeño, porque ninguno de los problemas encontrados es tan pequeño que se pueden pasar por alto fácilmente.
AGREGADO: jerryjvl solicitó algunos ejemplos. Aquí está el primer problema. Consiste en una pequeña cantidad de líneas de código separadas, que juntas 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 estaban usando el ILST del grupo de listas (similar a una clase de lista). Se implementan de la manera habitual, con "ocultación de información", lo que significa que no se suponía que los usuarios de la clase tuvieran que preocuparse por cómo se implementaron. Cuando se escribieron estas líneas (de aproximadamente 800 líneas de código) no se pensó en 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. Es incluso mejor encontrar y arreglar los que se crean, aunque "deberían haberse evitado" (en retrospectiva).
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 agregando elementos a sus extremos. (La solución fue recopilar los elementos en matrices y crear las listas de una 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 fue destilado de un proyecto real en el que ayudé. En ese proyecto, los problemas de rendimiento fueron mucho más dramáticos (al igual que las aceleraciones), como llamar a una rutina de acceso a la base de datos dentro de un bucle interno para ver si una tarea estaba terminada.
REFERENCIA AGREGADA: El código fuente, tanto original como rediseñado, se puede encontrar en www.ddj.com , para 1993, en el archivo 9311.zip, archivos slug.asc y slug.zip.
EDITAR 26/11/2011: ahora hay un proyecto de SourceForge que contiene el código fuente en Visual C ++ y una descripción detallada de cómo se ajustó. Solo pasa por la primera mitad del escenario descrito anteriormente, y no sigue exactamente la misma secuencia, pero aún así obtiene una aceleración de orden de magnitud de 2-3.