Tengo una aplicación C ++, que se ejecuta en Linux, que estoy en proceso de optimización. ¿Cómo puedo determinar qué áreas de mi código se ejecutan lentamente?
Si su objetivo es usar un generador de perfiles, use uno de los sugeridos.
Sin embargo, si tiene prisa y puede interrumpir manualmente su programa bajo el depurador mientras es subjetivamente lento, hay una manera simple de encontrar problemas de rendimiento.
Simplemente deténgalo varias veces, y cada vez mire la pila de llamadas. Si hay algún código que está desperdiciando un porcentaje del tiempo, 20% o 50% o lo que sea, esa es la probabilidad de que lo atrape en el acto en cada muestra. Entonces, ese es aproximadamente el porcentaje de muestras en el que lo verá. No se requieren conjeturas educadas. Si tiene una idea de cuál es el problema, esto lo probará o lo desaprobará.
Puede tener múltiples problemas de rendimiento de diferentes tamaños. Si limpia alguno de ellos, los restantes tomarán un porcentaje mayor y serán más fáciles de detectar en los pases posteriores. Este efecto de aumento , cuando se combina con múltiples problemas, puede conducir a factores de aceleración verdaderamente masivos.
Advertencia : los programadores tienden a ser escépticos de esta técnica a menos que la hayan usado ellos mismos. Dirán que los perfiladores le brindan esta información, pero eso solo es cierto si toman muestras de toda la pila de llamadas y luego le permiten examinar un conjunto aleatorio de muestras. (Los resúmenes son donde se pierde la información). Los gráficos de llamadas no le brindan la misma información, porque
También dirán que solo funciona en programas de juguetes, cuando en realidad funciona en cualquier programa, y parece funcionar mejor en programas más grandes, porque tienden a tener más problemas para encontrar. Dirán que a veces encuentra cosas que no son problemas, pero eso solo es cierto si ves algo una vez . Si ve un problema en más de una muestra, es real.
PD Esto también se puede hacer en programas de subprocesos múltiples si hay una manera de recopilar muestras de la pila de llamadas del grupo de subprocesos en un momento dado, como en Java.
PPS Como una generalidad aproximada, mientras más capas de abstracción tenga en su software, es más probable que descubra que esa es la causa de los problemas de rendimiento (y la oportunidad de acelerar).
Adicional : Puede que no sea obvio, pero la técnica de muestreo de pila funciona igualmente bien en presencia de recursividad. La razón es que el tiempo que se ahorraría al eliminar una instrucción se aproxima por la fracción de muestras que la contienen, independientemente de la cantidad de veces que pueda ocurrir dentro de una muestra.
Otra objeción que escucho a menudo es: " Se detendrá en algún lugar al azar, y perderá el verdadero problema ". Esto viene de tener un concepto previo de cuál es el verdadero problema. Una propiedad clave de los problemas de rendimiento es que desafían las expectativas. El muestreo te dice que algo es un problema, y tu primera reacción es de incredulidad. Eso es natural, pero puede estar seguro de que si encuentra un problema, es real y viceversa.
Agregado : Permítanme hacer una explicación bayesiana de cómo funciona. Supongamos que hay alguna instrucción I
(llamada o no) que está en la pila de llamadas una fracción f
del tiempo (y por lo tanto cuesta tanto). Por simplicidad, supongamos que no sabemos qué f
es, pero supongamos que es 0.1, 0.2, 0.3, ... 0.9, 1.0, y la probabilidad previa de cada una de estas posibilidades es 0.1, por lo que todos estos costos son igualmente probables a priori.
Luego, supongamos que tomamos solo 2 muestras de pila y vemos instrucciones I
en ambas muestras, observación designada o=2/2
. Esto nos da nuevas estimaciones de la frecuencia f
de I
acuerdo con esto:
Prior
P(f=x) x P(o=2/2|f=x) P(o=2/2&&f=x) P(o=2/2&&f >= x) P(f >= x | o=2/2)
0.1 1 1 0.1 0.1 0.25974026
0.1 0.9 0.81 0.081 0.181 0.47012987
0.1 0.8 0.64 0.064 0.245 0.636363636
0.1 0.7 0.49 0.049 0.294 0.763636364
0.1 0.6 0.36 0.036 0.33 0.857142857
0.1 0.5 0.25 0.025 0.355 0.922077922
0.1 0.4 0.16 0.016 0.371 0.963636364
0.1 0.3 0.09 0.009 0.38 0.987012987
0.1 0.2 0.04 0.004 0.384 0.997402597
0.1 0.1 0.01 0.001 0.385 1
P(o=2/2) 0.385
La última columna dice que, por ejemplo, la probabilidad de que f
> = 0.5 sea del 92%, por encima del supuesto anterior del 60%.
Supongamos que los supuestos anteriores son diferentes. Supongamos que suponemos P(f=0.1)
.991 (casi seguro), y todas las demás posibilidades son casi imposibles (0.001). En otras palabras, nuestra certeza previa es que I
es barata. Entonces obtenemos:
Prior
P(f=x) x P(o=2/2|f=x) P(o=2/2&& f=x) P(o=2/2&&f >= x) P(f >= x | o=2/2)
0.001 1 1 0.001 0.001 0.072727273
0.001 0.9 0.81 0.00081 0.00181 0.131636364
0.001 0.8 0.64 0.00064 0.00245 0.178181818
0.001 0.7 0.49 0.00049 0.00294 0.213818182
0.001 0.6 0.36 0.00036 0.0033 0.24
0.001 0.5 0.25 0.00025 0.00355 0.258181818
0.001 0.4 0.16 0.00016 0.00371 0.269818182
0.001 0.3 0.09 0.00009 0.0038 0.276363636
0.001 0.2 0.04 0.00004 0.00384 0.279272727
0.991 0.1 0.01 0.00991 0.01375 1
P(o=2/2) 0.01375
Ahora dice que P(f >= 0.5)
es 26%, por encima del supuesto anterior de 0.6%. Entonces Bayes nos permite actualizar nuestra estimación del costo probable de I
. Si la cantidad de datos es pequeña, no nos dice con precisión cuál es el costo, solo que es lo suficientemente grande como para que valga la pena arreglarlo.
Otra forma de verlo se llama la Regla de Sucesión . Si lanzas una moneda 2 veces, y sale cara en ambas ocasiones, ¿qué te dice eso sobre el probable peso de la moneda? La forma respetada de responder es decir que es una distribución Beta, con un valor promedio (number of hits + 1) / (number of tries + 2) = (2+1)/(2+2) = 75%
.
(La clave es que vemos I
más de una vez. Si solo lo vemos una vez, eso no nos dice mucho, excepto que f
> 0.)
Entonces, incluso un número muy pequeño de muestras puede decirnos mucho sobre el costo de las instrucciones que ve. (Y va a ver con una frecuencia de, en promedio, proporcional a su costo. Si n
se toman muestras, y f
es el costo, entonces I
van a aparecer en nf+/-sqrt(nf(1-f))
las muestras. Ejemplo, n=10
, f=0.3
, es decir 3+/-1.4
muestras.)
Agregado : para dar una idea intuitiva de la diferencia entre la medición y el muestreo aleatorio de la pila:
ahora hay perfiladores que muestrean la pila, incluso en el tiempo del reloj de pared, pero lo que sale son las mediciones (o ruta caliente, o punto caliente, desde el cual un "cuello de botella" puede esconderse fácilmente). Lo que no le muestran (y podrían fácilmente) son las muestras en sí mismas. Y si su objetivo es encontrar el cuello de botella, el número de ellos que necesita ver es, en promedio , 2 dividido por la fracción de tiempo que lleva. Entonces, si toma el 30% del tiempo, 2 / .3 = 6.7 muestras, en promedio, lo mostrarán, y la probabilidad de que 20 muestras lo muestren es 99.2%.
Aquí hay una ilustración poco convencional de la diferencia entre examinar mediciones y examinar muestras de pila. El cuello de botella podría ser una gran gota como esta, o numerosas pequeñas, no hay diferencia.
La medida es horizontal; le dice qué fracción de tiempo toman rutinas específicas. El muestreo es vertical. Si hay alguna forma de evitar lo que está haciendo todo el programa en ese momento, y si lo ve en una segunda muestra , ha encontrado el cuello de botella. Eso es lo que marca la diferencia: ver la razón completa del tiempo que se pasa, no solo cuánto.
Puedes usar Valgrind con las siguientes opciones
valgrind --tool=callgrind ./(Your binary)
Generará un archivo llamado callgrind.out.x
. Luego puede usar la kcachegrind
herramienta para leer este archivo. Le dará un análisis gráfico de las cosas con resultados como qué líneas cuestan cuánto.
./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg
Supongo que estás usando GCC. La solución estándar sería perfilar con gprof .
Asegúrese de agregar -pg
a la compilación antes de perfilar:
cc -o myprog myprog.c utils.c -g -pg
Todavía no lo he probado, pero he oído cosas buenas sobre google-perftools . Definitivamente vale la pena intentarlo.
Pregunta relacionada aquí .
Algunas otras palabras de moda si gprof
no hacen el trabajo por usted: Valgrind , Intel VTune , Sun DTrace .
Los nuevos núcleos (por ejemplo, los últimos núcleos de Ubuntu) vienen con las nuevas herramientas 'perf' ( apt-get install linux-tools
) AKA perf_events .
¡Estos vienen con perfiladores de muestreo clásicos ( página de manual ), así como con el impresionante diagrama de tiempo !
Lo importante es que estas herramientas pueden ser la creación de perfiles del sistema y no solo la creación de perfiles de procesos: pueden mostrar la interacción entre subprocesos, procesos y el núcleo y le permiten comprender la programación y las dependencias de E / S entre procesos.
perf report
parece darme los nombres de las funciones con los padres de la llamada ... (por lo que es una especie de vista de mariposa invertida)
gprof2dot
y perf script
. Muy buena herramienta!
perf
existe en archive.li/9r927#selection-767.126-767.271 (¿Por qué los dioses por lo que decidió eliminar esa página de la base de conocimientos por lo que es más allá de mí ....)
Usaría Valgrind y Callgrind como base para mi conjunto de herramientas de creación de perfiles. Lo importante es saber que Valgrind es básicamente una máquina virtual:
(wikipedia) Valgrind es, en esencia, una máquina virtual que utiliza técnicas de compilación justo a tiempo (JIT), incluida la recompilación dinámica. Nada del programa original se ejecuta directamente en el procesador host. En cambio, Valgrind primero traduce el programa en una forma temporal y más simple llamada Representación Intermedia (IR), que es una forma basada en SSA y neutral del procesador. Después de la conversión, una herramienta (ver más abajo) es libre de hacer cualquier transformación que desee en el IR, antes de que Valgrind traduzca el IR nuevamente en el código de la máquina y permita que el procesador host lo ejecute.
Callgrind es un generador de perfiles sobre eso. El principal beneficio es que no tiene que ejecutar su aplicación durante horas para obtener resultados confiables. Incluso una segunda carrera es suficiente para obtener resultados sólidos y confiables, ya que Callgrind es un generador de perfiles sin sondeo .
Otra herramienta construida sobre Valgrind es Massif. Lo uso para perfilar el uso de memoria del montón. Funciona muy bien Lo que hace es que le da instantáneas del uso de la memoria: información detallada QUÉ contiene QUÉ porcentaje de memoria, y QUIÉN lo había puesto allí. Dicha información está disponible en diferentes puntos de tiempo de ejecución de la aplicación.
La respuesta para ejecutar valgrind --tool=callgrind
no está completa sin algunas opciones. Por lo general, no queremos perfilar 10 minutos de tiempo de inicio lento en Valgrind y queremos perfilar nuestro programa cuando está realizando alguna tarea.
Entonces esto es lo que recomiendo. Ejecute el programa primero:
valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp
Ahora, cuando funciona y queremos comenzar a generar perfiles, debemos ejecutar en otra ventana:
callgrind_control -i on
Esto activa la creación de perfiles. Para apagarlo y detener toda la tarea, podemos usar:
callgrind_control -k
Ahora tenemos algunos archivos llamados callgrind.out. * En el directorio actual. Para ver resultados de perfiles, use:
kcachegrind callgrind.out.*
Recomiendo en la siguiente ventana hacer clic en el encabezado de la columna "Self", de lo contrario, muestra que "main ()" es la tarea que requiere más tiempo. "Self" muestra cuánto tiempo llevó cada función en sí, no junto con las personas dependientes.
CALLGRIND_TOGGLE_COLLECT
que habilitar / deshabilitar la recopilación mediante programación; ver stackoverflow.com/a/13700817/288875
Esta es una respuesta a la respuesta Gprof de Nazgob .
He estado usando Gprof los últimos días y ya he encontrado tres limitaciones significativas, una de las cuales no he visto documentada en ningún otro lugar (todavía):
No funciona correctamente en código multiproceso, a menos que use una solución alternativa
El gráfico de llamadas se confunde con los punteros de función. Ejemplo: Tengo una función llamada multithread()
que me permite realizar múltiples subprocesos de una función específica sobre una matriz específica (ambas pasadas como argumentos). Sin embargo, Gprof considera que todas las llamadas multithread()
son equivalentes para calcular el tiempo que se pasa en los niños. Dado que algunas funciones que paso multithread()
tardan mucho más que otras, mis gráficos de llamadas son en su mayoría inútiles. (Para aquellos que se preguntan si el problema es el enhebrado aquí: no, multithread()
opcionalmente, y en este caso, ejecuté todo secuencialmente solo en el hilo de llamada).
Aquí dice que "... las cifras del número de llamadas se obtienen contando, no muestreando. Son completamente precisas ...". Sin embargo, encuentro que mi gráfico de llamadas me da 5345859132 + 784984078 como estadísticas de llamadas a mi función más llamada, donde se supone que el primer número son llamadas directas y las segundas llamadas recursivas (que son todas de sí mismo). Como esto implicaba que tenía un error, puse contadores largos (64 bits) en el código y volví a hacer lo mismo. Mis recuentos: 5345859132 directo y 78094395406 llamadas autorrecurrentes. Hay muchos dígitos allí, así que señalaré que las llamadas recursivas que mido son 78 mil millones, frente a 784 millones de Gprof: un factor de 100 diferentes. Ambas ejecuciones fueron de un solo subproceso y código no optimizado, uno compilado -g
y el otro -pg
.
Este fue GNU Gprof (GNU Binutils para Debian) 2.18.0.20080103 ejecutándose bajo Debian Lenny de 64 bits, si eso ayuda a alguien.
Use Valgrind, callgrind y kcachegrind:
valgrind --tool=callgrind ./(Your binary)
genera callgrind.out.x. Léalo usando kcachegrind.
Use gprof (agregar -pg):
cc -o myprog myprog.c utils.c -g -pg
(no tan bueno para subprocesos múltiples, punteros de función)
Utiliza google-perftools:
Utiliza el muestreo de tiempo, se revelan los cuellos de botella de E / S y CPU.
Intel VTune es el mejor (gratis para fines educativos).
Otros: AMD Codeanalyst (desde que se reemplazó con AMD CodeXL), OProfile, herramientas 'perf' (apt-get install linux-tools)
Encuesta de técnicas de perfilado C ++
En esta respuesta, usaré varias herramientas diferentes para analizar algunos programas de prueba muy simples, a fin de comparar concretamente cómo funcionan esas herramientas.
El siguiente programa de prueba es muy simple y hace lo siguiente:
main
llamadas fast
y maybe_slow
3 veces, una de las maybe_slow
llamadas es lenta
La llamada lenta de maybe_slow
es 10 veces más larga y domina el tiempo de ejecución si consideramos las llamadas a la función secundaria common
. Idealmente, la herramienta de creación de perfiles podrá indicarnos la llamada lenta específica.
ambos fast
y maybe_slow
call common
, que representa la mayor parte de la ejecución del programa
La interfaz del programa es:
./main.out [n [seed]]
y el programa hace O(n^2)
bucles en total. seed
es solo para obtener una salida diferente sin afectar el tiempo de ejecución.
C Principal
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
uint64_t __attribute__ ((noinline)) common(uint64_t n, uint64_t seed) {
for (uint64_t i = 0; i < n; ++i) {
seed = (seed * seed) - (3 * seed) + 1;
}
return seed;
}
uint64_t __attribute__ ((noinline)) fast(uint64_t n, uint64_t seed) {
uint64_t max = (n / 10) + 1;
for (uint64_t i = 0; i < max; ++i) {
seed = common(n, (seed * seed) - (3 * seed) + 1);
}
return seed;
}
uint64_t __attribute__ ((noinline)) maybe_slow(uint64_t n, uint64_t seed, int is_slow) {
uint64_t max = n;
if (is_slow) {
max *= 10;
}
for (uint64_t i = 0; i < max; ++i) {
seed = common(n, (seed * seed) - (3 * seed) + 1);
}
return seed;
}
int main(int argc, char **argv) {
uint64_t n, seed;
if (argc > 1) {
n = strtoll(argv[1], NULL, 0);
} else {
n = 1;
}
if (argc > 2) {
seed = strtoll(argv[2], NULL, 0);
} else {
seed = 0;
}
seed += maybe_slow(n, seed, 0);
seed += fast(n, seed);
seed += maybe_slow(n, seed, 1);
seed += fast(n, seed);
seed += maybe_slow(n, seed, 0);
seed += fast(n, seed);
printf("%" PRIX64 "\n", seed);
return EXIT_SUCCESS;
}
gprof
gprof requiere recompilar el software con instrumentación, y también utiliza un enfoque de muestreo junto con esa instrumentación. Por lo tanto, logra un equilibrio entre la precisión (el muestreo no siempre es completamente exacto y puede omitir funciones) y la ralentización de la ejecución (la instrumentación y el muestreo son técnicas relativamente rápidas que no ralentizan mucho la ejecución).
gprof está integrado en GCC / binutils, por lo que todo lo que tenemos que hacer es compilar con la -pg
opción para habilitar gprof. Luego ejecutamos el programa normalmente con un parámetro CLI de tamaño que produce una ejecución de duración razonable de unos segundos ( 10000
):
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000
Por razones educativas, también haremos una ejecución sin optimizaciones habilitadas. Tenga en cuenta que esto es inútil en la práctica, ya que normalmente solo le importa optimizar el rendimiento del programa optimizado:
gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000
Primero, time
nos dice que el tiempo de ejecución con y sin -pg
fue el mismo, lo cual es genial: ¡no hay desaceleración! Sin embargo, he visto cuentas de ralentizaciones 2x - 3x en software complejo, por ejemplo, como se muestra en este ticket .
Debido a que compilamos -pg
, ejecutar el programa produce un archivo gmon.out
que contiene los datos de creación de perfiles.
Podemos observar ese archivo gráficamente con gprof2dot
lo solicitado en: ¿Es posible obtener una representación gráfica de los resultados de gprof?
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg
Aquí, la gprof
herramienta lee la gmon.out
información de rastreo y genera un informe legible por humanos main.gprof
, que gprof2dot
luego se lee para generar un gráfico.
La fuente de gprof2dot está en: https://github.com/jrfonseca/gprof2dot
Observamos lo siguiente para la -O0
carrera:
y para la -O3
carrera:
La -O0
salida es bastante autoexplicativa. Por ejemplo, muestra que las 3 maybe_slow
llamadas y sus llamadas secundarias ocupan el 97.56% del tiempo de ejecución total, aunque la ejecución de maybe_slow
sí mismo sin hijos representa el 0.00% del tiempo de ejecución total, es decir, casi todo el tiempo dedicado a esa función se gastó en niño llama.
TODO: ¿por qué main
falta en la -O3
salida, aunque puedo verlo en un bt
en GDB? Falta la función de la salida de GProf . Creo que es porque gprof también está basado en muestreo además de su instrumentación compilada, y -O3
main
es demasiado rápido y no tiene muestras.
Elijo la salida SVG en lugar de PNG porque la SVG se puede buscar con Ctrl + F y el tamaño del archivo puede ser aproximadamente 10 veces más pequeño. Además, el ancho y la altura de la imagen generada pueden ser enormes con decenas de miles de píxeles para software complejo, y GNOME eog
3.28.1 produce errores en ese caso para PNG, mientras que mi navegador abre automáticamente los SVG. Sin embargo, gimp 2.8 funcionó bien, ver también:
pero incluso así, arrastrará la imagen mucho para encontrar lo que desea, vea, por ejemplo, esta imagen de un ejemplo de software "real" tomado de este ticket :
¿Puedes encontrar la pila de llamadas más crítica fácilmente con todas esas pequeñas líneas de espagueti sin clasificar que se cruzan entre sí? Puede haber mejores dot
opciones, estoy seguro, pero no quiero ir allí ahora. Lo que realmente necesitamos es un visor dedicado adecuado para él, pero aún no he encontrado uno:
Sin embargo, puede usar el mapa de colores para mitigar un poco esos problemas. Por ejemplo, en la gran imagen anterior, finalmente logré encontrar el camino crítico a la izquierda cuando hice la brillante deducción de que el verde viene después del rojo, seguido finalmente por un azul más y más oscuro.
Alternativamente, también podemos observar la salida de texto de la gprof
herramienta binutils incorporada que guardamos previamente en:
cat main.gprof
Por defecto, esto produce una salida extremadamente detallada que explica lo que significan los datos de salida. Como no puedo explicarlo mejor que eso, te dejaré leerlo tú mismo.
Una vez que haya entendido el formato de salida de datos, puede reducir la verbosidad para mostrar solo los datos sin el tutorial con la -b
opción:
gprof -b main.out
En nuestro ejemplo, los resultados fueron para -O0
:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
100.35 3.67 3.67 123003 0.00 0.00 common
0.00 3.67 0.00 3 0.00 0.03 fast
0.00 3.67 0.00 3 0.00 1.19 maybe_slow
Call graph
granularity: each sample hit covers 2 byte(s) for 0.27% of 3.67 seconds
index % time self children called name
0.09 0.00 3003/123003 fast [4]
3.58 0.00 120000/123003 maybe_slow [3]
[1] 100.0 3.67 0.00 123003 common [1]
-----------------------------------------------
<spontaneous>
[2] 100.0 0.00 3.67 main [2]
0.00 3.58 3/3 maybe_slow [3]
0.00 0.09 3/3 fast [4]
-----------------------------------------------
0.00 3.58 3/3 main [2]
[3] 97.6 0.00 3.58 3 maybe_slow [3]
3.58 0.00 120000/123003 common [1]
-----------------------------------------------
0.00 0.09 3/3 main [2]
[4] 2.4 0.00 0.09 3 fast [4]
0.09 0.00 3003/123003 common [1]
-----------------------------------------------
Index by function name
[1] common [4] fast [3] maybe_slow
y para -O3
:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
100.52 1.84 1.84 123003 14.96 14.96 common
Call graph
granularity: each sample hit covers 2 byte(s) for 0.54% of 1.84 seconds
index % time self children called name
0.04 0.00 3003/123003 fast [3]
1.79 0.00 120000/123003 maybe_slow [2]
[1] 100.0 1.84 0.00 123003 common [1]
-----------------------------------------------
<spontaneous>
[2] 97.6 0.00 1.79 maybe_slow [2]
1.79 0.00 120000/123003 common [1]
-----------------------------------------------
<spontaneous>
[3] 2.4 0.00 0.04 fast [3]
0.04 0.00 3003/123003 common [1]
-----------------------------------------------
Index by function name
[1] common
Como un resumen muy rápido para cada sección, por ejemplo:
0.00 3.58 3/3 main [2]
[3] 97.6 0.00 3.58 3 maybe_slow [3]
3.58 0.00 120000/123003 common [1]
se centra en la función que queda sangrada ( maybe_flow
). [3]
es la identificación de esa función. Encima de la función, están sus llamadores, y debajo de ella los callees.
Para -O3
, vea aquí como en la salida gráfica que maybe_slow
y fast
no tiene un padre conocido, que es lo que la documentación dice que <spontaneous>
significa.
No estoy seguro de si hay una buena manera de hacer perfiles línea por línea con gprof: el tiempo `gprof` empleado en líneas de código particulares
valgrind callgrind
valgrind ejecuta el programa a través de la máquina virtual valgrind. Esto hace que la creación de perfiles sea muy precisa, pero también produce una gran desaceleración del programa. También he mencionado kcachegrind anteriormente en: Herramientas para obtener un gráfico de llamada de función pictórica de código
callgrind es la herramienta de valgrind para perfilar código y kcachegrind es un programa de KDE que puede visualizar la salida de cachegrind.
Primero tenemos que eliminar la -pg
bandera para volver a la compilación normal, de lo contrario, la ejecución realmente falla Profiling timer expired
, y sí, esto es tan común que lo hice y había una pregunta de desbordamiento de pila.
Entonces compilamos y ejecutamos como:
sudo apt install kcachegrind valgrind
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time valgrind --tool=callgrind valgrind --dump-instr=yes \
--collect-jumps=yes ./main.out 10000
Habilito --dump-instr=yes --collect-jumps=yes
porque esto también volca la información que nos permite ver un desglose del rendimiento por línea de ensamblaje, a un costo general agregado relativamente pequeño.
De buenas a primeras, time
nos dice que el programa tardó 29,5 segundos en ejecutarse, por lo que tuvimos una desaceleración de aproximadamente 15x en este ejemplo. Claramente, esta desaceleración será una seria limitación para cargas de trabajo más grandes. En el "ejemplo de software del mundo real" mencionado aquí , observé una desaceleración de 80x.
La ejecución genera un archivo de datos de perfil llamado, callgrind.out.<pid>
por ejemplo, callgrind.out.8554
en mi caso. Vemos ese archivo con:
kcachegrind callgrind.out.8554
que muestra una GUI que contiene datos similares a la salida textual de gprof:
Además, si vamos a la parte inferior derecha de la pestaña "Gráfico de llamadas", vemos un gráfico de llamadas que podemos exportar haciendo clic derecho para obtener la siguiente imagen con cantidades irrazonables de borde blanco :-)
Creo que fast
no se muestra en ese gráfico porque kcachegrind debe haber simplificado la visualización porque esa llamada toma muy poco tiempo, este será probablemente el comportamiento que desea en un programa real. El menú del botón derecho tiene algunas configuraciones para controlar cuándo eliminar dichos nodos, pero no pude hacer que mostrara una llamada tan corta después de un intento rápido. Si hago clic en fast
la ventana de la izquierda, muestra un gráfico de llamadas fast
, por lo que esa pila fue capturada. Nadie había encontrado todavía una manera de mostrar el gráfico de llamada de gráfico completo: hacer que callgrind muestre todas las llamadas de función en el gráfico de llamada kcachegrind
TODO en software C ++ complejo, veo algunas entradas de tipo <cycle N>
, por ejemplo, <cycle 11>
donde esperaría nombres de funciones, ¿qué significa eso? Me di cuenta de que hay un botón de "Detección de ciclos" para activarlo y desactivarlo, pero ¿qué significa?
perf
desde linux-tools
perf
parece utilizar exclusivamente mecanismos de muestreo de kernel de Linux. Esto hace que sea muy simple de configurar, pero tampoco totalmente preciso.
sudo apt install linux-tools
time perf record -g ./main.out 10000
Esto agregó 0.2s a la ejecución, por lo que estamos bien en cuanto al tiempo, pero aún no veo mucho interés, después de expandir el common
nodo con la flecha derecha del teclado:
Samples: 7K of event 'cycles:uppp', Event count (approx.): 6228527608
Children Self Command Shared Object Symbol
- 99.98% 99.88% main.out main.out [.] common
common
0.11% 0.11% main.out [kernel] [k] 0xffffffff8a6009e7
0.01% 0.01% main.out [kernel] [k] 0xffffffff8a600158
0.01% 0.00% main.out [unknown] [k] 0x0000000000000040
0.01% 0.00% main.out ld-2.27.so [.] _dl_sysdep_start
0.01% 0.00% main.out ld-2.27.so [.] dl_main
0.01% 0.00% main.out ld-2.27.so [.] mprotect
0.01% 0.00% main.out ld-2.27.so [.] _dl_map_object
0.01% 0.00% main.out ld-2.27.so [.] _xstat
0.00% 0.00% main.out ld-2.27.so [.] __GI___tunables_init
0.00% 0.00% main.out [unknown] [.] 0x2f3d4f4944555453
0.00% 0.00% main.out [unknown] [.] 0x00007fff3cfc57ac
0.00% 0.00% main.out ld-2.27.so [.] _start
Entonces trato de comparar el -O0
programa para ver si eso muestra algo, y solo ahora, por fin, veo un gráfico de llamadas:
Samples: 15K of event 'cycles:uppp', Event count (approx.): 12438962281
Children Self Command Shared Object Symbol
+ 99.99% 0.00% main.out [unknown] [.] 0x04be258d4c544155
+ 99.99% 0.00% main.out libc-2.27.so [.] __libc_start_main
- 99.99% 0.00% main.out main.out [.] main
- main
- 97.54% maybe_slow
common
- 2.45% fast
common
+ 99.96% 99.85% main.out main.out [.] common
+ 97.54% 0.03% main.out main.out [.] maybe_slow
+ 2.45% 0.00% main.out main.out [.] fast
0.11% 0.11% main.out [kernel] [k] 0xffffffff8a6009e7
0.00% 0.00% main.out [unknown] [k] 0x0000000000000040
0.00% 0.00% main.out ld-2.27.so [.] _dl_sysdep_start
0.00% 0.00% main.out ld-2.27.so [.] dl_main
0.00% 0.00% main.out ld-2.27.so [.] _dl_lookup_symbol_x
0.00% 0.00% main.out [kernel] [k] 0xffffffff8a600158
0.00% 0.00% main.out ld-2.27.so [.] mmap64
0.00% 0.00% main.out ld-2.27.so [.] _dl_map_object
0.00% 0.00% main.out ld-2.27.so [.] __GI___tunables_init
0.00% 0.00% main.out [unknown] [.] 0x552e53555f6e653d
0.00% 0.00% main.out [unknown] [.] 0x00007ffe1cf20fdb
0.00% 0.00% main.out ld-2.27.so [.] _start
TODO: ¿qué pasó con la -O3
ejecución? ¿Es simplemente eso maybe_slow
y fast
fueron demasiado rápidos y no obtuvieron ninguna muestra? ¿Funciona bien con -O3
programas más grandes que tardan más en ejecutarse? ¿Me perdí alguna opción de CLI? Descubrí que estaba a punto -F
de controlar la frecuencia de la muestra en Hertz, pero la aumenté al máximo permitido por defecto de -F 39500
(podría aumentarse con sudo
) y todavía no veo llamadas claras.
Una cosa genial perf
es la herramienta FlameGraph de Brendan Gregg, que muestra los tiempos de la pila de llamadas de una manera muy ordenada que le permite ver rápidamente las grandes llamadas. La herramienta está disponible en: https://github.com/brendangregg/FlameGraph y también se menciona en su tutorial de perf en: http://www.brendangregg.com/perf.html#FlameGraphs Cuando corrí perf
sin sudo
lo conseguí ERROR: No stack counts found
por ahora lo haré con sudo
:
git clone https://github.com/brendangregg/FlameGraph
sudo perf record -F 99 -g -o perf_with_stack.data ./main.out 10000
sudo perf script -i perf_with_stack.data | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flamegraph.svg
pero en un programa tan simple, la salida no es muy fácil de entender, ya que no podemos ver fácilmente maybe_slow
ni fast
en ese gráfico:
En un ejemplo más complejo queda claro lo que significa el gráfico:
TODO hay un registro de [unknown]
funciones en ese ejemplo, ¿por qué es eso?
Otras interfaces de interfaz gráfica de usuario que pueden valer la pena incluyen:
Complemento Eclipse Trace Compass: https://www.eclipse.org/tracecompass/
Pero esto tiene el inconveniente de que primero tiene que convertir los datos al formato de rastreo común, que se puede hacer perf data --to-ctf
, pero debe habilitarse en el momento de la compilación / tener lo perf
suficientemente nuevo, cualquiera de los cuales no es el caso para el rendimiento en Ubuntu 18.04
https://github.com/KDAB/hotspot
La desventaja de esto es que parece que no hay un paquete de Ubuntu, y su construcción requiere Qt 5.10, mientras que Ubuntu 18.04 está en Qt 5.9.
gperftools
Anteriormente llamado "Google Performance Tools", fuente: https://github.com/gperftools/gperftools Basado en muestras.
Primero instale gperftools con:
sudo apt install google-perftools
Luego, podemos habilitar el generador de perfiles de CPU gperftools de dos maneras: en tiempo de ejecución o en tiempo de compilación.
En el tiempo de ejecución, tenemos que pasar establecer el LD_PRELOAD
punto al libprofiler.so
que puede encontrar locate libprofiler.so
, por ejemplo, en mi sistema:
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \
CPUPROFILE=prof.out ./main.out 10000
Alternativamente, podemos construir la biblioteca en el momento del enlace, distribuyendo el paso LD_PRELOAD
en tiempo de ejecución:
gcc -Wl,--no-as-needed,-lprofiler,--as-needed -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
CPUPROFILE=prof.out ./main.out 10000
Ver también: gperftools - archivo de perfil no volcado
La mejor manera de ver estos datos que he encontrado hasta ahora es hacer que la salida de pprof tenga el mismo formato que kcachegrind toma como entrada (sí, la herramienta Valgrind-project-viewer-tool) y usar kcachegrind para ver eso:
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out
Después de ejecutar cualquiera de esos métodos, obtenemos un prof.out
archivo de datos de perfil como salida. Podemos ver ese archivo gráficamente como un SVG con:
google-pprof --web main.out prof.out
que da como un gráfico de llamada familiar como otras herramientas, pero con la unidad torpe de número de muestras en lugar de segundos.
Alternativamente, también podemos obtener algunos datos textuales con:
google-pprof --text main.out prof.out
lo que da:
Using local file main.out.
Using local file prof.out.
Total: 187 samples
187 100.0% 100.0% 187 100.0% common
0 0.0% 100.0% 187 100.0% __libc_start_main
0 0.0% 100.0% 187 100.0% _start
0 0.0% 100.0% 4 2.1% fast
0 0.0% 100.0% 187 100.0% main
0 0.0% 100.0% 183 97.9% maybe_slow
Ver también: Cómo usar las herramientas de Google Perf
Probado en Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, Linux kernel 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.
-fno-omit-frame-pointer
flag o usar una alternativa diferente: grabar con --call-graph "dwarf"
o --call-graph "lbr"
dependiendo de su escenario.
Para programas de subproceso único, puede usar igprof , The Ignominous Profiler: https://igprof.org/ .
Es un perfilador de muestreo, en la línea de la ... larga ... respuesta de Mike Dunlavey, que envolverá los resultados en un árbol de pila de llamadas navegable, anotado con el tiempo o la memoria gastada en cada función, ya sea acumulativa o por función.
También vale la pena mencionar son
He usado HPCToolkit y VTune y son muy efectivos para encontrar el poste largo en la carpa y no necesitan que se vuelva a compilar su código (excepto que debe usar -g -O o RelWithDebInfo type build en CMake para obtener resultados significativos) . He oído que TAU tiene capacidades similares.
Estos son los dos métodos que uso para acelerar mi código:
Para aplicaciones vinculadas a la CPU:
Para aplicaciones vinculadas de E / S:
nótese bien
Si no tiene un perfilador, use el perfilador del pobre. Haga clic en pausa mientras depura su aplicación. La mayoría de los conjuntos de desarrolladores se dividirán en ensamblados con números de línea comentados. Es estadísticamente probable que aterrice en una región que está consumiendo la mayoría de sus ciclos de CPU.
Para la CPU, la razón para crear un perfil en el modo DEBUG es que si intentaste crear un perfil en el modo RELEASE , el compilador reducirá las matemáticas, vectorizará los bucles y las funciones en línea que tienden a englobar tu código en un desastre no asignable cuando se ensambla. Un desorden no asignable significa que su generador de perfiles no podrá identificar claramente lo que está tardando tanto porque el ensamblado puede no corresponder con el código fuente bajo optimización . Si necesita el rendimiento (p. Ej., Sensible al tiempo) del modo RELEASE , desactive las funciones del depurador según sea necesario para mantener un rendimiento utilizable.
Para el enlace de E / S, el generador de perfiles aún puede identificar operaciones de E / S en modo LIBERACIÓN porque las operaciones de E / S están vinculadas externamente a una biblioteca compartida (la mayoría de las veces) o, en el peor de los casos, darán como resultado un sistema. vector de interrupción de llamada (que también es fácilmente identificable por el generador de perfiles).
Puede usar la biblioteca iprof:
https://gitlab.com/Neurochrom/iprof
https://github.com/Neurochrom/iprof
Es multiplataforma y le permite no medir el rendimiento de su aplicación también en tiempo real. Incluso puedes acoplarlo con un gráfico en vivo. Descargo de responsabilidad completo: soy el autor.
En el trabajo tenemos una herramienta realmente agradable que nos ayuda a monitorear lo que queremos en términos de programación. Esto ha sido útil en numerosas ocasiones.
Está en C ++ y debe personalizarse según sus necesidades. Desafortunadamente no puedo compartir código, solo conceptos. Usas un "grande"volatile
búfer que contiene marcas de tiempo e ID de evento que puede volcar post mortem o después de detener el sistema de registro (y volcar esto en un archivo, por ejemplo).
Usted recupera el llamado búfer grande con todos los datos y una pequeña interfaz lo analiza y muestra eventos con nombre (arriba / abajo + valor) como lo hace un osciloscopio con colores (configurados en el .hpp
archivo).
Personaliza la cantidad de eventos generados para centrarse únicamente en lo que desea. Nos ayudó mucho para problemas de programación al tiempo que consumía la cantidad de CPU que queríamos en función de la cantidad de eventos registrados por segundo.
Necesitas 3 archivos:
toolname.hpp // interface
toolname.cpp // code
tool_events_id.hpp // Events ID
El concepto es definir eventos de tool_events_id.hpp
esa manera:
// EVENT_NAME ID BEGIN_END BG_COLOR NAME
#define SOCK_PDU_RECV_D 0x0301 //@D00301 BGEEAAAA # TX_PDU_Recv
#define SOCK_PDU_RECV_F 0x0302 //@F00301 BGEEAAAA # TX_PDU_Recv
También define algunas funciones en toolname.hpp
:
#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_WARN 1
// ...
void init(void);
void probe(id,payload);
// etc
En cualquier parte de su código puede usar:
toolname<LOG_LEVEL>::log(EVENT_NAME,VALUE);
los probe
función utiliza algunas líneas de ensamblaje para recuperar la marca de tiempo del reloj lo antes posible y luego establece una entrada en el búfer. También tenemos un incremento atómico para encontrar de forma segura un índice donde almacenar el evento de registro. Por supuesto, el buffer es circular.
Espero que la idea no se ofusque por la falta de código de muestra.
En realidad, un poco sorprendido, no muchos mencionaron sobre google / benchmark , aunque es un poco engorroso fijar el área específica del código, especialmente si la base del código es un poco grande, sin embargo, encontré esto realmente útil cuando se usa en combinación concallgrind
En mi humilde opinión, la clave aquí es identificar la pieza que está causando el cuello de botella. Sin embargo, primero trataría de responder las siguientes preguntas y elegiría la herramienta basada en eso
valgrind
con la combinación de callrind
y kcachegrind
debería proporcionar una estimación decente en los puntos anteriores y una vez que se haya establecido que hay problemas con alguna sección del código, sugeriría que un micro punto de referencia google benchmark
sea un buen lugar para comenzar.
Use el -pg
indicador cuando compile y vincule el código y ejecute el archivo ejecutable. Mientras se ejecuta este programa, los datos de creación de perfiles se recopilan en el archivo a.out.
Hay dos tipos diferentes de perfiles
1- Perfiles planos:
al ejecutar el comando gprog --flat-profile a.out
se obtienen los siguientes datos
: qué porcentaje del tiempo total se dedicó a la función,
cuántos segundos se dedicaron a una función, incluidas y excluidas las llamadas a subfunciones,
el número de llamadas,
- el tiempo promedio por llamada.
2- gráfico que
nos perfila el comando gprof --graph a.out
para obtener los siguientes datos para cada función que incluye
- En cada sección, una función está marcada con un número de índice.
- Por encima de la función, hay una lista de funciones que llaman a la función.
- Debajo de la función, hay una lista de funciones que son llamadas por la función.
Para obtener más información, puede consultar https://sourceware.org/binutils/docs-2.32/gprof/
Como nadie mencionó Arm MAP, lo agregaría ya que personalmente he usado Map para perfilar un programa científico de C ++.
Arm MAP es el generador de perfiles para códigos C, C ++, Fortran y F90 paralelos, multiproceso o de un solo subproceso. Proporciona un análisis en profundidad y una localización precisa del cuello de botella en la línea de origen. A diferencia de la mayoría de los perfiladores, está diseñado para poder perfilar pthreads, OpenMP o MPI para código paralelo y enhebrado.
MAP es un software comercial.
utiliza un software de depuración ¿cómo identificar dónde se ejecuta el código lentamente?
solo piense que tiene un obstáculo mientras está en movimiento, entonces disminuirá su velocidad
como las operaciones de bucle de reasignación no deseadas, desbordamientos de búfer, búsquedas, pérdidas de memoria, etc., consume más potencia de ejecución que afectará negativamente al rendimiento del código. Asegúrese de agregar -pg a la compilación antes de perfilar:
g++ your_prg.cpp -pg
o cc my_program.cpp -g -pg
según su compilador
No lo he probado todavía, pero he oído cosas buenas sobre google-perftools. Definitivamente vale la pena intentarlo.
valgrind --tool=callgrind ./(Your binary)
Generará un archivo llamado gmon.out o callgrind.out.x. Luego puede usar la herramienta kcachegrind o depurador para leer este archivo. Le dará un análisis gráfico de las cosas con resultados como qué líneas cuestan cuánto.
creo que sí
code
perfiladores. Sin embargo, la inversión prioritaria, el alias de caché, la contención de recursos, etc., pueden ser factores para la optimización y el rendimiento. Creo que la gente lee información en mi código lento . Las preguntas frecuentes hacen referencia a este hilo.