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.